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