Skip to content

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.

Copy the example that matches your tier and fill in secrets:

Terminal window
cp .env.pro.example .env.pro # or .env.lite.example / .env.max.example

Pass the env file to every docker compose invocation:

Terminal window
docker compose --env-file .env.pro up -d

The 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:

VariableRequirement
SECRET_KEYMust be set and ≥ 32 characters
ENCRYPTION_SALTMust be set and changed from any default
POSTGRES_PASSWORDMust be set
LOGDB_URLMust be set (full postgresql+asyncpg://… connection string to the TimescaleDB log database)
REDIS_PASSWORDMust be set

The container exits with a startup error. Running with placeholder secrets in production is a critical vulnerability - the hard failure is intentional.

Terminal window
# SECRET_KEY - use ≥ 48 chars for headroom above the 32-char minimum
python -c "import secrets; print(secrets.token_urlsafe(48))"
# ENCRYPTION_SALT
python -c "import secrets; print(secrets.token_urlsafe(32))"
# POSTGRES_PASSWORD, LOGDB_PASSWORD, REDIS_PASSWORD
python -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:password
python -c "import secrets; print('admin:' + secrets.token_urlsafe(24))"

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.

VariableRequired in prodDescription
SECRET_KEYYesJWT signing key and Fernet key derivation input. Min 32 chars.
ENCRYPTION_SALTYesCombined with SECRET_KEY via PBKDF2 to derive the Fernet key for all stored credentials.
POSTGRES_PASSWORDYesPassword for the primary PostgreSQL database.
LOGDB_PASSWORDYesPassword for the TimescaleDB log database.
REDIS_PASSWORDYesPassword for Valkey (the redis service).
FLOWER_BASIC_AUTHRecommendedHTTP Basic Auth for the Flower dashboard - format user:password.
VariableDefaultDescription
ENVIRONMENTdevelopmentSet 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_CONCURRENCY2Gunicorn worker count. Each worker uses ~300-375 MB - size API_MEM_LIMIT accordingly.
CELERY_CONCURRENCY4Prefork pool size for the default Celery worker.
LOG_LEVELINFOPython 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.
VariableExampleDescription
CADDY_SITE_ADDRESSfreesdn.example.comControls Caddy TLS mode. See Networking and TLS.
CADDY_ACME_EMAILadmin@example.comEmail for Let’s Encrypt certificate notifications.
FORWARDED_ALLOW_IPS172.29.0.0/16Subnets trusted to supply X-Forwarded-* headers. Set to the Docker network subnet so the API trusts the edge proxy.
VariableDefaultDescription
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_METRICStrueMaster switch for the metrics subsystem. The endpoint still requires METRICS_AUTH_TOKEN in production.
VariableDescription
BACKUP_GPG_RECIPIENTGPG recipient key ID for encrypting database dumps. Required in production - fail-closes without it unless BACKUP_ALLOW_PLAINTEXT=1.
BACKUP_GPG_PUBLIC_KEY_PATHHost 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.
VariableDefaultDescription
DB_POOL_SIZE20SQLAlchemy 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_OVERFLOW30Additional connections above DB_POOL_SIZE allowed during spikes.

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:

.env.pro
COMPOSE_PROJECT_NAME=freesdn-pro
NETWORK_SUBNET=172.29.0.0/16
FORWARDED_ALLOW_IPS=172.29.0.0/16
# .env.staging (on the same host)
COMPOSE_PROJECT_NAME=freesdn-staging
NETWORK_SUBNET=172.30.0.0/16
FORWARDED_ALLOW_IPS=172.30.0.0/16

Next steps: Deployment Tiers - tier model and up commands. Compose Profiles - opt-in services.

All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.