Production Deployment
Breeze ships as pre-built Docker images on GitHub Container Registry. A single docker compose up -d brings up a fully working production stack with automatic TLS.
What Gets Deployed
Section titled “What Gets Deployed”The core stack (docker-compose.yml) includes:
| Service | Image | Purpose |
|---|---|---|
| Binaries Init | ghcr.io/lanternops/breeze/binaries |
Copies agent/viewer binaries to a shared volume, then exits |
| Caddy | caddy:2.8-alpine |
Reverse proxy, auto-TLS, security headers |
| API | ghcr.io/lanternops/breeze/api |
Hono API server |
| Web | ghcr.io/lanternops/breeze/web |
Astro SSR dashboard |
| Portal | ghcr.io/lanternops/breeze/portal |
Astro SSR customer portal, served under /portal (see Customer Portal) |
| PostgreSQL | pgvector/pgvector:pg16 |
Primary database |
| Redis | redis:7-alpine |
Job queue, caching, rate limiting |
| Coturn | coturn/coturn:4-alpine |
TURN relay server for WebRTC remote desktop (opt-in via --profile turn) |
The pgvector extension is required only if you enable the Workspace built-in (BREEZE_WORKSPACE_ENABLED=true), which uses it for content embeddings. Workspace ships inside the API image but is off by default: with the flag unset, no Workspace migrations run and any Postgres 16 image works. The bundled POSTGRES_IMAGE_REF default is pgvector-capable regardless, so enabling Workspace later needs no database swap — supply your own Postgres image and you must provide pgvector yourself before setting the flag, or the API will fail to start with extension "vector" is not available.
An optional monitoring stack (Prometheus, Grafana, Alertmanager, Loki, Promtail, exporters) is available as a separate overlay — see Monitoring.
An optional m365-graph-read-executor sidecar (published as ghcr.io/lanternops/breeze/m365-graph-read-executor) isolates the Microsoft 365 customer Graph-read certificate and Key Vault access away from the API. It is only exercised when Customer Graph-read consent is enabled (M365_CUSTOMER_GRAPH_READ_ONBOARDING_ENABLED=true); otherwise it sits idle and can be left undeployed. It is not part of the bundled compose files by default — see Customer Microsoft 365 Graph-read consent for the full variable contract, including the private base URL and read-only signing-key mount it depends on.
Deploy Steps
Section titled “Deploy Steps”-
Prepare the server
You need Docker and Docker Compose on a Linux VPS. See Prerequisites.
-
Clone and configure
Terminal window git clone https://github.com/LanternOps/breeze.gitcd breezecp .env.example .env -
Set your domain and secrets
Edit
.envand set these required values:Terminal window BREEZE_DOMAIN=breeze.yourdomain.comACME_EMAIL=admin@yourdomain.comGenerate all secrets at once. These commands rewrite the placeholder values
.env.exampleships in place — do not append a second copy of a key to the end of the file:Terminal window for key in JWT_SECRET APP_ENCRYPTION_KEY MFA_ENCRYPTION_KEY \ENROLLMENT_KEY_PEPPER MFA_RECOVERY_CODE_PEPPER \METRICS_SCRAPE_TOKEN SESSION_SECRET AGENT_ENROLLMENT_SECRET; dosed -i "s|^${key}=.*|${key}=$(openssl rand -hex 32)|" .envdone# Canonical base64 decoding to >= 32 bytes; must not reuse JWT_SECRET.# The API refuses to boot in production without this one.sed -i "s|^PARTNER_API_CURSOR_SIGNING_KEY=.*|PARTNER_API_CURSOR_SIGNING_KEY=$(openssl rand -base64 32)|" .envPOSTGRES_PASSWORD="$(openssl rand -base64 24 | tr -d '/+=')"REDIS_PASSWORD="$(openssl rand -hex 32)"sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${POSTGRES_PASSWORD}|" .envsed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=${REDIS_PASSWORD}|" .envsed -i "s|^REDIS_URL=.*|REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379|" .envOn macOS, use
sed -i ''instead ofsed -i.Confirm no required secret was left at its placeholder:
Terminal window grep -nE 'replace-with|generate-a-random|change-in-production|your-super-secret|changeme' .env \| grep -v '^[0-9]*:GRAFANA_ADMIN_PASSWORD='That should print nothing.
GRAFANA_ADMIN_PASSWORDis filtered out because it belongs to the optional monitoring stack and is generated in that section below;TURN_SECRETis only needed if you enable the TURN profile in step 4.Then set these required deployment-mode flags. Without them the API refuses to boot in production:
Terminal window # Self-hosted by default. Set to "true" only if you're running the hosted SaaS edition.IS_HOSTED=false# Trust anchor (raw base64 Ed25519 public key) for the official Breeze GitHub# releases. This is a public key — also embedded in the agent and CI — so it is# safe to commit. Leave the default below unless you build and sign your own binaries.RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS=yzx8ftmcls6uBetFC5SYnZhBo+cbur3IX50TbBthTso=Then set the initial admin account. The API creates it automatically the first time it starts against an empty database — in production this is required: without both variables set, that first-boot seed throws and the API crash-loops instead of starting.
Terminal window BREEZE_BOOTSTRAP_ADMIN_EMAIL=you@yourdomain.comBREEZE_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -base64 24)"If Breeze is behind a reverse proxy, also set
TRUST_PROXY_HEADERS=trueand list the proxy’s CIDRs inTRUSTED_PROXY_CIDRS. See Environment Variables for details. -
Configure TURN server
The TURN server enables remote desktop connectivity across NATs and firewalls. Set the server’s public IP address:
Terminal window # Set to this server's public IP address (required for remote desktop)TURN_HOST=203.0.113.10# Optional: TURN realm (defaults to breeze.local)TURN_REALM=breeze.localGenerate the shared secret coturn uses to mint credentials:
Terminal window sed -i "s|^TURN_SECRET=.*|TURN_SECRET=$(openssl rand -hex 32)|" .envCoturn is behind a Docker Compose profile and does not start by default. To enable it:
Terminal window docker compose --profile turn up -dOr add
COMPOSE_PROFILES=turnto your.envfile for persistent activation. -
Start the stack
Terminal window docker compose up -dThat’s it. On first start, the API container automatically runs database migrations and seeds the initial admin user with the
BREEZE_BOOTSTRAP_ADMIN_EMAIL/BREEZE_BOOTSTRAP_ADMIN_PASSWORDyou set in step 3. Caddy obtains a TLS certificate from Let’s Encrypt. -
Verify the deployment
Terminal window # Check healthcurl https://breeze.yourdomain.com/health# Check running containersdocker compose ps# View API logsdocker compose logs -f api
Adding Monitoring
Section titled “Adding Monitoring”The monitoring stack lives in a separate compose overlay file (docker-compose.monitoring.yml) and includes Prometheus, Grafana, Alertmanager, Loki, Promtail, and database exporters.
To deploy with monitoring:
docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -dAdd a Grafana password to .env:
GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -d '/+=')Grafana is available at http://127.0.0.1:3000 (localhost only by default).
Pinning a Version
Section titled “Pinning a Version”.env.example ships with an explicit BREEZE_VERSION pin, so a fresh deployment is already pinned. Change it deliberately when you upgrade. There is no latest fallback: every image reference in docker-compose.yml is digest/tag-pinned and required, so clearing BREEZE_VERSION (or any of the *_IMAGE_REF variables) makes docker compose refuse to run at all, with an error like Set BREEZE_VERSION in .env, rather than silently pulling a newer image. To pin to a specific release:
# In .envBREEZE_VERSION=0.67.1Then pull and restart:
docker compose pull && docker compose up -dSigned, digest-pinned image references (v0.112.0 and later)
Section titled “Signed, digest-pinned image references (v0.112.0 and later)”Since v0.112.0 every release publishes a signed image inventory — release-artifact-manifest.json plus its .ed25519 signature on the GitHub Release — listing the exact repository@sha256:… digest of the API, web, portal and binaries images. .env.example now ships the four BREEZE_*_IMAGE_REF variables as @sha256:replace-with-signed-release-digest placeholders rather than :${BREEZE_VERSION} tags, so a fresh install pins to verified digests and a GHCR tag or package page is never the authorization source.
-
Guided installer:
guided-setup.shdownloads the inventory for the release you pick, verifies its signature againstRELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS, and fills the four image refs itself. Verification is fail-closed: if it cannot download or verify the inventory, no image is pulled. For a release older than 0.112.0 the installer warns that the release predates signed inventories and falls back to tag-tracking refs. -
Manual setup: resolve the digests yourself with the verifier script that ships with the release, then copy the four lines it emits into
.env:Terminal window TAG=v0.112.0 # the release you are pinningcurl -fsSLO https://github.com/lanternops/breeze/releases/download/$TAG/release-artifact-manifest.jsoncurl -fsSLO https://github.com/lanternops/breeze/releases/download/$TAG/release-artifact-manifest.json.ed25519curl -fsSLO https://raw.githubusercontent.com/lanternops/breeze/$TAG/scripts/release/verify-release-images.shRELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS="$(grep '^RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS=' .env | cut -d= -f2-)" \bash verify-release-images.sh \--manifest release-artifact-manifest.json \--signature release-artifact-manifest.json.ed25519 \--expected-repository lanternops/breeze \--expected-release "$TAG" \--emit-env images.envcat images.env # BREEZE_API_IMAGE_REF=…@sha256:…, and the web, portal and binaries refsRepeat this on every upgrade — a digest pin does not follow
BREEZE_VERSION, so bumping the version alone changes nothing until the four refs are updated too. KeepBREEZE_VERSIONin step with the pinned release: it still selects the agent release your fleet is offered. -
Existing installs whose
.envstill carries the olderghcr.io/lanternops/breeze/api:${BREEZE_VERSION}form keep working; the upgrade guide’sdocker compose pullline is unchanged for them. Switching to digest pins is recommended but not required.
Resource Tuning
Section titled “Resource Tuning”Override default resource limits via environment variables:
# Redis memory limit (default: 256mb)REDIS_MAXMEMORY=512mbRedis Authentication
Section titled “Redis Authentication”Redis password authentication is required in production. The API will refuse to start if REDIS_URL doesn’t carry a password. The secret-generation loop above already produces REDIS_PASSWORD; pair it with a REDIS_URL that uses it:
REDIS_PASSWORD=$(openssl rand -hex 32)REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379The docker-compose.yml passes REDIS_PASSWORD to the Redis container automatically. See Environment Variables for details.
Updating
Section titled “Updating”cd breezegit pull origin maindocker compose pulldocker compose up -dPre-built images are pulled from GHCR. Database migrations run automatically on startup.