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
| Service | Image / build | Container | Host port → container | Purpose |
|---|---|---|---|---|
mongodb | mongo:7.0 | vulnmgmt-mongodb | 27017 → 27017 | Primary datastore (inventory, assets, config). Persisted to the mongodb_data volume. |
backend | built from webapp/backend/Dockerfile | vulnmgmt-backend | 8000 → 8000 | FastAPI API. Bind-mounts ./backend and runs uvicorn --reload. |
frontend | built from webapp/frontend/Dockerfile | vulnmgmt-frontend | 4321 → 4321 | Astro SSR app (Bun build → Node runtime). Baked image. |
worker | built from Dockerfile.worker | vulnmgmt-worker | — | APScheduler background jobs (uv run -m vulnmgmt.worker.scheduler). |
postgres | postgres:15-alpine | vulnmgmt-postgres | 5432 → 5432 | Relational store used by the worker. Persisted to postgres_data. |
pgadmin | dpage/pgadmin4:latest | vulnmgmt-pgadmin | 8080 → 80 | Postgres 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: backend → mongodb; frontend →
backend; worker → postgres (waits for its healthcheck); pgadmin →
postgres.
The env-file model
There is one source of secrets: the root .env (../.env relative to the
compose file). Two mechanisms consume it:
- The
backendservice usesenv_file: ../.env, which injects every variable in that file into the container. - Individual services additionally map specific variables through the
environment:block with${VAR}interpolation — this is how Compose builds derived values like theMONGODB_URLconnection 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.
| Group | Variables |
|---|---|
| Mongo / DB | MONGO_ROOT_USERNAME, MONGO_ROOT_PASSWORD, MONGODB_URL, DATABASE_NAME, DB_PASSWORD |
| Auth / JWT | JWT_SECRET, JWT_ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, BETTER_AUTH_SECRET, BETTER_AUTH_URL, PUBLIC_API_URL, FRONTEND_INTERNAL_URL |
| App / runtime | ENVIRONMENT, CORS_ORIGINS, RATE_LIMIT_PER_MINUTE, MAX_UPLOAD_SIZE, UPLOAD_DIR |
| SentinelOne | SENTINELONE_API_TOKEN, SENTINELONE_CONSOLE_URL |
| Rapid7 | RAPID7_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 intel | VIRUSTOTAL_API_KEY, OTX_API_KEY, GREYNOISE_API_KEY, SHODAN_API_KEY, NVD_API_KEY |
| AI / email | ANTHROPIC_API_KEY, OPENAI_API_KEY, RESEND_API_KEY, AUTH_FROM_EMAIL, AUTH_FROM_NAME, ADVISORY_FROM_EMAIL, ADVISORY_FROM_NAME |
| Worker / alerts | TEAMS_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:
docker compose build frontend && docker compose up -d frontendPUBLIC_* 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).
# Start everything (detached)docker compose up -d
# Rebuild + restart only the frontend after a UI changedocker compose build frontend && docker compose up -d frontend
# Rebuild the backend after a dependency / Dockerfile changedocker 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 logsdocker compose logs -f backenddocker compose logs -f frontend worker
# Restart a service (e.g. after editing a mounted backend file, if needed)docker compose restart backend
# Status + healthdocker compose ps
# Stop / tear down (volumes are preserved unless you add -v)docker compose downSee Deployment for the production build/recreate flow, MongoDB persistence, disk hygiene, and health checks.