Skip to content

Docker & local setup

Lockstep VM runs as a set of Docker Compose services defined in webapp/docker-compose.yml. All services join one bridge network (vulnmgmt-network) and read their secrets from a single root .env file.

Services

ServiceImage / buildContainerHost port → containerPurpose
mongodbmongo:7.0vulnmgmt-mongodb27017 → 27017Primary datastore (inventory, assets, config). Persisted to the mongodb_data volume.
backendbuilt from webapp/backend/Dockerfilevulnmgmt-backend8000 → 8000FastAPI API. Bind-mounts ./backend and runs uvicorn --reload.
frontendbuilt from webapp/frontend/Dockerfilevulnmgmt-frontend4321 → 4321Astro SSR app (Bun build → Node runtime). Baked image.
workerbuilt from Dockerfile.workervulnmgmt-workerAPScheduler background jobs (uv run -m vulnmgmt.worker.scheduler).
postgrespostgres:15-alpinevulnmgmt-postgres5432 → 5432Relational store used by the worker. Persisted to postgres_data.
pgadmindpage/pgadmin4:latestvulnmgmt-pgadmin8080 → 80Postgres admin UI (dev convenience).

Key URLs once up:

  • Frontend / app: http://localhost:4321
  • Backend API + Swagger: http://localhost:8000 (/docs, /redoc, /health)
  • pgAdmin: http://localhost:8080

Volumes & dependencies

Named volumes: mongodb_data, postgres_data, backend_uploads, pgadmin_data. backend also bind-mounts ../certs:/app/certs:ro (the Intune/Entra PEM) and ./backend:/app (source). depends_on: backendmongodb; frontendbackend; workerpostgres (waits for its healthcheck); pgadminpostgres.

The env-file model

There is one source of secrets: the root .env (../.env relative to the compose file). Two mechanisms consume it:

  1. The backend service uses env_file: ../.env, which injects every variable in that file into the container.
  2. Individual services additionally map specific variables through the environment: block with ${VAR} interpolation — this is how Compose builds derived values like the MONGODB_URL connection string, and how it applies defaults (${VAR:-default}) or hard requirements (${VAR:?message} fails fast if unset).

Required-or-fail variables (Compose refuses to start without them): MONGO_ROOT_PASSWORD, JWT_SECRET, BETTER_AUTH_SECRET.

The backend also seeds each integration’s MongoDB config document from these env vars on first startup (see main.py lifecycle in Backend & services), so a fresh instance is ready to sync without touching the UI.

Integration environment-variable groups

Grouped by name only. Fill values in the root .env.

GroupVariables
Mongo / DBMONGO_ROOT_USERNAME, MONGO_ROOT_PASSWORD, MONGODB_URL, DATABASE_NAME, DB_PASSWORD
Auth / JWTJWT_SECRET, JWT_ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, BETTER_AUTH_SECRET, BETTER_AUTH_URL, PUBLIC_API_URL, FRONTEND_INTERNAL_URL
App / runtimeENVIRONMENT, CORS_ORIGINS, RATE_LIMIT_PER_MINUTE, MAX_UPLOAD_SIZE, UPLOAD_DIR
SentinelOneSENTINELONE_API_TOKEN, SENTINELONE_CONSOLE_URL
Rapid7RAPID7_API_KEY, RAPID7_REGION, RAPID7_ORG_ID
Intune / Entra (Graph)INTUNE_TENANT_ID, INTUNE_CLIENT_ID, INTUNE_CERT_THUMBPRINT, INTUNE_CERT_KEY_PATH
ConnectWise Automate (RMM)CW_AUTOMATE_URL, CW_AUTOMATE_USERNAME, CW_AUTOMATE_PASSWORD, CW_AUTOMATE_CLIENT_ID
ConnectWise (PSA)CONNECTWISE_URL, CONNECTWISE_COMPANY_ID, CONNECTWISE_PUBLIC_KEY, CONNECTWISE_PRIVATE_KEY, CONNECTWISE_CLIENT_ID
Threat intelVIRUSTOTAL_API_KEY, OTX_API_KEY, GREYNOISE_API_KEY, SHODAN_API_KEY, NVD_API_KEY
AI / emailANTHROPIC_API_KEY, OPENAI_API_KEY, RESEND_API_KEY, AUTH_FROM_EMAIL, AUTH_FROM_NAME, ADVISORY_FROM_EMAIL, ADVISORY_FROM_NAME
Worker / alertsTEAMS_WEBHOOK_URL, WORKER_LOG_LEVEL, WORKER_TIMEZONE, ALERT_CRITICAL_CVE_THRESHOLD, ALERT_SLA_WARNING_DAYS, ALERT_DAILY_SUMMARY_ENABLED

The Intune app registration’s certificate lives at INTUNE_CERT_KEY_PATH (default /app/certs/intune.pem), mounted read-only into the backend from the host certs/ directory. Entra reuses the same Azure app + cert.

Backend hot-reload vs baked frontend

These two services behave very differently — the most common source of “my change isn’t showing up” confusion.

Backend — live-mounted, auto-reloads. The compose file bind-mounts ./backend:/app and the image’s CMD runs uvicorn app.main:app --reload. Edit a Python file on the host and uvicorn restarts the app in place. A rebuild is only needed when dependencies change (pyproject.toml) or the Dockerfile itself.

The backend image (python:3.11-slim) also installs libmagic1 (for upload MIME sniffing) and curl (for the container healthcheck), and installs the parent project (/project/src on PYTHONPATH) plus the backend deps via uv.

Frontend — baked image, needs a rebuild. The frontend Dockerfile is a multi-stage Bun build: it runs bun run build to produce dist/ and the runtime stage serves it with Node (node ./dist/server/entry.mjs). The source is not mounted, so editing frontend files has no effect until you rebuild:

Terminal window
docker compose build frontend && docker compose up -d frontend

PUBLIC_* variables are inlined into the client bundle at build time, so changing them also requires a frontend rebuild — not just a restart.

Common commands

Run these from webapp/ (where docker-compose.yml lives).

Terminal window
# Start everything (detached)
docker compose up -d
# Rebuild + restart only the frontend after a UI change
docker compose build frontend && docker compose up -d frontend
# Rebuild the backend after a dependency / Dockerfile change
docker compose build backend && docker compose up -d backend
# Recreate the backend to pick up new .env values (see Deployment)
docker compose up -d --force-recreate backend
# Tail logs
docker compose logs -f backend
docker compose logs -f frontend worker
# Restart a service (e.g. after editing a mounted backend file, if needed)
docker compose restart backend
# Status + health
docker compose ps
# Stop / tear down (volumes are preserved unless you add -v)
docker compose down

See Deployment for the production build/recreate flow, MongoDB persistence, disk hygiene, and health checks.