os.getenv() fallback and the .env / tfvars template that ships
alongside it.
For the detailed treatment of a surface — startup validation order, footguns, and
the exact code path that reads each value — follow the per-service links:
Gateway env vars ·
Dashboard build & config ·
Drone OS services ·
AWS environments & Terraform.
This page is the flat, copy-pasteable index across all of them.
The four config surfaces
| Surface | Source of truth | Format | Read when | Detail page |
|---|---|---|---|---|
| Gateway Service | src/application/settings.py (defaults) + .env.example / docker-compose.yml / SSM | shell env | Container start (import time) | Gateway env vars |
| Dashboard | src/environments/environment*.ts | TypeScript object | ng build (compile time) | Dashboard build & config |
| SkyCore Drone OS | .env (template .env.example) | shell env | On-drone docker compose up | Drone OS services |
| AWS / Terraform | skyhub_terraform/environments/<env>/settings.tfvars | HCL | terraform apply | Environments & state |
Default mismatches you must reconcile
The single most common source of “works in dev, breaks in prod” is a Python fallback insettings.py that disagrees with the value shipped in the env template or baked into the
production task definition. These are the known divergences:
| Variable | settings.py fallback | .env.example / prod | Why it matters |
|---|---|---|---|
DEPLOYMENT_ENVIRONMENT | server | .env.example: local | Flips CORS, JWT default, SITL, and IP-trust behavior |
USER_SITL_MAX_COUNT | 3 | .env.example / prod: 5 | Max SITL drones per user |
SEND_QUEUE_SIZE | 30 | prod: 200 | Per-connection outgoing rosbridge queue depth |
SITL_VIDEO_STREAM_DRONE_STATE | ARMED | .env.example / prod: CONNECTED | Drone state at which SITL video starts |
LOG_LEVEL | 20 (INFO) | prod: 10 (DEBUG) | Root logger verbosity |
Gateway Service (skyhub_gateway_service)
Every constant below is defined in src/application/settings.py unless noted. Defaults
shown are the Python os.getenv() fallback. Secrets are redacted — supply real values
via .env (local) or SSM (prod).
Deployment & environment
Deployment & environment
| Variable | Default | Purpose |
|---|---|---|
DEPLOYMENT_ENVIRONMENT | server | Master switch. local sets IS_LOCAL_ENVIRONMENT=true (permissive CORS, insecure JWT fallback, X-Drone-IP local trust, dev SITL). Any other value = strict/non-local. |
ENABLE_SITL | true | Gates lazy SITLDroneService init and SKYHUB_SITL_* X-Drone-IP trust in middleware. |
ENABLE_REGISTRATION | true | Toggles public user registration (auth_routes.py). |
ADMINS_ONLY | false | Defined but referenced nowhere in src/ — dead config. |
APP_ENVIRONMENT | production | Read only in main.py __main__ (not in settings.py). dev runs db.create_all() instead of Alembic migrations. |
FLASK_APP | main.py | Flask CLI entrypoint for flask db migrate / flask db upgrade. |
LOG_LEVEL | 20 (INFO) | Numeric root logger level; also the OTEL LoggingHandler level. Prod uses 10. |
SOCKET_IP | (unset) | Bind host for socketio.run() in __main__ dev mode. .env.example: 0.0.0.0. |
IS_LOCAL_ENVIRONMENT is derived (DEPLOYMENT_ENVIRONMENT == "local"), not a raw
env var. Note DEPLOYMENT_ENVIRONMENT (local/server) and APP_ENVIRONMENT (dev/production)
are two different switches — see Startup & validation.Database (PostgreSQL)
Database (PostgreSQL)
| Variable | Default | Purpose |
|---|---|---|
DB_USERNAME | "" | PostgreSQL user (.strip()ed). .env.example: idrobots. |
DB_PASSWORD | "" | PostgreSQL password. .env.example: idrobots. |
DB_IP | "" | PostgreSQL host. Critical — validate_critical_config() raises RuntimeError if empty. .env.example: skyhub-postgres. |
DB_NAME | "" | Database name. .env.example: skyhub. |
postgresql://… in src/connector/db_connection.py. See
Migrations & DB connection and the
Database schema reference.Authentication & JWT
Authentication & JWT
| Variable | Default | Purpose |
|---|---|---|
JWT_SECRET_KEY | test-secret-key-for-development-only (local only) | HS256 signing key for access/refresh JWTs and the Socket.IO handshake. Required — raises RuntimeError at import on non-local deployments; the insecure fallback is used only when IS_LOCAL_ENVIRONMENT. |
AWS & cloud identity
AWS & cloud identity
| Variable | Default | Purpose |
|---|---|---|
REGION | eu-central-1 | AWS region. Critical — validated at startup. Used for S3/asset ops. |
RESOURCE_TAG | skyhub-dev | Resource-tag prefix; used in drone_routes (installer, SSM /<RESOURCE_TAG>/pull_role). |
ACCOUNT_ID | 123 | AWS account id; used in drone installer/ECR references. |
ASSET_BUCKET | skyhub-prod-assets | S3 bucket for drone/user assets (video, image, logs). |
VPN_BUCKET | (unset) | S3 bucket of per-user/per-drone WireGuard access.conf. Critical on non-local deployments. Prod: skyhub-prod-user-vpn. |
INSTALLER_BUCKET | skyhubcore | S3 bucket holding docker-compose(.prod).yml handed to drones via presigned URL. |
ECR_REPO → DOCKER_REPO | <aws-account-id>.dkr.ecr.eu-central-1.amazonaws.com | Private ECR registry returned to drones for image pulls. |
AWS_ACCESS_KEY_ID | (unset) | boto3 credential (implicit) for S3 presign / asset ops. |
AWS_SECRET_ACCESS_KEY | (unset) | boto3 credential (implicit). |
AWS_DEFAULT_REGION | eu-central-1 | boto3 region auto-detect for the S3 client. |
VPN & network CIDRs
VPN & network CIDRs
| Variable | Default | Purpose |
|---|---|---|
VPN_SERVICE_IP | (unset) | Host of the external WireGuard status service, queried by VPN_Service. |
VPN_SERVICE_PORT | 5050 | Port of the external VPN status service. |
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; middleware trusts 10.71.* sources as authenticated drones. |
SITL_HOST | <office-docker-host> (from DOCKER_HOST_IP) | IP used to order/represent SITL drones in IPService. |
10.71.* trust and jumphost routing are covered in
VPN middleware & jumphost and
Network topology.Jumphost & rosbridge connection
Jumphost & rosbridge connection
| Variable | Default | Purpose |
|---|---|---|
JUMPHOST_IP | "" (empty) | If set, rosbridge Connections route through ws://JUMPHOST_IP:JUMPHOST_PORT and add x-drone-ip / x-drone-port headers. Empty string = direct connection to the drone IP. |
JUMPHOST_PORT | 9090 | Port on the jumphost fronting rosbridge. |
MAX_RECONNECT_ATTEMPTS | 15 | SmartSocket max reconnect attempts before is_expired. |
MAX_RECONNECT_TIME | 60 | Max seconds disconnected before giving up (s). |
DRONE_REACHABILITY_TIMEOUT | 2 | Connect/read timeout (s) for the HTTP probe before opening a rosbridge socket. |
ROSBRIDGE_SERVICE_TIMEOUT | 30.0 | Timeout (s) for rosbridge service calls (mission upload, geofence, param) — raised to 30 for WireGuard latency. |
REQUEUE | false | If true, failed outgoing rosbridge messages are re-queued instead of dropped. |
SEND_QUEUE_SIZE | 30 | Max size of the per-connection outgoing rosbridge queue. Prod: 200. |
TELEMETRY_THROTTLE_RATE | 200 | Min interval (ms) between telemetry messages via rosbridge throttle_rate; 0 disables throttling. |
Socket.IO
Socket.IO
| Variable | Default | Purpose |
|---|---|---|
SOCKETIO_PING_TIMEOUT | 60 | Flask-SocketIO ping timeout (s). |
SOCKETIO_PING_INTERVAL | 25 | Flask-SocketIO ping interval (s). |
SOCKETIO_MESSAGE_QUEUE | (unset) | Optional Redis URL for multi-worker fan-out. Required to scale beyond 1 gunicorn worker (rooms and the connection pool are per-process). |
DO_UNSUBSCRIBE | true | If true, unsubscribe_telemetry actually stops the underlying rosbridge subscription. |
Docker & SITL orchestration
Docker & SITL orchestration
| Variable | Default | Purpose |
|---|---|---|
DOCKER_HOST | (unset) | Docker daemon socket/host for SITL & control services. |
DOCKER_HOST_IP | (unset) | IP SITL containers connect back to; SITL drones use this instead of drone.ip. Also feeds SITL_HOST. .env.example: <office-docker-host>. |
REMOTE_DOCKER_ENABLED | false | Provision SITL containers on a remote Docker host and enable ECR-authenticated pulls. |
REMOTE_DOCKER_HOST | ssh://nexus0@<office-docker-host> | Docker SDK base_url (ssh:// uses the system ssh client; tcp:// uses the entrypoint SSH tunnel). |
SITL_IMAGE_NAME | ardupilot | ArduPilot SITL image. |
CORE_IMAGE_NAME | core:latest | Core (MAVROS + rosbridge) container image. |
GAMEPAD_IMAGE_NAME | skyhub-gamepad:latest | Gamepad control container image. |
USER_SITL_MAX_COUNT | 3 | Max SITL drones per user. .env.example / prod: 5. |
SITL_CPU_LIMIT | 2.0 | CPUs per SITL container. |
SITL_MEMORY_LIMIT | 4g | Memory per SITL container. |
CORE_CPU_LIMIT / CORE_MEMORY_LIMIT | 2.0 / 2g | Core container cgroup limits. |
GAMEPAD_CPU_LIMIT / GAMEPAD_MEMORY_LIMIT | 0.5 / 512m | Gamepad container cgroup limits. |
GAMEPAD_API_URL | http://localhost:5000 | Backend URL gamepad containers call for execution tracking (must be reachable from the remote Docker host). |
SITL_REDIS_IMAGE | redis:7-alpine | Redis image for SITL container groups. |
SITL_REDIS_HOST | host.docker.internal | Redis host for SITL/gamepad containers. |
REDIS_PASSWORD → SITL_REDIS_PASSWORD | skyhub_redis_secret | Redis auth for SITL containers. |
SITL default ArduPilot parameters
SITL default ArduPilot parameters
Applied to every newly created SITL drone (fence + battery + logging defaults).
| Variable | Default | Purpose |
|---|---|---|
SITL_FENCE_ENABLE | 1 | FENCE_ENABLE. |
SITL_FENCE_TYPE | 7 | FENCE_TYPE bitmask (alt + circle + polygon). |
SITL_FENCE_ACTION | 1 | FENCE_ACTION (1 = RTL on breach). |
SITL_FENCE_ALT_MAX | 100 | Max fence altitude (m). |
SITL_FENCE_RADIUS | 300 | Circle fence radius (m). |
SITL_BATT_VOLTAGE | 12.587 | Simulated 3S battery voltage. |
SITL_BATT_CAPACITY | 3300 | Battery capacity (mAh). |
SITL_BATT_LOW_VOLT | 10.5 | Low-voltage failsafe threshold. |
SITL_BATT_CRT_VOLT | 9.6 | Critical-voltage failsafe threshold. |
SITL_BATT_ARM_VOLT | 10.0 | Min voltage to allow arming. |
SITL_BATT_FS_LOW_ACT | 2 | Low-battery failsafe action (2 = RTL). |
LOG_ERASE_AFTER_DOWNLOAD | false | Erase FC logs after successful S3 upload. |
Video (Janus / WHIP)
Video (Janus / WHIP)
| Variable | Default | Purpose |
|---|---|---|
JANUS_URL | (unset) | Janus Gateway REST base URL for video-room management. .env.example: http://janus-gateway:8088/janus. |
WHIP_SERVER_URL | (unset) | WHIP server base URL for video ingest. .env.example: http://simple-whip-server:7080. |
SITL_VIDEO_STREAM_DRONE_STATE → VIDEO_STREAM_DRONE_STATE | ARMED | Drone state at which SITL video streaming starts. .env.example / prod: CONNECTED. |
Observability (OpenTelemetry / SigNoz)
Observability (OpenTelemetry / SigNoz)
| Variable | Default | Purpose |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | (unset) | If unset, OTEL init is skipped entirely. Base OTLP-HTTP endpoint; exporters append /v1/traces and /v1/logs. Compose default: http://<office-docker-host>:4318. |
OTEL_EXPORTER_OTLP_HEADERS | (unset) | Comma-separated key=value headers (e.g. signoz-access-token=<redacted>) attached to span + log exporters. |
OTEL_SERVICE_NAME | skyhub_gateway_service | service.name resource attribute. |
OTEL_RESOURCE_ATTRIBUTES | deployment.environment={DEPLOYMENT_ENVIRONMENT} | Defined in settings.py but never applied — main.py builds Resource() directly, so this var is silently dropped. |
Billing (Stripe)
Billing (Stripe)
| Variable | Default | Purpose |
|---|---|---|
STRIPE_SECRET_KEY | "" | Server API key. Redact — use sk_test_* in dev, sk_live_xxx in prod. |
STRIPE_PUBLISHABLE_KEY | "" | Client key (pk_test_* / pk_live_xxx). |
STRIPE_WEBHOOK_SECRET | "" | Webhook signature secret (whsec_…). |
STRIPE_PRICE_ID | "" | Price id for the €120/vehicle/year subscription. |
STRIPE_SUCCESS_URL | https://skyhub.ai/billing/success | Checkout success redirect. |
STRIPE_CANCEL_URL | https://skyhub.ai/billing/cancel | Checkout cancel redirect. |
Email (SMTP)
Email (SMTP)
| Variable | Default | Purpose |
|---|---|---|
TEST_EMAIL_SERVER | true | If true, EmailService logs emails instead of sending via SMTP. |
MAIL_SERVER | smtp.gmail.com | SMTP host. Read directly in email_service.py, not in settings.py. |
MAIL_PORT | 587 | SMTP port. |
MAIL_USERNAME | [email protected] | SMTP sender. |
MAIL_PASSWORD | (unset / <redacted>) | SMTP app password. Committed in .env.example for dev — redact in prod. |
MAIL_* and APP_ENVIRONMENT bypass settings.py (read via os.getenv directly) — easy
to miss when auditing configuration.Container / entrypoint-only (build & deploy)
Container / entrypoint-only (build & deploy)
These are consumed by
containers/docker-entrypoint.sh or docker-compose.yml — not
by settings.py. See Gateway build & runtime.| Variable | Default | Purpose |
|---|---|---|
SSH_PRIVATE_KEY / SSH_PUBLIC_KEY / SSH_KNOWN_HOSTS | (unset) | Injected from SSM into /root/.ssh so the container can reach the remote Docker host. |
REMOTE_DOCKER_SSH_TARGET | (unset) | Entrypoint-only. With REMOTE_DOCKER_ENABLED=true, opens an ssh -L 2375 tunnel to the remote daemon. |
JUMPHOST_PUBLIC_IP | <prod-ingress-ip> | Entrypoint ssh ProxyCommand hop to reach the office Docker host (<office-docker-host>) from AWS. |
PYTHONPATH | /app/src | Import root so main:app / src.main:app resolve. |
GATEWAY_PORT | 5000 | Host port mapping (compose). |
GATEWAY_CLOUDFLARE_PORT | 2053 | Cloudflare-tunnel-friendly host port also mapped to 5000. |
Dashboard (skyhub_dashboard)
The Angular SPA has no runtime env vars — configuration is a TypeScript object in
src/environments/environment*.ts, and angular.json swaps one file in at build time via
fileReplacements. Change a value → rebuild and redeploy. The five files and the build
configuration that selects each:
| Build config | Env file | production | Selected by |
|---|---|---|---|
production (default) | environment.prod.ts | true | npm run build (no args) |
aws-dev | environment.aws-dev.ts | true | ng build --configuration aws-dev |
development | environment.ts | false | ng serve (default) |
local | environment.local.ts | false | npm run start:local / build:local |
e2e | environment.e2e.ts | true | Playwright webServer |
Keys and per-environment values
| Key | Purpose | dev / local | prod / e2e | aws-dev |
|---|---|---|---|---|
url | Gateway REST base (/api/v1) | http://localhost:5000/api/v1 | https://prod.skyhub.ai:5000/api/v1 | https://dev.skyhub.ai:5000/api/v1 |
janusGatewayUrl | Janus WebSocket (WebRTC video) | ws://localhost:8188 (local) / wss://prod.skyhub.ai:8188 (dev default) | wss://prod.skyhub.ai:8188 | wss://dev.skyhub.ai:8188 |
ws_proxy | Gamepad WS proxy (redispad) | ws://localhost:7070 (local) / wss://prod.skyhub.ai:7070 (dev default) | wss://prod.skyhub.ai:7070 | wss://ws_proxy.skyhub-dev.internal:7070 |
janusIceServers | STUN/ICE servers | ['stun:stun.l.google.com:19302'] | same | same |
assetsUrl | S3 base for drone assets | https://skyhub-prod-assets.s3.eu-central-1.amazonaws.com/drone (all envs) | same | same |
mapbox.accessToken | Mapbox GL public token | pk.eyJ1… (committed, all envs) | same | same |
stripePublishableKey | Stripe publishable key | pk_live_xxx (present only in environment.ts + environment.prod.ts) | present (prod) | absent |
enableIsaacSim | Isaac Sim feature flag | false | false | false |
sseDebounceTime | Stream update debounce (ms) | 1000 | 1000 | 1000 |
httpSessionExpiryTime | Client session expiry (minutes) | 8 | 8 | 8 |
DEFAULT_LAT / DEFAULT_LNG | Initial map center (Plovdiv, BG) | 42.1354 / 24.7453 | same | same |
environment.ts is the dev default (production: false) yet it already carries the
live Stripe key and the prod Janus / ws_proxy / assets URLs — only its url
points at localhost. environment.e2e.ts deliberately targets the real production API
(https://prod.skyhub.ai:5000), so Playwright smoke runs hit prod. Full build/deploy
detail: Dashboard build & config ·
App state & video.SkyCore Drone OS (skyhub_core)
The on-drone stack is a docker-compose microservice bundle configured through a single
.env (template .env.example). Every value uses the ${VAR:-default} pattern, so unset
vars fall back to the compose default. COMPOSE_PROFILES decides which containers even
start. Below are the load-bearing groups; see Drone OS services for
the exhaustive per-module treatment.
- Profiles & identity
- Camera & video
- RTK NTRIP
- Gamepad & guided control
- SLAM / Isaac & ArUco landing
| Variable | Default | Purpose |
|---|---|---|
COMPOSE_PROFILES | mavproxy,core,rtk,gamepad | Comma-separated container profiles to start. Options: mavproxy, core, camera, rtk, gamepad, slam. |
SKYHUB_SERVER_URL | http://whip.skyhub-prod.internal:7080 | WHIP ingest endpoint the on-drone video pipeline pushes H264 to. |
API_URL | https://prod.skyhub.ai:5000 | Gateway base for drone-originated callbacks (executions, assets). |
ROS_DOMAIN_ID | 1 | ROS2 DDS domain id. |
FCUURL | udp://127.0.0.1:14550@ | MAVLink connection to the FCU (via mavp2p). |
IP_OVERRIDE | wg0 IP, else 10.223.x locally | Overrides the drone’s WireGuard IP used to namespace Redis channels ({ip}:gamepad_input, {ip}:aruco_tracking, …). Legacy typo IP_OVRIDE still honored. |
IP_OVERRIDE is central to the Redis message bus — see
Redis message bus and
Redis channels & MAVLink port map.AWS infrastructure (skyhub_terraform)
Terraform inputs live in environments/<env>/inputs.tf (declarations) and are set per
environment in environments/prod/settings.tfvars (prod) — dev supplies values on the CLI.
Applied with terraform apply -var-file=settings.tfvars.
| Variable | Prod value / default | Purpose |
|---|---|---|
client_name | skyhub | Resource naming prefix. |
client_description | Skyhub | Human label. |
environment | prod (default dev) | Environment discriminator (drives skyhub-prod-* naming). |
environment_description | Skyhub Production Cloud | Human label. |
aws_region | eu-central-1 | Target AWS region. |
github_token | <redacted> | PAT for CodeBuild source access. Committed in settings.tfvars — rotate/redact. |
slack_webhook | <redacted> | Slack incoming webhook for build notifications. Committed — redact. |
video_port_start / video_port_end | 10000 / 10099 | Janus primary RTP media port range. |
video_port_start_2 / video_port_end_2 | 20000 / 20099 | Secondary media/relay port range. |
custom_ssh_port | 3377 | Non-standard SSH port on the WireGuard/jumphost instance. |
172.31.0.0/16, the single t4g.nano WireGuard
jumphost at public <prod-ingress-ip>, self-hosted Postgres/Redis) is described in
AWS environments & state and
Production config. Prod ingress is nginx running on that
t4g.nano WireGuard EC2, acting as a TLS-terminating reverse proxy in front of the ECS
services — there is no Cloudflare tunnel or ALB/ELB in prod. GATEWAY_CLOUDFLARE_PORT
/ 2053 is only a local/optional dev-compose artifact and is not used in production.
Secrets hygiene
Related references
Gateway env vars (detailed)
Startup validation order, footguns, and the code path reading each gateway value.
Dashboard build & config
The five Angular environment files, build configurations, and CI/deploy flow.
Drone OS services
On-drone container profiles and how
COMPOSE_PROFILES selects them.Environments & Terraform state
tfvars, remote state, and the prod/dev environment split.
HTTP & Socket.IO API reference
Full endpoint and event reference for the gateway.
Redis channels & MAVLink ports
Where
IP_OVERRIDE, ports, and channel naming come together.
