This guide covers building, publishing, and deploying ActaLog using Docker containers with GitHub Container Registry (ghcr.io).
- Overview
- Prerequisites
- Quick Start
- Building Images
- Publishing to GitHub Container Registry
- Automated Builds with GitHub Actions
- Rapid Testing with Development Builds
- Production Deployment
- Creating Stable Releases
- Troubleshooting
ActaLog uses a multi-stage Docker build that creates an optimized production image containing both the Go backend and Vue.js frontend. Images are published to GitHub Container Registry for easy distribution and deployment.
Image Location: ghcr.io/OWNER/REPO:TAG
Key Features:
- Multi-stage build (frontend, backend, runtime)
- Optimized Alpine-based runtime (< 50MB)
- Multi-architecture support (amd64, arm64)
- Automated builds on every push
- Build number tagging for rapid testing
- Semantic versioning for stable releases
- Docker 20.10+ with BuildKit
- Docker Compose 2.0+
- Git
- GitHub account with repository access
- GitHub Personal Access Token (PAT) with
write:packagespermission - Repository must be public OR you need
read:packageson private repos
# Pull latest image from GitHub Container Registry
docker pull ghcr.io/OWNER/REPO:latest
# Run with default SQLite database
docker run -d \
--name actalog \
-p 8080:8080 \
-v actalog-data:/app/data \
-v actalog-uploads:/app/uploads \
-e JWT_SECRET=your_secure_secret_here \
ghcr.io/OWNER/REPO:latest
# Access the application
open http://localhost:8080cd docker
# Copy and configure environment
cp .env.example .env
# Edit .env with your configuration
# Start the application
docker compose up -d
# View logs
docker compose logs -f
# Stop the application
docker compose downThe build script automatically extracts version information and creates properly tagged images.
# Build with default 'dev' tag
./docker/scripts/build.sh
# Build with custom tag
./docker/scripts/build.sh v0.9.0
# Build for specific platform
DOCKER_PLATFORM=linux/arm64 ./docker/scripts/build.sh
# Build for multiple platforms (requires buildx)
DOCKER_PLATFORM=linux/amd64,linux/arm64 ./docker/scripts/build.shWhat the script does:
- Extracts version and build number from
pkg/version/version.go - Builds multi-stage Docker image
- Tags image with:
- Custom tag (or 'dev')
- Build number tag (
build-62)
- Loads image into local Docker daemon
- Displays pull and push instructions
# From project root
docker build \
-f docker/Dockerfile \
-t ghcr.io/OWNER/REPO:dev \
.
# Multi-platform build
docker buildx build \
--platform linux/amd64,linux/arm64 \
-f docker/Dockerfile \
-t ghcr.io/OWNER/REPO:dev \
--push \
.# Option 1: Using GitHub CLI (recommended)
gh auth login
# Option 2: Using Personal Access Token
export GITHUB_TOKEN=ghp_yourpersonalaccesstoken
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
# Option 3: Interactive login
docker login ghcr.io
# Username: YOUR_GITHUB_USERNAME
# Password: YOUR_PERSONAL_ACCESS_TOKEN# Push image with specific tag
./docker/scripts/push.sh dev
# Push stable release
./docker/scripts/push.sh v0.9.0What the script does:
- Checks GitHub Container Registry authentication
- Pushes image with specified tag
- Pushes build-specific tag (
build-62) - Displays pull command and package URL
# Push specific tag
docker push ghcr.io/OWNER/REPO:dev
# Push multiple tags
docker push ghcr.io/OWNER/REPO:v0.9.0
docker push ghcr.io/OWNER/REPO:latestAfter first push, packages are private by default.
- Go to
https://github.com/OWNER/REPO/packages - Click on
actalogpackage - Go to Package settings
- Scroll to Danger Zone
- Click Change visibility � Public
The GitHub Actions workflow (.github/workflows/docker-build.yml) automatically builds and pushes Docker images on:
- Push to
mainordevelopbranches � builds and pushes - Push tags matching
v*� builds release with semantic versioning - Pull requests to
main� builds only (no push) - Manual workflow dispatch � build with custom tag
Images are automatically tagged based on the event:
| Event | Tags Created | Example |
|---|---|---|
Push to main |
latest, main-SHA, build-N |
latest, main-a1b2c3d, build-42 |
Push to develop |
develop, develop-SHA, build-N |
develop, develop-x9y8z7w, build-43 |
Tag v1.2.3 |
v1.2.3, v1.2, v1, build-N |
v1.2.3, v1.2, v1, build-44 |
| PR #123 | pr-123 |
pr-123 |
| Manual trigger | Custom tag, build-N |
manual, build-45 |
No secrets required! The workflow uses the built-in GITHUB_TOKEN which has automatic access to GitHub Container Registry.
- Go to Actions tab in GitHub
- Click on Build and Push Docker Image workflow
- View build summary with:
- Build metadata
- All tags created
- Pull command
You need to test changes on a remote server without creating formal releases.
Every build automatically gets a unique build-N tag based on GitHub Actions run number.
Workflow:
# 1. Push code to main/develop branch
git add .
git commit -m "feat: add new feature"
git push origin main
# 2. GitHub Actions automatically builds and tags:
# - ghcr.io/OWNER/REPO:main-abc1234
# - ghcr.io/OWNER/REPO:build-62
# - ghcr.io/OWNER/REPO:latest
# 3. On remote server, pull latest build
ssh user@remote-server
docker pull ghcr.io/OWNER/REPO:build-62
# 4. Update docker-compose.yml or restart container
export TAG=build-62
docker compose up -d
# 5. Verify deployment
docker ps
curl http://localhost:8080/api/version# Local development
make build # Increments build number
git add . && git commit -m "..." && git push
# Watch GitHub Actions (2-5 minutes)
gh run watch
# Deploy on remote
ssh user@server "docker pull ghcr.io/OWNER/REPO:build-63 && docker compose up -d"
# Test
curl http://server:8080/api/version
# {"version":"0.9.0-beta","build":63,...}Create scripts/deploy-dev.sh:
#!/bin/bash
set -e
SERVER="user@your-server.com"
BUILD_NUM=$(grep -E "^\s*Build\s*=\s*[0-9]+" pkg/version/version.go | awk '{print $3}')
echo "Deploying build ${BUILD_NUM} to ${SERVER}..."
# SSH and pull latest build
ssh $SERVER << EOF
cd /opt/actalog
export TAG=build-${BUILD_NUM}
docker pull ghcr.io/OWNER/REPO:\$TAG
docker compose up -d
docker ps
curl -s http://localhost:8080/api/version | jq .
EOF
echo "Deployment complete!"Usage:
# Make changes
vim internal/handler/some_handler.go
# Build, commit, push
make build
git add . && git commit -m "fix: bug fix" && git push
# Wait for GitHub Actions, then deploy
gh run watch && ./scripts/deploy-dev.sh1. Create deployment directory:
ssh user@production-server
mkdir -p /opt/actalog
cd /opt/actalog2. Copy docker-compose.yml and .env:
# Download from repository
curl -O https://raw.githubusercontent.com/OWNER/REPO/main/docker/docker-compose.yml
curl -O https://raw.githubusercontent.com/OWNER/REPO/main/docker/.env.example
mv .env.example .env3. Configure environment:
vim .envRequired configuration:
GITHUB_OWNER=your-github-username
TAG=v0.9.0 # Use stable release tag
# Change this!
JWT_SECRET=GENERATE_SECURE_RANDOM_STRING_HERE
# Optional: PostgreSQL instead of SQLite
DB_DRIVER=postgres
DB_HOST=postgres
DB_PORT=5432
DB_NAME=actalog
DB_USER=actalog
DB_PASSWORD=secure_password4. Start services:
docker compose up -d5. Verify deployment:
docker compose ps
docker compose logs -f
curl http://localhost:8080/health
curl http://localhost:8080/api/versiondocker run -d \
--name actalog \
--restart unless-stopped \
-p 8080:8080 \
-v actalog-data:/app/data \
-v actalog-uploads:/app/uploads \
-e DB_DRIVER=sqlite3 \
-e DB_NAME=/app/data/actalog.db \
-e JWT_SECRET=your_secure_secret_here \
-e CORS_ORIGINS=https://yourdomain.com \
ghcr.io/OWNER/REPO:v0.9.0docker-compose.yml:
version: '3.8'
services:
actalog:
image: ghcr.io/OWNER/REPO:v0.9.0
restart: unless-stopped
ports:
- "8080:8080"
environment:
- DB_DRIVER=postgres
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=actalog
- DB_USER=actalog
- DB_PASSWORD=${DB_PASSWORD}
- JWT_SECRET=${JWT_SECRET}
volumes:
- actalog-uploads:/app/uploads
depends_on:
- postgres
networks:
- actalog-network
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=actalog
- POSTGRES_USER=actalog
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- actalog-network
volumes:
actalog-uploads:
postgres-data:
networks:
actalog-network:Stable releases use semantic versioning tags (v1.2.3) and are automatically built by GitHub Actions.
1. Update version in code:
Edit pkg/version/version.go:
const (
Major = 0
Minor = 9
Patch = 0
PreRelease = "beta" // or "" for stable
Build = 62 // Auto-incremented
)Edit web/package.json:
{
"version": "0.9.0"
}2. Update changelog:
Edit docs/CHANGELOG.md:
## [0.9.0-beta] - 2025-01-23
### Added
- Feature X
- Feature Y
### Fixed
- Bug Z3. Commit and push changes:
git add pkg/version/version.go web/package.json docs/CHANGELOG.md
git commit -m "chore: bump version to v0.9.0-beta"
git push origin main4. Create and push Git tag:
# Create annotated tag
git tag -a v0.9.0-beta -m "Release v0.9.0-beta
- Offline PWA support
- Docker deployment
- 1RM calculation
- Performance improvements"
# Push tag to GitHub
git push origin v0.9.0-beta5. GitHub Actions automatically:
- Detects the tag
- Builds multi-platform images
- Tags images with:
v0.9.0-betav0.9v0build-62
- Pushes to GitHub Container Registry
- Creates build summary
6. Create GitHub Release (optional):
# Using GitHub CLI
gh release create v0.9.0-beta \
--title "ActaLog v0.9.0-beta" \
--notes-file docs/CHANGELOG.md \
--prerelease
# Or manually at: https://github.com/OWNER/REPO/releases/new7. Verify release:
# Pull release image
docker pull ghcr.io/OWNER/REPO:v0.9.0-beta
# Verify version
docker run --rm ghcr.io/OWNER/REPO:v0.9.0-beta /app/actalog --version
# ActaLog v0.9.0-beta+build.62| Channel | Tag Pattern | Use Case | Stability |
|---|---|---|---|
latest |
Auto from main |
Latest stable | Production-ready |
develop |
Auto from develop |
Bleeding edge | Testing |
v1.2.3 |
Git tag | Specific version | Production |
v1.2 |
Auto from tag | Minor version | Production |
v1 |
Auto from tag | Major version | Production |
build-N |
Every build | Rapid testing | Development |
Stable Release (Recommended):
docker pull ghcr.io/OWNER/REPO:v0.9.0Latest Stable:
docker pull ghcr.io/OWNER/REPO:latestSpecific Minor Version (auto-updates patches):
docker pull ghcr.io/OWNER/REPO:v0.9Beta/Pre-release:
docker pull ghcr.io/OWNER/REPO:v0.9.0-betaProblem: Docker build fails with "npm ci" errors
Solution:
# Clear npm cache in Dockerfile or rebuild without cache
docker build --no-cache -f docker/Dockerfile .Problem: Go build fails with "missing module"
Solution:
# Ensure go.mod and go.sum are up to date
go mod tidy
git add go.mod go.sum
git commit -m "chore: update go modules"Problem: "unauthorized: authentication required"
Solution:
# Re-authenticate
docker logout ghcr.io
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdinProblem: "denied: permission_denied"
Solution:
- Verify your GitHub token has
write:packagespermission - Ensure you have push access to the repository
- Check package visibility settings
Problem: Container starts but /health returns 404
Solution:
# Check logs
docker logs actalog
# Verify environment variables
docker exec actalog env | grep DB_
# Check if migrations ran
docker exec actalog ls -la /app/migrationsProblem: Database connection errors
Solution:
# For SQLite
docker exec actalog ls -la /app/data/
# For PostgreSQL
docker exec actalog ping postgres
docker compose logs postgresProblem: Container can't connect to database running on the host machine
dial tcp 127.0.0.1:3306: connect: connection refused
This happens because 127.0.0.1 inside a container refers to the container itself, not the host machine.
Solution for Linux:
- Update
docker-compose.ymlto usehost.docker.internal:
services:
actalog:
image: ghcr.io/johnzastrow/actalog:latest
extra_hosts:
- "host.docker.internal:host-gateway" # Required for Linux
environment:
- DB_DRIVER=mysql
- DB_HOST=host.docker.internal # NOT 127.0.0.1
- DB_PORT=3306
- DB_NAME=acta
- DB_USER=acta
- DB_PASSWORD=${DB_PASSWORD}Alternative: Use Docker bridge IP (172.17.0.1) instead of host.docker.internal.
- Configure MariaDB/MySQL to accept connections from Docker:
Check that MariaDB is listening on all interfaces (not just localhost):
sudo grep -E "bind-address|skip-networking" /etc/mysql/mariadb.conf.d/*.cnfIf bind-address = 127.0.0.1, change to bind-address = 0.0.0.0 or comment it out, then restart MariaDB:
sudo systemctl restart mariadb- Grant database user permission to connect from Docker network:
-- For Docker bridge network (most secure)
GRANT ALL PRIVILEGES ON acta.* TO 'acta'@'172.17.%' IDENTIFIED BY 'your_password';
FLUSH PRIVILEGES;
-- Or allow from any host (less secure, but easier for testing)
GRANT ALL PRIVILEGES ON acta.* TO 'acta'@'%' IDENTIFIED BY 'your_password';
FLUSH PRIVILEGES;- Verify connectivity:
# Restart container
docker compose down && docker compose up -d
# Check logs
docker compose logs -f actalog
# Test from inside container
docker exec -it actalog sh -c "nc -zv host.docker.internal 3306"Note for Docker Desktop (Mac/Windows): The extra_hosts directive is not needed—host.docker.internal works automatically.
- Check firewall rules (if connection times out):
If the connection times out instead of being refused, a firewall may be blocking Docker's network:
# Check UFW status
sudo ufw status
# Allow MariaDB from Docker network
sudo ufw allow from 172.17.0.0/16 to any port 3306
sudo ufw reload
# For PostgreSQL
sudo ufw allow from 172.17.0.0/16 to any port 5432
sudo ufw reloadProblem: "pull access denied"
Solution:
# For private repositories, authenticate first
docker login ghcr.io
# Or make package public (see Publishing section)Problem: "manifest unknown"
Solution:
# Verify tag exists
gh api /user/packages/container/actalog/versions
# Or check at: https://github.com/OWNER/REPO/pkgs/container/actalog- Docker Documentation
- GitHub Container Registry Docs
- GitHub Actions Docker Build
- Docker Multi-platform Builds
# Build locally
./docker/scripts/build.sh v0.9.0
# Push to registry
./docker/scripts/push.sh v0.9.0
# Pull specific build for testing
docker pull ghcr.io/OWNER/REPO:build-62
# Deploy with Docker Compose
cd docker && docker compose up -d
# Create stable release
git tag -a v0.9.0 -m "Release v0.9.0"
git push origin v0.9.0
# Pull stable release
docker pull ghcr.io/OWNER/REPO:v0.9.0