Files
Gondulf/docs/designs/phase-5a-deployment-config.md
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

61 KiB

Phase 5a: Deployment Configuration

Purpose

Enable production deployment of Gondulf IndieAuth server using OCI-compliant containers. This design provides containerization with security best practices, volume persistence for SQLite database, backup automation, and comprehensive deployment documentation.

Container Engine Support: Both Podman (primary, recommended) and Docker (alternative) are fully supported.

Target Audience: Operators deploying Gondulf in production environments.

Key Goals:

  1. Production-ready OCI container with security hardening
  2. Simple deployment using podman-compose or docker-compose
  3. Rootless container support (Podman) for enhanced security
  4. Database backup and restore procedures
  5. Complete environment variable documentation
  6. Deployment verification checklist
  7. systemd integration for production deployments

Specification References

Design Overview

Phase 5a delivers production deployment configuration through:

  1. OCI-Compliant Containerfile/Dockerfile: Multi-stage build optimized for both Podman and Docker
  2. Compose Configuration: podman-compose/docker-compose files for orchestration
  3. Rootless Container Support: Designed for rootless Podman deployment (recommended)
  4. SQLite Backup Scripts: Automated backup compatible with both container engines
  5. systemd Integration: Service unit generation for production deployments
  6. Environment Documentation: Complete reference for all GONDULF_* variables
  7. Deployment Checklist: Step-by-step deployment and verification procedures

Deployment Model: Single-container deployment with volume-mounted SQLite database, designed for 10s of users per architecture constraints.

Primary Engine: Podman (rootless) for security benefits; Docker fully supported as alternative.

Security Posture: Rootless container (Podman), non-root user, minimal base image, secrets via environment variables, health checks for orchestration.

Component Details

1. Containerfile/Dockerfile (Multi-stage Build)

File Location: /Dockerfile (project root)

Compatibility: OCI-compliant, works with both podman build and docker build

Purpose: Build production-ready container image with security hardening and optimized size.

Note: Podman can use Dockerfile or Containerfile (identical syntax). The file will be named Dockerfile for compatibility with both engines.

Stage 1: Build Stage (builder)

Base Image: python:3.12-slim-bookworm

  • Rationale: Debian-based (familiar), slim variant (smaller), Python 3.12 (latest stable)
  • Alternatives considered: Alpine (rejected: musl libc compatibility issues with some Python packages)

Build Stage Responsibilities:

  1. Install uv package manager
  2. Copy pyproject.toml and uv.lock
  3. Install dependencies with uv (using --frozen for reproducibility)
  4. Run tests (fail build if tests fail)
  5. Create virtual environment in /app/.venv

Build Stage Dockerfile Fragment:

FROM python:3.12-slim-bookworm AS builder

# Install uv
RUN pip install --no-cache-dir uv

# Set working directory
WORKDIR /app

# Copy dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies (including dev and test for running tests)
RUN uv sync --frozen

# Copy source code
COPY src/ ./src/
COPY tests/ ./tests/

# Run tests (fail build if tests fail)
RUN uv run pytest tests/ --tb=short -v

# Create optimized production environment (no dev/test dependencies)
RUN uv sync --frozen --no-dev

Security Considerations:

  • Use --no-cache-dir to prevent cache poisoning
  • Use --frozen to ensure reproducible builds from uv.lock
  • Run tests during build to prevent deploying broken code
  • Separate dev/test dependencies from production

Stage 2: Runtime Stage

Base Image: python:3.12-slim-bookworm

  • Same base as builder for compatibility
  • Slim variant for minimal attack surface

Runtime Stage Responsibilities:

  1. Create non-root user gondulf (UID 1000, GID 1000)
  2. Copy virtual environment from builder stage
  3. Copy source code from builder stage
  4. Set up volume mount points
  5. Configure health check
  6. Set user to gondulf
  7. Define entrypoint

Runtime Stage Dockerfile Fragment:

FROM python:3.12-slim-bookworm

# Create non-root user
RUN groupadd -r -g 1000 gondulf && \
    useradd -r -u 1000 -g gondulf -m -d /home/gondulf gondulf

# Install runtime dependencies only (none currently, but pattern for future)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Set working directory
WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder --chown=gondulf:gondulf /app/.venv /app/.venv

# Copy application code from builder
COPY --from=builder --chown=gondulf:gondulf /app/src /app/src

# Create directories for data and backups
RUN mkdir -p /data /data/backups && \
    chown -R gondulf:gondulf /data

# Set environment variables
ENV PATH="/app/.venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

# Expose port
EXPOSE 8000

# Health check (calls /health endpoint)
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

# Switch to non-root user
USER gondulf

# Entrypoint
CMD ["uvicorn", "gondulf.main:app", "--host", "0.0.0.0", "--port", "8000"]

Security Hardening:

  • Non-root user (UID 1000, GID 1000) prevents privilege escalation
  • Compatible with rootless Podman (recommended deployment mode)
  • Minimal base image reduces attack surface
  • No shell in entrypoint (array form) prevents shell injection
  • Health check enables orchestration to detect failures
  • PYTHONDONTWRITEBYTECODE prevents .pyc files in volume mounts
  • PYTHONUNBUFFERED ensures logs are flushed immediately

Rootless Podman Considerations:

  • UID 1000 in container maps to user's subuid range on host
  • Named volumes preferred over bind mounts to avoid permission issues
  • Volume mount labels (:Z/:z) may be required on SELinux systems

Volume Mount Points:

  • /data: SQLite database and backups
    • /data/gondulf.db: Database file
    • /data/backups/: Backup directory

Environment Variables (see section 4 for complete list):

  • Required: GONDULF_SECRET_KEY, GONDULF_BASE_URL
  • Optional: All other GONDULF_* variables with defaults

Build Arguments (for future extensibility):

ARG PYTHON_VERSION=3.12
ARG UV_VERSION=latest

Image Size Optimization:

  • Multi-stage build discards build dependencies
  • Slim base image (not full Debian)
  • Clean up apt caches
  • No unnecessary files in final image

Expected Image Size: ~200-300 MB (Python 3.12-slim base ~120 MB + dependencies)

2. Compose Configuration (docker-compose.yml / podman-compose)

File Location: /docker-compose.yml (project root)

Compatibility: Works with both podman-compose and docker-compose

Purpose: Orchestrate Gondulf service with volume persistence, environment configuration, and optional nginx reverse proxy.

Engine Detection: Commands shown for both Podman and Docker throughout documentation.

Base Configuration (All Profiles)

version: '3.8'

services:
  gondulf:
    build:
      context: .
      dockerfile: Dockerfile
    image: gondulf:latest
    container_name: gondulf
    restart: unless-stopped

    # Volume mounts
    volumes:
      - gondulf_data:/data
      # Optional: Bind mount for backups (add :Z for SELinux with Podman)
      # - ./backups:/data/backups:Z

    # Environment variables (from .env file)
    env_file:
      - .env

    # Port mapping (development only, use nginx in production)
    ports:
      - "8000:8000"

    # Health check (inherited from Dockerfile, can override)
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

    # Network
    networks:
      - gondulf_network

volumes:
  gondulf_data:
    driver: local
    # Optional: specify mount point on host
    # driver_opts:
    #   type: none
    #   device: /var/lib/gondulf/data
    #   o: bind

networks:
  gondulf_network:
    driver: bridge

Production Profile (with nginx Reverse Proxy)

Purpose: Add nginx reverse proxy for TLS termination, security headers, and rate limiting.

docker-compose.override.yml (for production):

version: '3.8'

services:
  gondulf:
    # Remove direct port exposure in production
    ports: []
    # Ensure GONDULF_BASE_URL is HTTPS
    environment:
      - GONDULF_HTTPS_REDIRECT=true
      - GONDULF_SECURE_COOKIES=true
      - GONDULF_TRUST_PROXY=true

  nginx:
    image: nginx:1.25-alpine
    container_name: gondulf_nginx
    restart: unless-stopped

    ports:
      - "80:80"
      - "443:443"

    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro

    depends_on:
      gondulf:
        condition: service_healthy

    networks:
      - gondulf_network

    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3

nginx Configuration (nginx/conf.d/gondulf.conf):

upstream gondulf_backend {
    server gondulf:8000;
}

# HTTP redirect to HTTPS
server {
    listen 80;
    server_name auth.example.com;  # Replace with your domain

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$server_name$request_uri;
    }
}

# HTTPS server
server {
    listen 443 ssl http2;
    server_name auth.example.com;  # Replace with your domain

    # SSL configuration
    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security headers (in addition to application headers)
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=gondulf_limit:10m rate=10r/s;
    limit_req zone=gondulf_limit burst=20 nodelay;

    # Proxy configuration
    location / {
        proxy_pass http://gondulf_backend;
        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;

        # Proxy timeouts
        proxy_connect_timeout 10s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }

    # Health check endpoint (no rate limiting)
    location /health {
        proxy_pass http://gondulf_backend;
        limit_req off;
        access_log off;
    }
}

Usage with Podman (recommended):

# Development
podman-compose up

# Production
podman-compose -f docker-compose.yml -f docker-compose.override.yml up -d

# Alternative: Use podman directly without compose
podman build -t gondulf:latest .
podman run -d --name gondulf -p 8000:8000 -v gondulf_data:/data --env-file .env gondulf:latest

Usage with Docker:

# Development
docker-compose up

# Production
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d

Environment File (.env):

  • See section 4 for complete reference
  • Never commit .env to version control
  • Use .env.example as template

3. SQLite Backup Scripts

File Location: /scripts/backup.sh and /scripts/restore.sh

Purpose: Automated backup and restore of SQLite database using safe hot backup method.

Container Engine Support: Auto-detects and works with both Podman and Docker.

Requirements:

  • Support hot backups (no downtime required)
  • Use SQLite .backup command (VACUUM INTO alternative)
  • Auto-detect container engine (podman or docker)
  • Configurable backup retention
  • Timestamp-based backup naming
  • Compression for storage efficiency
  • Restore procedure documentation

Backup Script

File: scripts/backup.sh

#!/bin/bash
#
# Gondulf SQLite Database Backup Script
#
# Usage: ./backup.sh [backup_dir]
#
# Environment Variables:
#   GONDULF_DATABASE_URL - Database URL (default: sqlite:///./data/gondulf.db)
#   BACKUP_DIR - Backup directory (default: ./data/backups)
#   BACKUP_RETENTION_DAYS - Days to keep backups (default: 7)
#   COMPRESS_BACKUPS - Compress backups with gzip (default: true)
#   CONTAINER_NAME - Container name (default: gondulf)
#   CONTAINER_ENGINE - Force specific engine: podman or docker (default: auto-detect)
#

set -euo pipefail

# Auto-detect container engine
detect_container_engine() {
    if [ -n "${CONTAINER_ENGINE:-}" ]; then
        echo "$CONTAINER_ENGINE"
    elif command -v podman &> /dev/null; then
        echo "podman"
    elif command -v docker &> /dev/null; then
        echo "docker"
    else
        echo "ERROR: Neither podman nor docker found" >&2
        exit 1
    fi
}

CONTAINER_ENGINE=$(detect_container_engine)
CONTAINER_NAME="${CONTAINER_NAME:-gondulf}"

echo "Using container engine: $CONTAINER_ENGINE"

# Configuration
DATABASE_URL="${GONDULF_DATABASE_URL:-sqlite:///./data/gondulf.db}"
BACKUP_DIR="${1:-${BACKUP_DIR:-./data/backups}}"
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
COMPRESS="${COMPRESS_BACKUPS:-true}"

# Extract database path from URL
# sqlite:///./data/gondulf.db -> ./data/gondulf.db
# sqlite:////var/lib/gondulf/gondulf.db -> /var/lib/gondulf/gondulf.db
DB_PATH=$(echo "$DATABASE_URL" | sed 's|^sqlite:///||')

# Validate database exists
if [ ! -f "$DB_PATH" ]; then
    echo "ERROR: Database file not found: $DB_PATH"
    exit 1
fi

# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"

# Generate backup filename with timestamp
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/gondulf_backup_$TIMESTAMP.db"

echo "Starting backup: $DB_PATH -> $BACKUP_FILE"

# Perform backup using SQLite VACUUM INTO (safe hot backup)
# Execute inside container
$CONTAINER_ENGINE exec "$CONTAINER_NAME" sqlite3 "$DB_PATH" "VACUUM INTO '$BACKUP_FILE'"

# Copy backup out of container if using named volume
if [[ "$BACKUP_DIR" != /data/* ]]; then
    $CONTAINER_ENGINE cp "$CONTAINER_NAME:$BACKUP_FILE" "$BACKUP_FILE"
    $CONTAINER_ENGINE exec "$CONTAINER_NAME" rm "$BACKUP_FILE"
fi

# Verify backup was created
if [ ! -f "$BACKUP_FILE" ]; then
    echo "ERROR: Backup file was not created"
    exit 1
fi

# Verify backup integrity (inside container)
if ! $CONTAINER_ENGINE exec "$CONTAINER_NAME" sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;" | grep -q "ok"; then
    echo "ERROR: Backup integrity check failed"
    rm -f "$BACKUP_FILE"
    exit 1
fi

echo "Backup completed successfully: $BACKUP_FILE"

# Compress backup if enabled
if [ "$COMPRESS" = "true" ]; then
    echo "Compressing backup..."
    gzip "$BACKUP_FILE"
    BACKUP_FILE="$BACKUP_FILE.gz"
    echo "Compressed: $BACKUP_FILE"
fi

# Calculate backup size
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "Backup size: $BACKUP_SIZE"

# Clean up old backups
echo "Cleaning up backups older than $RETENTION_DAYS days..."
find "$BACKUP_DIR" -name "gondulf_backup_*.db*" -type f -mtime +$RETENTION_DAYS -delete

# List remaining backups
echo "Current backups:"
ls -lh "$BACKUP_DIR"/gondulf_backup_*.db* 2>/dev/null || echo "  (none)"

echo "Backup complete"

Script Permissions: chmod +x scripts/backup.sh

Backup Method: Uses VACUUM INTO for safe hot backups:

  • Atomic operation (all-or-nothing)
  • No locks on source database (read-only)
  • Produces clean, optimized backup
  • Equivalent to .backup command

Restore Script

File: scripts/restore.sh

#!/bin/bash
#
# Gondulf SQLite Database Restore Script
#
# Usage: ./restore.sh <backup_file>
#
# CAUTION: This will REPLACE the current database!
#

set -euo pipefail

# Check arguments
if [ $# -ne 1 ]; then
    echo "Usage: $0 <backup_file>"
    echo "Example: $0 ./data/backups/gondulf_backup_20231120_120000.db.gz"
    exit 1
fi

BACKUP_FILE="$1"
DATABASE_URL="${GONDULF_DATABASE_URL:-sqlite:///./data/gondulf.db}"
DB_PATH=$(echo "$DATABASE_URL" | sed 's|^sqlite:///||')

# Validate backup file exists
if [ ! -f "$BACKUP_FILE" ]; then
    echo "ERROR: Backup file not found: $BACKUP_FILE"
    exit 1
fi

# Create backup of current database before restore
if [ -f "$DB_PATH" ]; then
    CURRENT_BACKUP="$DB_PATH.pre-restore.$(date +%Y%m%d_%H%M%S)"
    echo "Creating safety backup of current database: $CURRENT_BACKUP"
    cp "$DB_PATH" "$CURRENT_BACKUP"
fi

# Decompress if needed
TEMP_FILE=""
if [[ "$BACKUP_FILE" == *.gz ]]; then
    echo "Decompressing backup..."
    TEMP_FILE=$(mktemp)
    gunzip -c "$BACKUP_FILE" > "$TEMP_FILE"
    RESTORE_FILE="$TEMP_FILE"
else
    RESTORE_FILE="$BACKUP_FILE"
fi

# Verify backup integrity before restore
echo "Verifying backup integrity..."
if ! sqlite3 "$RESTORE_FILE" "PRAGMA integrity_check;" | grep -q "ok"; then
    echo "ERROR: Backup integrity check failed"
    [ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
    exit 1
fi

# Perform restore
echo "Restoring database from: $BACKUP_FILE"
cp "$RESTORE_FILE" "$DB_PATH"

# Clean up temp file
[ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"

# Verify restored database
echo "Verifying restored database..."
if ! sqlite3 "$DB_PATH" "PRAGMA integrity_check;" | grep -q "ok"; then
    echo "ERROR: Restored database integrity check failed"
    if [ -f "$CURRENT_BACKUP" ]; then
        echo "Restoring previous database from safety backup..."
        cp "$CURRENT_BACKUP" "$DB_PATH"
    fi
    exit 1
fi

echo "Restore completed successfully"
echo "Previous database backed up to: $CURRENT_BACKUP"
echo "You may delete this safety backup once you've verified the restore"

Script Permissions: chmod +x scripts/restore.sh

Backup Automation

Cron Example with Engine Detection (host system):

#!/bin/bash
# /etc/cron.daily/gondulf-backup

# Detect container engine
if command -v podman &> /dev/null; then
    ENGINE="podman"
elif command -v docker &> /dev/null; then
    ENGINE="docker"
else
    echo "No container engine found" >&2
    exit 1
fi

# Run backup script
cd /opt/gondulf && ./scripts/backup.sh >> /var/log/gondulf-backup.log 2>&1

Alternative: Compose-based Backup:

# With Podman
podman-compose --profile backup run --rm backup

# With Docker
docker-compose --profile backup run --rm backup

Crontab Entry:

# Backup Gondulf database daily at 2 AM
0 2 * * * /etc/cron.daily/gondulf-backup

Backup Testing Procedure

Frequency: Monthly (minimum)

Procedure:

  1. Create test backup: ./scripts/backup.sh /tmp/test-backup
  2. Stop Gondulf service: docker-compose stop gondulf
  3. Restore backup: ./scripts/restore.sh /tmp/test-backup/gondulf_backup_*.db.gz
  4. Start Gondulf service: docker-compose start gondulf
  5. Verify service health: curl http://localhost:8000/health
  6. Verify data integrity: Check database content
  7. Document test results

Automated Testing (optional):

#!/bin/bash
# scripts/test-backup-restore.sh
set -euo pipefail

echo "Testing backup and restore..."

# Create test backup
./scripts/backup.sh /tmp/gondulf-test
BACKUP=$(ls -t /tmp/gondulf-test/gondulf_backup_*.db.gz | head -1)

# Test restore (to temp location)
TEMP_DB=$(mktemp)
gunzip -c "$BACKUP" > "$TEMP_DB"

# Verify integrity
if sqlite3 "$TEMP_DB" "PRAGMA integrity_check;" | grep -q "ok"; then
    echo "✓ Backup/restore test PASSED"
    rm -f "$TEMP_DB"
    exit 0
else
    echo "✗ Backup/restore test FAILED"
    rm -f "$TEMP_DB"
    exit 1
fi

4. systemd Integration for Production Deployments

Purpose: Production-grade service management with automatic startup, logging, and restart policies.

Compatibility: Works with both Podman and Docker, with Podman providing native systemd integration.

Generate Unit File:

# Start container first (if not already running)
podman run -d --name gondulf \
  -p 8000:8000 \
  -v gondulf_data:/data \
  --env-file /opt/gondulf/.env \
  gondulf:latest

# Generate systemd unit file
podman generate systemd --new --files --name gondulf

# This creates: container-gondulf.service

Install and Enable:

# For rootless (user service)
mkdir -p ~/.config/systemd/user/
mv container-gondulf.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now container-gondulf.service
systemctl --user status container-gondulf

# Enable lingering (allows service to run without login)
loginctl enable-linger $USER

# For rootful (system service) - not recommended
sudo mv container-gondulf.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now container-gondulf.service

Generated Unit Example:

# container-gondulf.service (auto-generated by Podman)
[Unit]
Description=Podman container-gondulf.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run \
  --cidfile=%t/%n.ctr-id \
  --cgroups=no-conmon \
  --rm \
  --sdnotify=conmon \
  --replace \
  --name gondulf \
  -p 8000:8000 \
  -v gondulf_data:/data \
  --env-file /opt/gondulf/.env \
  gondulf:latest
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Method 2: Compose with systemd (Works with Both Engines)

Create Unit File: /etc/systemd/system/gondulf.service (Docker/Podman)

For Docker Compose:

[Unit]
Description=Gondulf IndieAuth Server
Documentation=https://github.com/yourusername/gondulf
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/gondulf
ExecStart=/usr/bin/docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
ExecStop=/usr/bin/docker-compose down
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target

For Podman Compose (rootless):

[Unit]
Description=Gondulf IndieAuth Server (Rootless)
Documentation=https://github.com/yourusername/gondulf
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/gondulf
ExecStart=/usr/bin/podman-compose up -d
ExecStop=/usr/bin/podman-compose down
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=default.target

Install and Enable:

# System service (rootful Docker or rootful Podman)
sudo cp gondulf.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now gondulf.service
sudo systemctl status gondulf

# User service (rootless Podman)
mkdir -p ~/.config/systemd/user/
cp gondulf.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now gondulf.service
systemctl --user status gondulf
loginctl enable-linger $USER

Method 3: Direct Container Execution (Simplest)

Create Unit File: /etc/systemd/system/gondulf.service

For Podman (rootless recommended):

[Unit]
Description=Gondulf IndieAuth Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=gondulf
Group=gondulf
WorkingDirectory=/opt/gondulf
ExecStartPre=-/usr/bin/podman stop gondulf
ExecStartPre=-/usr/bin/podman rm gondulf
ExecStart=/usr/bin/podman run \
  --name gondulf \
  --rm \
  -p 8000:8000 \
  -v gondulf_data:/data \
  --env-file /opt/gondulf/.env \
  gondulf:latest
ExecStop=/usr/bin/podman stop -t 10 gondulf
Restart=always
RestartSec=10s

[Install]
WantedBy=multi-user.target

For Docker:

[Unit]
Description=Gondulf IndieAuth Server
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/gondulf
ExecStartPre=-/usr/bin/docker stop gondulf
ExecStartPre=-/usr/bin/docker rm gondulf
ExecStart=/usr/bin/docker run \
  --name gondulf \
  --rm \
  -p 8000:8000 \
  -v gondulf_data:/data \
  --env-file /opt/gondulf/.env \
  gondulf:latest
ExecStop=/usr/bin/docker stop -t 10 gondulf
Restart=always
RestartSec=10s

[Install]
WantedBy=multi-user.target

systemd Management Commands

Service Control:

# Start service
sudo systemctl start gondulf
# or for user service:
systemctl --user 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

# Enable auto-start on boot
sudo systemctl enable gondulf

# Disable auto-start
sudo systemctl disable gondulf

Rootless Podman Specifics:

# All commands use --user flag
systemctl --user start gondulf
systemctl --user status gondulf
journalctl --user -u gondulf -f

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

# Check linger status
loginctl show-user $USER | grep Linger

Advantages by Method

Method 1 (Podman Generate):

  • Native Podman integration
  • Optimal systemd features (sd_notify, cgroup integration)
  • Easiest for Podman users
  • Podman-only

Method 2 (Compose + systemd):

  • Works with both engines
  • Uses existing compose files
  • Familiar for Docker users
  • Requires compose tool installed

Method 3 (Direct Execution):

  • No additional dependencies (no compose)
  • Works with both engines
  • Simplest to understand
  • Must specify all options in unit file

Recommended Approach:

  • Podman deployments: Use Method 1 (podman generate systemd)
  • Docker deployments: Use Method 2 (compose + systemd)
  • Minimal installs: Use Method 3 (direct execution)

5. Environment Variable Documentation

File Location: /docs/deployment/environment-variables.md

Purpose: Complete reference for all GONDULF_* environment variables.

Complete Environment Variable Reference

# Environment Variable Reference

## Overview

Gondulf is configured entirely through environment variables with the `GONDULF_` prefix. This document provides a complete reference of all configuration options.

## Required Variables

These variables MUST be set for Gondulf to start.

### GONDULF_SECRET_KEY

**Purpose**: Cryptographic secret key for token signing and session security.

**Format**: String, minimum 32 characters (256 bits recommended)

**Generation**:
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"

Example: GONDULF_SECRET_KEY=Xj3kL9m2N5pQ8rS1tU4vW7xY0zA3bC6dE9fG2hJ5kM8nP1qR4sT7uV0wX3yZ6

Security: Never commit to version control. Never log. Never expose in responses.

Validation: Must be at least 32 characters.

GONDULF_BASE_URL

Purpose: Base URL of the Gondulf server for OAuth 2.0 metadata and redirects.

Format: Full URL including protocol (http:// or https://)

Examples:

  • Production: https://auth.example.com
  • Development: http://localhost:8000

Validation:

  • Must start with http:// or https://
  • Trailing slash is automatically removed
  • HTTPS required for production (warning if http:// for non-localhost)

Used For:

  • OAuth 2.0 metadata endpoint (/.well-known/oauth-authorization-server)
  • Issuer (iss) field in responses
  • Constructing callback URLs

Optional Variables

These variables have default values and are optional.

Database Configuration

GONDULF_DATABASE_URL

Purpose: SQLite database location.

Default: sqlite:////data/gondulf.db (absolute path in container)

Format: SQLite URL format

Examples:

  • Container (absolute): sqlite:////data/gondulf.db
  • Absolute path: sqlite:////var/lib/gondulf/gondulf.db
  • Relative path (dev): sqlite:///./data/gondulf.db

Notes:

  • Three slashes (///) for relative paths
  • Four slashes (////) for absolute paths
  • Directory must exist and be writable

SMTP Configuration

GONDULF_SMTP_HOST

Purpose: SMTP server hostname for sending verification emails.

Default: localhost

Examples:

  • Local mail server: localhost
  • Gmail: smtp.gmail.com
  • SendGrid: smtp.sendgrid.net
  • Mailgun: smtp.mailgun.org

GONDULF_SMTP_PORT

Purpose: SMTP server port.

Default: 587 (STARTTLS)

Common Ports:

  • 587: STARTTLS (recommended)
  • 465: Implicit TLS
  • 25: Unencrypted (not recommended)

Validation: Must be between 1 and 65535.

GONDULF_SMTP_USERNAME

Purpose: SMTP authentication username.

Default: None (no authentication)

Format: String

Notes: Required for most SMTP providers (Gmail, SendGrid, etc.)

GONDULF_SMTP_PASSWORD

Purpose: SMTP authentication password.

Default: None (no authentication)

Format: String

Security: Never commit to version control. Use app-specific passwords for Gmail.

GONDULF_SMTP_FROM

Purpose: Sender email address for verification emails.

Default: noreply@example.com

Format: Email address

Examples:

  • noreply@example.com
  • auth@example.com
  • gondulf@example.com

Notes: Some SMTP providers require this to match the authenticated account.

GONDULF_SMTP_USE_TLS

Purpose: Enable STARTTLS for SMTP connection.

Default: true

Format: Boolean (true or false)

Notes:

  • true: Use STARTTLS (port 587)
  • false: No encryption (not recommended, development only)

Token and Code Expiry

GONDULF_TOKEN_EXPIRY

Purpose: How long access tokens are valid (seconds).

Default: 3600 (1 hour)

Range: 300 to 86400 (5 minutes to 24 hours)

Examples:

  • 1 hour (default): 3600
  • 30 minutes: 1800
  • 2 hours: 7200
  • 24 hours (maximum): 86400

Validation:

  • Minimum: 300 seconds (5 minutes)
  • Maximum: 86400 seconds (24 hours)

Security: Shorter expiry improves security but may impact user experience.

GONDULF_CODE_EXPIRY

Purpose: How long authorization and verification codes are valid (seconds).

Default: 600 (10 minutes)

Range: Positive integer

Examples:

  • 10 minutes (default): 600
  • 5 minutes: 300
  • 15 minutes: 900

Notes: Per W3C IndieAuth spec, authorization codes should expire quickly.

Token Cleanup

GONDULF_TOKEN_CLEANUP_ENABLED

Purpose: Enable automatic cleanup of expired tokens.

Default: false (manual cleanup in v1.0.0)

Format: Boolean (true or false)

Notes: Set to false for v1.0.0 as automatic cleanup is not implemented.

GONDULF_TOKEN_CLEANUP_INTERVAL

Purpose: Cleanup interval in seconds (if enabled).

Default: 3600 (1 hour)

Range: Minimum 600 seconds (10 minutes)

Validation: Must be at least 600 seconds if cleanup is enabled.

Notes: Not used in v1.0.0 (cleanup is manual).

Security Configuration

GONDULF_HTTPS_REDIRECT

Purpose: Redirect HTTP requests to HTTPS.

Default: true (production), false (debug mode)

Format: Boolean (true or false)

Notes:

  • Automatically disabled in debug mode
  • Should be true in production
  • Requires proper TLS setup (nginx, load balancer, etc.)

GONDULF_TRUST_PROXY

Purpose: Trust X-Forwarded-* headers from reverse proxy.

Default: false

Format: Boolean (true or false)

When to Enable:

  • Behind nginx reverse proxy
  • Behind load balancer (AWS ELB, etc.)
  • Behind Cloudflare or similar CDN

Security: Only enable if you control the proxy. Untrusted proxies can spoof headers.

GONDULF_SECURE_COOKIES

Purpose: Set Secure flag on cookies (HTTPS only).

Default: true (production), false (debug mode)

Format: Boolean (true or false)

Notes:

  • Automatically adjusted based on debug mode
  • Should be true in production with HTTPS

Logging

GONDULF_LOG_LEVEL

Purpose: Logging verbosity level.

Default: INFO (production), DEBUG (debug mode)

Options: DEBUG, INFO, WARNING, ERROR, CRITICAL

Examples:

  • Development: DEBUG (verbose, all details)
  • Production: INFO (normal operations)
  • Production (quiet): WARNING (only warnings and errors)

Validation: Must be one of the valid log levels.

GONDULF_DEBUG

Purpose: Enable debug mode.

Default: false

Format: Boolean (true or false)

Debug Mode Effects:

  • Sets LOG_LEVEL to DEBUG (if not explicitly set)
  • Disables HTTPS redirect
  • Disables secure cookies
  • Enables uvicorn auto-reload (if run via main.py)
  • More verbose error messages

Security: NEVER enable in production.

Configuration Examples

Development (.env)

# Development configuration
GONDULF_SECRET_KEY=dev-secret-key-change-in-production-min-32-chars
GONDULF_BASE_URL=http://localhost:8000

# Database (relative path for development)
GONDULF_DATABASE_URL=sqlite:///./data/gondulf.db

# SMTP (local development - MailHog, etc.)
GONDULF_SMTP_HOST=localhost
GONDULF_SMTP_PORT=1025
GONDULF_SMTP_FROM=dev@localhost
GONDULF_SMTP_USE_TLS=false

# Logging
GONDULF_DEBUG=true
GONDULF_LOG_LEVEL=DEBUG

Production (.env)

# REQUIRED - Generate secure key
GONDULF_SECRET_KEY=<generate-with-secrets-module>
GONDULF_BASE_URL=https://auth.example.com

# Database (absolute path in container)
GONDULF_DATABASE_URL=sqlite:////data/gondulf.db

# SMTP (SendGrid example)
GONDULF_SMTP_HOST=smtp.sendgrid.net
GONDULF_SMTP_PORT=587
GONDULF_SMTP_USERNAME=apikey
GONDULF_SMTP_PASSWORD=<sendgrid-api-key>
GONDULF_SMTP_FROM=noreply@example.com
GONDULF_SMTP_USE_TLS=true

# Token expiry (1 hour)
GONDULF_TOKEN_EXPIRY=3600
GONDULF_CODE_EXPIRY=600

# Security (behind nginx)
GONDULF_HTTPS_REDIRECT=true
GONDULF_TRUST_PROXY=true
GONDULF_SECURE_COOKIES=true

# Logging
GONDULF_DEBUG=false
GONDULF_LOG_LEVEL=INFO

Production with Gmail SMTP (.env)

GONDULF_SECRET_KEY=<generate-with-secrets-module>
GONDULF_BASE_URL=https://auth.example.com

GONDULF_DATABASE_URL=sqlite:////data/gondulf.db

# Gmail SMTP (requires app-specific password)
# https://support.google.com/accounts/answer/185833
GONDULF_SMTP_HOST=smtp.gmail.com
GONDULF_SMTP_PORT=587
GONDULF_SMTP_USERNAME=your-email@gmail.com
GONDULF_SMTP_PASSWORD=<app-specific-password>
GONDULF_SMTP_FROM=your-email@gmail.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

Secrets Management

Development

Use .env file (NOT committed to version control):

  1. Copy .env.example to .env
  2. Fill in values
  3. Ensure .env is in .gitignore

Production - Docker Secrets

Use Docker secrets for sensitive values:

services:
  gondulf:
    secrets:
      - gondulf_secret_key
      - smtp_password
    environment:
      - GONDULF_SECRET_KEY_FILE=/run/secrets/gondulf_secret_key
      - GONDULF_SMTP_PASSWORD_FILE=/run/secrets/smtp_password

secrets:
  gondulf_secret_key:
    file: ./secrets/secret_key.txt
  smtp_password:
    file: ./secrets/smtp_password.txt

Note: Requires code modification to read from *_FILE environment variables (future enhancement).

Production - Environment Variables

Pass directly via docker-compose or orchestration:

docker run -e GONDULF_SECRET_KEY="..." -e GONDULF_BASE_URL="..." gondulf:latest

Production - External Secrets Manager

Future Enhancement: Support HashiCorp Vault, AWS Secrets Manager, etc.

Validation

Configuration is validated on startup in Config.load() and Config.validate():

  1. Missing Required Values: Raises ConfigurationError
  2. Invalid Formats: Raises ConfigurationError
  3. Out of Range Values: Raises ConfigurationError
  4. Warnings: Logged but do not prevent startup

Check logs on startup for validation messages.

Environment Variable Precedence

  1. Environment variables (highest priority)
  2. .env file (loaded by python-dotenv)
  3. Default values (lowest priority)

Troubleshooting

"GONDULF_SECRET_KEY is required"

Generate a secure key:

python -c "import secrets; print(secrets.token_urlsafe(32))"

"GONDULF_BASE_URL is required"

Set base URL:

  • Development: GONDULF_BASE_URL=http://localhost:8000
  • Production: GONDULF_BASE_URL=https://auth.example.com

SMTP connection failures

Check:

  1. SMTP host and port are correct
  2. Username and password are correct (app-specific password for Gmail)
  3. TLS setting matches port (587=STARTTLS, 465=TLS, 25=none)
  4. Firewall allows outbound connections on SMTP port

HTTP warning in production

If you see "GONDULF_BASE_URL uses http:// for non-localhost domain":

  • Change GONDULF_BASE_URL to use https://
  • Set up TLS termination (nginx, load balancer, etc.)
  • IndieAuth requires HTTPS in production

See Also

  • .env.example: Template for environment variables
  • docs/architecture/security.md: Security considerations
  • docs/deployment/docker.md: Docker deployment guide

**File Location**: `/docs/deployment/environment-variables.md`

### 6. Production Deployment Checklist

**File Location**: `/docs/deployment/checklist.md`

**Purpose**: Step-by-step deployment and verification procedures for both Podman and Docker.

#### Deployment Checklist Document

```markdown
# Production Deployment Checklist

## Pre-Deployment

### System Requirements

**Container Engine** (choose one):
- [ ] Podman 4.0+ installed (recommended for security)
  - [ ] podman-compose 1.0+ installed (if using compose)
  - [ ] Rootless mode configured (recommended)
- [ ] Docker 20.10+ installed (alternative)
  - [ ] docker-compose 1.29+ installed

**System Resources**:
- [ ] 1 GB RAM available (2 GB recommended)
- [ ] 5 GB disk space available

**Network**:
- [ ] Outbound SMTP access (port 587 or 465)
- [ ] Inbound HTTPS access (port 443)
- [ ] Domain name configured (DNS A/AAAA record)

**For Rootless Podman** (recommended):
- [ ] User subuid/subgid ranges configured (`/etc/subuid`, `/etc/subgid`)
- [ ] User lingering enabled (if using systemd)

### Configuration

- [ ] Clone repository: `git clone https://github.com/yourusername/gondulf.git`
- [ ] Copy `.env.example` to `.env`
- [ ] Generate `GONDULF_SECRET_KEY`:
  ```bash
  python -c "import secrets; print(secrets.token_urlsafe(32))"
  • Set GONDULF_BASE_URL (e.g., https://auth.example.com)
  • Configure SMTP settings (host, port, username, password, from)
  • Verify .env file is in .gitignore
  • Never commit .env to version control

TLS/SSL Certificates

  • Obtain TLS certificate (Let's Encrypt, commercial CA, etc.)
  • Place certificate in nginx/ssl/fullchain.pem
  • Place private key in nginx/ssl/privkey.pem
  • Set restrictive permissions: chmod 600 nginx/ssl/privkey.pem
  • Verify certificate validity: openssl x509 -in nginx/ssl/fullchain.pem -noout -dates

nginx Configuration

  • Update nginx/conf.d/gondulf.conf with your domain name
  • Review rate limiting settings
  • Review security headers
  • Test configuration: docker-compose config

Database Initialization

  • Create data directory: mkdir -p data/backups
  • Set permissions: chmod 700 data
  • Verify volume mount point exists (if using bind mount)

Deployment

Build and Start

Using Podman (recommended):

  • Build image:
    podman build -t gondulf:latest .
    # or with compose:
    podman-compose build
    
  • Verify build completed successfully (no errors)
  • Start services:
    podman-compose -f docker-compose.yml -f docker-compose.override.yml up -d
    # or without compose:
    podman run -d --name gondulf -p 8000:8000 -v gondulf_data:/data --env-file .env gondulf:latest
    
  • Check container status:
    podman ps
    # or with compose:
    podman-compose ps
    
  • Container should show "Up" status

Using Docker:

  • Build image:
    docker-compose build
    
  • Verify build completed successfully (no errors)
  • Start services:
    docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
    
  • Check container status:
    docker-compose ps
    
  • All containers should show "Up" status

Log Verification

  • Check Gondulf logs:

    # Podman
    podman logs gondulf
    # or with compose:
    podman-compose logs gondulf
    
    # Docker
    docker-compose logs gondulf
    
  • Verify "Gondulf startup complete" message

  • No ERROR or CRITICAL log messages

  • Database initialized successfully

  • Email service initialized successfully

  • DNS service initialized successfully

  • Check nginx logs:

    docker-compose logs nginx
    
  • No configuration errors

  • nginx started successfully

Post-Deployment Verification

Health Checks

  • Internal health check (from server):

    curl http://localhost:8000/health
    

    Expected: {"status":"healthy","database":"connected"}

  • External health check (from internet):

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

    Expected: {"status":"healthy","database":"connected"}

  • Verify HTTPS redirect:

    curl -I http://auth.example.com/
    

    Expected: 301 Moved Permanently with HTTPS location

Metadata Endpoint

  • Check OAuth metadata:

    curl https://auth.example.com/.well-known/oauth-authorization-server
    

    Expected: JSON with issuer, authorization_endpoint, token_endpoint

  • Verify issuer matches GONDULF_BASE_URL

  • Verify endpoints use HTTPS URLs

Security Headers

  • Check security headers:

    curl -I https://auth.example.com/
    

    Verify presence of:

    • Strict-Transport-Security
    • X-Frame-Options: DENY
    • X-Content-Type-Options: nosniff
    • X-XSS-Protection: 1; mode=block
    • Content-Security-Policy

TLS Configuration

Database

  • Verify database file exists:

    docker-compose exec gondulf ls -lh /data/gondulf.db
    
  • Check database integrity:

    docker-compose exec gondulf sqlite3 /data/gondulf.db "PRAGMA integrity_check;"
    

    Expected: ok

  • Verify database permissions (owner: gondulf)

Backup

  • Create initial backup:

    docker-compose --profile backup run --rm backup
    
  • Verify backup created:

    ls -lh data/backups/
    
  • Test backup integrity:

    gunzip -c data/backups/gondulf_backup_*.db.gz | sqlite3 - "PRAGMA integrity_check;"
    

    Expected: ok

Email

  • Test email delivery:

    • Access authorization endpoint (initiate auth flow)
    • Request verification code
    • Verify email received
  • Check SMTP logs:

    docker-compose logs gondulf | grep -i smtp
    
  • Verify no authentication failures

  • Verify no TLS errors

Monitoring

  • Set up backup cron job:

    crontab -e
    # Add: 0 2 * * * cd /opt/gondulf && docker-compose --profile backup run --rm backup
    
  • Set up log rotation:

    docker-compose logs --no-log-prefix gondulf > /var/log/gondulf.log
    
  • Optional: Set up health check monitoring (external service)

  • Optional: Set up uptime monitoring (Pingdom, UptimeRobot, etc.)

Integration Testing

IndieAuth Flow Test

  • Access test client (e.g., https://indieauth.com/setup)
  • Enter your domain (e.g., https://yoursite.example.com)
  • Verify IndieAuth discovery finds your server
  • Complete authorization flow
  • Verify verification code email received
  • Enter verification code
  • Verify successful authentication
  • Check token issued in database:
    docker-compose exec gondulf sqlite3 /data/gondulf.db "SELECT * FROM tokens;"
    

Domain Verification Test

  • Add DNS TXT record: _gondulf.yoursite.example.com = verified

  • Verify DNS propagation:

    dig +short TXT _gondulf.yoursite.example.com
    

    Expected: "verified"

  • Add rel="me" link to your website:

    <link rel="me" href="mailto:you@example.com">
    
  • Verify rel="me" discovery:

    curl -s https://yoursite.example.com | grep 'rel="me"'
    
  • Complete authentication with domain verification

Rollback Procedure

If deployment fails:

  1. Stop containers:

    docker-compose down
    
  2. Restore previous database (if modified):

    ./scripts/restore.sh data/backups/gondulf_backup_YYYYMMDD_HHMMSS.db.gz
    
  3. Revert to previous version:

    git checkout <previous-version-tag>
    
  4. Rebuild and restart:

    docker-compose build
    docker-compose up -d
    
  5. Verify health:

    curl http://localhost:8000/health
    

Post-Deployment Tasks

  • Document deployment date and version
  • Test backup restore procedure
  • Monitor logs for 24 hours
  • Update internal documentation
  • Notify users of new authentication server (if applicable)
  • Set up regular backup verification (monthly)
  • Schedule security update reviews (monthly)

Ongoing Maintenance

Daily

  • Check health endpoint
  • Review error logs (if any)

Weekly

  • Review backup logs
  • Check disk space usage
  • Review security logs

Monthly

  • Test backup restore procedure
  • Review and update dependencies
  • Check for security updates
  • Review TLS certificate expiry (renew if < 30 days)

Quarterly

  • Review and update documentation
  • Review security headers configuration
  • Test disaster recovery procedure
  • Review rate limiting effectiveness

Troubleshooting

Container won't start

  1. Check logs: docker-compose logs gondulf
  2. Verify configuration: docker-compose config
  3. Check environment variables: docker-compose exec gondulf env | grep GONDULF
  4. Verify SECRET_KEY is set and >= 32 characters
  5. Verify BASE_URL is set

Database connection errors

  1. Check database file permissions
  2. Verify volume mount: docker-compose exec gondulf ls -la /data
  3. Check database integrity: sqlite3 data/gondulf.db "PRAGMA integrity_check;"
  4. Review DATABASE_URL setting

Email not sending

  1. Check SMTP configuration in .env
  2. Test SMTP connection: telnet smtp.example.com 587
  3. Verify credentials (app-specific password for Gmail)
  4. Check firewall rules (outbound port 587/465)
  5. Review email logs: docker-compose logs gondulf | grep -i email

HTTPS errors

  1. Verify certificate files exist and are readable
  2. Check certificate validity: openssl x509 -in nginx/ssl/fullchain.pem -noout -dates
  3. Verify private key matches certificate
  4. Check nginx configuration: docker-compose exec nginx nginx -t
  5. Review nginx logs: docker-compose logs nginx

Health check failing

  1. Check database connectivity
  2. Verify application started successfully
  3. Check for ERROR logs
  4. Verify port 8000 accessible internally
  5. Test health endpoint: curl http://localhost:8000/health

Security Checklist

  • GONDULF_SECRET_KEY is unique and >= 32 characters
  • .env file is NOT committed to version control
  • TLS certificate is valid and trusted
  • HTTPS redirect is enabled
  • Security headers are present
  • Database file permissions are restrictive (600)
  • Container runs as non-root user
  • SMTP credentials are secure (app-specific password)
  • Rate limiting is configured in nginx
  • Backup files are secured (600 permissions)
  • Firewall configured (allow 80, 443; block 8000 externally)

Compliance

  • Privacy policy published (data collection disclosure)
  • Terms of service published (if applicable)
  • GDPR compliance reviewed (if serving EU users)
  • Security contact published (security@yourdomain.com)
  • Responsible disclosure policy published

Support

For issues or questions:


**File Location**: `/docs/deployment/checklist.md`

## Configuration Examples

### Development docker-compose

**File**: `docker-compose.dev.yml`
```yaml
version: '3.8'

services:
  gondulf:
    build:
      context: .
      dockerfile: Dockerfile
    image: gondulf:dev
    container_name: gondulf_dev
    restart: unless-stopped

    volumes:
      - ./data:/data
      - ./src:/app/src  # Live code reload

    env_file:
      - .env

    environment:
      - GONDULF_DEBUG=true
      - GONDULF_LOG_LEVEL=DEBUG

    ports:
      - "8000:8000"

    command: uvicorn gondulf.main:app --host 0.0.0.0 --port 8000 --reload

    networks:
      - gondulf_network

  # MailHog for local email testing
  mailhog:
    image: mailhog/mailhog:latest
    container_name: gondulf_mailhog
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI
    networks:
      - gondulf_network

networks:
  gondulf_network:
    driver: bridge

Development .env:

GONDULF_SECRET_KEY=dev-secret-key-change-in-production-min-32-chars
GONDULF_BASE_URL=http://localhost:8000

# Database (relative path for development)
GONDULF_DATABASE_URL=sqlite:///./data/gondulf.db

# MailHog SMTP (no auth, no TLS)
GONDULF_SMTP_HOST=mailhog
GONDULF_SMTP_PORT=1025
GONDULF_SMTP_FROM=dev@localhost
GONDULF_SMTP_USE_TLS=false

GONDULF_DEBUG=true
GONDULF_LOG_LEVEL=DEBUG

Usage: docker-compose -f docker-compose.dev.yml up

Security Considerations

Container Security

  1. Non-root User: Container runs as gondulf (UID 1000, GID 1000)

    • Mitigates privilege escalation attacks
    • Follows least privilege principle
  2. Minimal Base Image: Use python:3.12-slim-bookworm

    • Reduces attack surface (fewer packages)
    • Smaller image size (faster pulls, less storage)
  3. No Secrets in Image: Secrets loaded via environment variables

    • Prevents secret leakage in image layers
    • Enables secret rotation without rebuilding
  4. Read-only Filesystem (future):

    • --read-only flag with tmpfs for /tmp
    • Prevents runtime file modifications
    • Requires tmpfs volumes for writable directories
  5. Health Checks: Enable orchestration to detect failures

    • Automatic container restart on health failure
    • Load balancer can remove unhealthy instances

Volume Security

  1. Database Volume Permissions:

    • Set to 700 (owner-only access)
    • Owned by gondulf:gondulf (UID 1000)
  2. Backup Volume:

    • Optional host mount for external backups
    • Set to 600 for backup files
  3. Sensitive File Permissions:

    • .env: 600 (owner read/write only)
    • TLS private key: 600
    • Database: 600

Network Security

  1. Container Network Isolation:

    • Dedicated bridge network (gondulf_network)
    • No direct external access to Gondulf (nginx proxy)
  2. Port Exposure:

    • Development: Port 8000 exposed
    • Production: No direct exposure (nginx only)
  3. Reverse Proxy Benefits:

    • TLS termination at nginx
    • Rate limiting at nginx
    • Additional security headers
    • Shield application from direct internet exposure

Environment Variable Security

  1. Never Commit:

    • Add .env to .gitignore
    • Never log SECRET_KEY
    • Never expose in responses
  2. Validation on Startup:

    • SECRET_KEY length validation
    • BASE_URL format validation
    • SMTP configuration validation
  3. Docker Secrets (future enhancement):

    • Use Docker secrets for sensitive values
    • Mount secrets as files in /run/secrets/
    • Read from *_FILE environment variables

Testing Strategy

Build Testing

Test 1: Docker build succeeds

docker build -t gondulf:test .
echo $?  # Expected: 0 (success)

Test 2: Tests run during build

  • Verify pytest executes in build stage
  • Verify build fails if tests fail

Test 3: Image size

docker images gondulf:test
# Expected: < 500 MB

Runtime Testing

Test 4: Container starts successfully

docker-compose up -d
docker-compose ps
# Expected: gondulf container "Up"

Test 5: Health check passes

sleep 10  # Wait for startup
curl http://localhost:8000/health
# Expected: {"status":"healthy","database":"connected"}

Test 6: Non-root user

docker-compose exec gondulf whoami
# Expected: gondulf (not root)

Test 7: Database persistence

# Create data, restart, verify data persists
docker-compose restart gondulf
# Query database, verify data exists

Backup Testing

Test 8: Backup succeeds

docker-compose --profile backup run --rm backup
ls -lh data/backups/
# Expected: New .db.gz file

Test 9: Restore succeeds

./scripts/restore.sh data/backups/gondulf_backup_*.db.gz
# Expected: Exit 0, database restored

Test 10: Backup integrity

gunzip -c data/backups/gondulf_backup_*.db.gz | sqlite3 - "PRAGMA integrity_check;"
# Expected: ok

Security Testing

Test 11: HTTPS redirect (production)

curl -I http://localhost/
# Expected: 301 with Location: https://...

Test 12: Security headers present

curl -I https://localhost/
# Verify: HSTS, X-Frame-Options, CSP, etc.

Test 13: No secrets in logs

docker-compose logs gondulf | grep -i secret
# Expected: No SECRET_KEY values

Test 14: File permissions

docker-compose exec gondulf ls -la /data/gondulf.db
# Expected: -rw------- gondulf gondulf

Integration Testing

Test 15: OAuth metadata endpoint

curl http://localhost:8000/.well-known/oauth-authorization-server | jq
# Expected: Valid JSON with issuer, endpoints

Test 16: Authorization flow

  • Manually test complete IndieAuth flow
  • Verify email received
  • Verify token issued

Acceptance Criteria

Phase 5a is complete when:

Deliverables

  • /Dockerfile exists with multi-stage build
  • /docker-compose.yml exists with base configuration
  • /docker-compose.override.yml exists with nginx proxy
  • /scripts/backup.sh exists and is executable
  • /scripts/restore.sh exists and is executable
  • /docs/deployment/environment-variables.md exists with complete reference
  • /docs/deployment/checklist.md exists with deployment procedures
  • /nginx/conf.d/gondulf.conf exists with nginx configuration

Functionality

  • Docker image builds successfully
  • Container starts and runs without errors
  • Health check endpoint responds (200 OK)
  • Database persists across container restarts
  • Backup script creates valid backups
  • Restore script restores backups successfully
  • nginx reverse proxy works (if enabled)
  • HTTPS redirect works (if enabled)
  • Security headers present in responses

Security

  • Container runs as non-root user (gondulf)
  • Database file has restrictive permissions (600)
  • No secrets in Docker image layers
  • Environment variables validated on startup
  • TLS configuration uses strong ciphers (nginx)
  • Health check does not expose sensitive information

Documentation

  • All environment variables documented
  • Deployment checklist complete and accurate
  • Backup/restore procedures documented
  • Troubleshooting guide included
  • Configuration examples provided (dev, production)

Testing

  • All tests (1-16) pass
  • Build succeeds with tests running
  • Container starts successfully
  • Health check passes
  • Database persists
  • Backups succeed
  • Restore succeeds
  • Security headers present
  • No secrets in logs

Integration

  • Complete IndieAuth flow works in deployed container
  • Email verification works
  • Token issuance works
  • Metadata endpoint returns correct URLs

Implementation Notes for Developer

File Structure

Create the following files:

/Dockerfile
/docker-compose.yml
/docker-compose.override.yml
/docker-compose.dev.yml
/docker-compose.backup.yml
/scripts/backup.sh
/scripts/restore.sh
/scripts/test-backup-restore.sh
/nginx/conf.d/gondulf.conf
/docs/deployment/environment-variables.md
/docs/deployment/checklist.md
/.dockerignore

.dockerignore

Create .dockerignore to exclude unnecessary files from build context:

# Git
.git
.gitignore

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.venv
venv

# Testing
.pytest_cache
.coverage
htmlcov

# Documentation
docs
*.md
!README.md

# Environment
.env
.env.*
!.env.example

# Data
data/
backups/

# IDE
.vscode
.idea
*.swp
*.swo

# Docker
Dockerfile*
docker-compose*.yml
.dockerignore

Environment Variable Loading

Current Implementation: config.py uses python-dotenv to load .env file.

No Changes Required: Current implementation already supports .env file.

Docker Consideration: Environment variables passed to container override .env file values.

Database Volume Mount

Development: Bind mount for easy access

volumes:
  - ./data:/data

Production: Named volume for Docker management

volumes:
  - gondulf_data:/data

Ownership: Container runs as UID 1000, ensure host directory (if bind mount) is accessible.

SMTP Configuration in Docker

No Special Handling Required: SMTP client in container can connect to external SMTP servers.

Firewall: Ensure outbound SMTP ports (587, 465) are allowed.

MailHog for Development: Optional local SMTP server for testing (no external SMTP needed).

Health Check Endpoint

Existing Implementation: /health endpoint exists in main.py (lines 117-159).

Returns: {"status": "healthy", "database": "connected"} (200 OK)

Dockerfile Health Check: Uses Python urllib to call endpoint (no external dependencies).

nginx Configuration

Purpose: Reverse proxy with TLS termination, rate limiting, security headers.

Not Required: Can run without nginx (direct access to port 8000).

Production Recommended: nginx provides TLS, rate limiting, and defense-in-depth.

TLS Certificates: Use Let's Encrypt (certbot), commercial CA, or self-signed (dev only).

Backup Script Implementation

SQLite Backup Method: Use VACUUM INTO (SQL command) or .backup (CLI).

Chosen Approach: VACUUM INTO via sqlite3 CLI (universally available).

Hot Backup: VACUUM INTO is safe for hot backups (read-only lock).

Compression: Use gzip for space efficiency (typical 3-5x reduction).

Testing: Include integrity check (PRAGMA integrity_check) in both backup and restore.

Testing During Build

Build Stage: Runs pytest with all tests.

Build Failure: Build fails if any test fails (prevents deploying broken code).

Test Dependencies: Include dev and test dependencies in build stage only.

Production Image: No test dependencies (use --no-dev for final stage).

Multi-stage Build Benefits

  1. Smaller Image: Only runtime dependencies in final image
  2. Build Cache: Dependency layer cached separately from code
  3. Security: Build tools not in production image
  4. Testing: Tests run during build (fail fast)

Developer Workflow

  1. Review Design: Read this document completely
  2. Create Files: Create all files listed in structure
  3. Test Build: Build Docker image and verify
  4. Test Runtime: Start container and verify health
  5. Test Backup: Run backup and restore scripts
  6. Integration Test: Complete IndieAuth flow
  7. Documentation: Verify all docs are accurate
  8. Report: Create implementation report per standard format

Questions to Clarify (if any)

Ask "CLARIFICATION NEEDED:" before implementation if:

  • Any design detail is ambiguous
  • Technical approach is unclear
  • Testing strategy needs elaboration
  • Acceptance criteria is not measurable

References