Files
cv-site/doc/8-DEPLOYMENT.md
T
juanatsap 1f6f8e417e docs: Update skeleton loader implementation from hyperscript to JavaScript
MIGRATION SUMMARY:
- Moved skeleton loader logic from hyperscript to JavaScript (main.js)
- Changed from htmx:oobAfterSwap to htmx:afterSettle event
- Changed OOB swap from innerHTML to outerHTML for proper element replacement
- Added languageSwitching flag for state tracking
- Added 100ms delay after afterSettle for final render completion

DOCUMENTATION UPDATES:
- 2-MODERN-WEB-TECHNIQUES.md: Updated skeleton loader section with
2025-11-18 19:32:28 +00:00

1054 lines
21 KiB
Markdown

# 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 <PID>
# 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)**