# Production Deployment Guide This guide covers deploying the EDH Stats Tracker to production using Docker and GitHub Container Registry (GHCR). ## Prerequisites - Docker and Docker Compose installed on your server - GitHub account with access to this repository - GitHub Personal Access Token (PAT) with `write:packages` permission - Domain name (for CORS_ORIGIN configuration) - SSL certificates (optional, for HTTPS) ## Quick Start ### Option 1: Automatic Deployment Script (Local) 1. **Generate GitHub Token** - Go to GitHub → Settings → Developer settings → Personal access tokens - Create a new token with `write:packages` scope - Copy the token 2. **Run Deployment Script** ```bash chmod +x deploy.sh # With token as argument ./deploy.sh v1.0.0 ghcr_xxxxxxxxxxxxx # Or set as environment variable export GHCR_TOKEN=ghcr_xxxxxxxxxxxxx export GITHUB_USER=your-github-username ./deploy.sh v1.0.0 # Or use interactive mode ./deploy.sh v1.0.0 ``` **What the script does:** - Validates Docker and Docker buildx prerequisites - Builds images for **both** `linux/amd64` (AMD64 servers) and `linux/arm64` (Apple Silicon) - Pushes to GHCR automatically (no separate push step needed) - Generates deployment configuration 3. **Review Generated Configuration** - Check `docker-compose.prod.deployed.yml` - Verify image tags and versions ### Option 2: Automated CI/CD (GitHub Actions) 1. **Push Release Tag** ```bash git tag v1.0.0 git push origin v1.0.0 ``` 2. **GitHub Actions Automatically:** - Builds Docker images - Pushes to GHCR - Generates deployment config - Creates release with artifacts 3. **Download Deployment Config** - Go to GitHub Releases - Download `docker-compose.prod.deployed.yml` ## Server Setup ### 1. Install Docker & Docker Compose ```bash # Ubuntu/Debian curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh # Add user to docker group sudo usermod -aG docker $USER newgrp docker # Verify docker --version docker-compose --version ``` ### 2. Create Production Directory ```bash mkdir -p ~/edh-stats/data/database mkdir -p ~/edh-stats/data/logs cd ~/edh-stats ``` ### 3. Copy Deployment Configuration ```bash # Copy the docker-compose.prod.deployed.yml file to server scp docker-compose.prod.deployed.yml user@server:~/edh-stats/docker-compose.yml ``` ### 4. Create Environment File ```bash # Create .env file on server cat > ~/edh-stats/.env << EOF # Required: Set your domain CORS_ORIGIN=https://yourdomain.com # Optional: Enable user registration (default: false) ALLOW_REGISTRATION=false # Database backup path (optional) DATABASE_BACKUP_PATH=/data/backups EOF ``` ### 5. Generate JWT Secret The JWT_SECRET is already included in your `.env` file. The secret is generated automatically when you create the `.env` file: ```bash # Already done in step 4, but if you need to regenerate: openssl rand -base64 32 # Copy the output and update JWT_SECRET in .env ``` Your JWT secret is stored in the `.env` file which is protected by `.gitignore` (not committed to git). ## Deployment ### 1. Configure Docker Authentication to GHCR You need to authenticate Docker to pull private images from GitHub Container Registry (GHCR). Choose one of these methods: #### Option A: Store Credentials in `/etc/docker/daemon.json` (Recommended for Docker Services) This approach is recommended if you're using Dockge, systemd services, or other Docker management tools that run as services. The credentials are stored globally so all Docker processes can use them. **Step 1: Generate base64-encoded credentials** ```bash # Replace with your actual GitHub username and token echo -n "YOUR_GITHUB_USERNAME:YOUR_GITHUB_TOKEN" | base64 # Output example: # WU9VUl9HSVRIVUJfVVNFUk5BTUU6WU9VUl9HSVRIVUJfVE9LRU4= ``` **Step 2: Update Docker daemon configuration** ```bash sudo nano /etc/docker/daemon.json ``` Add or update the `auths` section. The full file should look like: ```json { "auths": { "ghcr.io": { "auth": "YOUR_BASE64_CREDENTIALS_HERE" } } } ``` **Step 3: Restart Docker** ```bash sudo systemctl restart docker # Wait a few seconds for Docker to restart sleep 3 # Verify authentication works docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:latest ``` #### Option B: Interactive Docker Login (Simpler but User-Specific) Use this if you're deploying manually and don't have other services pulling images. ```bash docker login ghcr.io # You'll be prompted for: # Username: YOUR_GITHUB_USERNAME # Password: YOUR_GITHUB_TOKEN (NOT your GitHub password!) # Verify login worked docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:latest ``` **Note:** With this approach, credentials are stored in `~/.docker/config.json` and only the current user can use them. If Docker runs as a different user (like in Dockge), authentication will fail. ### 2. Pull Latest Images ```bash cd ~/edh-stats # Pull images (this will use credentials from daemon.json or docker login) docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:latest docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-frontend:latest # If pull fails, verify authentication docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:v1.0.0 ``` ### 3. Start Services ```bash cd ~/edh-stats # Start in background docker-compose up -d # Verify services are running docker-compose ps # Check logs docker-compose logs -f backend docker-compose logs -f frontend ``` ### 4. Verify Deployment ```bash # Check backend health curl http://localhost:3000/api/health # Check frontend curl http://localhost/ # View logs docker-compose logs backend docker-compose logs frontend ``` ## SSL/TLS Configuration (Optional) ### Using Let's Encrypt with Certbot ```bash # Install certbot sudo apt-get install certbot # Generate certificate sudo certbot certonly --standalone -d yourdomain.com # Create SSL volume mapping in docker-compose.yml: # volumes: # - /etc/letsencrypt/live/yourdomain.com:/etc/nginx/certs:ro ``` ### Update nginx.prod.conf ```nginx server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; # ... rest of config } # Redirect HTTP to HTTPS server { listen 80; server_name yourdomain.com; return 301 https://$server_name$request_uri; } ``` ## Database Management ### Backup ```bash # Manual backup docker-compose exec backend cp /app/database/data/edh-stats.db /app/database/data/backup-$(date +%Y%m%d-%H%M%S).db # Or mount backup volume docker run -v edh-stats_sqlite_data:/data -v ~/backups:/backup \ busybox sh -c "cp /data/edh-stats.db /backup/edh-stats-$(date +%Y%m%d-%H%M%S).db" ``` ### Restore ```bash # Stop services docker-compose down # Restore from backup docker run -v edh-stats_sqlite_data:/data -v ~/backups:/backup \ busybox sh -c "cp /backup/edh-stats-YYYYMMDD-HHMMSS.db /data/edh-stats.db" # Start services docker-compose up -d ``` ## Updating to New Version ### 1. Pull New Images ```bash docker-compose pull ``` ### 2. Restart Services (Zero-Downtime Update) ```bash # Update and restart with health checks ensuring availability docker-compose up -d --no-deps --build ``` ### 3. Verify Update ```bash # Check version/logs docker-compose logs -f backend ``` ## Monitoring & Maintenance ### View Logs ```bash # Real-time logs docker-compose logs -f # Backend only docker-compose logs -f backend # Last 100 lines docker-compose logs --tail 100 # Specific time range docker-compose logs --since 2024-01-15T10:00:00Z --until 2024-01-15T11:00:00Z ``` ### Resource Monitoring ```bash # View resource usage docker stats # View service details docker-compose ps docker-compose stats ``` ### Health Checks ```bash # Backend health curl -s http://localhost:3000/api/health | jq . # Frontend connectivity curl -s http://localhost/ | head -20 ``` ## Troubleshooting ### Images Won't Pull / "No Matching Manifest" Error **Error Example:** ``` no matching manifest for linux/amd64 in the manifest list entries ``` This means the Docker image was built for a different CPU architecture than your server. **Common Cause:** - You built the image on Apple Silicon (ARM64) but your server is AMD64 (x86-64) - Or vice versa **Solution: Rebuild with Multi-Architecture Support** The updated `deploy.sh` script now automatically builds for both architectures: ```bash # Simply run deploy.sh again - it now handles multi-arch builds ./deploy.sh v1.0.4 $GHCR_TOKEN # The script will: # - Use Docker buildx to build for linux/amd64 and linux/arm64 # - Push both architectures to GHCR # - Your server can then pull the amd64 version ``` **Manual Fix (if needed):** ```bash # Enable buildx docker buildx create --use --name multiarch-builder # Rebuild backend for both architectures docker buildx build \ --platform linux/amd64,linux/arm64 \ --file ./backend/Dockerfile \ --target production \ --tag ghcr.io/YOUR_USER/edh-stats-backend:v1.0.4 \ --push \ ./backend ``` --- ### Images Won't Pull / "Unauthorized" Error **Error Example:** ``` Error response from daemon: Head "https://ghcr.io/v2/...": unauthorized ``` This usually means Docker isn't authenticated to pull from GHCR. **Solution 1: Verify daemon.json Configuration (Recommended)** ```bash # Check the configuration file cat /etc/docker/daemon.json # Should contain valid base64 credentials for ghcr.io # If missing or malformed, edit it: sudo nano /etc/docker/daemon.json # Then restart Docker sudo systemctl restart docker # Test pull docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:latest ``` **Solution 2: Use Interactive Login** ```bash docker login ghcr.io # Username: YOUR_GITHUB_USERNAME # Password: YOUR_GITHUB_TOKEN (NOT your password!) # Verify login worked docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:latest ``` **Solution 3: Test with a Public Image First** ```bash # If pulling private images fails, test with a public image docker pull nginx:latest # If this works, your Docker daemon is OK # If this fails, restart Docker: sudo systemctl restart docker ``` **Solution 4: Check Token Scope** ```bash # Make sure your GitHub token has read:packages scope # Go to: https://github.com/settings/tokens # Click on the token and verify it has: # - read:packages # - write:packages (for pushing) ``` **Solution 5: For Dockge or Other Services** ```bash # If Dockge or other services can't pull, ensure daemon.json is used # Not ~/.docker/config.json which is user-specific # Check who's running Docker ps aux | grep docker # Verify /etc/docker/daemon.json has correct permissions ls -l /etc/docker/daemon.json # Restart Docker to apply daemon.json changes sudo systemctl restart docker ``` ### Services Won't Start ```bash # Check logs docker-compose logs # Verify secrets exist docker secret ls # Verify configuration docker-compose config # Check ports are available sudo netstat -tulpn | grep LISTEN ``` ### Database Issues / "unable to open database file" **Error:** ``` Failed to initialize database: SqliteError: unable to open database file ``` This occurs when the Docker volume directory doesn't exist or lacks write permissions. **Solution:** ```bash # 1. Stop services docker-compose down # 2. Find the volume path docker volume inspect edh-stats_sqlite_data # Look for the "Mountpoint" value - example: /var/lib/docker/volumes/edh-stats_sqlite_data/_data # 3. Create directories with proper permissions VOLUME_PATH="/var/lib/docker/volumes/edh-stats_sqlite_data/_data" sudo mkdir -p "$VOLUME_PATH" sudo chmod 755 "$VOLUME_PATH" # 4. Do the same for logs volume LOGS_PATH="/var/lib/docker/volumes/edh-stats_app_logs/_data" sudo mkdir -p "$LOGS_PATH" sudo chmod 755 "$LOGS_PATH" # 5. Start services again docker-compose up -d # 6. Check logs docker-compose logs -f backend ``` **Or use the automatic init service:** If you're using the updated docker-compose (with `init-db` service), it will automatically create directories. Just run: ```bash docker-compose up -d docker-compose logs init-db # Watch initialization docker-compose logs -f backend ``` **Verify after fix:** ```bash # Check database file exists docker-compose exec backend ls -lh /app/database/data/ # Check database integrity docker-compose exec backend sqlite3 /app/database/data/edh-stats.db "PRAGMA integrity_check;" ``` ### Performance Issues ```bash # Check resource limits in docker-compose.yml # Backend limits: # memory: 512M # cpus: '0.5' # Monitor actual usage docker stats edh-stats-backend-1 # Increase limits if needed docker update --memory 1G --cpus 1.0 edh-stats-backend-1 ``` ## Security Best Practices 1. **Secrets Management** - Never commit `.env` file to Git (already in .gitignore) - Keep `.env` file secure on your server (chmod 600) - Rotate JWT_SECRET periodically by updating .env and restarting services - Backup `.env` file securely (offsite) 2. **Environment Variables** - Set CORS_ORIGIN to your domain - Keep LOG_LEVEL as 'warn' in production - Set ALLOW_REGISTRATION=false unless needed 3. **Network Security** - Use firewall to restrict access - Enable SSL/TLS for production - Use strong passwords for admin accounts 4. **Database** - Regular backups (daily recommended) - Monitor database size - Archive old game records periodically 5. **Monitoring** - Set up log aggregation - Monitor resource usage - Health checks enabled by default ## Rollback If issues occur after deployment: ```bash # Stop current version docker-compose down # Pull and start previous version docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-backend:v1.0.0 docker pull ghcr.io/YOUR_GITHUB_USER/edh-stats-frontend:v1.0.0 # Update docker-compose.yml to use previous version # Then restart docker-compose up -d ``` ## Support & Issues For deployment issues: 1. Check logs: `docker-compose logs` 2. Verify configuration: `docker-compose config` 3. Test connectivity: `docker-compose exec backend wget -O- http://localhost:3000/api/health` 4. Create GitHub issue with logs and configuration ## Versioning Strategy - **Stable Releases**: `v1.0.0`, `v1.1.0`, etc. - **Release Candidates**: `v1.0.0-rc1`, `v1.0.0-rc2` - **Development**: `main-abcd1234` (branch-commit) Always use tagged versions in production. Avoid using `latest` tag without pinning to specific version.