Files
MyClub/DOCS/PRODUCTION_DEPLOYMENT_GUIDE.md
Tomas Dvorak 087f30e82c dev day #80
2025-11-02 21:31:00 +01:00

13 KiB

Production Deployment Guide

Quick Production Deployment (15 Minutes)

Prerequisites

  • Docker & Docker Compose installed
  • Domain name configured
  • SSL certificate ready (Let's Encrypt recommended)
  • PostgreSQL 14+ database

Step 1: Clone & Configure (5 min)

# Clone repository
git clone <your-repo-url> fotbal-club-production
cd fotbal-club-production

# Copy environment template
cp .env.example .env

# Generate JWT secret (64 characters)
openssl rand -hex 32 > jwt_secret.txt

Edit .env file:

nano .env

Critical settings to change:

# Application
APP_ENV=production
DEBUG=false
PORT=8080

# JWT - CHANGE THIS!
JWT_SECRET=<paste-from-jwt_secret.txt>

# Database
DATABASE_URL=postgres://dbuser:dbpassword@localhost:5432/fotbal_club?sslmode=require

# SMTP - Real email service
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=<your-sendgrid-api-key>
SMTP_FROM=noreply@your-domain.cz
SMTP_FROM_NAME="Your Club Name"

# Migrations
RUN_MIGRATIONS=true
SEED_DATABASE=false

# CORS
ALLOWED_ORIGINS=https://your-domain.cz,https://www.your-domain.cz

Step 2: Database Setup (3 min)

# Start PostgreSQL (if using Docker)
docker-compose up -d db

# Wait for database to be ready
docker-compose exec db pg_isready

# Run migrations
docker-compose run --rm backend ./fotbal-club migrate

# Verify migrations
docker-compose exec db psql -U postgres -d fotbal_club -c "\dt"

Step 3: Build & Deploy (5 min)

# Build frontend
cd frontend
npm install --production
npm run build
cd ..

# Build backend
docker-compose build backend

# Start all services
docker-compose up -d

# Verify services are running
docker-compose ps

# Check logs
docker-compose logs -f backend | head -50

Step 4: Verify Deployment (2 min)

# Health check
curl http://localhost:8080/api/v1/health

# Expected response:
# {"status":"ok","database":"connected"}

# Check metrics
curl http://localhost:8080/metrics | grep "http_requests_total"

# Test authentication
curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"admin123"}'

Nginx Reverse Proxy Configuration

Install Nginx

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

Configure Site

sudo nano /etc/nginx/sites-available/fotbal-club
# Backend API
server {
    listen 80;
    server_name api.your-domain.cz;

    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.your-domain.cz;

    # SSL certificates (Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/api.your-domain.cz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.your-domain.cz/privkey.pem;
    
    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # Security headers (backend already sets these, but good to enforce)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
    limit_req zone=api_limit burst=200 nodelay;
    
    # Proxy settings
    location / {
        proxy_pass http://127.0.0.1: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;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # Uploads - longer timeout
    location ~ ^/(api/v1/upload|api/v1/admin/.*/(upload|image)) {
        client_max_body_size 10M;
        proxy_pass http://127.0.0.1:8080;
        proxy_request_buffering off;
        proxy_read_timeout 300s;
    }
    
    # Static files - long cache
    location ~ ^/(dist|uploads|cache)/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_cache_valid 200 7d;
        add_header Cache-Control "public, max-age=604800, immutable";
    }
    
    # Metrics endpoint - restrict access
    location /metrics {
        allow 127.0.0.1;
        allow <your-monitoring-server-ip>;
        deny all;
        proxy_pass http://127.0.0.1:8080;
    }
    
    # Access/error logs
    access_log /var/log/nginx/fotbal-club-access.log combined;
    error_log /var/log/nginx/fotbal-club-error.log warn;
}

# Frontend (static files)
server {
    listen 80;
    server_name your-domain.cz www.your-domain.cz;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.cz www.your-domain.cz;

    ssl_certificate /etc/letsencrypt/live/your-domain.cz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.cz/privkey.pem;
    
    root /var/www/fotbal-club/frontend/build;
    index index.html;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # React Router (SPA)
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }
    
    # Static assets - long cache
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # Proxy API requests to backend
    location /api {
        proxy_pass http://127.0.0.1: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;
    }
    
    access_log /var/log/nginx/fotbal-club-frontend-access.log combined;
    error_log /var/log/nginx/fotbal-club-frontend-error.log warn;
}

Enable Site & Get SSL

# Enable site
sudo ln -s /etc/nginx/sites-available/fotbal-club /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Get SSL certificate
sudo certbot --nginx -d your-domain.cz -d www.your-domain.cz -d api.your-domain.cz

# Reload Nginx
sudo systemctl reload nginx

# Auto-renewal
sudo certbot renew --dry-run

Database Backup Setup

Automated Daily Backups

# Create backup script
sudo nano /usr/local/bin/backup-fotbal-db.sh
#!/bin/bash
set -e

# Configuration
DB_NAME="fotbal_club"
DB_USER="postgres"
BACKUP_DIR="/var/backups/fotbal-club"
RETENTION_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/fotbal_club_$DATE.dump"

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup database
pg_dump -U $DB_USER -Fc $DB_NAME > $BACKUP_FILE

# Compress
gzip $BACKUP_FILE

# Delete old backups
find $BACKUP_DIR -name "*.dump.gz" -mtime +$RETENTION_DAYS -delete

# Upload to S3 (optional)
# aws s3 cp $BACKUP_FILE.gz s3://your-bucket/backups/

echo "Backup completed: $BACKUP_FILE.gz"
# Make executable
sudo chmod +x /usr/local/bin/backup-fotbal-db.sh

# Add to crontab (daily at 2 AM)
sudo crontab -e

Add line:

0 2 * * * /usr/local/bin/backup-fotbal-db.sh >> /var/log/fotbal-backup.log 2>&1

Monitoring Setup

Prometheus Configuration

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'fotbal-club'
    static_configs:
      - targets: ['localhost:8080']
    metrics_path: '/metrics'
    basic_auth:
      username: 'admin'
      password: '<secure-password>'

Grafana Dashboard Import

Use dashboard ID: 6417 (Gin metrics)
Modify for custom metrics


Security Hardening Checklist

Server Level

# Update system
sudo apt update && sudo apt upgrade -y

# Enable firewall
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# Fail2ban for SSH
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Disable root SSH login
sudo nano /etc/ssh/sshd_config
# Set: PermitRootLogin no
sudo systemctl restart sshd

Application Level

# Set file permissions
sudo chown -R app:app /app/uploads
sudo chmod 755 /app/uploads
sudo chmod 644 /app/uploads/*

# Secure environment files
chmod 600 .env
chown root:root .env

# Rotate logs
sudo nano /etc/logrotate.d/fotbal-club
/var/log/nginx/fotbal-club-*.log {
    daily
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

Performance Tuning

PostgreSQL Optimization

# Edit postgresql.conf
sudo nano /etc/postgresql/14/main/postgresql.conf
# Memory settings (for 4GB RAM server)
shared_buffers = 1GB
effective_cache_size = 3GB
maintenance_work_mem = 256MB
work_mem = 32MB

# Connections
max_connections = 200

# Checkpoints
checkpoint_completion_target = 0.9
wal_buffers = 16MB

# Query planner
random_page_cost = 1.1  # For SSD
effective_io_concurrency = 200

# Logging
log_min_duration_statement = 1000  # Log slow queries (1s+)

Docker Resource Limits

# docker-compose.yml
services:
  backend:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M
    restart: unless-stopped
    
  db:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G
    restart: unless-stopped

Maintenance Scripts

Health Check Script

#!/bin/bash
# /usr/local/bin/health-check.sh

URL="https://your-domain.cz/api/v1/health"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $URL)

if [ $RESPONSE -ne 200 ]; then
    echo "Health check failed! HTTP $RESPONSE"
    # Send alert
    curl -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" \
         -d "chat_id=<CHAT_ID>" \
         -d "text=⚠️ Fotbal Club Health Check Failed!"
    exit 1
fi

echo "Health check OK"

Database Maintenance

#!/bin/bash
# Weekly database maintenance

# Vacuum and analyze
psql -U postgres -d fotbal_club -c "VACUUM ANALYZE;"

# Reindex
psql -U postgres -d fotbal_club -c "REINDEX DATABASE fotbal_club;"

# Check table sizes
psql -U postgres -d fotbal_club -c "
SELECT 
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables 
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;
"

Troubleshooting

Service Won't Start

# Check logs
docker-compose logs backend --tail=100

# Common issues:
# 1. Port already in use
sudo lsof -i :8080
# Kill process if needed

# 2. Database connection failed
docker-compose exec db pg_isready

# 3. Permission denied
sudo chown -R app:app /app

High Memory Usage

# Check container stats
docker stats

# Restart services if needed
docker-compose restart backend

# Check for memory leaks
docker-compose exec backend ps aux --sort=-%mem | head

Slow Queries

# Enable query logging
psql -U postgres -d fotbal_club -c "
ALTER DATABASE fotbal_club SET log_min_duration_statement = 100;
"

# View slow queries
sudo tail -f /var/log/postgresql/postgresql-14-main.log | grep "duration:"

Rollback Procedure

Quick Rollback

# Stop current version
docker-compose down

# Checkout previous version
git checkout <previous-commit-hash>

# Rollback database migrations (if needed)
docker-compose run backend ./fotbal-club migrate down

# Restart with old version
docker-compose up -d

# Verify
curl http://localhost:8080/api/v1/health

Support & Contact

Log Locations

  • Backend: docker-compose logs backend
  • Database: /var/log/postgresql/
  • Nginx: /var/log/nginx/fotbal-club-*.log
  • System: /var/log/syslog

Useful Commands

# View real-time logs
docker-compose logs -f backend

# Check resource usage
docker stats

# Database console
docker-compose exec db psql -U postgres fotbal_club

# Restart specific service
docker-compose restart backend

# Clean up old images
docker system prune -a

Success Criteria

After deployment, verify:

  • Health endpoint returns 200
  • Homepage loads in < 2 seconds
  • Login works
  • Articles display correctly
  • File uploads work
  • Email sends successfully
  • SSL certificate valid
  • Metrics endpoint accessible
  • Database backups running
  • Logs are being written

Status: READY FOR PRODUCTION