Almost every environment variable the gateway reads flows through a single module — src/application/settings.py — which evaluates ~80 os.getenv() calls at import time, coerces types, and applies defaults. Routes, services, and the rosbridge layer all import these module-level constants, so the exact names, defaults, and coercion in settings.py are load-bearing: renaming or re-typing one is a breaking change across the whole codebase.A handful of variables bypass settings.py and are read directly with os.getenv() elsewhere (email, APP_ENVIRONMENT, AWS credentials), and a few values that look configurable are actually hardcoded. This page enumerates everything, groups it by concern, and calls out every place a default disagrees between settings.py, .env.example, and the exported production ECS task definition.
settings.py raises RuntimeErrorat import time if JWT_SECRET_KEY is empty on any non-local deployment (src/application/settings.py:118) — this fires before validate_critical_config() runs, so anything that imports settings triggers it. Set DEPLOYMENT_ENVIRONMENT=local to fall back to an insecure test key, or provide a real key.
settings.py defaults are what runs when a variable is unset. The .env.example template and the production ECS task definition (docs/aws_prod_env_vars.env) both override several of them, so the code default is frequently not what runs in production. Do not assume the settings.py value.
Variable
settings.py default
.env.example
Production (ECS)
DEPLOYMENT_ENVIRONMENT
server
local
server
USER_SITL_MAX_COUNT
3
5
5
SITL_CPU_LIMIT
2.0
2.0
1.0
SITL_VIDEO_STREAM_DRONE_STATE
ARMED
CONNECTED
CONNECTED
SEND_QUEUE_SIZE
30
(unset → 30)
200
LOG_LEVEL
20 (INFO)
20
10 (DEBUG)
REMOTE_DOCKER_HOST
ssh://nexus0@<office-docker-host>
ssh://nexus0@<office-docker-host>
tcp://jumphost-private.skyhub-prod.internal:2375
OTEL_EXPORTER_OTLP_ENDPOINT
unset (OTEL off)
http://<office-docker-host>:4318/v1/traces
http://jumphost-private.skyhub-prod.internal:4317
RESOURCE_TAG
skyhub-dev
(unset)
skyhub-prod
ACCOUNT_ID
123
(unset)
<aws-account-id>
OTLP endpoint footgun.docker-compose.yml defaults the base endpoint to http://<office-docker-host>:4318, and the exporters append /v1/traces and /v1/logs themselves. But .env.example bakes the signal path into the base var (.../v1/traces), which would make the log exporter POST to /v1/traces/v1/logs. Production points at port 4317 (the gRPC port) while the code uses the HTTP exporter. Keep this a bare host:4318 base URL. See OpenTelemetry & SigNoz.
Master switch. local derives IS_LOCAL_ENVIRONMENT=true, enabling permissive CORS, the insecure JWT fallback, local X-Drone-IP whitelisting, and optional VPN_BUCKET. Any other value = strict/non-local.
IS_LOCAL_ENVIRONMENT
derived
Not a raw env var — computed as DEPLOYMENT_ENVIRONMENT == "local" (settings.py:12). Gates CORS, the JWT secret fallback, the VPN_BUCKET requirement, and middleware IP whitelisting.
APP_ENVIRONMENT
production
Not in settings.py — read directly in main.py:265 and only in the __main__ block. If dev, runs db.create_all() instead of Flask-Migrate. Distinct from DEPLOYMENT_ENVIRONMENT.
ENABLE_SITL
true
Gates lazy SITLDroneService init (app.py:94) and allows SKYHUB_SITL_* names via X-Drone-IP in the VPN middleware.
ENABLE_REGISTRATION
true
Toggles public user registration (auth_routes.py:185,248).
ADMINS_ONLY
false
Dead config — defined in settings.py:6 but referenced nowhere else in src.
FLASK_APP
main.py
Flask CLI entrypoint for flask db migrate / upgrade. Read by the Flask CLI, not settings.py.
SOCKET_IP
None
Bind host for socketio.run() in the __main__ dev path (.env.example: 0.0.0.0). Ignored under gunicorn.
LOG_LEVEL
20 (INFO)
Numeric root logger level (configure_logging); also the level of the OTEL LoggingHandler. Prod ships 10 (DEBUG).
The URI is assembled in DBConnector as postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_IP}/{DB_NAME} (src/connector/db_connection.py:29). All four values are .strip()ped in settings.py.
Variable
Default
Purpose
DB_USERNAME
""
PostgreSQL user.
DB_PASSWORD
""
PostgreSQL password.
DB_IP
""
PostgreSQL host. Critical — validate_critical_config() raises if empty. Prod: database.skyhub-prod.internal.
HS256 signing key for access/refresh JWTs and the Socket.IO handshake. Required on non-local deployments — raises RuntimeError at import if empty (settings.py:118).
JWT_ACCESS_TOKEN_EXPIRES (10 min) and JWT_REFRESH_TOKEN_EXPIRES (12 h) are hardcoded timedeltas in main.py:247-248, not env-configurable — despite older docs listing them as env vars. The JWT algorithm is fixed to HS256, and the revocation BLOCKLIST is an in-memory set (does not survive restart or scale across workers).
Host of the external WireGuard status service queried by VPN_Service status endpoints.
VPN_SERVICE_PORT
5050
Port of the external VPN status service.
VPN_BUCKET
None
S3 bucket holding per-user/per-drone WireGuard access.conf. Critical only on non-local deployments (validated). Prod: skyhub-prod-user-vpn.
JUMPHOST_IP
""
If set, rosbridge Connections route through ws://JUMPHOST_IP:JUMPHOST_PORT and add x-drone-ip / x-drone-port headers for Nginx multiplexing. Empty string → direct connection (connection.py:27, self.direct = not self.jumphost_ip).
JUMPHOST_PORT
9090
Port on the jumphost that fronts rosbridge.
USER_NETWORK_CIDR
10.70.0.0/16
WireGuard user subnet; IPService allocates user IPs here.
DRONE_NETWORK_CIDR
10.71.0.0/16
WireGuard drone subnet. The middleware trusts any source whose IP starts with 10.71. and IPService allocates drone IPs here.
Jumphost mode is selected purely by whether JUMPHOST_IP is a non-empty string. Both the WebSocket Connection and its HTTP _test_connection() reachability probe must send identical x-drone-ip / x-drone-port headers, or the probe and the socket can diverge. Details in VPN Middleware & Jumphost.
Gate. If unset, OpenTelemetry init is skipped entirely (main.py:98). Base OTLP-HTTP endpoint; exporters append /v1/traces and /v1/logs.
OTEL_EXPORTER_OTLP_HEADERS
None
Comma-separated key=value headers (e.g. signoz-access-token=<redacted>) attached to both span and log exporters.
OTEL_SERVICE_NAME
skyhub_gateway_service
service.name resource attribute in traces/logs.
OTEL_RESOURCE_ATTRIBUTES
deployment.environment={DEPLOYMENT_ENVIRONMENT}
Defined but never applied — main.py builds Resource(attributes={...}) with hardcoded keys and does not read this var, so any extra attributes set here are silently dropped.
All four MAIL_* values are read directly with os.getenv() in src/service/email_service.py:13-16 — they are not surfaced in settings.py, so they are easy to miss when auditing config.
In settings.py. If true, EmailService runs a one-off SMTP login test at startup and warns (but continues) on failure; it does not disable real sending.
REGION is the value settings.py exposes and is validated at startup; boto3 clients additionally pick up the standard AWS_* credential vars from the environment implicitly (they are never read in code).
Variable
Default
Purpose
REGION
eu-central-1
AWS region. Critical — validated at startup. Used for S3/asset ops.
RESOURCE_TAG
skyhub-dev
AWS resource-tag prefix used for installer/resource naming in drone_routes. Prod: skyhub-prod.
ACCOUNT_ID
123
AWS account id used in installer/ECR references. Prod: <aws-account-id>.
ASSET_BUCKET
skyhub-prod-assets
S3 bucket for assets (video/image/logs).
INSTALLER_BUCKET
skyhubcore
S3 bucket for drone installer artifacts (drone_routes).
Adding a new frontend origin is a code change (main.py), not an env var. The two environment switches — DEPLOYMENT_ENVIRONMENT (local/server) and APP_ENVIRONMENT (dev/production) — are independent and easy to confuse: the first drives CORS/JWT/middleware behavior, the second only gates db.create_all() in the python main.py dev path.
The shipped .env.example boots a fully local dev stack (SITL enabled, insecure defaults). The smallest set that gets the gateway running against a local Postgres:
.env
DEPLOYMENT_ENVIRONMENT=localJWT_SECRET_KEY=<redacted> # optional locally; falls back to a test keyDB_USERNAME=idrobotsDB_PASSWORD=idrobotsDB_IP=skyhub-postgresDB_NAME=skyhubREGION=eu-central-1SOCKET_IP=0.0.0.0ENABLE_SITL=true