- Add clear disclaimers: personal site, not a template - Update CONTRIBUTING.md: not seeking contributions - Remove all Docker files and references (11 files) - Rewrite DEPLOYMENT.md without Docker (VPS/cloud focus) - Add comprehensive API documentation with verified endpoints - Add complete CUSTOMIZATION guide - Add project status sections to all major docs - Clarify MIT license but personal use intent Created documentation files: - API.md (70KB) - Complete API reference with live testing - API-QUICK-REFERENCE.md - Quick command reference - DEPLOYMENT.md (45KB) - VPS, cloud, manual deployment (no Docker) - CUSTOMIZATION.md (38KB) - Complete customization guide - PROJECT-DOCUMENTATION-SUMMARY.md - Complete project overview This is my personal CV site. While code is public (MIT), it's designed for my personal use, not as a template.
21 KiB
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
- Prerequisites
- Quick Start
- Deployment Methods
- Configuration
- Post-Deployment
- Troubleshooting
- 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:
# 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:
# 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:
# 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:
# Edit service file
nano config/systemd/cv.service
Ensure paths match your setup:
[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:
# 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:
# 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:
sudo apt install -y nginx
Create Nginx configuration (/etc/nginx/sites-available/cv):
# 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:
# 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
# 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):
# 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):
# Install as service
make install-service
# Update service
make update-service
Manual management:
# 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
Note: This requires building from source on Fly.io's infrastructure.
# 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
# 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:
# 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:
- Connect GitHub repository
- Railway auto-detects Go project
- Set environment variables in dashboard:
PORT= 8080GO_ENV= production
- Deploy automatically on git push
Render (create render.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
# 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
# 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:
cp .env.example .env
nano .env
Edit:
PORT=1999
GO_ENV=production
TEMPLATE_HOT_RELOAD=false
READ_TIMEOUT=30
WRITE_TIMEOUT=30
Load and run:
# 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):
PORT=1999
HOST=localhost
GO_ENV=development
TEMPLATE_HOT_RELOAD=true
READ_TIMEOUT=15
WRITE_TIMEOUT=15
Production (.env.production):
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:
# 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:
# 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:
# 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:
# 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:
# 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:
# 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:
# 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:
# 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:
# 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:
# 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:
# 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:
# Increase max connections
export GOMAXPROCS=4
# Adjust garbage collection
export GOGC=100 # Default, lower = more frequent GC
2. Nginx Tuning (/etc/nginx/nginx.conf):
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:
# 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:
# Install
sudo apt install -y fail2ban
# Configure
sudo nano /etc/fail2ban/jail.local
Add:
[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:
# 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:
- Run two instances:
# 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
- Deploy new version:
# 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:
# 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
- Use systemd for reliable process management
- Enable health checks for monitoring
- Set up monitoring to detect issues early
- Configure SSL/TLS for production (Let's Encrypt is free)
- Use reverse proxy (Nginx) for security and performance
- Implement log rotation to prevent disk space issues
- Automate backups of data, templates, and configuration
- Test rollback procedures before production deployments
- Use environment variables for configuration (never hardcode)
- Enable rate limiting to prevent abuse
For customization, see CUSTOMIZATION.md