This page is the canonical, tested path to a working SkyHub stack on one machine — no drones, no AWS, no video required. Every command below was executed and observed working on 2026-07-03. It gets you the Gateway Service HTTP + Socket.IO API on :5000 and the Dashboard SPA on :4200, backed by a throwaway Postgres, with a demo user you can log in as. For the full picture of how these pieces fit together, see the Platform Architecture Overview.

What you’ll run

Gateway

Flask + Socket.IO under gunicorn. Container listens on :5000. HTTP auth + REST, plus Socket.IO telemetry.

Dashboard

Angular 16 SPA via ng serve on :4200. Points at the local Gateway.

Postgres

Throwaway postgres container on :5432. Schema created by Alembic migrations.
The minimal path (below) skips the dotted box entirely — you only need Janus/WHIP/SITL for video or SITL drones.

Prerequisites

These are the exact versions observed working. The Gateway runs inside a container (Python 3.10.12, pinned by the Dockerfile), so your host Python version does not matter for the Docker path.
ToolVersion observedNotes
Docker29.1.3Required for the minimal path
Node.jsv24.11.1Angular 16 officially targets Node 18/20 — see the warning below
npm11.6.2Ships with Node 24
Angular CLI16.2.16Installed via the repo’s node_modules
Gateway container Python3.10.12Baked into Dockerfile — not your host Python
Postgres imagepostgres:latestUsed only for local dev
Node 24 vs Angular 16. The CLI prints Node.js version v24.11.1 ... Unsupported, but ng serve still compiles and serves in ~10s (esbuild). If you hit a build failure, fall back to Node 18 or 20.

Run the stack

1

Start Postgres and the Gateway

The minimal path uses two docker run commands on a shared network — no docker-compose, no Janus/WHIP. First build the image (once), then bring up Postgres and the Gateway.
# From the gateway repo root
docker build . -t skyhub-gateway-service

docker network create skyhub-doc-net

docker run -d --name skyhub-postgres --network skyhub-doc-net \
  -e POSTGRES_DB=skyhub -e POSTGRES_USER=idrobots -e POSTGRES_PASSWORD=idrobots \
  -p 5432:5432 postgres

# Wait for Postgres to accept connections
until docker exec skyhub-postgres pg_isready -U idrobots -d skyhub; do sleep 1; done

docker run -d --name skyhub_gw_doc --network skyhub-doc-net -p 5000:5000 \
  -e DB_IP=skyhub-postgres -e DB_USERNAME=idrobots -e DB_PASSWORD=idrobots -e DB_NAME=skyhub \
  -e DEPLOYMENT_ENVIRONMENT=local -e ENABLE_SITL=false \
  -e JWT_SECRET_KEY=<local-dev-secret> \
  -e SOCKET_IP=0.0.0.0 -e REGION=eu-central-1 \
  -e OTEL_EXPORTER_OTLP_ENDPOINT= -e FLASK_APP=main.py \
  skyhub-gateway-service:latest
DEPLOYMENT_ENVIRONMENT=local is the load-bearing flag here (see the warning below). ENABLE_SITL=false skips SITL Docker orchestration, and the empty OTEL_EXPORTER_OTLP_ENDPOINT disables OpenTelemetry export so the container doesn’t try to reach a SigNoz collector.
2

Apply database migrations

This step is mandatory. Under gunicorn the schema is not auto-created — db.create_all() lives only under the if __name__ == "__main__" block in src/main.py:262, which gunicorn never executes. Skip this and /login returns 500 because no tables exist.
docker exec -e FLASK_APP=main.py skyhub_gw_doc flask db upgrade
This applies the Alembic migrations in migrations/versions/ (head revision r3m4n5o6p7q8), creating the user, drone, mission, geofence, and mission_execution tables, plus the billing (subscription/payment), calendar (calendar_event/calendar_event_occurrence), and Isaac Sim (isaac_sim_instances) tables. See Migrations, DB Connection & Dev Mode for the create-all-vs-Alembic split.
3

Verify the Gateway is up

Two quick checks. The Swagger UI (flasgger) confirms the app booted; /api/v1/auth without a token confirms JWT is wired.
# Live Swagger UI (flasgger) — "SkyHub Gateway Service API" v1.0.0, basePath /api/v1
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/api/docs/   # 200

# Raw OpenAPI spec
curl -s http://localhost:5000/apispec.json | head -c 200

# Auth check without a token → 401
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/api/v1/auth # 401
Open http://localhost:5000/api/docs/ in a browser to explore all 91 documented paths.Gateway Swagger API docs at /api/docs/
4

Run the Dashboard

The local build configuration swaps in src/environments/environment.local.ts, which points every endpoint at localhost (url: http://localhost:5000/api/v1, janusGatewayUrl: ws://localhost:8188, ws_proxy: ws://localhost:7070) and sets production: false.
cd /home/skycore/Projects/skyhub_dashboard
npm install          # first time only
npm run start:local:no-open
start:local:no-open runs ng serve --configuration=local --host 0.0.0.0 (see package.json), serving at http://localhost:4200. A plain npm start (the development config, environment.ts) also targets localhost:5000, so either works locally.SkyHub dashboard login
5

Create a demo user and log in

Registration normally emails a signed token, but the committed SMTP credentials are broken (see the warning below), so mint the token directly with the JWT secret and post it to the registration endpoint. create_user in src/routes/auth_routes.py:205 deserializes exactly this token.
SECRET='<jwt-secret>'; EMAIL='[email protected]'; PASS='Demo12345!'

TOKEN=$(docker exec skyhub_gw_doc python -c \
  "from itsdangerous import URLSafeTimedSerializer as S; s=S('$SECRET'); \
   print(s.dumps({'email':'$EMAIL','origin':'http://localhost:4200'}, salt='$SECRET'))")

# Create the account
curl -X POST "http://localhost:5000/api/v1/register/$TOKEN" \
  -H "Content-Type: application/json" -d "{\"password\":\"$PASS\"}"

# Log in → { access_token, refresh_token }
curl -X POST http://localhost:5000/api/v1/login \
  -H "Content-Type: application/json" \
  -d "{\"username\":\"$EMAIL\",\"password\":\"$PASS\"}"
<jwt-secret> must match the JWT_SECRET_KEY you passed the Gateway container. Now log in at http://localhost:4200 with [email protected] / Demo12345!.Logged-in home: 3D drone over a satellite mapThe home view opens over a Mapbox satellite map and, with no vehicles yet, auto-prompts an Add Vehicle dialog. To add a simulated drone you need the SITL path — see SITL Drone Lifecycle.

Critical gotchas

DEPLOYMENT_ENVIRONMENT=local is essential. src/application/settings.py:11 defaults it to server. In non-local mode the Gateway (a) hard-fails at startup unless a real JWT_SECRET_KEY is set, (b) requires VPN_BUCKET (validate_critical_config in src/main.py:137), and (c) restricts CORS to production origins only (https://skyhub.ai, https://api.skyhub.ai) — which blocks the Dashboard on localhost:4200. Local mode allows the insecure default secret, relaxes CORS to localhost:4200, and sets cors_allowed_origins="*" for Socket.IO (src/main.py:210).
SMTP is broken; email verification and password reset will not send. The committed Gmail app password (MAIL_PASSWORD in docker-compose.yml) fails with 535 BadCredentials. This is non-fatal — every other flow works — but it’s why the demo-user step mints the registration token by hand instead of clicking a link.
Never print or commit .env secrets. The repo’s .env and docker-compose.yml defaults contain real AWS keys and a Gmail app password. Redact them in logs and issues; use <redacted> / placeholder values in any shared snippet.

Choosing a run path

Full stack via docker-compose (video / SITL)

You only need this heavier path for WebRTC video or SITL drones. docker compose up builds the Gateway and pulls in sibling services — skyhub-redis, skyhub-postgres, janus-gateway, simple-whip-server, and skyhub-ws-proxy — several of which build from adjacent repos (../skyhub_janus, ../skyhub_whip, ../skyhub_ws_proxy). It also bind-mounts /var/run/docker.sock and your ~/.ssh keys so the Gateway can orchestrate SITL containers.
From docker-compose.yml:
ServiceImage / buildHost portsNeeded for
skyhub-gateway-servicebuilds from Dockerfile5000, 2053Always
skyhub-postgrespostgres5432Always
skyhub-redisredis:7-alpine6379SITL pub/sub, Socket.IO scaling
janus-gateway../skyhub_janus8088, 8089, 8188, 10000-10099/udpVideo rooms
simple-whip-server../skyhub_whip7080Video ingest
skyhub-ws-proxy../skyhub_ws_proxy (host network)7070Gamepad control
Compose sets DEPLOYMENT_ENVIRONMENT=local and ENABLE_SITL=true by default and mounts the working tree over /app (so running code is your checkout, not the baked image).
docker build . -t skyhub_gateway_service
docker-compose up -d
docker exec -it skyhub_gateway_service flask db upgrade   # still required
Because compose mounts ~/.ssh keys and the Docker socket into a root container, treat it as host-privileged. Skip it unless you actually need video or SITL.
The Dashboard e2e configuration (npm run start:e2e) exists for Playwright, but it points at the real production API (https://prod.skyhub.ai:5000 via environment.e2e.ts). Do not use it for local development — use start:local / start:local:no-open.

Where to go next

Gateway Service Overview

How the control-plane hub authenticates, brokers telemetry, and dispatches commands.

Dashboard Overview

App shell, bootstrap, and structure of the Angular SPA you just ran.

Platform Architecture

The six-repo topology and how local mode maps onto production.

SITL Drone Lifecycle

Spawn a simulated ArduPilot drone to fly without hardware.

Environment Variables

Every Gateway knob, including the ones this quickstart sets.

Core Concepts & Glossary

Terminology used across these docs.