Files
gems/docs/deployment.md
T
joakim 7ea78d3b54 docs: add comprehensive deployment guide and Caddy configuration
- Create detailed deployment documentation
- Add Caddyfile.example with security headers and API proxy
- Document SystemD service setup for Go API
- Include database backup strategy
- Add troubleshooting guide
- Document OAuth configuration steps
- Add build and deployment commands
- Test production build successfully (340KB static site)
2026-01-06 16:19:34 +01:00

6.9 KiB

Opal-Web Deployment Guide

Overview

This guide covers deploying the Opal Task Manager PWA to production on opal.example.com with Caddy as the web server.

Prerequisites

  • Server with Caddy installed
  • Domain: opal.example.com (DNS configured)
  • Authentik OAuth configured at auth.example.com
  • Opal Go API server binary built

Architecture

Browser → Caddy (opal.example.com)
           ├─ / → Static PWA (SvelteKit build)
           └─ /api/* → Go API Server (:8080)

Step 1: Build the Frontend

Local Build

cd opal-web

# Install dependencies if needed
bun install

# Build for production
bun run build

# Output will be in: build/

The build creates a static site ready to deploy.

Step 2: Deploy Static Files

Option A: Direct Copy

# On your local machine
rsync -avz --delete build/ user@server:/var/www/opal/

Option B: Server-Side Build

# On the server
cd /opt/opal-web
git pull
bun install
bun run build
cp -r build/* /var/www/opal/

Set Permissions

sudo chown -R www-data:www-data /var/www/opal
sudo chmod -R 755 /var/www/opal

Step 3: Configure Backend

Create Environment File

sudo mkdir -p /etc/opal
sudo nano /etc/opal/opal.env

Add:

# Server
SERVER_ADDR=:8080

# Database  
OPAL_DB_PATH=/var/lib/opal/opal.db

# OAuth (from Authentik setup)
OAUTH_ENABLED=true
OAUTH_ISSUER=https://auth.example.com/application/o/opal/
OAUTH_CLIENT_ID=your_client_id_from_authentik
OAUTH_CLIENT_SECRET=your_client_secret_from_authentik
OAUTH_REDIRECT_URI=https://opal.example.com/auth/callback

# JWT (generate with: openssl rand -hex 32)
JWT_SECRET=your_generated_jwt_secret_here
JWT_EXPIRY=3600

# Refresh Token
REFRESH_TOKEN_EXPIRY=604800

Create SystemD Service

sudo nano /etc/systemd/system/opal-api.service
[Unit]
Description=Opal Task API Server
After=network.target

[Service]
Type=simple
User=opal
Group=opal
WorkingDirectory=/var/lib/opal
EnvironmentFile=/etc/opal/opal.env
ExecStart=/usr/local/bin/opal server start --addr :8080 --db /var/lib/opal/opal.db
Restart=always
RestartSec=5

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/opal

[Install]
WantedBy=multi-user.target

Setup Database and User

# Create user
sudo useradd -r -s /bin/false opal

# Create directories
sudo mkdir -p /var/lib/opal
sudo chown opal:opal /var/lib/opal
sudo chmod 750 /var/lib/opal

# Build and install binary
cd opal-task
go build -o opal main.go
sudo cp opal /usr/local/bin/
sudo chmod 755 /usr/local/bin/opal

# Generate first API key (optional - for CLI access)
sudo -u opal opal server keygen --name "Admin CLI" --db /var/lib/opal/opal.db
# Save the generated key!

# Start service
sudo systemctl daemon-reload
sudo systemctl enable opal-api
sudo systemctl start opal-api
sudo systemctl status opal-api

Step 4: Configure Caddy

Create Caddyfile

sudo nano /etc/caddy/Caddyfile

Add:

opal.example.com {
	# Root directory for static PWA
	root * /var/www/opal
	
	# API proxy
	handle /api/* {
		uri strip_prefix /api
		reverse_proxy localhost:8080
	}
	
	# Static file serving with SPA fallback
	handle {
		try_files {path} /index.html
		file_server
	}
	
	# Security headers
	header {
		# HTTPS only
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
		
		# Prevent clickjacking
		X-Frame-Options "SAMEORIGIN"
		
		# XSS protection
		X-Content-Type-Options "nosniff"
		
		# CSP (adjust as needed)
		Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://auth.example.com;"
	}
	
	# Logging
	log {
		output file /var/log/caddy/opal.log
		format json
	}
	
	# Compression
	encode gzip
}

Test and Reload Caddy

# Test configuration
sudo caddy validate --config /etc/caddy/Caddyfile

# Reload
sudo systemctl reload caddy

# Check status
sudo systemctl status caddy

Step 5: Verify Deployment

Check Services

# API server
sudo systemctl status opal-api
sudo journalctl -u opal-api -n 50

# Caddy
sudo systemctl status caddy
sudo journalctl -u caddy -n 50

Test Endpoints

# Health check
curl https://opal.example.com/api/health

# OAuth login URL
curl https://opal.example.com/api/auth/login

# Static files
curl -I https://opal.example.com/

Browser Testing

  1. Navigate to https://opal.example.com
  2. Should see the Opal login page
  3. Click "Login with OAuth"
  4. Should redirect to Authentik
  5. After login, should redirect back to task list
  6. Test creating a task
  7. Test PWA install prompt (mobile/desktop)

Step 6: Update Deployment (Future)

# Frontend update
cd opal-web
git pull
bun install
bun run build
rsync -avz --delete build/ server:/var/www/opal/

# Backend update
cd opal-task
git pull
go build -o opal main.go
scp opal server:/tmp/
ssh server "sudo systemctl stop opal-api && sudo cp /tmp/opal /usr/local/bin/ && sudo systemctl start opal-api"

Troubleshooting

API Not Responding

# Check if running
sudo systemctl status opal-api

# Check logs
sudo journalctl -u opal-api -f

# Test locally
curl http://localhost:8080/health

OAuth Not Working

  • Verify redirect URI in Authentik matches exactly
  • Check OAuth client ID and secret in env file
  • Ensure JWT secret is set
  • Check Caddy is proxying /api/* correctly

PWA Not Installing

  • Ensure HTTPS is working
  • Check manifest.json is accessible
  • Verify service worker is registered (Browser DevTools → Application)
  • Icons must be served correctly

Static Files 404

  • Check file permissions in /var/www/opal
  • Verify Caddy root directive points to correct path
  • Ensure index.html exists in root

Monitoring

Logs

# API logs
sudo journalctl -u opal-api -f

# Caddy logs
sudo tail -f /var/log/caddy/opal.log

# System logs
dmesg | grep opal

Database Backup

# Backup script
#!/bin/bash
BACKUP_DIR="/var/backups/opal"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"
sudo -u opal sqlite3 /var/lib/opal/opal.db ".backup '$BACKUP_DIR/opal_$DATE.db'"

# Keep last 7 days
find "$BACKUP_DIR" -name "opal_*.db" -mtime +7 -delete

Add to crontab:

0 2 * * * /usr/local/bin/backup-opal.sh

Security Checklist

  • HTTPS enforced (Caddy auto)
  • OAuth2 authentication configured
  • JWT tokens with expiry
  • API key hashing (bcrypt)
  • SystemD service hardening
  • Database file permissions (750)
  • Security headers (CSP, HSTS, etc.)
  • Regular backups configured
  • Fail2ban for API rate limiting (optional)
  • Log monitoring/alerting (optional)

Performance Tips

  • Caddy handles compression and HTTP/2
  • Service worker caches static assets
  • API responses cached by service worker
  • Consider CDN for static assets (future)

Support

  • Check logs first
  • Verify all environment variables
  • Test OAuth flow in incognito
  • Check network tab in DevTools