Skip to content

Deployment

gladsonsam edited this page Jun 20, 2026 · 6 revisions

Deployment

Production deployment guide for Vantyr — the Rust/Axum server plus its bundled React dashboard. The shipped artifact is a single Docker image (API + dashboard) backed by PostgreSQL.

See also: Configuration · Environment-template · Development · Security · Usage


Requirements

Component Requirement
Server Docker + Docker Compose v2 (recommended), or a native build of vantyr-server + PostgreSQL — see Development.
Database PostgreSQL 16 (bundled in Compose as postgres:16-alpine).
Agent Windows 10/11 (64-bit) or Linux. Built and distributed separately — see Development → Agent.

The server image is fully self-contained: the React dashboard is compiled to static files at build time and served from /app/static. There is no Node.js in the production image and no separate frontend container to run.


Quick start (Docker Compose)

cp .env.example .env
# edit .env: set POSTGRES_PASSWORD and ADMIN_PASSWORD at minimum
docker compose up -d

Compose reads .env automatically for variable substitution, and the server service also loads it via env_file: .env. Only POSTGRES_PASSWORD and ADMIN_PASSWORD are required; everything else has a safe default.

By default the dashboard and API are then available at http://<host>:9000. Log in with ADMIN_USERNAME (default admin) and your ADMIN_PASSWORD.

The default docker compose up runs over plaintext HTTP (ENFORCE_HTTPS=false in .env.example). This exposes session cookies, agent tokens, screen frames and keystroke data on the wire — only acceptable on localhost or a trusted LAN. For anything internet-exposed, put Vantyr behind a TLS-terminating reverse proxy (below).

Building the image locally

The Compose file references the published GHCR image but also includes a build: stanza, so you can build from source instead of pulling:

docker compose up -d --build

This rebuilds ghcr.io/gladsonsam/vantyr/server:latest locally from server/Dockerfile.


What the image builds

server/Dockerfile is a 3-stage build:

Stage Base Produces
1 — frontend node:20-alpine npm ci && npm run build → React SPA in dist/
2 — server rust:1 cargo build --release --locked -p vantyr-server (the agent dir is not a workspace member, so no Windows deps are pulled)
3 — runtime debian:bookworm-slim the server binary + SPA, run as non-root uid 10001

Key runtime facts:

  • The dashboard is baked into /app/static (STATIC_DIR=/app/static); the binary serves it as the SPA fallback.
  • SQL migrations are embedded (sqlx::migrate!) and run at startup — no separate migration step.
  • A Docker HEALTHCHECK polls /healthz (--interval=10s --timeout=3s --retries=6).
  • The image exposes port 9000.

Compose services

Service Image Notes
db postgres:16-alpine Data on the pgdata named volume. Published only on 127.0.0.1:5432 (not exposed to the LAN). Has its own pg_isready healthcheck; the server depends_on it service_healthy.
server ghcr.io/gladsonsam/vantyr/server:latest Runs with network_mode: host so mDNS/LAN discovery works (Docker's bridge does not forward multicast). DATABASE_URL is built from the POSTGRES_* vars and points at 127.0.0.1:5432.

Because server uses host networking, it binds directly to the host's port 9000 (from LISTEN_ADDR, default 0.0.0.0:9000) — there is no Docker port mapping for it.


Health & readiness

Endpoint Purpose
GET /healthz Liveness. Always reachable, even when ENFORCE_HTTPS=true. Used by the Docker HEALTHCHECK.
GET /readyz Readiness — includes a database connectivity check. Use this as the load-balancer / orchestrator readiness probe.
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:9000/healthz   # expect 200
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:9000/readyz    # 200 once DB is up

Reverse proxy / TLS

No TLS or reverse-proxy config ships in the repo. For any deployment beyond localhost or a trusted LAN, terminate TLS in front of the server (Caddy / nginx / Traefik) and configure these two variables in .env:

Variable Set to
ENFORCE_HTTPS true — the server then returns 426 Upgrade Required on requests it sees as plain HTTP. It trusts the proxy's X-Forwarded-Proto: https header (and wss for WebSocket upgrades).
TRUSTED_PROXY_CIDRS the CIDR(s)/IP(s) of your proxy, e.g. 10.0.0.0/8,172.16.0.0/12. Required so X-Forwarded-For / X-Real-IP is trusted for login rate limiting and lockout. Leave empty to trust no proxy (keys on the direct TCP peer).

Also set PUBLIC_BASE_URL=https://your-host (used for deep links, OIDC redirects, and mDNS advertisement). Agents should then connect over wss://your-host/ws/agent.

Traefik + mDNS on Linux

A plain Docker bridge with Traefik labels gives you TLS, but mDNS from inside the container will not reach the LAN. To get both, keep the default stack shape — network_mode: host on server, Postgres on 127.0.0.1 — and attach Traefik labels that forward to the host's port 9000:

labels:
  - traefik.enable=true
  - traefik.http.routers.vantyr.rule=Host(`your.domain.example`)
  - traefik.http.routers.vantyr.service=vantyr
  - traefik.http.routers.vantyr.entrypoints=websecure
  - traefik.http.routers.vantyr.tls=true
  - traefik.http.routers.vantyr.tls.certresolver=cloudflare
  - traefik.http.services.vantyr.loadbalancer.server.url=http://172.17.0.1:9000
  • Replace 172.17.0.1 with your docker0 gateway if different (ip -br addr show docker0).
  • Do not set loadbalancer.server.scheme/port alongside server.url — Traefik rejects that combination.
  • With network_mode: host, omit traefik.docker.network on this service.
  • In .env set ENFORCE_HTTPS=true, PUBLIC_BASE_URL=https://your.domain.example, VANTYR_MDNS_PORT=443.

Traefik 404 ⇒ the Host() rule doesn't match the browser hostname; 502 ⇒ the backend server.url IP/port is wrong.


mDNS / LAN discovery

The server advertises itself on the LAN so agents can auto-discover it. It needs a resolvable WebSocket URL; set one of:

  • PUBLIC_BASE_URL (HTTP or HTTPS — the server derives ws:// / wss:// for the TXT record), or
  • VANTYR_MDNS_WSS_URL explicitly (e.g. wss://host:port/ws/agent).
Variable When to set
VANTYR_MDNS_PORT When TLS terminates on a different port (e.g. 443 behind Traefik). Defaults to the listen port (9000).
VANTYR_MDNS_ADDRESSES Comma-separated LAN IPs if the host advertises the wrong interface.

Allow UDP 5353 on the server host firewall. Host networking is required for multicast to reach the LAN; Docker Desktop on Windows/macOS does not replicate Linux host networking, so prefer a manual agent WebSocket URL + correct PUBLIC_BASE_URL there.


Observability (Prometheus)

A Prometheus metrics endpoint is exposed at GET /metrics when METRICS_ENABLED=true (the default). It is exempt from the HTTPS-required check. Point a Prometheus scrape job at it for per-method/status HTTP counters plus pool/agent/viewer gauges. Set LOG_JSON=true for structured JSON logs (Loki/ELK).


Data persistence & volumes

Data Lives in
PostgreSQL data the pgdata named volume (/var/lib/postgresql/data).
Telemetry, screenshots, keystrokes, software inventory, audit log, alert-event screenshot blobs all in Postgres — backing up the database backs up everything.

There are no host bind mounts by default. Back up with docker compose exec db pg_dump (or snapshot the pgdata volume while stopped). Telemetry growth is bounded by the retention job — see Configuration for RETENTION_INTERVAL_SECS, ALERT_EVENT_RETENTION_DAYS, SOFTWARE_INVENTORY_RETENTION_DAYS, SCRIPT_EXECUTION_RETENTION_DAYS, and METRICS_RETENTION_DAYS.


Upgrades

docker compose pull          # fetch the new server image (or: git pull && docker compose build)
docker compose up -d
  • Migrations run automatically at startup (embedded via sqlx::migrate!), so an upgrade applies any new SQL migrations on first boot of the new image. There are 55+ migrations in server/migrations/.
  • Never edit an already-applied migration — a checksum guard will refuse to start. (Migration 0021 is intentionally absent; numbering jumps 0020 → 0022.)
  • The agent updates independently of the server (minisign-verified MSI auto-update on Windows); upgrading the server does not push agent updates.

Native (non-Docker) deployment

Build and run the server binary directly:

cargo build --release -p vantyr-server
DATABASE_URL=postgres://user:pass@host:5432/db \
ADMIN_PASSWORD=... \
STATIC_DIR=./static \
  ./target/release/vantyr-server

Provide the same environment variables as the Docker path (see Configuration and Environment-template), point STATIC_DIR at the built dashboard (frontend/dist), and ensure PostgreSQL is reachable via DATABASE_URL.

Home

Install and configure

Day to day

Integrations

Developers and security

Clone this wiki locally