# Deployment Guide **Note**: This is my personal CV website. While the code is open-source (MIT license), this deployment guide is primarily for my own use. The site may be modified without notice. Complete guide for deploying the CV/Resume application to various platforms and environments. ## Table of Contents - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Deployment Methods](#deployment-methods) - [VPS Deployment](#vps-deployment) - [Cloud Platforms](#cloud-platforms) - [Manual Deployment](#manual-deployment) - [Configuration](#configuration) - [Post-Deployment](#post-deployment) - [Troubleshooting](#troubleshooting) - [Updates & Maintenance](#updates--maintenance) --- ## Introduction This guide covers deploying the CV/Resume web application built with Go 1.25.1, HTMX, and chromedp for PDF generation. The application supports bilingual content (English/Spanish) and requires Chromium for PDF export functionality. **What this guide covers:** - VPS deployment with systemd - Cloud platform deployment (Fly.io, Google Cloud Run, AWS) - Manual deployment - Configuration for different environments - Security hardening and production best practices - Monitoring, logging, and health checks - Zero-downtime updates and rollback procedures --- ## Prerequisites ### For All Deployment Methods - **Go 1.25.1+** (if building from source) - **Chromium/Chrome** (for PDF generation via chromedp) - **Git** (to clone repository) - Basic command-line knowledge ### For Specific Methods - **Cloud Platforms**: Account on chosen platform (Fly.io, GCP, AWS, etc.) - **VPS**: SSH access, sudo privileges, domain name (optional) - **Production**: SSL certificate (Let's Encrypt recommended) ### System Requirements - **CPU**: 1 core minimum (2+ recommended) - **RAM**: 512MB minimum (1GB+ recommended for PDF generation) - **Disk**: 200MB for application + dependencies - **Network**: Port 1999 (default) or your chosen port --- ## Quick Start The fastest way to get started: ```bash # Clone repository git clone https://github.com/juanatsap/cv-site.git cd cv-site # Copy environment configuration cp .env.example .env # Build and run make build GO_ENV=production ./cv-server # Verify deployment curl http://localhost:1999/health ``` Access the application at **http://localhost:1999** --- ## Deployment Methods ### VPS Deployment Traditional server deployment with systemd service management. #### 1. Systemd Service Setup **Prerequisites**: ```bash # Install Go 1.25.1 wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc source ~/.bashrc # Install Chromium sudo apt update sudo apt install -y chromium-browser curl git # Verify installations go version chromium-browser --version ``` **Deploy application**: ```bash # Clone repository cd /home/txeo/Git/yo git clone https://github.com/juanatsap/cv-site.git cv cd cv # Build production binary make build # Test binary ./cv-server --version ./cv-server # Test locally, press Ctrl+C to stop ``` **Install systemd service**: The project includes `config/systemd/cv.service`. Review and customize: ```bash # Edit service file nano config/systemd/cv.service ``` Ensure paths match your setup: ```ini [Unit] Description=CV Website Service After=network.target Wants=network-online.target [Service] Type=simple User=txeo Group=txeo WorkingDirectory=/home/txeo/Git/yo/cv ExecStart=/home/txeo/Git/yo/cv/cv-server Environment="GO_ENV=production" Environment="PORT=1999" Restart=always RestartSec=5 StartLimitInterval=60 StartLimitBurst=3 StandardOutput=append:/var/log/cv.log StandardError=append:/var/log/cv.log SyslogIdentifier=cv [Install] WantedBy=multi-user.target ``` **Install and start**: ```bash # Create log file sudo touch /var/log/cv.log sudo chown txeo:txeo /var/log/cv.log # Install service sudo cp config/systemd/cv.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable cv sudo systemctl start cv # Check status sudo systemctl status cv # View logs sudo journalctl -u cv -f tail -f /var/log/cv.log ``` **Manage service**: ```bash # Stop sudo systemctl stop cv # Restart sudo systemctl restart cv # Reload after config changes sudo systemctl daemon-reload sudo systemctl restart cv # View logs sudo journalctl -u cv --since "1 hour ago" ``` #### 2. Nginx Reverse Proxy Configuration **Install Nginx**: ```bash sudo apt install -y nginx ``` **Create Nginx configuration** (`/etc/nginx/sites-available/cv`): ```nginx # Rate limiting zone limit_req_zone $binary_remote_addr zone=cv_limit:10m rate=10r/s; # Upstream backend upstream cv_backend { server 127.0.0.1:1999 max_fails=3 fail_timeout=30s; } # HTTP → HTTPS redirect server { listen 80; listen [::]:80; server_name your-domain.com; # ACME challenge for Let's Encrypt location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$server_name$request_uri; } } # HTTPS server server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name your-domain.com; # SSL configuration ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Gzip compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # Static files caching location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { proxy_pass http://cv_backend; expires 1y; add_header Cache-Control "public, immutable"; } # Main proxy location / { # Rate limiting limit_req zone=cv_limit burst=20 nodelay; proxy_pass http://cv_backend; proxy_http_version 1.1; # Headers 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; # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; # Buffering proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; proxy_busy_buffers_size 8k; } # Health check endpoint (no rate limit) location /health { proxy_pass http://cv_backend; access_log off; } # Access and error logs access_log /var/log/nginx/cv-access.log; error_log /var/log/nginx/cv-error.log; } ``` **Enable configuration**: ```bash # Enable site sudo ln -s /etc/nginx/sites-available/cv /etc/nginx/sites-enabled/ # Test configuration sudo nginx -t # Reload Nginx sudo systemctl reload nginx ``` #### 3. SSL/TLS with Let's Encrypt ```bash # Install Certbot sudo apt install -y certbot python3-certbot-nginx # Obtain certificate (Nginx plugin) sudo certbot --nginx -d your-domain.com # Or manual with webroot sudo certbot certonly --webroot -w /var/www/certbot -d your-domain.com # Test auto-renewal sudo certbot renew --dry-run # Auto-renewal is configured via systemd timer sudo systemctl status certbot.timer ``` **Certificate renewal cron** (if not using systemd timer): ```bash # Add to crontab sudo crontab -e # Add line: 0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx" ``` #### 4. Process Management **Makefile targets** (already included): ```bash # Install as service make install-service # Update service make update-service ``` **Manual management**: ```bash # Check if running ps aux | grep cv-server # Kill process pkill cv-server # Start with nohup (not recommended, use systemd) nohup ./cv-server > /var/log/cv.log 2>&1 & ``` --- ### Cloud Platforms #### Fly.io (Recommended for Quick Cloud Deployment) **Prerequisites**: Install [flyctl](https://fly.io/docs/hands-on/install-flyctl/) **Note**: This requires building from source on Fly.io's infrastructure. ```bash # Login fly auth login # Initialize app fly launch --no-deploy # Configure fly.toml cat > fly.toml << 'EOF' app = "your-cv-app-name" primary_region = "mad" # Madrid, Spain (choose your region) [build] [build.args] GO_VERSION = "1.25.1" [env] PORT = "8080" # Fly.io uses 8080 internally GO_ENV = "production" TEMPLATE_HOT_RELOAD = "false" [http_service] internal_port = 8080 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 1 processes = ["app"] [[vm]] cpu_kind = "shared" cpus = 1 memory_mb = 512 [[services.http_checks]] interval = 30000 grace_period = "10s" method = "get" path = "/health" protocol = "http" timeout = 5000 EOF # Deploy fly deploy # Open in browser fly open # View logs fly logs # Scale fly scale count 2 # SSH into VM fly ssh console ``` #### Google Cloud Run ```bash # Create a simple Dockerfile for Cloud Run cat > Dockerfile.cloudrun << 'EOF' FROM golang:1.25-alpine AS builder RUN apk add --no-cache git WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o cv-server . FROM alpine:latest RUN apk --no-cache add chromium ca-certificates COPY --from=builder /app/cv-server /cv-server COPY data /data COPY templates /templates COPY static /static EXPOSE 8080 CMD ["/cv-server"] EOF # Build and push to Google Container Registry gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/cv-server # Deploy to Cloud Run gcloud run deploy cv-server \ --image gcr.io/YOUR_PROJECT_ID/cv-server \ --platform managed \ --region europe-west1 \ --allow-unauthenticated \ --port 8080 \ --memory 512Mi \ --cpu 1 \ --min-instances 0 \ --max-instances 10 \ --set-env-vars GO_ENV=production,TEMPLATE_HOT_RELOAD=false,PORT=8080 # Get service URL gcloud run services describe cv-server --region europe-west1 --format 'value(status.url)' # View logs gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=cv-server" --limit 50 --format json ``` #### AWS (EC2 or Lightsail) For AWS deployment without containers, deploy directly to EC2 or Lightsail: ```bash # SSH into your EC2/Lightsail instance ssh -i your-key.pem ubuntu@your-instance-ip # Follow VPS deployment steps above # Install Go, Chromium, clone repo, build, and set up systemd ``` #### Railway / Render These platforms can build from source using their build systems. **Railway**: 1. Connect GitHub repository 2. Railway auto-detects Go project 3. Set environment variables in dashboard: - `PORT` = 8080 - `GO_ENV` = production 4. Deploy automatically on git push **Render** (create `render.yaml`): ```yaml services: - type: web name: cv-server env: go buildCommand: go build -o cv-server startCommand: ./cv-server envVars: - key: GO_ENV value: production - key: PORT value: 10000 healthCheckPath: /health ``` --- ### Manual Deployment Build and run without services. #### 1. Build from Source ```bash # Clone repository git clone https://github.com/juanatsap/cv-site.git cd cv-site # Install dependencies go mod download go mod verify # Build production binary make build # Or manual build with optimizations CGO_ENABLED=0 go build -ldflags="-s -w" -o cv-server . # Verify binary ./cv-server --version file cv-server # Should show "statically linked" ``` #### 2. Run as Standalone Binary ```bash # Development mode GO_ENV=development ./cv-server # Production mode GO_ENV=production PORT=1999 ./cv-server # With custom configuration PORT=8080 TEMPLATE_HOT_RELOAD=false ./cv-server # Background with nohup nohup ./cv-server > app.log 2>&1 & # Stop background process pkill cv-server ``` #### 3. Environment Variables Create `.env` file: ```bash cp .env.example .env nano .env ``` Edit: ```bash PORT=1999 GO_ENV=production TEMPLATE_HOT_RELOAD=false READ_TIMEOUT=30 WRITE_TIMEOUT=30 ``` Load and run: ```bash # Load .env and run export $(cat .env | xargs) && ./cv-server ``` --- ## Configuration ### Environment Variables Reference | Variable | Default | Description | Values | |----------|---------|-------------|--------| | `PORT` | `1999` | Server port | Any valid port (1-65535) | | `HOST` | `localhost` | Bind address | `localhost`, `0.0.0.0`, IP address | | `GO_ENV` | `development` | Environment mode | `development`, `production` | | `TEMPLATE_HOT_RELOAD` | `true` | Auto-reload templates | `true`, `false` | | `TEMPLATE_DIR` | `templates` | Templates directory | Path to templates | | `PARTIALS_DIR` | `templates/partials` | Partials directory | Path to partials | | `DATA_DIR` | `data` | CV data directory | Path to JSON files | | `READ_TIMEOUT` | `15` | Read timeout (seconds) | Integer | | `WRITE_TIMEOUT` | `15` | Write timeout (seconds) | Integer | | `CHROME_BIN` | Auto-detected | Chrome binary path | Full path to chrome/chromium | ### .env File Setup **Development** (`.env`): ```bash PORT=1999 HOST=localhost GO_ENV=development TEMPLATE_HOT_RELOAD=true READ_TIMEOUT=15 WRITE_TIMEOUT=15 ``` **Production** (`.env.production`): ```bash PORT=1999 HOST=0.0.0.0 GO_ENV=production TEMPLATE_HOT_RELOAD=false READ_TIMEOUT=30 WRITE_TIMEOUT=30 BASE_URL=https://your-domain.com VERSION=1.0.0 ``` ### Production vs Development Settings | Setting | Development | Production | |---------|-------------|------------| | `GO_ENV` | `development` | `production` | | `TEMPLATE_HOT_RELOAD` | `true` (fast iteration) | `false` (performance) | | `HOST` | `localhost` (local only) | `0.0.0.0` (external access) | | `READ_TIMEOUT` | `15s` (relaxed) | `30s` (prevent slow clients) | | `WRITE_TIMEOUT` | `15s` (relaxed) | `30s` (prevent slow responses) | | Logging | Verbose | Structured JSON | | Error Details | Full stack traces | Generic messages | --- ## Post-Deployment ### Health Checks **Manual check**: ```bash # Basic health curl http://localhost:1999/health # With details curl -s http://localhost:1999/health | jq . # Expected response: { "status": "healthy", "timestamp": "2025-11-09T12:00:00Z", "version": "1.0.0" } ``` **Automated monitoring**: ```bash # Simple monitoring script cat > /usr/local/bin/cv-monitor.sh << 'EOF' #!/bin/bash HEALTH_URL="http://localhost:1999/health" THRESHOLD=3 FAILURES=0 while true; do if ! curl -sf "$HEALTH_URL" > /dev/null; then FAILURES=$((FAILURES + 1)) if [ $FAILURES -ge $THRESHOLD ]; then echo "Service unhealthy, restarting..." systemctl restart cv FAILURES=0 fi else FAILURES=0 fi sleep 30 done EOF chmod +x /usr/local/bin/cv-monitor.sh ``` ### Monitoring Setup **External monitoring** (Uptime Robot, Pingdom, etc.): - URL: `https://your-domain.com/health` - Interval: 60 seconds - Expected status: 200 - Expected response: `"healthy"` ### Log Management **Systemd logs**: ```bash # View all logs sudo journalctl -u cv # Follow logs sudo journalctl -u cv -f # Last 100 lines sudo journalctl -u cv -n 100 # Since timestamp sudo journalctl -u cv --since "2025-11-09 12:00:00" # Logs from last boot sudo journalctl -u cv -b ``` **Application logs**: ```bash # View log file tail -f /var/log/cv.log # Last 50 lines tail -n 50 /var/log/cv.log # Search for errors grep -i error /var/log/cv.log # Log rotation sudo nano /etc/logrotate.d/cv ``` **Logrotate configuration** (`/etc/logrotate.d/cv`): ``` /var/log/cv.log { daily rotate 14 compress delaycompress missingok notifempty create 0644 txeo txeo postrotate systemctl reload cv > /dev/null 2>&1 || true endscript } ``` ### Backup Strategies **1. Application Backup**: ```bash # Backup script cat > /usr/local/bin/cv-backup.sh << 'EOF' #!/bin/bash BACKUP_DIR="/backups/cv" DATE=$(date +%Y%m%d_%H%M%S) mkdir -p "$BACKUP_DIR" # Backup application tar -czf "$BACKUP_DIR/cv-app-$DATE.tar.gz" \ /home/txeo/Git/yo/cv/data \ /home/txeo/Git/yo/cv/templates \ /home/txeo/Git/yo/cv/static \ /home/txeo/Git/yo/cv/.env # Keep only last 30 days find "$BACKUP_DIR" -name "cv-app-*.tar.gz" -mtime +30 -delete echo "Backup completed: $BACKUP_DIR/cv-app-$DATE.tar.gz" EOF chmod +x /usr/local/bin/cv-backup.sh ``` **2. Automated daily backups**: ```bash # Add to crontab crontab -e # Daily at 2 AM 0 2 * * * /usr/local/bin/cv-backup.sh ``` --- ## Troubleshooting ### Common Deployment Issues #### 1. Port Already in Use **Error**: `bind: address already in use` **Solution**: ```bash # Find process using port 1999 sudo lsof -i :1999 sudo netstat -tulpn | grep 1999 # Kill process sudo kill -9 # Or change port PORT=8080 ./cv-server ``` #### 2. Chromium Not Found **Error**: `chrome not found` or PDF generation fails **Solution**: ```bash # Install Chromium # Ubuntu/Debian sudo apt install -y chromium-browser # Alpine apk add --no-cache chromium # Verify which chromium-browser chromium-browser --version # Set environment variable export CHROME_BIN=/usr/bin/chromium-browser ``` #### 3. Permission Denied **Error**: `permission denied` accessing files **Solution**: ```bash # Fix ownership sudo chown -R txeo:txeo /home/txeo/Git/yo/cv # Fix permissions chmod +x cv-server chmod -R 755 static templates data # Check SELinux (if applicable) sudo setenforce 0 # Temporary ``` #### 4. Template Not Found **Error**: `template not found` or rendering errors **Solution**: ```bash # Verify directory structure ls -la templates/ ls -la data/ # Check environment variables echo $TEMPLATE_DIR echo $DATA_DIR # Ensure working directory is correct pwd cd /home/txeo/Git/yo/cv ``` #### 5. Health Check Fails **Error**: Service marked unhealthy **Solution**: ```bash # Check service is running curl http://localhost:1999/health # Check logs journalctl -u cv -n 50 # Verify port is accessible nc -zv localhost 1999 ``` ### Performance Tuning **1. Go Runtime Tuning**: ```bash # Increase max connections export GOMAXPROCS=4 # Adjust garbage collection export GOGC=100 # Default, lower = more frequent GC ``` **2. Nginx Tuning** (`/etc/nginx/nginx.conf`): ```nginx worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 4096; use epoll; multi_accept on; } http { # Connection limits keepalive_timeout 65; keepalive_requests 100; # Buffer sizes client_body_buffer_size 128k; client_max_body_size 10m; client_header_buffer_size 1k; large_client_header_buffers 4 8k; } ``` **3. System Limits** (`/etc/security/limits.conf`): ``` txeo soft nofile 65536 txeo hard nofile 65536 ``` ### Security Hardening **1. Firewall Configuration**: ```bash # UFW (Ubuntu) sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw enable # Block direct access to app port sudo ufw deny 1999/tcp ``` **2. Fail2Ban for Nginx**: ```bash # Install sudo apt install -y fail2ban # Configure sudo nano /etc/fail2ban/jail.local ``` Add: ```ini [nginx-http-auth] enabled = true [nginx-limit-req] enabled = true filter = nginx-limit-req logpath = /var/log/nginx/cv-error.log ``` --- ## Updates & Maintenance ### Rolling Updates **Systemd Service**: ```bash # Pull latest code cd /home/txeo/Git/yo/cv git pull origin main # Build new binary make build # Test binary ./cv-server --version # Restart service (brief downtime) sudo systemctl restart cv # Verify curl http://localhost:1999/health ``` ### Zero-Downtime Deployment **Nginx + Multiple Backends**: 1. **Run two instances**: ```bash # Start second instance on different port PORT=1998 ./cv-server & # Update Nginx upstream upstream cv_backend { server 127.0.0.1:1999 weight=1; server 127.0.0.1:1998 weight=1; } # Reload Nginx sudo systemctl reload nginx ``` 2. **Deploy new version**: ```bash # Stop first instance pkill -f "PORT=1999" # Start updated instance PORT=1999 ./cv-server-new & # Verify health curl http://localhost:1999/health # Stop second instance pkill -f "PORT=1998" ``` ### Rollback Procedures **Manual rollback**: ```bash # Git rollback cd /home/txeo/Git/yo/cv git log --oneline -10 # Find commit hash git checkout abc123 # Rebuild and restart make build sudo systemctl restart cv # Verify curl http://localhost:1999/health ``` --- ## Best Practices Summary 1. **Use systemd** for reliable process management 2. **Enable health checks** for monitoring 3. **Set up monitoring** to detect issues early 4. **Configure SSL/TLS** for production (Let's Encrypt is free) 5. **Use reverse proxy** (Nginx) for security and performance 6. **Implement log rotation** to prevent disk space issues 7. **Automate backups** of data, templates, and configuration 8. **Test rollback procedures** before production deployments 9. **Use environment variables** for configuration (never hardcode) 10. **Enable rate limiting** to prevent abuse --- **For customization, see [7-CUSTOMIZATION.md](7-CUSTOMIZATION.md)**