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

2376 lines
61 KiB
Markdown

# 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
- **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:
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**:
```dockerfile
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**:
```dockerfile
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):
```dockerfile
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)
```yaml
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):
```yaml
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`):
```nginx
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):
```bash
# 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**:
```bash
# 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`
```bash
#!/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`
```bash
#!/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):
```bash
#!/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**:
```bash
# With Podman
podman-compose --profile backup run --rm backup
# With Docker
docker-compose --profile backup run --rm backup
```
**Crontab Entry**:
```cron
# 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):
```bash
#!/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**:
```bash
# 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**:
```bash
# 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**:
```ini
# 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**:
```ini
[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):
```ini
[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**:
```bash
# 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):
```ini
[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**:
```ini
[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**:
```bash
# 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**:
```bash
# 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
```markdown
# 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)
```bash
# 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)
```bash
# 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)
```bash
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:
```yaml
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:
```bash
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:
```bash
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:
```bash
podman build -t gondulf:latest .
# or with compose:
podman-compose build
```
- [ ] Verify build completed successfully (no errors)
- [ ] Start services:
```bash
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:
```bash
podman ps
# or with compose:
podman-compose ps
```
- [ ] Container should show "Up" status
**Using Docker**:
- [ ] Build image:
```bash
docker-compose build
```
- [ ] Verify build completed successfully (no errors)
- [ ] Start services:
```bash
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
```
- [ ] Check container status:
```bash
docker-compose ps
```
- [ ] All containers should show "Up" status
### Log Verification
- [ ] Check Gondulf logs:
```bash
# 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:
```bash
docker-compose logs nginx
```
- [ ] No configuration errors
- [ ] nginx started successfully
## Post-Deployment Verification
### Health Checks
- [ ] Internal health check (from server):
```bash
curl http://localhost:8000/health
```
Expected: `{"status":"healthy","database":"connected"}`
- [ ] External health check (from internet):
```bash
curl https://auth.example.com/health
```
Expected: `{"status":"healthy","database":"connected"}`
- [ ] Verify HTTPS redirect:
```bash
curl -I http://auth.example.com/
```
Expected: `301 Moved Permanently` with HTTPS location
### Metadata Endpoint
- [ ] Check OAuth metadata:
```bash
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:
```bash
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
- [ ] 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:
```bash
docker-compose exec gondulf ls -lh /data/gondulf.db
```
- [ ] Check database integrity:
```bash
docker-compose exec gondulf sqlite3 /data/gondulf.db "PRAGMA integrity_check;"
```
Expected: `ok`
- [ ] Verify database permissions (owner: gondulf)
### Backup
- [ ] Create initial backup:
```bash
docker-compose --profile backup run --rm backup
```
- [ ] Verify backup created:
```bash
ls -lh data/backups/
```
- [ ] Test backup integrity:
```bash
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:
```bash
docker-compose logs gondulf | grep -i smtp
```
- [ ] Verify no authentication failures
- [ ] Verify no TLS errors
### Monitoring
- [ ] Set up backup cron job:
```bash
crontab -e
# Add: 0 2 * * * cd /opt/gondulf && docker-compose --profile backup run --rm backup
```
- [ ] Set up log rotation:
```bash
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:
```bash
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:
```bash
dig +short TXT _gondulf.yoursite.example.com
```
Expected: `"verified"`
- [ ] Add rel="me" link to your website:
```html
<link rel="me" href="mailto:you@example.com">
```
- [ ] Verify rel="me" discovery:
```bash
curl -s https://yoursite.example.com | grep 'rel="me"'
```
- [ ] Complete authentication with domain verification
## Rollback Procedure
If deployment fails:
1. [ ] Stop containers:
```bash
docker-compose down
```
2. [ ] Restore previous database (if modified):
```bash
./scripts/restore.sh data/backups/gondulf_backup_YYYYMMDD_HHMMSS.db.gz
```
3. [ ] Revert to previous version:
```bash
git checkout <previous-version-tag>
```
4. [ ] Rebuild and restart:
```bash
docker-compose build
docker-compose up -d
```
5. [ ] Verify health:
```bash
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:
- 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**:
```bash
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**
```bash
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**
```bash
docker images gondulf:test
# Expected: < 500 MB
```
### Runtime Testing
**Test 4: Container starts successfully**
```bash
docker-compose up -d
docker-compose ps
# Expected: gondulf container "Up"
```
**Test 5: Health check passes**
```bash
sleep 10 # Wait for startup
curl http://localhost:8000/health
# Expected: {"status":"healthy","database":"connected"}
```
**Test 6: Non-root user**
```bash
docker-compose exec gondulf whoami
# Expected: gondulf (not root)
```
**Test 7: Database persistence**
```bash
# Create data, restart, verify data persists
docker-compose restart gondulf
# Query database, verify data exists
```
### Backup Testing
**Test 8: Backup succeeds**
```bash
docker-compose --profile backup run --rm backup
ls -lh data/backups/
# Expected: New .db.gz file
```
**Test 9: Restore succeeds**
```bash
./scripts/restore.sh data/backups/gondulf_backup_*.db.gz
# Expected: Exit 0, database restored
```
**Test 10: Backup integrity**
```bash
gunzip -c data/backups/gondulf_backup_*.db.gz | sqlite3 - "PRAGMA integrity_check;"
# Expected: ok
```
### Security Testing
**Test 11: HTTPS redirect (production)**
```bash
curl -I http://localhost/
# Expected: 301 with Location: https://...
```
**Test 12: Security headers present**
```bash
curl -I https://localhost/
# Verify: HSTS, X-Frame-Options, CSP, etc.
```
**Test 13: No secrets in logs**
```bash
docker-compose logs gondulf | grep -i secret
# Expected: No SECRET_KEY values
```
**Test 14: File permissions**
```bash
docker-compose exec gondulf ls -la /data/gondulf.db
# Expected: -rw------- gondulf gondulf
```
### Integration Testing
**Test 15: OAuth metadata endpoint**
```bash
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
```yaml
volumes:
- ./data:/data
```
**Production**: Named volume for Docker management
```yaml
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
- 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/