Files
Gondulf/deployment
Phil Skentelbery 01dcaba86b feat(deploy): merge Phase 5a deployment configuration
Complete containerized deployment system with Docker/Podman support.

Key features:
- Multi-stage Dockerfile with Python 3.11-slim base
- Docker Compose configurations for production and development
- Nginx reverse proxy with security headers and rate limiting
- Systemd service units for Docker, Podman, and docker-compose
- Backup/restore scripts with integrity verification
- Podman compatibility (ADR-009)

All tests pass including Podman verification testing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 19:16:54 -07:00
..

Gondulf Deployment Guide

This guide covers deploying Gondulf IndieAuth Server using OCI-compliant containers with both Podman (recommended) and Docker (alternative).

Table of Contents

  1. Quick Start
  2. Container Engine Support
  3. Prerequisites
  4. Building the Container Image
  5. Development Deployment
  6. Production Deployment
  7. Backup and Restore
  8. systemd Integration
  9. Troubleshooting
  10. Security Considerations

Quick Start

# 1. Clone and configure
git clone https://github.com/yourusername/gondulf.git
cd gondulf
cp .env.example .env
# Edit .env with your settings

# 2. Build image
podman build -t gondulf:latest .

# 3. Run container
podman run -d --name gondulf \
  -p 8000:8000 \
  -v gondulf_data:/data:Z \
  --env-file .env \
  gondulf:latest

# 4. Verify health
curl http://localhost:8000/health

Docker (Alternative)

# 1. Clone and configure
git clone https://github.com/yourusername/gondulf.git
cd gondulf
cp .env.example .env
# Edit .env with your settings

# 2. Build and run with compose
docker-compose up -d

# 3. Verify health
curl http://localhost:8000/health

Container Engine Support

Gondulf supports both Podman and Docker with identical functionality.

Podman (Primary)

Advantages:

  • Daemonless architecture (no background process)
  • Rootless mode for enhanced security
  • Native systemd integration
  • Pod support for multi-container applications
  • OCI-compliant

Recommended for: Production deployments, security-focused environments

Docker (Alternative)

Advantages:

  • Wide ecosystem and tooling support
  • Familiar to most developers
  • Extensive documentation

Recommended for: Development, existing Docker environments

Compatibility Matrix

Feature Podman Docker
Container build
Container runtime
Compose files (podman-compose) (docker-compose)
Rootless mode Native ⚠️ Experimental
systemd integration Built-in ⚠️ Manual
Health checks

Prerequisites

System Requirements

  • Operating System: Linux (recommended), macOS, Windows (WSL2)
  • CPU: 1 core minimum, 2+ cores recommended
  • RAM: 512 MB minimum, 1 GB+ recommended
  • Disk: 5 GB available space

Container Engine

Choose ONE:

Option 1: Podman (Recommended)

# Fedora/RHEL/CentOS
sudo dnf install podman podman-compose

# Ubuntu/Debian
sudo apt install podman podman-compose

# Verify installation
podman --version
podman-compose --version

Option 2: Docker

# Ubuntu/Debian
sudo apt install docker.io docker-compose

# Or install from Docker's repository:
# https://docs.docker.com/engine/install/

# Verify installation
docker --version
docker-compose --version

For enhanced security, configure rootless Podman:

# 1. Check subuid/subgid configuration
grep $USER /etc/subuid
grep $USER /etc/subgid

# Should show: username:100000:65536 (or similar)
# If missing, run:
sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER

# 2. Enable user lingering (services persist after logout)
loginctl enable-linger $USER

# 3. Verify rootless setup
podman system info | grep rootless
# Should show: runRoot: /run/user/1000/...

Building the Container Image

Using Podman

# Build image
podman build -t gondulf:latest .

# Verify build
podman images | grep gondulf

# Test run
podman run --rm gondulf:latest python -m gondulf --version

Using Docker

# Build image
docker build -t gondulf:latest .

# Verify build
docker images | grep gondulf

# Test run
docker run --rm gondulf:latest python -m gondulf --version

Build Arguments

The Dockerfile supports multi-stage builds that include testing:

# Build with tests (default)
podman build -t gondulf:latest .

# If build fails, tests have failed - check build output

Development Deployment

Development deployment includes:

  • Live code reload
  • MailHog for local email testing
  • Debug logging enabled
  • No TLS requirements

Using Podman Compose

# Start development environment
podman-compose -f docker-compose.yml -f docker-compose.development.yml up

# Access services:
# - Gondulf: http://localhost:8000
# - MailHog UI: http://localhost:8025

# View logs
podman-compose logs -f gondulf

# Stop environment
podman-compose down

Using Docker Compose

# Start development environment
docker-compose -f docker-compose.yml -f docker-compose.development.yml up

# Access services:
# - Gondulf: http://localhost:8000
# - MailHog UI: http://localhost:8025

# View logs
docker-compose logs -f gondulf

# Stop environment
docker-compose down

Development Configuration

Create .env file from .env.example:

cp .env.example .env

Edit .env with development settings:

GONDULF_SECRET_KEY=dev-secret-key-minimum-32-characters
GONDULF_BASE_URL=http://localhost:8000
GONDULF_DATABASE_URL=sqlite:///./data/gondulf.db
GONDULF_SMTP_HOST=mailhog
GONDULF_SMTP_PORT=1025
GONDULF_SMTP_USE_TLS=false
GONDULF_DEBUG=true
GONDULF_LOG_LEVEL=DEBUG

Production Deployment

Production deployment includes:

  • nginx reverse proxy with TLS termination
  • Rate limiting and security headers
  • Persistent volume for database
  • Health checks and auto-restart
  • Proper logging configuration

Step 1: Configuration

# 1. Copy environment template
cp .env.example .env

# 2. Generate secret key
python -c "import secrets; print(secrets.token_urlsafe(32))"

# 3. Edit .env with your production settings
nano .env

Production .env example:

GONDULF_SECRET_KEY=<generated-secret-key-from-step-2>
GONDULF_BASE_URL=https://auth.example.com
GONDULF_DATABASE_URL=sqlite:////data/gondulf.db
GONDULF_SMTP_HOST=smtp.sendgrid.net
GONDULF_SMTP_PORT=587
GONDULF_SMTP_USERNAME=apikey
GONDULF_SMTP_PASSWORD=<your-sendgrid-api-key>
GONDULF_SMTP_FROM=noreply@example.com
GONDULF_SMTP_USE_TLS=true
GONDULF_HTTPS_REDIRECT=true
GONDULF_TRUST_PROXY=true
GONDULF_SECURE_COOKIES=true
GONDULF_DEBUG=false
GONDULF_LOG_LEVEL=INFO

Step 2: TLS Certificates

Obtain TLS certificates (Let's Encrypt recommended):

# Create SSL directory
mkdir -p deployment/nginx/ssl

# Option 1: Let's Encrypt (recommended)
sudo certbot certonly --standalone -d auth.example.com
sudo cp /etc/letsencrypt/live/auth.example.com/fullchain.pem deployment/nginx/ssl/
sudo cp /etc/letsencrypt/live/auth.example.com/privkey.pem deployment/nginx/ssl/

# Option 2: Self-signed (development/testing only)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout deployment/nginx/ssl/privkey.pem \
  -out deployment/nginx/ssl/fullchain.pem

# Secure permissions
chmod 600 deployment/nginx/ssl/privkey.pem
chmod 644 deployment/nginx/ssl/fullchain.pem

Step 3: nginx Configuration

Edit deployment/nginx/conf.d/gondulf.conf:

# Change server_name to your domain
server_name auth.example.com;  # ← CHANGE THIS
# Build image
podman build -t gondulf:latest .

# Start services
podman-compose -f docker-compose.yml -f docker-compose.production.yml up -d

# Verify health
curl https://auth.example.com/health

# View logs
podman-compose logs -f

Step 5: Deploy with Docker (Alternative)

# Build and start
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d

# Verify health
curl https://auth.example.com/health

# View logs
docker-compose logs -f

Step 6: Verify Deployment

# 1. Check health endpoint
curl https://auth.example.com/health
# Expected: {"status":"healthy","database":"connected"}

# 2. Check OAuth metadata
curl https://auth.example.com/.well-known/oauth-authorization-server | jq
# Expected: JSON with issuer, authorization_endpoint, token_endpoint

# 3. Verify HTTPS redirect
curl -I http://auth.example.com/
# Expected: 301 redirect to HTTPS

# 4. Check security headers
curl -I https://auth.example.com/ | grep -E "(Strict-Transport|X-Frame|X-Content)"
# Expected: HSTS, X-Frame-Options, X-Content-Type-Options headers

# 5. Test TLS configuration
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=auth.example.com
# Target: Grade A or higher

Backup and Restore

Automated Backups

The backup scripts auto-detect Podman or Docker.

Create Backup

# Using included script (works with both Podman and Docker)
./deployment/scripts/backup.sh

# Or with custom backup directory
./deployment/scripts/backup.sh /path/to/backups

# Or using compose (Podman)
podman-compose --profile backup run --rm backup

# Or using compose (Docker)
docker-compose --profile backup run --rm backup

Backup details:

  • Uses SQLite VACUUM INTO for safe hot backups
  • No downtime required
  • Automatic compression (gzip)
  • Integrity verification
  • Automatic cleanup of old backups (default: 7 days retention)

Scheduled Backups with cron

# Create cron job for daily backups at 2 AM
crontab -e

# Add this line:
0 2 * * * cd /path/to/gondulf && ./deployment/scripts/backup.sh >> /var/log/gondulf-backup.log 2>&1

Restore from Backup

CAUTION: This will replace the current database!

# Restore from backup
./deployment/scripts/restore.sh /path/to/backups/gondulf_backup_20251120_120000.db.gz

# The script will:
# 1. Stop the container (if running)
# 2. Create a safety backup of current database
# 3. Restore from the specified backup
# 4. Verify integrity
# 5. Restart the container (if it was running)

Test Backup/Restore

# Run automated backup/restore tests
./deployment/scripts/test-backup-restore.sh

# This verifies:
# - Backup creation
# - Backup integrity
# - Database structure
# - Compression
# - Queryability

systemd Integration

Method 1: Podman-Generated Unit (Recommended)

# 1. Start container normally first
podman run -d --name gondulf \
  -p 8000:8000 \
  -v gondulf_data:/data:Z \
  --env-file /home/$USER/gondulf/.env \
  gondulf:latest

# 2. Generate systemd unit file
cd ~/.config/systemd/user/
podman generate systemd --new --files --name gondulf

# 3. Stop the manually-started container
podman stop gondulf
podman rm gondulf

# 4. Enable and start service
systemctl --user daemon-reload
systemctl --user enable --now container-gondulf.service

# 5. Enable lingering (service runs without login)
loginctl enable-linger $USER

# 6. Verify status
systemctl --user status container-gondulf

Method 2: Custom Unit File

# 1. Copy unit file
mkdir -p ~/.config/systemd/user/
cp deployment/systemd/gondulf-podman.service ~/.config/systemd/user/gondulf.service

# 2. Edit paths if needed
nano ~/.config/systemd/user/gondulf.service

# 3. Reload and enable
systemctl --user daemon-reload
systemctl --user enable --now gondulf.service
loginctl enable-linger $USER

# 4. Verify status
systemctl --user status gondulf

systemd User Service Commands:

# Start service
systemctl --user start gondulf

# Stop service
systemctl --user stop gondulf

# Restart service
systemctl --user restart gondulf

# Check status
systemctl --user status gondulf

# View logs
journalctl --user -u gondulf -f

# Disable service
systemctl --user disable gondulf

Docker (System Service)

# 1. Copy unit file
sudo cp deployment/systemd/gondulf-docker.service /etc/systemd/system/gondulf.service

# 2. Edit paths in the file
sudo nano /etc/systemd/system/gondulf.service
# Change WorkingDirectory to your installation path

# 3. Reload and enable
sudo systemctl daemon-reload
sudo systemctl enable --now gondulf.service

# 4. Verify status
sudo systemctl status gondulf

systemd System Service Commands:

# Start service
sudo systemctl start gondulf

# Stop service
sudo systemctl stop gondulf

# Restart service
sudo systemctl restart gondulf

# Check status
sudo systemctl status gondulf

# View logs
sudo journalctl -u gondulf -f

# Disable service
sudo systemctl disable gondulf

Compose-Based systemd Service

For deploying with docker-compose or podman-compose:

# For Podman (rootless):
cp deployment/systemd/gondulf-compose.service ~/.config/systemd/user/gondulf.service
# Edit to use podman-compose
systemctl --user daemon-reload
systemctl --user enable --now gondulf.service

# For Docker (rootful):
sudo cp deployment/systemd/gondulf-compose.service /etc/systemd/system/gondulf.service
# Edit to use docker-compose and add docker.service dependency
sudo systemctl daemon-reload
sudo systemctl enable --now gondulf.service

Troubleshooting

Container Won't Start

Check logs:

# Podman
podman logs gondulf
# or
podman-compose logs gondulf

# Docker
docker logs gondulf
# or
docker-compose logs gondulf

Common issues:

  1. Missing SECRET_KEY:

    ERROR: GONDULF_SECRET_KEY is required
    

    Solution: Set GONDULF_SECRET_KEY in .env (minimum 32 characters)

  2. Missing BASE_URL:

    ERROR: GONDULF_BASE_URL is required
    

    Solution: Set GONDULF_BASE_URL in .env

  3. Port already in use:

    Error: bind: address already in use
    

    Solution:

    # Check what's using port 8000
    sudo ss -tlnp | grep 8000
    
    # Use different port
    podman run -p 8001:8000 ...
    

Database Issues

Check database file:

# Podman
podman exec gondulf ls -la /data/

# Docker
docker exec gondulf ls -la /data/

Check database integrity:

# Podman
podman exec gondulf sqlite3 /data/gondulf.db "PRAGMA integrity_check;"

# Docker
docker exec gondulf sqlite3 /data/gondulf.db "PRAGMA integrity_check;"

Expected output: ok

Permission Errors (Rootless Podman)

If you see permission errors with volumes:

# 1. Check subuid/subgid configuration
grep $USER /etc/subuid
grep $USER /etc/subgid

# 2. Add if missing
sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER

# 3. Restart user services
systemctl --user daemon-reload

# 4. Use :Z label for SELinux systems
podman run -v ./data:/data:Z ...

SELinux Issues

On SELinux-enabled systems (RHEL, Fedora, CentOS):

# Check for SELinux denials
sudo ausearch -m AVC -ts recent

# Solution 1: Add :Z label to volumes (recommended)
podman run -v gondulf_data:/data:Z ...

# Solution 2: Temporarily permissive (testing only)
sudo setenforce 0

# Solution 3: Create SELinux policy (advanced)
# Use audit2allow to generate policy from denials

Email Not Sending

Check SMTP configuration:

# Test SMTP connection from container
podman exec gondulf sh -c "timeout 5 bash -c '</dev/tcp/smtp.example.com/587' && echo 'Port open' || echo 'Port closed'"

# Check logs for SMTP errors
podman logs gondulf | grep -i smtp

Common SMTP issues:

  1. Authentication failure: Verify username/password (use app-specific password for Gmail)
  2. TLS error: Check GONDULF_SMTP_USE_TLS matches port (587=STARTTLS, 465=TLS, 25=none)
  3. Firewall: Ensure outbound connections allowed on SMTP port

Health Check Failing

# Check health status
podman inspect gondulf --format='{{.State.Health.Status}}'

# View health check logs
podman inspect gondulf --format='{{range .State.Health.Log}}{{.Output}}{{end}}'

# Test health endpoint manually
curl http://localhost:8000/health

nginx Issues

Test nginx configuration:

# Podman
podman exec gondulf_nginx nginx -t

# Docker
docker exec gondulf_nginx nginx -t

Check nginx logs:

# Podman
podman logs gondulf_nginx

# Docker
docker logs gondulf_nginx

Security Considerations

Container Security (Rootless Podman)

Rootless Podman provides defense-in-depth:

  • No root daemon
  • User namespace isolation
  • UID mapping (container UID 1000 → host subuid range)
  • Limited attack surface

TLS/HTTPS Requirements

IndieAuth requires HTTPS in production:

  • Obtain valid TLS certificate (Let's Encrypt recommended)
  • Configure nginx for TLS termination
  • Enable HSTS headers
  • Use strong ciphers (TLS 1.2+)

Secrets Management

Never commit secrets to version control:

# Verify .env is gitignored
git check-ignore .env
# Should output: .env

# Ensure .env has restrictive permissions
chmod 600 .env

Production secrets best practices:

  • Use strong SECRET_KEY (32+ characters)
  • Use app-specific passwords for email (Gmail, etc.)
  • Rotate secrets regularly
  • Consider secrets management tools (Vault, AWS Secrets Manager)

Network Security

Firewall configuration:

# Allow HTTPS (443)
sudo ufw allow 443/tcp

# Allow HTTP (80) for Let's Encrypt challenges and redirects
sudo ufw allow 80/tcp

# Block direct access to container port (8000)
# Don't expose port 8000 externally in production

Rate Limiting

nginx configuration includes rate limiting:

  • Authorization endpoint: 10 req/s (burst 20)
  • Token endpoint: 20 req/s (burst 40)
  • General endpoints: 30 req/s (burst 60)

Adjust in deployment/nginx/conf.d/gondulf.conf as needed.

Security Headers

The following security headers are automatically set:

  • Strict-Transport-Security (HSTS)
  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • X-XSS-Protection: 1; mode=block
  • Referrer-Policy
  • Content-Security-Policy (set by application)

Regular Security Updates

# Update base image
podman pull python:3.12-slim-bookworm

# Rebuild container
podman build -t gondulf:latest .

# Recreate container
podman stop gondulf
podman rm gondulf
podman run -d --name gondulf ...

Additional Resources

Support

For issues or questions: