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