Files
edh-stats/deploy.sh
Michael Skrynski 4986c4f6b8 feat: add complete password requirements to registration form
- Display all 5 backend password validation requirements
- Added: minimum 8 characters (existing)
- Added: maximum 100 characters
- Added: at least one lowercase letter
- Added: at least one uppercase letter
- Added: at least one number
- Each requirement shows real-time validation with green checkmark
- Requirements match exactly with backend/src/utils/validators.js

feat: add Traefik labels to frontend container

- Enable Traefik auto-discovery for frontend service
- Configure HTTP routing with Host rule
- Setup both web (HTTP) and websecure (HTTPS) entrypoints
- Configure load balancer backend port (80)
- Include optional TLS/Let's Encrypt configuration
- Change edh.example.com to your actual domain

feat: configure production compose file to use Traefik network

- Generated docker-compose.prod.deployed.yml now uses traefik-network
- Frontend service joined to both edh-stats-network and traefik-network
- Added Traefik labels for routing and TLS configuration
- traefik-network configured as external (must be created by Traefik)
- Removed external ports from frontend (Traefik handles routing)
- Updated setup instructions to mention Traefik prerequisites
- Changed TLS to enabled by default in production

refactor: remove frontend port exposure and Traefik labels from dev compose

- Removed port 8081 exposure from development docker-compose.yml
- Removed Traefik labels from development environment
- Development compose now simple and focused on local testing
- Production compose (via deploy.sh) handles Traefik routing via DNS
- Frontend only accessible through backend API in development
- Cleaner separation: dev is simple, prod uses Traefik

Switch Postgres data to host path and bump version Update Traefik rule,
expose port, bump version

feat: add database migration script for PostgreSQL

- Created migrate-database.sh script for exporting/importing databases
- Supports source and target database configuration via CLI options
- Validates connections before migration
- Verifies data integrity after import (table and row counts)
- Can skip import and just export to file for manual restore
- Supports Docker Compose container names
- Includes comprehensive error handling and user prompts
- Added DATABASE_MIGRATION.md documentation with usage examples
- Handles common scenarios: dev→prod, backups, restore
- Security considerations for password handling

refactor: simplify migration script to run directly in PostgreSQL container

- Removed network/remote host configuration (no longer needed)
- Script now runs inside PostgreSQL container with docker compose exec
- Simplified to use only source-db, target-db, output-file, skip-import options
- No external dependencies - uses container's pg_dump and psql
- Much simpler usage: docker compose exec postgres /scripts/migrate-database.sh
- Updated documentation with container-based examples
- Added real-world integration examples (daily backups, deployments, recovery)
- Includes troubleshooting and access patterns for backup files

feat: mount scripts directory into PostgreSQL container

- Added ./scripts:/scripts:ro volume mount to postgres service
- Makes migrate-database.sh and other scripts accessible inside container
- Read-only mount for security (scripts can't be modified inside container)
- Allows running: docker compose exec postgres /scripts/migrate-database.sh
- Scripts are shared between host and container for easy access

docs: clarify that migration script must run as postgres user

- Added -u postgres flag to all docker compose exec commands
- Explains why postgres user is required (PostgreSQL role authentication)
- Created shell alias for convenience
- Updated all scenarios and examples
- Updated troubleshooting section
- Clarifies connection issues related to user authentication

feat: save database backups to host by default

- Added ./backups volume mount to postgres container
- Changed default backup location from /tmp to /backups
- /backups is mounted to ./backups on host for easy access
- Script automatically creates /backups directory if needed
- Updated help and examples with -u postgres flag
- Summary now shows both container and host backup paths
- Backups are immediately available on host machine
- No need for docker cp to retrieve backups

feat: add --skip-export flag for import-only database operations

- Allows importing from existing backup files without re-exporting
- Added validate_backup_file() function to check backup existence
- Updated main() to handle import-only mode with proper validation
- Updated summary output to show import-only mode details
- Updated help text with import-only example
- Prevents using both --skip-import and --skip-export together

docs: update database migration guide for import-only mode

- Document new --skip-export flag for import-only operations
- Add example for quick restore from backup without re-export
- Update command options table with mode combinations
- Update all scenarios and examples to use /backups mount
- Clarify file location and volume mounting (./backups on host)
- Add Scenario 5 for quick restore from backup
- Simplify examples and fix container paths

feat: clear existing data before importing in migration script

- Added clear_database() function to drop all tables, views, and sequences
- Drops and recreates public schema with proper permissions
- Ensures clean import without data conflicts
- Updated warning message to clarify data deletion
- clear_database() called before import starts
- Maintains database integrity and grants

docs: update migration guide to explain data clearing on import

- Clarify that existing data is deleted before import
- Explain the drop/recreate schema process
- Add notes to scenarios about data clearing
- Document the import process sequence
- Update version to 2.2

fix: remove verbose flag from pg_dump to prevent SQL syntax errors

- Removed -v flag from pg_dump export command
- Verbose output was being included in SQL file as comments
- These comments caused 'syntax error at or near pg_dump' errors during import
- Backup files will now be clean SQL without pg_dump metadata comments

docs: document pg_dump verbose output fix and troubleshooting

- Added troubleshooting section for pg_dump syntax errors
- Noted that v2.3 fixes this issue
- Directed users to create new backups if needed
- Updated version to 2.3
- Clarified file location is /backups/

docs: add critical warning about using migration script for imports

- Added prominent warning against using psql directly with -f flag
- Explained why direct psql causes 'relation already exists' errors
- Added troubleshooting section for these errors
- Emphasized that script handles data clearing automatically
- Clear examples of wrong vs right approach

fix: remove pg_dump restrict commands that block data import

- Added clean_backup_file() function to remove \restrict and \unrestrict
- pg_dump adds these security commands which prevent data loading
- Script now automatically cleans backup files before importing
- Removes lines starting with \restrict or \unrestrict
- Ensures all data (users, games, commanders) imports successfully
- Called automatically during import process

docs: add troubleshooting for pg_dump restrict commands blocking imports

- Document the \restrict and \unrestrict security commands issue
- Explain why they block data from being imported
- Show that migration script v2.4+ removes them automatically
- Update version to 2.4
- Add detailed troubleshooting section for empty imports
2026-01-18 13:09:53 +01:00

517 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
##############################################################################
# EDH Stats Tracker - Production Deployment Script
#
# This script builds Docker images and pushes them to GitHub Container Registry
# Usage: ./deploy.sh [VERSION] [GHCR_TOKEN]
#
# Example: ./deploy.sh 1.0.0 ghcr_xxxxxxxxxxxxx
#
# Prerequisites:
# - Docker and Docker Compose installed
# - GitHub Personal Access Token (with write:packages permission)
# - Set GITHUB_REGISTRY_USER environment variable or pass as argument
##############################################################################
set -e # Exit on any error
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
REGISTRY="ghcr.io"
GITHUB_USER="${GITHUB_USER:=$(git config --get user.name | tr ' ' '-' | tr '[:upper:]' '[:lower:]')}"
PROJECT_NAME="edh-stats"
VERSION="${1:-latest}"
GHCR_TOKEN="${2}"
# Image names
BACKEND_IMAGE="${REGISTRY}/${GITHUB_USER}/${PROJECT_NAME}-backend:${VERSION}"
FRONTEND_IMAGE="${REGISTRY}/${GITHUB_USER}/${PROJECT_NAME}-frontend:${VERSION}"
BACKEND_IMAGE_LATEST="${REGISTRY}/${GITHUB_USER}/${PROJECT_NAME}-backend:latest"
FRONTEND_IMAGE_LATEST="${REGISTRY}/${GITHUB_USER}/${PROJECT_NAME}-frontend:latest"
##############################################################################
# Helper Functions
##############################################################################
print_header() {
echo -e "\n${BLUE}════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} $1${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}\n"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
##############################################################################
# Validation
##############################################################################
validate_prerequisites() {
print_header "Validating Prerequisites"
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed. Please install Docker first."
exit 1
fi
print_success "Docker is installed"
# Check if Docker daemon is running
if ! docker info > /dev/null 2>&1; then
print_error "Docker daemon is not running. Please start Docker."
exit 1
fi
print_success "Docker daemon is running"
# Check if Docker buildx is available
if ! docker buildx version > /dev/null 2>&1; then
print_warning "Docker buildx not found. Creating builder..."
docker buildx create --use --name multiarch-builder > /dev/null 2>&1 || true
if ! docker buildx version > /dev/null 2>&1; then
print_error "Docker buildx is required for multi-architecture builds."
print_error "Please ensure you have Docker with buildx support."
exit 1
fi
print_success "Docker buildx enabled"
else
print_success "Docker buildx is available"
fi
# Check if Git is installed
if ! command -v git &> /dev/null; then
print_error "Git is not installed. Please install Git first."
exit 1
fi
print_success "Git is installed"
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not in a Git repository. Please run this script from the project root."
exit 1
fi
print_success "Running from Git repository"
}
check_github_token() {
if [ -z "$GHCR_TOKEN" ]; then
print_warning "GitHub token not provided. You'll be prompted for credentials when pushing."
print_info "Set GHCR_TOKEN environment variable or pass as second argument to skip this prompt"
read -sp "Enter GitHub Container Registry Token (or press Enter to use 'docker login'): " GHCR_TOKEN
echo
if [ -z "$GHCR_TOKEN" ]; then
print_info "Attempting to use existing Docker credentials..."
if ! docker info | grep -q "Username"; then
print_warning "No Docker credentials found. Running 'docker login'..."
docker login "${REGISTRY}"
fi
fi
fi
}
update_version_file() {
print_header "Updating Version File"
local version_file="./frontend/public/version.txt"
local current_version=""
# Check if version file exists
if [ -f "$version_file" ]; then
current_version=$(cat "$version_file")
print_info "Current version: $current_version"
fi
# Update version file with new version (strip 'v' prefix if present)
local new_version="${VERSION#v}"
echo "$new_version" > "$version_file"
print_success "Updated version file to: $new_version"
}
##############################################################################
# Build Functions
##############################################################################
build_backend() {
print_header "Building Backend Image"
print_info "Building: ${BACKEND_IMAGE}"
print_info "Building for architectures: linux/amd64"
docker buildx build \
--platform linux/amd64 \
--file ./backend/Dockerfile \
--target production \
--tag "${BACKEND_IMAGE}" \
--tag "${BACKEND_IMAGE_LATEST}" \
--build-arg NODE_ENV=production \
--push \
./backend
print_success "Backend image built and pushed successfully"
}
build_frontend() {
print_header "Building Frontend Image"
print_info "Building: ${FRONTEND_IMAGE}"
print_info "Building for architectures: linux/amd64"
# Note: Dockerfile.prod is now a permanent file in the repository
# It uses a multi-stage build to compile Tailwind CSS in production
docker buildx build \
--platform linux/amd64 \
--file ./frontend/Dockerfile.prod \
--tag "${FRONTEND_IMAGE}" \
--tag "${FRONTEND_IMAGE_LATEST}" \
--push \
.
print_success "Frontend image built and pushed successfully"
}
##############################################################################
# Push Functions
##############################################################################
login_to_registry() {
print_header "Authenticating with GitHub Container Registry"
if [ -n "$GHCR_TOKEN" ]; then
print_info "Logging in with provided token..."
echo "$GHCR_TOKEN" | docker login "${REGISTRY}" -u "${GITHUB_USER}" --password-stdin > /dev/null 2>&1
else
print_info "Using existing Docker authentication..."
fi
print_success "Successfully authenticated with registry"
}
push_backend() {
print_header "Pushing Backend Image"
# Images are already pushed during buildx build step
print_success "Backend image already pushed: ${BACKEND_IMAGE}"
print_success "Latest backend image pushed: ${BACKEND_IMAGE_LATEST}"
}
push_frontend() {
print_header "Pushing Frontend Image"
# Images are already pushed during buildx build step
print_success "Frontend image already pushed: ${FRONTEND_IMAGE}"
print_success "Latest frontend image pushed: ${FRONTEND_IMAGE_LATEST}"
}
##############################################################################
# Verification Functions
##############################################################################
verify_images() {
print_header "Verifying Built Images"
print_info "Note: Using buildx for optimized builds"
print_info "Images are built for linux/amd64"
print_info "Images are pushed directly to registry (not stored locally)"
print_success "Backend image built and pushed: ${BACKEND_IMAGE}"
print_success "Frontend image built and pushed: ${FRONTEND_IMAGE}"
}
##############################################################################
# Generate Configuration
##############################################################################
generate_deployment_config() {
print_header "Generating Deployment Configuration"
local config_file="docker-compose.prod.deployed.yml"
print_info "Creating deployment configuration: ${config_file}"
cat > "${config_file}" << EOF
# Generated production deployment configuration
# Version: ${VERSION}
# Generated: $(date -u +'%Y-%m-%dT%H:%M:%SZ')
# GitHub User: ${GITHUB_USER}
#
# IMPORTANT: Prerequisites
# - Traefik must be running with 'traefik-network' created
# - Create a .env file with these variables:
# DB_NAME=edh_stats
# DB_USER=postgres
# DB_PASSWORD=\$(openssl rand -base64 32)
# JWT_SECRET=\$(openssl rand -base64 32)
# CORS_ORIGIN=https://yourdomain.com
# LOG_LEVEL=warn
# ALLOW_REGISTRATION=false
# DB_SEED=false
#
# FIRST TIME SETUP:
# 1. Ensure Traefik is running and traefik-network exists
# 2. Update frontend domain in labels (edh.example.com -> yourdomain.com)
# 3. Create .env file with above variables
# 4. Run: docker-compose -f docker-compose.prod.deployed.yml up -d
# 5. Database migrations will run automatically via db-migrate service
# 6. Monitor logs: docker-compose logs -f db-migrate
services:
# PostgreSQL database service
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=\${DB_USER:-postgres}
- POSTGRES_PASSWORD=\${DB_PASSWORD}
- POSTGRES_DB=\${DB_NAME}
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'PGPASSWORD=\${DB_PASSWORD} pg_isready -U postgres -h localhost']
interval: 10s
timeout: 5s
retries: 5
networks:
- edh-stats-network
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
# Database migration service - runs once on startup
db-migrate:
image: ${BACKEND_IMAGE}
depends_on:
postgres:
condition: service_healthy
environment:
- NODE_ENV=production
- DB_HOST=\${DB_HOST:-postgres}
- DB_NAME=\${DB_NAME}
- DB_USER=\${DB_USER:-postgres}
- DB_PASSWORD=\${DB_PASSWORD}
command: node src/database/migrate.js migrate
networks:
- edh-stats-network
restart: 'no'
backend:
image: ${BACKEND_IMAGE}
ports:
- '3002:3000'
depends_on:
db-migrate:
condition: service_completed_successfully
environment:
- NODE_ENV=production
- DB_HOST=\${DB_HOST:-postgres}
- DB_NAME=\${DB_NAME}
- DB_USER=\${DB_USER:-postgres}
- DB_PASSWORD=\${DB_PASSWORD}
- JWT_SECRET=\${JWT_SECRET}
- CORS_ORIGIN=\${CORS_ORIGIN:-https://yourdomain.com}
- LOG_LEVEL=\${LOG_LEVEL:-warn}
- ALLOW_REGISTRATION=\${ALLOW_REGISTRATION:-false}
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000/api/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- edh-stats-network
stop_grace_period: 30s
frontend:
image: ${FRONTEND_IMAGE}
restart: unless-stopped
healthcheck:
test:
- CMD
- curl
- http://localhost:80/health
interval: 10s
timeout: 5s
retries: 5
networks:
- edh-stats-network
- traefik-network
depends_on:
- backend
labels:
# Enable Traefik discovery for this service
- traefik.enable=true
# Routing rule - change edh.example.com to your domain
- traefik.http.routers.edh-stats-frontend.rule=Host(\`edh.zlor.fi\`)
# Entry points: web (HTTP) and websecure (HTTPS)
- traefik.http.routers.edh-stats-frontend.entrypoints=web,websecure
# Service configuration
- traefik.http.routers.edh-stats-frontend.service=edh-stats-frontend
# Backend port (nginx internal port)
- traefik.http.services.edh-stats-frontend.loadbalancer.server.port=80
# Enable TLS with Let's Encrypt
- traefik.http.routers.edh-stats-frontend.tls=true
- traefik.http.routers.edh-stats-frontend.tls.certresolver=letsencrypt
deploy:
resources:
limits:
memory: 256M
cpus: '0.25'
reservations:
memory: 128M
cpus: '0.125'
volumes:
postgres_data:
driver: local
networks:
edh-stats-network:
driver: bridge
traefik-network:
external: true
name: traefik-network
x-dockge:
urls:
- https://edh.zlor.fi
EOF
print_success "Deployment configuration generated: ${config_file}"
}
##############################################################################
# Cleanup
##############################################################################
cleanup_temp_files() {
print_header "Cleaning Up Temporary Files"
# Note: Dockerfile.prod is now a permanent file in the repository
# and should not be deleted after the build completes
print_info "No temporary files to clean up"
}
##############################################################################
# Summary
##############################################################################
print_summary() {
print_header "Deployment Summary"
echo "Backend Image: ${BACKEND_IMAGE}"
echo "Frontend Image: ${FRONTEND_IMAGE}"
echo ""
echo "Latest Tags:"
echo " Backend: ${BACKEND_IMAGE_LATEST}"
echo " Frontend: ${FRONTEND_IMAGE_LATEST}"
echo ""
echo "Registry: ${REGISTRY}"
echo "GitHub User: ${GITHUB_USER}"
echo "Version: ${VERSION}"
echo ""
echo "Version Management:"
echo " Frontend version file updated: ./frontend/public/version.txt"
echo " Version displayed in footer: v${VERSION#v}"
echo ""
echo "Next Steps:"
echo " 1. Commit version update:"
echo " git add frontend/public/version.txt"
echo " git commit -m \"Bump version to ${VERSION#v}\""
echo " 2. Pull images: docker pull ${BACKEND_IMAGE}"
echo " 3. Create .env file with PostgreSQL credentials:"
echo " DB_PASSWORD=\$(openssl rand -base64 32)"
echo " JWT_SECRET=\$(openssl rand -base64 32)"
echo " 4. Set production secrets:"
echo " - CORS_ORIGIN=https://yourdomain.com"
echo " - ALLOW_REGISTRATION=false"
echo " 5. Deploy: docker-compose -f docker-compose.prod.deployed.yml up -d"
echo " 6. Monitor migrations: docker-compose logs -f db-migrate"
echo ""
}
##############################################################################
# Main Execution
##############################################################################
main() {
print_header "EDH Stats Tracker - Production Deployment"
print_info "Starting deployment process..."
print_info "Version: ${VERSION}"
print_info "GitHub User: ${GITHUB_USER}"
print_info "Registry: ${REGISTRY}"
echo ""
# Validation
validate_prerequisites
# Check token
check_github_token
# Update version file
update_version_file
# Build images
build_backend
build_frontend
# Verify images
verify_images
# Authenticate
login_to_registry
# Push images
push_backend
push_frontend
# Generate config
generate_deployment_config
# Cleanup
cleanup_temp_files
# Summary
print_summary
print_success "Deployment completed successfully!"
}
# Run main function
main