Skip to content

Configuration Reference

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:

Terminal window
cp .env.pro.example .env.pro
# edit .env.pro, then:
docker compose --env-file .env.pro up -d

VariableDefaultRequired in prod?Purpose
SECRET_KEY""YesHMAC 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""YesSalt for PBKDF2 Fernet key derivation used to encrypt all credential columns. Must be unique per deployment. Use secrets.token_urlsafe(32).
ENVIRONMENTdevelopmentYesRuntime mode. Accepted values (exact, lowercase): development, staging, production. Any other value (including prod or PRODUCTION) causes a startup failure.
DEBUGfalseNoEnable debug tracing. Never set true in production.
PUBLIC_BASE_URLhttp://localhost:8000YesExternally-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_REGISTRATIONfalseNoAllow unauthenticated self-registration. Keep false in production unless you want open sign-ups.
AUDIT_HMAC_KEY(derived from SECRET_KEY)NoOverride 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.

VariableDefaultRequired in prod?Purpose
POSTGRES_HOSTlocalhostNoHostname of the primary PostgreSQL 18.4 instance. In docker-compose this is the postgres service.
POSTGRES_PORT5432NoPort. Override to 6432 when PgBouncer is in front.
POSTGRES_USERfreesdnNoDatabase user.
POSTGRES_PASSWORD""YesMust be a strong random password in production.
POSTGRES_DBfreesdnNoDatabase name.
DATABASE_URL(auto-built)NoFull postgresql+asyncpg://… URL. If set, overrides the individual POSTGRES_* fields.
DB_POOL_SIZE20NoSQLAlchemy connection pool size per Gunicorn worker. Sized for 1,000+ devices / 50+ controllers.
DB_MAX_OVERFLOW30NoMaximum connections above DB_POOL_SIZE before raising TooManyConnections.
DB_POOL_TIMEOUT30NoSeconds to wait for a pool connection before raising.
LOGDB_URLNoneYesFull 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.
LOGDB_POOL_SIZE10NoConnection pool size for the LogDB.
LOGDB_MAX_OVERFLOW15NoOverflow above LOGDB_POOL_SIZE.

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.

VariableDefaultRequired in prod?Purpose
REDIS_HOSTlocalhostNoValkey/Redis hostname for single-node mode. Ignored when REDIS_SENTINELS is set.
REDIS_PORT6379NoPort for single-node mode.
REDIS_PASSWORDNoneYesRequired in production/staging. The app refuses to start without it.
REDIS_DB0NoRedis logical database index.
REDIS_SENTINELS""NoComma-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_NAMEfreesdn-masterNoSentinel master name. Must match the Sentinel configuration.
REDIS_URL(auto-built)NoFull redis://… URL. Auto-assembled from the individual fields.
CELERY_BROKER_URL(same as REDIS_URL)NoOverride Celery broker independently if needed.
CELERY_RESULT_BACKEND(same as REDIS_URL)NoOverride Celery result backend independently.

VariableDefaultRequired in prod?Purpose
ALGORITHMHS256NoJWT signing algorithm.
ACCESS_TOKEN_EXPIRE_MINUTES30NoAccess token TTL in minutes.
REFRESH_TOKEN_EXPIRE_DAYS7NoRefresh token TTL in days.
PASSWORD_MIN_LENGTH12NoMinimum password length enforced on creation/change.
PASSWORD_REQUIRE_UPPERCASEtrueNoRequire at least one uppercase letter.
PASSWORD_REQUIRE_LOWERCASEtrueNoRequire at least one lowercase letter.
PASSWORD_REQUIRE_DIGITtrueNoRequire at least one digit.
PASSWORD_REQUIRE_SPECIALtrueNoRequire at least one special character.

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.

VariableDefaultRequired in prod?Purpose
CORS_ORIGINS["http://localhost:3000","http://localhost:5173"]NoJSON 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.

VariableDefaultRequired in prod?Purpose
RATE_LIMIT_ENABLEDtrueNoToggles the public agent-download rate limiter only. Does not affect the global RateLimitMiddleware or auth-endpoint limits (those are always active).
RATE_LIMIT_DEFAULT100/minuteNoReserved. Currently has no effect - the middleware reads only RATE_LIMIT_RPM and RATE_LIMIT_BURST.
RATE_LIMIT_AUTH5/minuteNoDefined 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_RPM600NoGlobal per-principal request limit (RPM). Keyed by authenticated user JWT sub when an access cookie is present, otherwise by client IP.
RATE_LIMIT_BURST120NoBurst allowance above RATE_LIMIT_RPM for short spikes from an SPA issuing parallel requests.

VariableDefaultRequired in prod?Purpose
ADAPTER_READ_ONLYtrueNoWhen 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_ONLYtrueNoOmada-specific write gate. Omada writes are blocked when either this or ADAPTER_READ_ONLY is true (logical OR). To allow live Omada writes you must set both ADAPTER_READ_ONLY=false and OMADA_READ_ONLY=false (plus force=true per call). These are independent flags, not aliases.
PROXMOX_UPLOAD_DIR/var/lib/freesdn/uploadsNoDirectory 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.

VariableDefaultRequired in prod?Purpose
ENABLE_METRICStrueNoToggle the /metrics endpoint on or off.
METRICS_AUTH_TOKENNoneYes (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_DOCStrueNoExpose 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_PROFILINGfalseNoEnable the py-spy / pyinstrument profiling middleware. Never enable in production.

VariableDefaultRequired in prod?Purpose
MARKETPLACE_PUBLISHER_PUBLIC_KEY""Yes (for marketplace sync)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_UNSIGNED0NoSet 1 only for a fully-trusted private/dev registry with no public distribution. Never set in production.
PLUGIN_ENABLE_DIRECT_URL_INSTALLSfalseNoAllow plugins to be installed from a direct URL (not the marketplace). Enable only in controlled environments.
PLUGIN_ALLOW_RUNTIME_PYTHON_DEPSfalseNoAllow 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_URLhttps://pypi.org/simple/NoPyPI index for hash-pinned plugin dependency installs when PLUGIN_ALLOW_RUNTIME_PYTHON_DEPS=true.

VariableDefaultRequired in prod?Purpose
BACKUP_GPG_RECIPIENT""Yes in prodGPG 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 prodHost 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""Norclone remote name for off-site replication (the dr compose profile). Supports S3-compatible, B2, SFTP, WebDAV, and 60+ other rclone backends.
BACKUP_OFFSITE_PATHfreesdn-backupsNoBucket or directory path on the off-site remote.
BACKUP_OFFSITE_INTERVAL_HOURS24NoOff-site sync cycle in hours.
BACKUP_OFFSITE_RETENTION_DAYS30NoOff-site retention window in days (independent of local 7-day retention).
BACKUP_OFFSITE_CONFIG_PATH./secrets/rclone.confNoHost 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.

VariableDefaultPurpose
CADDY_SITE_ADDRESS:80Controls 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_EMAILadmin@example.comContact email for the Let’s Encrypt ACME account. Required when CADDY_SITE_ADDRESS is a domain.
EDGE_HTTP_PORT8080Host 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_PORT8443Host 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_SUBNET172.28.0.0/16Docker 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:

VariableDefaultPurpose
LLM_GLOBALLY_ENABLEDfalseGlobal 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.


VariableDefaultPurpose
GO2RTC_URLhttp://go2rtc:1984Internal URL of the go2rtc restream sidecar. The backend registers streams here; go2rtc is not publicly exposed.
CAMERA_EVENT_RETENTION_DAYS90Days to retain camera alert events before the daily prune task removes them.
EVIDENCE_DIR/data/evidencePersistent directory for forensic export legal hold archives (clips + SHA-256 integrity hashes). Mount a durable volume in production.
VAPID_PUBLIC_KEY""ECDSA P-256 VAPID public key (base64url) for browser push notifications on camera alerts. Leave blank to disable WebPush.
VAPID_PRIVATE_KEY""Matching VAPID private key. Generate with vapid --gen from the py-vapid CLI.
VAPID_SUBJECTmailto:mailto: or https:// contact sent in the VAPID JWT sub claim.

VariableDefaultPurpose
FABRIC_ARTIFACT_DURABLE_DIR/data/fabric_artifactsPersistent 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.

VariableDefaultPurpose
STRICT_STARTUPfalseWhen 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_DEPSfalseWhen 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.

VariableDefaultPurpose
LOG_LEVELINFOPython log level: DEBUG, INFO, WARNING, ERROR.
LOG_FORMATjsonjson (structured, for log aggregators; the default for all environments) or text (human-readable; set explicitly for local development if preferred).

VariableDefaultPurpose
ENFORCE_ORG_QUOTASfalseOff 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).

VariableDefaultPurpose
WEB_CONCURRENCY2Number 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_CONCURRENCY4Concurrent task slots for the default/priority Celery worker.
CELERY_IO_CONCURRENCY2Concurrent task slots for the discovery/sync/metrics IO worker.
WORKER_QUEUESdefault,priorityQueues consumed by the main Celery worker.
WORKER_IO_QUEUESdiscovery,sync,metricsQueues 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.

  • Troubleshooting - boot failures, metrics 404, writes not applying, and more
  • Deployment - tier env files, compose profiles, and the install.sh installer

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.