Production deployment with Docker
The image
One image serves both missions:
festin serve ...(default CMD) — the dashboardfestin scan ...— one-shot CLI scans
Design choices:
python:3.13-slimmulti-stage; venv built withuvfor lockfile fidelity.- Tor included so
--torworks out of the box. - Non-root user
festin, data in/data(volume). HEALTHCHECKwired to/api/v1/health.
Quick start
export FESTIN_JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
docker run -d --name festin \
-p 8420:8420 \
-e FESTIN_JWT_SECRET=$FESTIN_JWT_SECRET \
-v festin-data:/data \
cr0hn/festin:latest
# register the admin (bootstrap)
curl -X POST localhost:8420/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "a-strong-password"}'
Prebuilt images: cr0hn/festin — multi-arch (amd64 + arm64), published automatically by CI on every push to master and on v* tags.
docker compose (recommended)
The included docker-compose.yml adds restart policy, log rotation, healthcheck and an optional Tor sidecar:
export FESTIN_JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
docker compose up -d
# anonymized-scanning variant
docker compose --profile tor up -d
Production configuration
| Variable | Default | Purpose |
|---|---|---|
FESTIN_JWT_SECRET | dev fallback | JWT signing — always set in production |
FESTIN_DB_DSN | SQLite path | postgres://user:pass@host:5432/festin → PostgreSQL backend |
FESTIN_QUEUE | memory | streaq = durable Redis Streams queue |
FESTIN_REDIS_URL | redis://localhost:6379/0 | Redis endpoint (streaq mode) |
FESTIN_RATE_LIMIT_ATTEMPTS / _WINDOW | 5 / 60 | login + register throttle |
Full reference: configuration.
Production topology (SQLite)
services:
festin:
image: cr0hn/festin:latest
environment:
FESTIN_JWT_SECRET: ${FESTIN_JWT_SECRET}
volumes: [festin-data:/data]
ports: ["8420:8420"]
Production topology (PostgreSQL + queue workers)
services:
festin-api:
image: cr0hn/festin:latest
environment:
FESTIN_JWT_SECRET: ${FESTIN_JWT_SECRET}
FESTIN_DB_DSN: ${FESTIN_DB_DSN} # postgres://...
FESTIN_QUEUE: streaq
FESTIN_REDIS_URL: redis://redis:6379/0
ports: ["8420:8420"]
deploy: {replicas: 2}
festin-worker:
image: cr0hn/festin:latest
command: festin-worker
environment:
FESTIN_DB_DSN: ${FESTIN_DB_DSN}
FESTIN_REDIS_URL: redis://redis:6379/0
deploy: {replicas: 2} # scan throughput knob
redis:
image: redis:7-alpine
The API only enqueues; workers consume. Restart API pods freely — queued scans survive in Redis. Scale festin-worker replicas for scan throughput. Full HA patterns: high availability.
Operation
# logs
docker compose logs -f festin
# one-shot CLI scan using the same image
docker compose run --rm festin scan example.com --quiet --export sarif --output /data/scan.sarif
docker compose exec festin cat /data/scan.sarif
# backup the SQLite database (use the .backup command — safe while running)
docker compose exec festin python -c \
"import sqlite3; sqlite3.connect('/data/festin.db').backup('/data/backup.db')"
# upgrade
docker compose pull && docker compose up -d
Reverse proxy (TLS)
FestIn serves plain HTTP — terminate TLS in front:
Rate limiting
Login and register are rate-limited in-app (5 attempts / 60 s per IP by default — see configuration). Adding a proxy-level limit as well is recommended as a second layer.
Data lifecycle
- Everything lives in one SQLite file (
/data/festin.dbin the container). - Migrations are idempotent (
CREATE TABLE IF NOT EXISTS+ALTER TABLEguards) — upgrading is: stop, swap image, start. - Back up with
sqlite3 ... ".backup"(see above); a rawcpwhile running can catch a mid-write file.