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>
2376 lines
61 KiB
Markdown
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/
|