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>
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:
- Production-ready OCI container with security hardening
- Simple deployment using podman-compose or docker-compose
- Rootless container support (Podman) for enhanced security
- Database backup and restore procedures
- Complete environment variable documentation
- Deployment verification checklist
- systemd integration for production deployments
Specification References
- 12-Factor App: https://12factor.net/ (configuration, processes, disposability)
- OCI Specification: https://opencontainers.org/ (container image and runtime standards)
- Podman Documentation: https://docs.podman.io/
- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/
- Dockerfile Best Practices: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- Container Security: NIST 800-190 Application Container Security Guide
- Rootless Containers: https://rootlesscontaine.rs/
- SQLite Backup: https://www.sqlite.org/backup.html
Design Overview
Phase 5a delivers production deployment configuration through:
- OCI-Compliant Containerfile/Dockerfile: Multi-stage build optimized for both Podman and Docker
- Compose Configuration: podman-compose/docker-compose files for orchestration
- Rootless Container Support: Designed for rootless Podman deployment (recommended)
- SQLite Backup Scripts: Automated backup compatible with both container engines
- systemd Integration: Service unit generation for production deployments
- Environment Documentation: Complete reference for all GONDULF_* variables
- 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:
- Install uv package manager
- Copy pyproject.toml and uv.lock
- Install dependencies with uv (using
--frozenfor reproducibility) - Run tests (fail build if tests fail)
- 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-dirto prevent cache poisoning - Use
--frozento 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:
- Create non-root user
gondulf(UID 1000, GID 1000) - Copy virtual environment from builder stage
- Copy source code from builder stage
- Set up volume mount points
- Configure health check
- Set user to
gondulf - 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
PYTHONDONTWRITEBYTECODEprevents .pyc files in volume mountsPYTHONUNBUFFEREDensures 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
.envto version control - Use
.env.exampleas 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
.backupcommand (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
.backupcommand
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:
- Create test backup:
./scripts/backup.sh /tmp/test-backup - Stop Gondulf service:
docker-compose stop gondulf - Restore backup:
./scripts/restore.sh /tmp/test-backup/gondulf_backup_*.db.gz - Start Gondulf service:
docker-compose start gondulf - Verify service health:
curl http://localhost:8000/health - Verify data integrity: Check database content
- 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.
Method 1: Podman-Generated systemd Units (Recommended for Podman)
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://orhttps:// - 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 TLS25: 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.comauth@example.comgondulf@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
truein 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
truein 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_LEVELtoDEBUG(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):
- Copy
.env.exampleto.env - Fill in values
- Ensure
.envis 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():
- Missing Required Values: Raises
ConfigurationError - Invalid Formats: Raises
ConfigurationError - Out of Range Values: Raises
ConfigurationError - Warnings: Logged but do not prevent startup
Check logs on startup for validation messages.
Environment Variable Precedence
- Environment variables (highest priority)
.envfile (loaded bypython-dotenv)- 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:
- SMTP host and port are correct
- Username and password are correct (app-specific password for Gmail)
- TLS setting matches port (587=STARTTLS, 465=TLS, 25=none)
- 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_URLto usehttps:// - Set up TLS termination (nginx, load balancer, etc.)
- IndieAuth requires HTTPS in production
See Also
.env.example: Template for environment variablesdocs/architecture/security.md: Security considerationsdocs/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
.envfile is in.gitignore - Never commit
.envto 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.confwith 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/healthExpected:
{"status":"healthy","database":"connected"} -
External health check (from internet):
curl https://auth.example.com/healthExpected:
{"status":"healthy","database":"connected"} -
Verify HTTPS redirect:
curl -I http://auth.example.com/Expected:
301 Moved Permanentlywith HTTPS location
Metadata Endpoint
-
Check OAuth metadata:
curl https://auth.example.com/.well-known/oauth-authorization-serverExpected: JSON with
issuer,authorization_endpoint,token_endpoint -
Verify
issuermatchesGONDULF_BASE_URL -
Verify endpoints use HTTPS URLs
Security Headers
-
Check security headers:
curl -I https://auth.example.com/Verify presence of:
Strict-Transport-SecurityX-Frame-Options: DENYX-Content-Type-Options: nosniffX-XSS-Protection: 1; mode=blockContent-Security-Policy
TLS Configuration
-
Test TLS with SSL Labs: https://www.ssllabs.com/ssltest/analyze.html?d=auth.example.com
Target: Grade A or higher
-
Verify TLS 1.2 or 1.3 only
-
Verify strong cipher suites
-
No certificate warnings in browser
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
-
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.comExpected:
"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:
-
Stop containers:
docker-compose down -
Restore previous database (if modified):
./scripts/restore.sh data/backups/gondulf_backup_YYYYMMDD_HHMMSS.db.gz -
Revert to previous version:
git checkout <previous-version-tag> -
Rebuild and restart:
docker-compose build docker-compose up -d -
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
- Check logs:
docker-compose logs gondulf - Verify configuration:
docker-compose config - Check environment variables:
docker-compose exec gondulf env | grep GONDULF - Verify SECRET_KEY is set and >= 32 characters
- Verify BASE_URL is set
Database connection errors
- Check database file permissions
- Verify volume mount:
docker-compose exec gondulf ls -la /data - Check database integrity:
sqlite3 data/gondulf.db "PRAGMA integrity_check;" - Review DATABASE_URL setting
Email not sending
- Check SMTP configuration in
.env - Test SMTP connection:
telnet smtp.example.com 587 - Verify credentials (app-specific password for Gmail)
- Check firewall rules (outbound port 587/465)
- Review email logs:
docker-compose logs gondulf | grep -i email
HTTPS errors
- Verify certificate files exist and are readable
- Check certificate validity:
openssl x509 -in nginx/ssl/fullchain.pem -noout -dates - Verify private key matches certificate
- Check nginx configuration:
docker-compose exec nginx nginx -t - Review nginx logs:
docker-compose logs nginx
Health check failing
- Check database connectivity
- Verify application started successfully
- Check for ERROR logs
- Verify port 8000 accessible internally
- Test health endpoint:
curl http://localhost:8000/health
Security Checklist
GONDULF_SECRET_KEYis unique and >= 32 characters.envfile 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:
- GitHub Issues: https://github.com/yourusername/gondulf/issues
- Documentation: https://github.com/yourusername/gondulf/docs
- Security: security@yourdomain.com (PGP key available)
**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
-
Non-root User: Container runs as
gondulf(UID 1000, GID 1000)- Mitigates privilege escalation attacks
- Follows least privilege principle
-
Minimal Base Image: Use
python:3.12-slim-bookworm- Reduces attack surface (fewer packages)
- Smaller image size (faster pulls, less storage)
-
No Secrets in Image: Secrets loaded via environment variables
- Prevents secret leakage in image layers
- Enables secret rotation without rebuilding
-
Read-only Filesystem (future):
--read-onlyflag with tmpfs for /tmp- Prevents runtime file modifications
- Requires tmpfs volumes for writable directories
-
Health Checks: Enable orchestration to detect failures
- Automatic container restart on health failure
- Load balancer can remove unhealthy instances
Volume Security
-
Database Volume Permissions:
- Set to 700 (owner-only access)
- Owned by gondulf:gondulf (UID 1000)
-
Backup Volume:
- Optional host mount for external backups
- Set to 600 for backup files
-
Sensitive File Permissions:
.env: 600 (owner read/write only)- TLS private key: 600
- Database: 600
Network Security
-
Container Network Isolation:
- Dedicated bridge network (
gondulf_network) - No direct external access to Gondulf (nginx proxy)
- Dedicated bridge network (
-
Port Exposure:
- Development: Port 8000 exposed
- Production: No direct exposure (nginx only)
-
Reverse Proxy Benefits:
- TLS termination at nginx
- Rate limiting at nginx
- Additional security headers
- Shield application from direct internet exposure
Environment Variable Security
-
Never Commit:
- Add
.envto.gitignore - Never log SECRET_KEY
- Never expose in responses
- Add
-
Validation on Startup:
- SECRET_KEY length validation
- BASE_URL format validation
- SMTP configuration validation
-
Docker Secrets (future enhancement):
- Use Docker secrets for sensitive values
- Mount secrets as files in
/run/secrets/ - Read from
*_FILEenvironment 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
- ✅
/Dockerfileexists with multi-stage build - ✅
/docker-compose.ymlexists with base configuration - ✅
/docker-compose.override.ymlexists with nginx proxy - ✅
/scripts/backup.shexists and is executable - ✅
/scripts/restore.shexists and is executable - ✅
/docs/deployment/environment-variables.mdexists with complete reference - ✅
/docs/deployment/checklist.mdexists with deployment procedures - ✅
/nginx/conf.d/gondulf.confexists 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
- Smaller Image: Only runtime dependencies in final image
- Build Cache: Dependency layer cached separately from code
- Security: Build tools not in production image
- Testing: Tests run during build (fail fast)
Developer Workflow
- Review Design: Read this document completely
- Create Files: Create all files listed in structure
- Test Build: Build Docker image and verify
- Test Runtime: Start container and verify health
- Test Backup: Run backup and restore scripts
- Integration Test: Complete IndieAuth flow
- Documentation: Verify all docs are accurate
- 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
- W3C IndieAuth Specification: https://www.w3.org/TR/indieauth/
- 12-Factor App Methodology: https://12factor.net/
- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/
- Dockerfile Best Practices: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- SQLite Backup API: https://www.sqlite.org/backup.html
- NIST 800-190 Container Security: https://csrc.nist.gov/publications/detail/sp/800-190/final
- OAuth 2.0 Security Best Practices: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
- nginx Security Headers: https://owasp.org/www-project-secure-headers/