Configuration
FreeSDN is configured entirely through environment variables in a tier env file. There are no config files to edit inside the container - every knob is an env var documented in .env.<tier>.example.
The env file model
Section titled “The env file model”Copy the example that matches your tier and fill in secrets:
cp .env.pro.example .env.pro # or .env.lite.example / .env.max.examplePass the env file to every docker compose invocation:
docker compose --env-file .env.pro up -dThe env file controls both Docker Compose variables (resource limits, profiles) and the application’s own env vars (secrets, feature flags, URLs).
Secure-by-default: production refuses to boot with placeholder secrets
Section titled “Secure-by-default: production refuses to boot with placeholder secrets”In ENVIRONMENT=production (and staging), the API validates secrets at startup. The app refuses to boot if any of the following are unset, empty, or set to a known-insecure value. The exact blocklist differs per variable: SECRET_KEY rejects values such as "changeme" or "secret"; POSTGRES_PASSWORD rejects values such as "postgres", "password", and "changeme"; ENCRYPTION_SALT rejects only the two shipped placeholder values ("freesdn-credential-salt-v1" and "CHANGE_ME_generate_a_unique_salt") - any other non-empty string, including weak ones like "changeme", is accepted, so use a randomly generated value:
| Variable | Requirement |
|---|---|
SECRET_KEY | Must be set and ≥ 32 characters |
ENCRYPTION_SALT | Must be set and changed from any default |
POSTGRES_PASSWORD | Must be set |
LOGDB_URL | Must be set (full postgresql+asyncpg://… connection string to the TimescaleDB log database) |
REDIS_PASSWORD | Must be set |
The container exits with a startup error. Running with placeholder secrets in production is a critical vulnerability - the hard failure is intentional.
Generating secrets
Section titled “Generating secrets”# SECRET_KEY - use ≥ 48 chars for headroom above the 32-char minimumpython -c "import secrets; print(secrets.token_urlsafe(48))"
# ENCRYPTION_SALTpython -c "import secrets; print(secrets.token_urlsafe(32))"
# POSTGRES_PASSWORD, LOGDB_PASSWORD, REDIS_PASSWORDpython -c "import secrets; print(secrets.token_urlsafe(32))"
# METRICS_AUTH_TOKEN (for the /metrics scrape gate)openssl rand -hex 32
# FLOWER_BASIC_AUTH - format is user:passwordpython -c "import secrets; print('admin:' + secrets.token_urlsafe(24))"Key environment variables
Section titled “Key environment variables”The full list with defaults is in .env.pro.example (and the other tier examples). The tables below cover the variables you are most likely to set.
Identity and secrets
Section titled “Identity and secrets”| Variable | Required in prod | Description |
|---|---|---|
SECRET_KEY | Yes | JWT signing key and Fernet key derivation input. Min 32 chars. |
ENCRYPTION_SALT | Yes | Combined with SECRET_KEY via PBKDF2 to derive the Fernet key for all stored credentials. |
POSTGRES_PASSWORD | Yes | Password for the primary PostgreSQL database. |
LOGDB_PASSWORD | Yes | Password for the TimescaleDB log database. |
REDIS_PASSWORD | Yes | Password for Valkey (the redis service). |
FLOWER_BASIC_AUTH | Recommended | HTTP Basic Auth for the Flower dashboard - format user:password. |
Application runtime
Section titled “Application runtime”| Variable | Default | Description |
|---|---|---|
ENVIRONMENT | development | Set to production in every tier env file. Disables Swagger/ReDoc in production unconditionally - ENABLE_DOCS only controls docs availability in non-production environments (development/staging). Never rely on the code default for production. |
WEB_CONCURRENCY | 2 | Gunicorn worker count. Each worker uses ~300-375 MB - size API_MEM_LIMIT accordingly. |
CELERY_CONCURRENCY | 4 | Prefork pool size for the default Celery worker. |
LOG_LEVEL | INFO | Python logging level. Use DEBUG for active troubleshooting, INFO for normal operational visibility, WARNING to reduce log noise in production. |
CORS_ORIGINS | ["http://localhost:3000", "http://localhost:5173"] | JSON array of allowed CORS origins. The SPA and API are same-origin via Caddy, so this only matters if a separate frontend origin calls the API directly. In production, set this explicitly to your HTTPS domain. |
Edge and TLS
Section titled “Edge and TLS”| Variable | Example | Description |
|---|---|---|
CADDY_SITE_ADDRESS | freesdn.example.com | Controls Caddy TLS mode. See Networking and TLS. |
CADDY_ACME_EMAIL | admin@example.com | Email for Let’s Encrypt certificate notifications. |
FORWARDED_ALLOW_IPS | 172.29.0.0/16 | Subnets trusted to supply X-Forwarded-* headers. Set to the Docker network subnet so the API trusts the edge proxy. |
Metrics gate
Section titled “Metrics gate”| Variable | Default | Description |
|---|---|---|
METRICS_AUTH_TOKEN | (unset) | When set, enables the /metrics Prometheus endpoint behind a Bearer token check. Without this, /metrics is not served in production. See Monitoring. |
ENABLE_METRICS | true | Master switch for the metrics subsystem. The endpoint still requires METRICS_AUTH_TOKEN in production. |
Backups
Section titled “Backups”| Variable | Description |
|---|---|
BACKUP_GPG_RECIPIENT | GPG recipient key ID for encrypting database dumps. Required in production - fail-closes without it unless BACKUP_ALLOW_PLAINTEXT=1. |
BACKUP_GPG_PUBLIC_KEY_PATH | Host path to the GPG public key file (relative to the compose project directory). Docker mounts this file read-only into the backup container. Example: ./secrets/backup-public.asc. |
Database pool (advanced)
Section titled “Database pool (advanced)”| Variable | Default | Description |
|---|---|---|
DB_POOL_SIZE | 20 | SQLAlchemy async pool size per API worker. With WEB_CONCURRENCY=2, that is 40 Postgres connections from the API alone, plus Celery workers. Enable PgBouncer (pooling profile) if connections are exhausted. |
DB_MAX_OVERFLOW | 30 | Additional connections above DB_POOL_SIZE allowed during spikes. |
Compose project name and subnet
Section titled “Compose project name and subnet”COMPOSE_PROJECT_NAME namespaces all Docker resources - container names, networks, and volumes. If you run multiple FreeSDN instances on the same host (e.g. staging alongside production), give each a distinct project name and NETWORK_SUBNET:
COMPOSE_PROJECT_NAME=freesdn-proNETWORK_SUBNET=172.29.0.0/16FORWARDED_ALLOW_IPS=172.29.0.0/16
# .env.staging (on the same host)COMPOSE_PROJECT_NAME=freesdn-stagingNETWORK_SUBNET=172.30.0.0/16FORWARDED_ALLOW_IPS=172.30.0.0/16Next steps: Deployment Tiers - tier model and up commands. Compose Profiles - opt-in services.