docs/DOCKER.md
Rama main · réplica de lectura del repositorio original.
# Docker Deployment Guide
This guide covers Docker deployment for ActaLog, including the single-port production architecture, multi-stage build process, and deployment options.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Quick Start](#quick-start)
- [Building Images](#building-images)
- [Image Labels (OCI Metadata)](#image-labels-oci-metadata)
- [Deployment Options](#deployment-options)
- [Configuration](#configuration)
- [Volumes and Data Persistence](#volumes-and-data-persistence)
- [Database Options](#database-options)
- [Troubleshooting](#troubleshooting)
## Architecture Overview
### Single-Port Production Architecture
ActaLog uses a **production-optimized single-port architecture** where the Go backend serves both the API and the frontend static files from port 8080.
**Key Benefits:**
- Simpler deployment (only one port to expose)
- No CORS configuration needed
- Lower resource usage (no Node.js in production)
- Industry-standard pattern for production SPAs
- Easier reverse proxy configuration
**How It Works:**
```
Port 8080 → Go Application
├── /api/* → Backend API endpoints
├── /uploads/* → User-uploaded files
└── /* → Frontend static files (with SPA routing)
```
**Route Priority:**
1. Health check: `/health` → API health status
2. API routes: `/api/*` → Backend handlers
3. Uploads: `/uploads/*` → Static file server
4. Frontend: `/*` → Serves static files or `index.html` for SPA routing
**Implementation:** See `cmd/actalog/main.go:251-262` for frontend directory configuration and lines 418-436 for static file serving logic.
### Multi-Stage Build Process
The Dockerfile uses a multi-stage build to create an optimized production image:
```dockerfile
# Stage 1: Build Frontend (Node.js)
FROM node:20-alpine AS frontend-builder
# ... builds /app/web/dist
# Stage 2: Build Backend (Go)
FROM golang:1.23-alpine AS backend-builder
# ... builds /app/bin/actalog
# Stage 3: Runtime (Alpine)
FROM alpine:latest
# Copies both frontend and backend
```
**Why Multi-Stage?**
- Smaller final image (~50MB vs 1GB+)
- No build tools in production image
- Security: minimal attack surface
- Separate build caching for frontend and backend
## Quick Start
### Pull and Run (Simplest)
```bash
# Pull the latest image from GitHub Container Registry
docker pull ghcr.io/johnzastrow/actalog:latest
# Run with SQLite (single-container deployment)
docker run -d \
-p 8080:8080 \
-v actalog-data:/app/data \
-v actalog-uploads:/app/uploads \
--name actalog \
ghcr.io/johnzastrow/actalog:latest
# Access the application
open http://localhost:8080
```
### Using Docker Compose (Recommended for Production)
```bash
# SQLite (development/single-user)
docker-compose -f docker/docker-compose.sqlite.yml up -d
# PostgreSQL (production recommended)
docker-compose -f docker/docker-compose.postgresql.yml up -d
# MariaDB (alternative production database)
docker-compose -f docker/docker-compose.mariadb.yml up -d
```
## Building Images
### Using Build Script (Recommended)
```bash
# Build and tag image (can run from anywhere in the repo)
./docker/scripts/build.sh v0.10.0
# Or from within the scripts directory
cd docker/scripts
./build.sh v0.10.0
# Push to GitHub Container Registry (automatically uses gh CLI auth if available)
./docker/scripts/push.sh v0.10.0
```
**Note:** Both scripts automatically detect the repository root and change to it, so you can run them from any directory within the repository.
**What the build script does:**
1. Checks for uncommitted changes (fails if dirty)
2. Runs multi-stage Docker build
3. Tags with version and 'latest'
4. Displays instructions for pushing
**What the push script does:**
1. Checks if Docker is already logged in to ghcr.io
2. **Automatically authenticates using `gh` CLI if available** (recommended)
3. Falls back to manual login if `gh` is not installed/authenticated
4. Pushes tagged images to GitHub Container Registry
**Automatic Authentication (Recommended):**
If you have the GitHub CLI (`gh`) installed and authenticated, the push script will automatically use your GitHub token:
```bash
# One-time setup (if not already authenticated)
gh auth login
# Now push script will use gh token automatically
./docker/scripts/push.sh v0.10.0
```
**Manual Authentication (Alternative):**
If `gh` CLI is not available, the script will prompt you to log in manually:
```bash
# Using Personal Access Token (PAT)
docker login ghcr.io
# Or using environment variables
echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USERNAME --password-stdin
```
**Required GitHub Token Scopes:**
- `write:packages` - Push images to GHCR
- `read:packages` - Pull images from GHCR
- `delete:packages` - Delete image versions (optional)
### Manual Build
```bash
# Build from root of repository
docker build -t actalog:local -f docker/Dockerfile .
# Run locally built image
docker run -p 8080:8080 -v actalog-data:/app/data actalog:local
```
### Build Arguments
The Dockerfile supports build-time configuration:
```bash
docker build \
--build-arg GOTOOLCHAIN=auto \
-t actalog:custom \
-f docker/Dockerfile .
```
## Image Labels (OCI Metadata)
The build script automatically adds [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/main/annotations.md) labels to Docker images. These labels provide metadata about the image that can be viewed by users and registries like GitHub Container Registry.
### What Are OCI Labels?
OCI (Open Container Initiative) labels are standardized metadata embedded in Docker images. They help identify:
- What the image contains and does
- Who created it and when
- Where to find documentation and source code
- Version and licensing information
**GitHub Container Registry Integration:** The `org.opencontainers.image.description` label is automatically displayed on your package page in GHCR, below the package name.
### User-Editable Labels
The build script (`docker/scripts/build.sh`) includes a clearly marked section at the top for customizing image metadata:
```bash
# ============================================================================
# USER-EDITABLE LABELS - Customize these for your deployment
# ============================================================================
# Description: A short text description of the image (max 512 characters)
# This will appear on the GitHub package page below the package name
IMAGE_DESCRIPTION="ActaLog - A mobile-first CrossFit workout tracker..."
# Vendor/Organization name
IMAGE_VENDOR="John Zastrow"
# License identifier (SPDX format recommended)
IMAGE_LICENSE="MIT"
# Authors (comma-separated if multiple)
IMAGE_AUTHORS="John Zastrow"
# Documentation URL (optional)
IMAGE_DOCUMENTATION=""
```
**To customize:** Edit these variables in `docker/scripts/build.sh` before building your image.
### Dynamic Labels (Auto-Generated)
The following labels are automatically extracted at build time:
| Label | Source | Example Value |
|-------|--------|---------------|
| `org.opencontainers.image.version` | `pkg/version/version.go` | `0.11.0-beta+build.71` |
| `org.opencontainers.image.created` | Build timestamp | `2025-11-26T14:30:00Z` |
| `org.opencontainers.image.revision` | Git commit hash | `61c7b43...` (full SHA) |
| `org.opencontainers.image.ref.name` | Git branch | `main` |
| `org.opencontainers.image.source` | Repository URL | `https://github.com/johnzastrow/actalog` |
| `build.number` | `pkg/version/version.go` | `71` |
| `build.git.commit` | Git + dirty check | `61c7b43` or `61c7b43-dirty` |
| `build.git.branch` | Git branch | `main` |
### Complete Label Reference
| OCI Standard Label | Description |
|-------------------|-------------|
| `org.opencontainers.image.title` | Image name ("ActaLog") |
| `org.opencontainers.image.description` | Short description (max 512 chars) - shown on GHCR |
| `org.opencontainers.image.version` | Semantic version with build number |
| `org.opencontainers.image.created` | RFC 3339 timestamp of build |
| `org.opencontainers.image.revision` | Full git commit SHA |
| `org.opencontainers.image.source` | URL to source code repository |
| `org.opencontainers.image.url` | URL to project homepage |
| `org.opencontainers.image.vendor` | Organization/person name |
| `org.opencontainers.image.licenses` | SPDX license identifier |
| `org.opencontainers.image.authors` | Contact information for authors |
| `org.opencontainers.image.ref.name` | Git reference (branch/tag) |
| `org.opencontainers.image.base.name` | Base image used (`alpine:latest`) |
### Viewing Image Labels
**View all labels (formatted with jq):**
```bash
docker inspect ghcr.io/johnzastrow/actalog:latest \
--format '{{json .Config.Labels}}' | jq
```
**View a specific label:**
```bash
docker inspect ghcr.io/johnzastrow/actalog:latest \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}'
```
**Example output:**
```json
{
"org.opencontainers.image.title": "ActaLog",
"org.opencontainers.image.description": "ActaLog - A mobile-first CrossFit workout tracker...",
"org.opencontainers.image.version": "0.11.0-beta+build.71",
"org.opencontainers.image.created": "2025-11-26T14:30:00Z",
"org.opencontainers.image.revision": "61c7b43abc123def456...",
"org.opencontainers.image.source": "https://github.com/johnzastrow/actalog",
"org.opencontainers.image.vendor": "John Zastrow",
"org.opencontainers.image.licenses": "MIT",
"org.opencontainers.image.authors": "John Zastrow",
"org.opencontainers.image.ref.name": "main",
"org.opencontainers.image.base.name": "alpine:latest",
"build.number": "71",
"build.git.commit": "61c7b43",
"build.git.branch": "main"
}
```
### Build Script Output
When you run the build script, it displays the metadata being applied:
```
============================================
ActaLog Docker Build Script
============================================
Repository: /home/user/actalog
Version: 0.11.0-beta (Build 71)
Registry: ghcr.io
Image: johnzastrow/actalog
Tag: latest
Platform: linux/amd64
Build Metadata:
Git Commit: 61c7b43
Git Branch: main
Build Date: 2025-11-26T14:30:00Z
Image Labels (edit in script header):
Description: ActaLog - A mobile-first CrossFit workout tracker...
Vendor: John Zastrow
License: MIT
Building Docker image...
```
### GitHub Container Registry Display
After pushing to GHCR, the `org.opencontainers.image.description` label appears:
1. Go to: `https://github.com/USERNAME/REPO/pkgs/container/actalog`
2. The description appears below the package name
3. Version and other metadata may be shown depending on GHCR features
**Note:** It may take a few minutes for GHCR to process and display the label after pushing.
### Custom Labels
You can add additional custom labels by modifying the `docker buildx build` command in `build.sh`:
```bash
docker buildx build \
# ... existing labels ...
--label "com.example.custom-label=value" \
--label "com.example.environment=production" \
# ... rest of command ...
```
**Naming convention:** Custom labels should use a reverse domain name prefix (e.g., `com.yourcompany.labelname`).
## Deployment Options
### Option 1: Single Container with SQLite
**Best for:** Development, testing, single-user deployments
```bash
docker run -d \
-p 8080:8080 \
-e DB_DRIVER=sqlite3 \
-e DB_NAME=/app/data/actalog.db \
-v actalog-data:/app/data \
-v actalog-uploads:/app/uploads \
--name actalog \
--restart unless-stopped \
ghcr.io/johnzastrow/actalog:latest
```
**Advantages:**
- Simplest deployment (one container)
- No external database needed
- Easy backups (copy volume)
**Limitations:**
- No horizontal scaling
- Single point of failure
### Option 2: Multi-Container with PostgreSQL
**Best for:** Production, multi-user, high-availability
```bash
# Using docker-compose
docker-compose -f docker/docker-compose.postgresql.yml up -d
```
**docker-compose.postgresql.yml:**
```yaml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: actalog
POSTGRES_PASSWORD: changeme
POSTGRES_DB: actalog
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
actalog:
image: ghcr.io/johnzastrow/actalog:latest
ports:
- "8080:8080"
environment:
DB_DRIVER: postgres
DB_HOST: postgres
DB_PORT: 5432
DB_USER: actalog
DB_PASSWORD: changeme
DB_NAME: actalog
DB_SSLMODE: disable
volumes:
- actalog-uploads:/app/uploads
depends_on:
- postgres
restart: unless-stopped
volumes:
postgres-data:
actalog-uploads:
```
**Advantages:**
- Production-ready database
- Better performance at scale
- Proper transaction support
- Can scale horizontally
### Option 3: Behind Reverse Proxy (Nginx/Caddy)
**Best for:** Production with TLS, multiple services
```nginx
# /etc/nginx/sites-available/actalog
server {
listen 80;
server_name actalog.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name actalog.example.com;
ssl_certificate /etc/letsencrypt/live/actalog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/actalog.example.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Increase timeouts for large uploads
client_max_body_size 50M;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
```
**Caddy (simpler alternative):**
```caddyfile
actalog.example.com {
reverse_proxy localhost:8080
}
```
## Configuration
### Environment Variables
All configuration is done via environment variables:
**Database:**
```bash
DB_DRIVER=sqlite3 # sqlite3, postgres, mysql
DB_NAME=/app/data/actalog.db # For SQLite
DB_HOST=postgres # For PostgreSQL/MySQL
DB_PORT=5432 # Database port
DB_USER=actalog # Database user
DB_PASSWORD=changeme # Database password
DB_NAME=actalog # Database name
DB_SSLMODE=disable # PostgreSQL SSL mode
DB_SCHEMA=public # PostgreSQL schema
```
**Application:**
```bash
PORT=8080 # Application port
APP_URL=https://actalog.example.com # Base URL for emails
CORS_ORIGINS=https://actalog.example.com # CORS allowed origins
ALLOW_REGISTRATION=true # Allow new user registration
```
**Authentication:**
```bash
JWT_SECRET=changeme-in-production # JWT signing secret (REQUIRED)
JWT_EXPIRATION=24h # Access token lifetime
JWT_REFRESH_DURATION=168h # Refresh token lifetime (7 days)
```
**Email (Optional):**
```bash
EMAIL_ENABLED=true
EMAIL_FROM=noreply@actalog.example.com
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=your-email@gmail.com
EMAIL_SMTP_PASSWORD=your-app-password
EMAIL_REQUIRE_VERIFICATION=false
```
**Security:**
```bash
MAX_LOGIN_ATTEMPTS=5 # Max failed login attempts
ACCOUNT_LOCKOUT_DURATION=30m # Account lock duration
```
**Frontend (Build-time):**
```bash
FRONTEND_DIR=/app/web/dist # Frontend static files location
```
### Docker Environment File
Create `.env` file for docker-compose:
```bash
# .env
DB_DRIVER=postgres
DB_HOST=postgres
DB_PORT=5432
DB_USER=actalog
DB_PASSWORD=your-secure-password-here
DB_NAME=actalog
JWT_SECRET=your-jwt-secret-here-min-32-chars
EMAIL_ENABLED=true
EMAIL_FROM=noreply@actalog.example.com
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=your-email@gmail.com
EMAIL_SMTP_PASSWORD=your-app-password
APP_URL=https://actalog.example.com
CORS_ORIGINS=https://actalog.example.com
```
Then reference in docker-compose.yml:
```yaml
services:
actalog:
env_file: .env
```
## Volumes and Data Persistence
### Required Volumes
1. **`/app/data`** - Database files (for SQLite) and application data
```bash
-v actalog-data:/app/data
```
2. **`/app/uploads`** - User-uploaded files (avatars, etc.)
```bash
-v actalog-uploads:/app/uploads
```
### Backup Strategy
#### SQLite Backup
```bash
# Create backup
docker exec actalog sqlite3 /app/data/actalog.db ".backup '/app/data/backup.db'"
# Copy backup to host
docker cp actalog:/app/data/backup.db ./actalog-backup-$(date +%Y%m%d).db
# Or use volume mount
docker run --rm \
-v actalog-data:/data \
-v $(pwd)/backups:/backups \
alpine cp /data/actalog.db /backups/actalog-$(date +%Y%m%d).db
```
#### PostgreSQL Backup
```bash
# Dump database
docker exec postgres pg_dump -U actalog actalog > backup.sql
# Restore database
docker exec -i postgres psql -U actalog actalog < backup.sql
```
#### Uploads Backup
```bash
# Tar and compress uploads
docker run --rm \
-v actalog-uploads:/uploads \
-v $(pwd)/backups:/backups \
alpine tar czf /backups/uploads-$(date +%Y%m%d).tar.gz -C /uploads .
```
## Database Options
### SQLite (Default)
**Configuration:**
```bash
-e DB_DRIVER=sqlite3
-e DB_NAME=/app/data/actalog.db
```
**Pros:**
- Zero configuration
- Fast for single-user
- Easy backups
**Cons:**
- No concurrent writes
- Limited scalability
### PostgreSQL (Recommended for Production)
**Configuration:**
```bash
-e DB_DRIVER=postgres
-e DB_HOST=postgres
-e DB_PORT=5432
-e DB_USER=actalog
-e DB_PASSWORD=changeme
-e DB_NAME=actalog
-e DB_SSLMODE=disable
-e DB_SCHEMA=public
```
**Pros:**
- Production-grade
- Horizontal scaling
- ACID compliance
- Advanced features
### MariaDB/MySQL
**Configuration:**
```bash
-e DB_DRIVER=mysql
-e DB_HOST=mariadb
-e DB_PORT=3306
-e DB_USER=actalog
-e DB_PASSWORD=changeme
-e DB_NAME=actalog
```
**Pros:**
- Widely supported
- Good performance
- Familiar to many admins
## Troubleshooting
### Container Won't Start
**Check logs:**
```bash
docker logs actalog
```
**Common issues:**
- Missing JWT_SECRET environment variable
- Database connection failure
- Port 8080 already in use
### Frontend Not Serving
**Verify frontend files exist:**
```bash
docker exec actalog ls -la /app/web/dist
```
**Check logs for:**
```
Serving frontend from: /app/web/dist
```
**If missing:**
- Image might not have been built correctly
- Rebuild with: `./docker/scripts/build.sh`
### Database Connection Errors
**SQLite:**
```bash
# Check if database file exists
docker exec actalog ls -la /app/data/
# Check permissions
docker exec actalog ls -la /app/data/actalog.db
```
**PostgreSQL/MariaDB:**
```bash
# Test database connection
docker exec actalog nc -zv postgres 5432
# Check database logs
docker logs postgres
```
### 404 Errors for Frontend Routes
This is expected behavior! The Go backend serves `index.html` for all non-existent paths to support Vue Router's history mode.
**If still getting 404s:**
1. Check that `/` returns the frontend
2. Verify frontend files exist: `docker exec actalog ls /app/web/dist`
3. Check logs for static file serving configuration
### Permission Denied Errors
The container runs as non-root user `actalog` (UID 1000):
```bash
# If you need to fix permissions on volumes
docker run --rm -v actalog-data:/data alpine chown -R 1000:1000 /data
docker run --rm -v actalog-uploads:/uploads alpine chown -R 1000:1000 /uploads
```
### High Memory Usage
**Check resource usage:**
```bash
docker stats actalog
```
**Limit resources:**
```bash
docker run -d \
--memory="512m" \
--cpus="1.0" \
-p 8080:8080 \
ghcr.io/johnzastrow/actalog:latest
```
### Seed Data Not Loading
The application automatically imports seed data (movements and WODs) on first run.
**Check if seeds were imported:**
```bash
docker exec actalog ls -la /app/data/.seeds_imported
```
**If marker file doesn't exist:**
```bash
# Manually run seed import
docker exec actalog /app/scripts/init-seeds.sh
```
## Advanced Topics
### Custom Seed Data
Mount your own seed files:
```bash
docker run -d \
-p 8080:8080 \
-v ./custom-movements.csv:/app/seeds/movements.csv:ro \
-v ./custom-wods.csv:/app/seeds/wods.csv:ro \
ghcr.io/johnzastrow/actalog:latest
```
### Health Checks
The image includes a built-in health check:
```dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
```
**Check container health:**
```bash
docker inspect --format='{{.State.Health.Status}}' actalog
```
### Multi-Instance Deployment
For horizontal scaling:
1. Use shared database (PostgreSQL/MariaDB)
2. Use shared uploads storage (NFS/S3)
3. Run multiple containers behind load balancer
```bash
# Instance 1
docker run -d --name actalog-1 -p 8081:8080 \
-e DB_DRIVER=postgres -e DB_HOST=postgres-server \
-v shared-uploads:/app/uploads \
ghcr.io/johnzastrow/actalog:latest
# Instance 2
docker run -d --name actalog-2 -p 8082:8080 \
-e DB_DRIVER=postgres -e DB_HOST=postgres-server \
-v shared-uploads:/app/uploads \
ghcr.io/johnzastrow/actalog:latest
# Load balancer forwards to both instances
```
## Security Best Practices
1. **Change default JWT secret:**
```bash
JWT_SECRET=$(openssl rand -base64 32)
```
2. **Use environment files for secrets:**
- Never commit `.env` files
- Use Docker secrets or external secret management
3. **Enable HTTPS:**
- Use reverse proxy (Nginx/Caddy) with TLS
- Required for PWA features
4. **Restrict CORS:**
```bash
CORS_ORIGINS=https://yourdomain.com
```
5. **Regular updates:**
```bash
docker pull ghcr.io/johnzastrow/actalog:latest
docker-compose down && docker-compose up -d
```
6. **Network isolation:**
```yaml
services:
postgres:
networks:
- backend
actalog:
networks:
- backend
- frontend
```
## Production Checklist
- [ ] PostgreSQL or MariaDB configured (not SQLite)
- [ ] Custom JWT_SECRET set (min 32 characters)
- [ ] CORS_ORIGINS configured for your domain
- [ ] TLS/HTTPS enabled via reverse proxy
- [ ] Volumes configured for data persistence
- [ ] Backup strategy implemented
- [ ] Health checks enabled
- [ ] Resource limits set
- [ ] Logging configured
- [ ] Monitoring in place
## Support
For issues or questions:
- GitHub Issues: https://github.com/johnzastrow/actalog/issues
- Documentation: See `docs/` directory
- Architecture: See `docs/ARCHITECTURE.md`
- Development: See `CLAUDE.md`