AuditTrail Deployment Guide

Docker Compose Production Setup

2026-04-08 update: the in-container nginx service was removed (audit fix #88). The reference production deployment runs Caddy on the host as the single TLS / reverse-proxy layer and proxies through the docker-compose network to the api and web containers (both bound to 127.0.0.1). The compose file ships with two services only: api and web.

The included docker-compose.yml defines two services: the FastAPI API and the Next.js frontend. Both expose their ports on 127.0.0.1 so they are reachable only from the host's loopback interface.

Build and Start

bash
cd AuditTrailCodebase
 
# Set the secret key (required for production)
export AUDITTRAIL_SECRET_KEY=$(openssl rand -hex 32)
 
# Run schema migrations BEFORE starting the API container
docker compose run --rm api alembic upgrade head
 
# Build and start all services
docker compose up -d --build

The stack exposes (host-loopback only):

PortServiceDescription
127.0.0.1:8000apiFastAPI backend
127.0.0.1:3000webNext.js frontend

A host reverse proxy (Caddy / nginx / Traefik) terminates TLS and proxies public traffic to these loopback ports.

Persistent Data

The API stores its SQLite database in a named Docker volume api-data, mapped to /app/data inside the container. This volume survives container rebuilds.

To inspect the volume:

bash
docker volume inspect audittrailcodebase_api-data

Environment Variables for Production

Create a .env file in the project root or set these variables in your deployment environment. See .env.example for the canonical, fully-commented list.

bash
# REQUIRED: Change from the default. Generate with: openssl rand -hex 32
AUDITTRAIL_SECRET_KEY=your-production-secret-key
 
# Database URL
# SQLite (default, fine for small deployments):
DATABASE_URL=sqlite+aiosqlite:///data/audittrail.db
# PostgreSQL (recommended for production):
# DATABASE_URL=postgresql+asyncpg://user:password@db-host:5432/audittrail
 
# Disable debug mode
AUDITTRAIL_DEBUG=false
 
# Lock down CORS to your actual domain
AUDITTRAIL_CORS_ORIGINS=["https://your-domain.com"]
 
# PII redaction (enabled by default, keep it on)
AUDITTRAIL_PII_REDACTION_ENABLED=true
 
# Data retention (hourly background job hard-deletes traces older than this)
AUDITTRAIL_RETENTION_DAYS=90
 
# Authentication — JWT TTL bounds blast radius of stale tokens
AUDITTRAIL_JWT_EXPIRY_HOURS=1
 
# Real LLM ablation engine (off by default — costs money when enabled)
AUDITTRAIL_ABLATION_REAL_LLM_ENABLED=false
AUDITTRAIL_ABLATION_LLM_MODEL=anthropic:claude-3-5-haiku-latest
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
 
# Frontend → backend rewrite (server-side only — NEVER prefixed NEXT_PUBLIC_)
# Set inside the web container's environment block in docker-compose.yml.
API_INTERNAL_URL=http://api:8000/api
 
# Required so Next.js 16's standalone server.js binds to all interfaces
# inside the container instead of the container ID.
HOSTNAME=0.0.0.0

Do NOT set NEXT_PUBLIC_API_URL or NEXT_PUBLIC_WS_URL in production. The frontend uses relative URLs (/api) so it works on any host without rebuild, and the WebSocket base is derived from window.location at runtime. Setting NEXT_PUBLIC_* vars at runtime has no effect because they are baked into the browser bundle at build time.

Reverse Proxy (Caddy on the host)

The reference production setup runs Caddy on the Lightsail host with this Caddyfile:

caddy
auditrail.imaginaerium.in {
    encode zstd gzip
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Frame-Options DENY
        X-Content-Type-Options nosniff
        Referrer-Policy strict-origin-when-cross-origin
    }
 
    @api path /api/*
    reverse_proxy @api 127.0.0.1:8000
 
    @ws path /ws/*
    reverse_proxy @ws 127.0.0.1:8000
 
    reverse_proxy 127.0.0.1:3000
}

Caddy auto-provisions and renews TLS certificates from Let's Encrypt — no manual cert management required.

If you prefer nginx, the historical configuration that used to ship with this repo (with WebSocket upgrade headers, gzip, security headers, and 300s ablation timeouts) is preserved in the Wave 5 audit notes for reference.

Database Migrations

Schema management is Alembic-only as of v1.0.2. Run migrations every time you deploy a new version, BEFORE starting the API container, so any new columns / tables are in place when the app boots:

bash
docker compose run --rm api alembic upgrade head

The init_db() call inside the FastAPI lifespan only runs Base.metadata.create_all for fresh-install convenience and ensures SQLite is in WAL mode — it does NOT patch existing schemas.

SQLite WAL Mode Considerations

SQLite is the default database for AuditTrail. In production with SQLite, note the following:

WAL (Write-Ahead Logging) mode is recommended for better concurrent read performance. SQLite enables WAL mode automatically with aiosqlite, but you can verify it:

bash
sqlite3 data/audittrail.db "PRAGMA journal_mode;"

If it does not return wal, enable it:

bash
sqlite3 data/audittrail.db "PRAGMA journal_mode=WAL;"

Limitations of SQLite in production:

  • File-level write locking -- only one writer at a time. Under heavy ingest load, writes queue up.
  • No connection pooling -- each request opens/closes the file.
  • Database file must be on a local filesystem (not NFS or network shares).
  • Maximum recommended concurrent users: ~10-20 for an observability dashboard.

When to switch to PostgreSQL: If you experience write contention (slow ingest under load), need concurrent multi-user access, or want JSONB queries for metadata search, set DATABASE_URL to a PostgreSQL connection string:

DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/audittrail

No code changes are required -- SQLAlchemy abstracts the database layer. Run Alembic migrations after switching:

bash
cd apps/api
alembic upgrade head

Data Backup

SQLite Backup

Use the SQLite .backup command for a consistent snapshot (safe even while the server is running):

bash
sqlite3 data/audittrail.db ".backup data/audittrail_backup_$(date +%Y%m%d).db"

For Docker deployments, exec into the container:

bash
docker compose exec api sqlite3 /app/data/audittrail.db \
  ".backup /app/data/audittrail_backup.db"

Then copy the backup out:

bash
docker compose cp api:/app/data/audittrail_backup.db ./backups/

Automated Backup Script

bash
#!/bin/bash
# backup.sh -- Run via cron: 0 2 * * * /path/to/backup.sh
BACKUP_DIR="/path/to/backups"
DATE=$(date +%Y%m%d_%H%M%S)
 
mkdir -p "$BACKUP_DIR"
 
docker compose exec -T api sqlite3 /app/data/audittrail.db \
  ".backup /app/data/backup_${DATE}.db"
 
docker compose cp "api:/app/data/backup_${DATE}.db" "$BACKUP_DIR/"
 
# Remove backups older than 30 days
find "$BACKUP_DIR" -name "backup_*.db" -mtime +30 -delete

Generated Reports

PDF reports are stored at data/reports/ inside the API container. Back these up separately if needed:

bash
docker compose cp api:/app/data/reports/ ./backups/reports/

Monitoring

Health Endpoint Polling

The API exposes GET /api/v1/health for health checks. The Docker Compose file already configures a health check that polls this endpoint every 30 seconds.

For external monitoring, poll the health endpoint and alert on:

  • status is not "ok" (database connectivity issue)
  • db_connected is false
  • rules_loaded is 0 (rule directory misconfigured)
  • Endpoint is unreachable (service down)

Example with curl:

bash
curl -sf http://localhost:8000/api/v1/health | python3 -c "
import json, sys
data = json.load(sys.stdin)
if data['status'] != 'ok':
    print(f'ALERT: AuditTrail health degraded: {data}')
    sys.exit(1)
print('OK')
"

Instance Info

GET /api/v1/instance provides additional metrics useful for dashboards:

bash
curl http://localhost:8000/api/v1/instance

Returns version, connected agent count, rule count, and uptime.

Container Logs

bash
# All services
docker compose logs -f
 
# API only
docker compose logs -f api
 
# Last 100 lines
docker compose logs --tail=100 api

Security Checklist

Before deploying to production, verify each item:

Authentication and Secrets

  • Change the secret key. The default dev-secret-key-change-in-production is not safe. Generate a new one:
    bash
    openssl rand -hex 32
  • Set AUDITTRAIL_DEBUG=false. Debug mode exposes verbose error details.

Network Security

  • Configure CORS origins. Set AUDITTRAIL_CORS_ORIGINS to your actual domain(s) only. Do not use ["*"] in production.
  • Enable TLS/SSL. Serve all traffic over HTTPS. WebSocket connections should use wss://.
  • Remove direct port exposure. Remove the ports mappings for api and web services so they are only accessible through nginx.
  • Review nginx rate limiting. The default is 10 req/s per IP with burst of 20. Adjust for your traffic patterns.

Data Protection

  • Enable PII redaction. Set AUDITTRAIL_PII_REDACTION_ENABLED=true (enabled by default). This redacts personally identifiable information from ingested trace data.
  • Configure data retention. Set AUDITTRAIL_RETENTION_DAYS to comply with your data retention policy.
  • Secure the database file. If using SQLite, ensure the data/ directory has restrictive file permissions. The Dockerfile creates a dedicated audittrail user for this purpose.
  • Protect API keys. The secret key portion (sk-at-...) is shown only once at creation time. It is stored as an Argon2 hash.

Constitutional Rules

  • Review default rules. The rules/ directory ships with 6 default rules (bulk delete prevention, cost guard, latency SLO, PII guard, token budget, cite sources). Review and customize for your use case.
  • Mount rules as read-only. The Docker Compose file mounts ./rules:/app/rules:ro -- keep the :ro flag to prevent the application from modifying rule files.

Backup

  • Set up automated backups. See the Data Backup section above.
  • Test restore procedure. Verify you can restore from a backup by copying it back and restarting the service.