Configuration is split between the Python application and the Docker Compose stack.
Most variables - from Identity/Secrets through Fabric/App-Interconnect - are read at startup by
app/core/config.py via Pydantic Settings.
The Backups/GPG, Edge/Caddy, and Worker/API Sizing sections document
Compose-layer variables consumed by the pg-backup container shell script,
the Caddy edge service, and Celery worker commands respectively; they are not
Settings fields. MARKETPLACE_PUBLISHER_PUBLIC_KEY and MARKETPLACE_ALLOW_UNSIGNED
are read via os.getenv() in marketplace.py, not via Settings.
Variable names have no prefix (e.g. SECRET_KEY, not FREESDN_SECRET_KEY).
Start by copying a tier env-file template and filling in the secrets:
HMAC key for JWT signing and Fernet key derivation. Minimum 32 characters; use python -c "import secrets; print(secrets.token_urlsafe(64))". Must not equal any known-insecure value (changeme, secret, etc.).
ENCRYPTION_SALT
""
Yes
Salt for PBKDF2 Fernet key derivation used to encrypt all credential columns. Must be unique per deployment. Use secrets.token_urlsafe(32).
ENVIRONMENT
development
Yes
Runtime mode. Accepted values (exact, lowercase): development, staging, production. Any other value (including prod or PRODUCTION) causes a startup failure.
DEBUG
false
No
Enable debug tracing. Never set true in production.
PUBLIC_BASE_URL
http://localhost:8000
Yes
Externally-reachable URL for this instance. Used to build agent WebSocket URLs and other outbound links. Set to your domain, e.g. https://freesdn.example.com.
ALLOW_REGISTRATION
false
No
Allow unauthenticated self-registration. Keep false in production unless you want open sign-ups.
AUDIT_HMAC_KEY
(derived from SECRET_KEY)
No
Override the HMAC key used to chain audit log rows. If unset, derived deterministically from SECRET_KEY at startup. See GET /api/v1/audit/validate for the tamper-evidence check.
Hostname of the primary PostgreSQL 18.4 instance. In docker-compose this is the postgres service.
POSTGRES_PORT
5432
No
Port. Override to 6432 when PgBouncer is in front.
POSTGRES_USER
freesdn
No
Database user.
POSTGRES_PASSWORD
""
Yes
Must be a strong random password in production.
POSTGRES_DB
freesdn
No
Database name.
DATABASE_URL
(auto-built)
No
Full postgresql+asyncpg://… URL. If set, overrides the individual POSTGRES_* fields.
DB_POOL_SIZE
20
No
SQLAlchemy connection pool size per Gunicorn worker. Sized for 1,000+ devices / 50+ controllers.
DB_MAX_OVERFLOW
30
No
Maximum connections above DB_POOL_SIZE before raising TooManyConnections.
DB_POOL_TIMEOUT
30
No
Seconds to wait for a pool connection before raising.
LOGDB_URL
None
Yes
Full postgresql+asyncpg://… URL for the TimescaleDB log database (metrics, events, heartbeats, time-series). Required in production/staging. In docker-compose, auto-assembled from LOGDB_USER / LOGDB_PASSWORD / LOGDB_DB.
FreeSDN uses Valkey (drop-in Redis-compatible) for cache, broker, and task results.
The redis:// URL scheme and redis service name are retained for compatibility.
Variable
Default
Required in prod?
Purpose
REDIS_HOST
localhost
No
Valkey/Redis hostname for single-node mode. Ignored when REDIS_SENTINELS is set.
REDIS_PORT
6379
No
Port for single-node mode.
REDIS_PASSWORD
None
Yes
Required in production/staging. The app refuses to start without it.
REDIS_DB
0
No
Redis logical database index.
REDIS_SENTINELS
""
No
Comma-separated host:port list of Sentinel nodes, e.g. redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379. When set, all clients use Sentinel-resolved master for automatic failover. Empty = single-node direct connection (Lite/Pro).
REDIS_MASTER_NAME
freesdn-master
No
Sentinel master name. Must match the Sentinel configuration.
REDIS_URL
(auto-built)
No
Full redis://… URL. Auto-assembled from the individual fields.
FreeSDN validates CORS origins strictly. Wildcards (*) and null are rejected.
In production, http:// is only permitted for loopback addresses, which are then
stripped from the final list.
Variable
Default
Required in prod?
Purpose
CORS_ORIGINS
["http://localhost:3000","http://localhost:5173"]
No
JSON array of allowed origins. Must be absolute URLs (scheme://host[:port]), no paths, no wildcards. In production set to your HTTPS domain, e.g. ["https://freesdn.example.com"]. Localhost origins are automatically stripped in production.
Toggles the public agent-download rate limiter only. Does not affect the global RateLimitMiddleware or auth-endpoint limits (those are always active).
RATE_LIMIT_DEFAULT
100/minute
No
Reserved. Currently has no effect - the middleware reads only RATE_LIMIT_RPM and RATE_LIMIT_BURST.
RATE_LIMIT_AUTH
5/minute
No
Defined for documentation purposes. The auth-endpoint rate limit is currently hardcoded at 5 attempts per 60-second window per IP (constant _AUTH_RATE_LIMIT in endpoints/auth.py); changing this env var has no effect. The following endpoints enforce it: /auth/token, /auth/login, /auth/register, /auth/password/reset-request, and /auth/login/mfa. /auth/password/reset is not covered by this limiter (it is protected by single-use JWT token blacklisting instead).
RATE_LIMIT_RPM
600
No
Global per-principal request limit (RPM). Keyed by authenticated user JWT sub when an access cookie is present, otherwise by client IP.
RATE_LIMIT_BURST
120
No
Burst allowance above RATE_LIMIT_RPM for short spikes from an SPA issuing parallel requests.
When true, writes to any managed controller (Omada, OPNsense, Hikvision, etc.) are staged instead of applied. Set false only after validating your adapter configuration in staging.
OMADA_READ_ONLY
true
No
Omada-specific write gate. Omada writes are blocked when either this orADAPTER_READ_ONLY is true (logical OR). To allow live Omada writes you must set bothADAPTER_READ_ONLY=falseandOMADA_READ_ONLY=false (plus force=true per call). These are independent flags, not aliases.
PROXMOX_UPLOAD_DIR
/var/lib/freesdn/uploads
No
Directory the Proxmox storage-upload applier may read from. Guards against a malicious staging payload using a path like /etc/passwd. Mount a dedicated volume.
The Prometheus exposition endpoint (GET /metrics) is mounted internally on the
API container at port 8000. It is not exposed through the Caddy edge in the
shipped compose.
Variable
Default
Required in prod?
Purpose
ENABLE_METRICS
true
No
Toggle the /metrics endpoint on or off.
METRICS_AUTH_TOKEN
None
Yes (if scraping)
When set, /metrics requires Authorization: Bearer <token>. In production without this token the endpoint is not served (fail-closed). Generate with openssl rand -hex 32. Configure Prometheus with a matching bearer_token_file.
ENABLE_DOCS
true
No
Expose Swagger (/api/v1/docs) and ReDoc (/api/v1/redoc). Docs are automatically disabled when ENVIRONMENT=production regardless of this flag. Set false explicitly to also suppress them in staging.
ENABLE_PROFILING
false
No
Enable the py-spy / pyinstrument profiling middleware. Never enable in production.
Hex-encoded Ed25519 public key of the catalog publisher. Catalog entries not signed with the matching private key are refused. Generate a keypair with backend/scripts/sign_marketplace_catalog.py keygen.
MARKETPLACE_ALLOW_UNSIGNED
0
No
Set 1 only for a fully-trusted private/dev registry with no public distribution. Never set in production.
PLUGIN_ENABLE_DIRECT_URL_INSTALLS
false
No
Allow plugins to be installed from a direct URL (not the marketplace). Enable only in controlled environments.
PLUGIN_ALLOW_RUNTIME_PYTHON_DEPS
false
No
Allow pip to install plugin Python dependencies at runtime. When enabled, --require-hashes is enforced and installs are pinned to PLUGIN_PYPI_INDEX_URL.
PLUGIN_PYPI_INDEX_URL
https://pypi.org/simple/
No
PyPI index for hash-pinned plugin dependency installs when PLUGIN_ALLOW_RUNTIME_PYTHON_DEPS=true.
GPG recipient email used to asymmetrically encrypt pg_dump output. Without this, the pg-backup container refuses to write any dumps - plaintext (unencrypted) dumps are blocked by default in all environments via BACKUP_ALLOW_PLAINTEXT (default 0). Set BACKUP_ALLOW_PLAINTEXT=1 in dev/homelab to allow unencrypted dumps.
BACKUP_GPG_PUBLIC_KEY_PATH
""
Yes in prod
Host path to the ASCII-armoured GPG public key, e.g. ./secrets/backup-public.asc. The matching private key must live on a separate secured restore host - the backup container can encrypt but never decrypt.
BACKUP_OFFSITE_REMOTE
""
No
rclone remote name for off-site replication (the dr compose profile). Supports S3-compatible, B2, SFTP, WebDAV, and 60+ other rclone backends.
BACKUP_OFFSITE_PATH
freesdn-backups
No
Bucket or directory path on the off-site remote.
BACKUP_OFFSITE_INTERVAL_HOURS
24
No
Off-site sync cycle in hours.
BACKUP_OFFSITE_RETENTION_DAYS
30
No
Off-site retention window in days (independent of local 7-day retention).
BACKUP_OFFSITE_CONFIG_PATH
./secrets/rclone.conf
No
Host path to the rclone config file. If unset, Docker mounts ./secrets/rclone.conf by default.
The Caddy edge is always active in the shipped compose. TLS is automatic - no
cert scripts are needed.
Variable
Default
Purpose
CADDY_SITE_ADDRESS
:80
Controls TLS mode. :80 = plain HTTP (use behind an upstream LB). localhost = internal CA certificate. yourdomain.com = automatic Let’s Encrypt certificate (ports 80+443 must be reachable and DNS must resolve).
CADDY_ACME_EMAIL
admin@example.com
Contact email for the Let’s Encrypt ACME account. Required when CADDY_SITE_ADDRESS is a domain.
EDGE_HTTP_PORT
8080
Host port Caddy binds for HTTP. The base compose default is 8080 (no root privileges needed). Pro and Max tier env files set this to 80; the install.sh domain installer also sets it to 80 when a real domain is supplied.
EDGE_HTTPS_PORT
8443
Host port Caddy binds for HTTPS. Default is 8443 (no root required). Pro/Max tier env files set 443 for production deployments with a real domain and ACME TLS.
FORWARDED_ALLOW_IPS
(set by tier env-file)
CIDR(s) from which Gunicorn trusts X-Forwarded-* headers. Set to match NETWORK_SUBNET - each tier env-file (env.lite / .env.pro / .env.max) sets this explicitly to the matching subnet. It is not automatically derived from NETWORK_SUBNET: the compose default fallback is 172.28.0.0/16 (Lite subnet). If you customise NETWORK_SUBNET you must also set FORWARDED_ALLOW_IPS to the same CIDR. Never set to * - this allows spoofed client IPs, bypassing the per-IP rate limiter on auth endpoints.
NETWORK_SUBNET
172.28.0.0/16
Docker bridge CIDR. Each tier uses a distinct subnet so multiple instances can coexist on one host (Lite=172.28, Pro=172.29, Max=172.30).
AI provider credentials are configured per Organisation via the UI
(Settings → AI Policy → Providers), not via environment variables. The env
layer exposes two overrides:
Variable
Default
Purpose
LLM_GLOBALLY_ENABLED
false
Global kill switch. Must be true before any Organisation can use AI features. The three-layer governance chain: global kill switch → per-org policy (DISABLED / LOCAL_ONLY / CLOUD_APPROVED) → PII redaction before any cloud call.
AI_PROVIDER_PROXY_HOSTS
""
Comma-separated hostnames that may be used as HTTPS proxies for AI provider API calls. For reverse-proxy deployments where api.openai.com / api.anthropic.com is not directly reachable. Scheme must be https. Note: this variable is not yet declared in app/core/config.py’s Settings class; because Settings uses extra="ignore", setting it in the env file has no effect until the field is added to Settings. Track [GitHub issue TBD].
The three supported providers are OpenAI, Anthropic, and Ollama
(configured per org; no global API-key env var - credentials are encrypted in
the database). Ollama base_url defaults to http://localhost:11434.
Persistent store for staged Fabric write blobs (e.g. camera snapshots destined for TrueNAS datasets). Blobs persist here until an operator approves the staged change. Mount a persistent volume - the default is ephemeral and a container restart loses pending blobs.
FABRIC_WEBHOOK_ALLOWED_HOSTS
""
Comma-separated hostnames/IPs that the fabric.webhook operation may reach on private/LAN/tailnet addresses (e.g. n8n.example.net,192.168.1.150). Deploy-owner controlled - not per-org operator input. Destinations are DNS-pinned + TLS-verified; cloud-metadata is never reachable. Empty = public destinations only.
FABRIC_WEBHOOK_SIGNING_SECRET
""
Shared secret for HMAC-signing outbound fabric.webhook bodies. When set, every outbound POST carries X-Fabric-Signature: sha256=<hmac> so the receiver can verify origin. Empty = unsigned.
When true, the app refuses to start if event_bus or modules fail to initialise. Non-critical subsystems (automation, plugins, device_sync) always degrade gracefully.
READINESS_STRICT_DEPS
false
When true, /health/ready returns 503 if Redis or LogDB are unreachable, in addition to the primary DB. Default false prevents a Valkey Sentinel failover window from pulling all API instances simultaneously.
Off by default - a self-hosted FreeSDN install is unlimited. Enable only when running FreeSDN as a multi-tenant SaaS and you want to enforce per-org tier quotas (set each org’s settings["tier"] via the admin API).
Number of Gunicorn worker processes. Rule of thumb: (2 × CPU cores) + 1. Each worker holds its own DB connection pool (DB_POOL_SIZE + DB_MAX_OVERFLOW).
CELERY_CONCURRENCY
4
Concurrent task slots for the default/priority Celery worker.
CELERY_IO_CONCURRENCY
2
Concurrent task slots for the discovery/sync/metrics IO worker.
WORKER_QUEUES
default,priority
Queues consumed by the main Celery worker.
WORKER_IO_QUEUES
discovery,sync,metrics
Queues consumed by the IO-bound Celery worker (io-worker profile).
API_MEM_LIMIT
(tier-dependent)
Docker memory limit for the API container. Sizing rule: >= WEB_CONCURRENCY × 375 MB + 250 MB headroom. Pro tier: 1G with 2 workers. Max tier: 2G with 4 workers.