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.