14 KiB
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:packagespermission - Domain name (for CORS_ORIGIN configuration)
- SSL certificates (optional, for HTTPS)
Quick Start
Option 1: Automatic Deployment Script (Local)
-
Generate GitHub Token
- Go to GitHub → Settings → Developer settings → Personal access tokens
- Create a new token with
write:packagesscope - Copy the token
-
Run Deployment Script
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.0What the script does:
- Validates Docker and Docker buildx prerequisites
- Builds images for both
linux/amd64(AMD64 servers) andlinux/arm64(Apple Silicon) - Pushes to GHCR automatically (no separate push step needed)
- Generates deployment configuration
-
Review Generated Configuration
- Check
docker-compose.prod.deployed.yml - Verify image tags and versions
- Check
Option 2: Automated CI/CD (GitHub Actions)
-
Push Release Tag
git tag v1.0.0 git push origin v1.0.0 -
GitHub Actions Automatically:
- Builds Docker images
- Pushes to GHCR
- Generates deployment config
- Creates release with artifacts
-
Download Deployment Config
- Go to GitHub Releases
- Download
docker-compose.prod.deployed.yml
Server Setup
1. Install Docker & Docker Compose
# 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
mkdir -p ~/edh-stats/data/database
mkdir -p ~/edh-stats/data/logs
cd ~/edh-stats
3. Copy Deployment Configuration
# 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
# 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:
# 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
# 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
sudo nano /etc/docker/daemon.json
Add or update the auths section. The full file should look like:
{
"auths": {
"ghcr.io": {
"auth": "YOUR_BASE64_CREDENTIALS_HERE"
}
}
}
Step 3: Restart Docker
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.
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
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
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
# 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
# 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
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
# 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
# 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
docker-compose pull
2. Restart Services (Zero-Downtime Update)
# Update and restart with health checks ensuring availability
docker-compose up -d --no-deps --build
3. Verify Update
# Check version/logs
docker-compose logs -f backend
Monitoring & Maintenance
View Logs
# 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
# View resource usage
docker stats
# View service details
docker-compose ps
docker-compose stats
Health Checks
# 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:
# 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):
# 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)
# 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
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
# 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
# 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
# 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
# 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:
# 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:
docker-compose up -d
docker-compose logs init-db # Watch initialization
docker-compose logs -f backend
Verify after fix:
# 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
# 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
-
Secrets Management
- Never commit
.envfile to Git (already in .gitignore) - Keep
.envfile secure on your server (chmod 600) - Rotate JWT_SECRET periodically by updating .env and restarting services
- Backup
.envfile securely (offsite)
- Never commit
-
Environment Variables
- Set CORS_ORIGIN to your domain
- Keep LOG_LEVEL as 'warn' in production
- Set ALLOW_REGISTRATION=false unless needed
-
Network Security
- Use firewall to restrict access
- Enable SSL/TLS for production
- Use strong passwords for admin accounts
-
Database
- Regular backups (daily recommended)
- Monitor database size
- Archive old game records periodically
-
Monitoring
- Set up log aggregation
- Monitor resource usage
- Health checks enabled by default
Rollback
If issues occur after deployment:
# 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:
- Check logs:
docker-compose logs - Verify configuration:
docker-compose config - Test connectivity:
docker-compose exec backend wget -O- http://localhost:3000/api/health - 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.