Quickstart
Get a working Breeze instance running with a single docker compose up.
Guided setup (recommended)
Section titled “Guided setup (recommended)”The guided installer downloads the templates, generates every required secret in the right format, checks that the version you pick has published container images, and starts the stack:
mkdir breeze && cd breezecurl -fsSLO https://raw.githubusercontent.com/lanternops/breeze/main/scripts/guided-setup.shbash guided-setup.shOn Linux the installer also offers to register a boot service (breeze-rmm) so
the stack comes back up on its own after a reboot. Accept it on a server you
want running unattended; you can install or update it later with
bash guided-setup.sh --install-systemd.
Prefer to configure .env yourself? The manual path is below.
Manual setup
Section titled “Manual setup”-
Download the compose file and environment template
Terminal window mkdir breeze && cd breezecurl -fsSLO https://raw.githubusercontent.com/lanternops/breeze/main/docker-compose.ymlcurl -fsSLO https://raw.githubusercontent.com/lanternops/breeze/main/.env.example# Caddy config — the compose file bind-mounts this, so it must exist on disk firstcurl -fsSL --create-dirs -o docker/Caddyfile.prod https://raw.githubusercontent.com/lanternops/breeze/main/docker/Caddyfile.prodcp .env.example .env -
Set your domain
Terminal window # Domain (use "localhost" for local testing — see the headless note below)BREEZE_DOMAIN=localhost -
Generate the secrets
.env.exampleships placeholder values for every secret. Replace them in place — edit the existing line for each key rather than appending a second copy at the end of the file.This one-liner rewrites each placeholder with a fresh value:
Terminal window for key in JWT_SECRET SESSION_SECRET AGENT_ENROLLMENT_SECRET \APP_ENCRYPTION_KEY MFA_ENCRYPTION_KEY ENROLLMENT_KEY_PEPPER \MFA_RECOVERY_CODE_PEPPER METRICS_SCRAPE_TOKEN; dosed -i "s|^${key}=.*|${key}=$(openssl rand -hex 32)|" .envdone# Must be canonical base64 decoding to >= 32 bytes, and must not reuse JWT_SECRETsed -i "s|^PARTNER_API_CURSOR_SIGNING_KEY=.*|PARTNER_API_CURSOR_SIGNING_KEY=$(openssl rand -base64 32)|" .env# Database and Redis. Redis auth is REQUIRED — the API refuses to start without it.POSTGRES_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. -
Check the version pin
.env.examplepinsBREEZE_VERSIONto a specific release. Confirm it is the one you want before starting:Terminal window grep '^BREEZE_VERSION=' .envTo upgrade later, set it to a newer release from the releases page and re-run
docker compose up -d. -
Start Breeze
Terminal window docker compose up -dOn first boot the API container automatically:
- Runs database migrations
- Seeds the default admin user
- Starts the background job workers
-
Verify
Terminal window # Wait for API to be healthy (~30s on first boot)docker compose logs -f api --since 1mOnce you see
Breeze API running, open the dashboard:- Dashboard:
https://localhost(accept the self-signed cert) - Health check:
curl -k https://localhost/health
- Dashboard:
Reaching the dashboard from another machine
Section titled “Reaching the dashboard from another machine”BREEZE_DOMAIN becomes Caddy’s site address, and Caddy only answers requests
whose Host header matches it. With BREEZE_DOMAIN=localhost the ports are
published on all interfaces, but a browser on your laptop sends
Host: 192.168.1.50, which matches nothing — so a minimal/headless server
install has no reachable UI even though curl -k https://localhost/health
returns OK on the box itself.
Pick whichever fits:
-
SSH tunnel — leave
BREEZE_DOMAIN=localhostand forward the port:Terminal window ssh -L 8443:127.0.0.1:443 user@your-server# then browse to https://localhost:8443A browser origin includes the port, so
https://localhost:8443is a different origin from thehttps://localhostthat the generated.envallows, and the API rejects sign-in requests coming from it. Pick one of:Terminal window # Forward the SAME port — 443 is privileged, so run this as rootsudo ssh -L 443:127.0.0.1:443 user@your-server# then browse to https://localhostTerminal window # …or keep the high port and add the tunnel origin to .env on the server# (comma-separated, no spaces, no trailing paths)CORS_ALLOWED_ORIGINS=https://localhost,https://localhost:8443docker compose up -d apiThe API now accepts same-origin requests on its own, so this entry is optional — a browser reaching Breeze through the tunnel is already treated as same-origin. It stays correct either way.
-
LAN address — serve the machine’s IP or hostname directly:
Terminal window BREEZE_DOMAIN=192.168.1.50PUBLIC_APP_URL=https://192.168.1.50DASHBOARD_URL=https://192.168.1.50Caddy cannot get a public certificate for a private address, so it serves its internal self-signed cert — accept the browser warning.
-
Internal domain — a name that resolves only on your LAN or VPN, or a host whose ports 80 and 443 are not reachable from the internet:
Terminal window BREEZE_DOMAIN=breeze.mydomain.comPUBLIC_APP_URL=https://breeze.mydomain.comDASHBOARD_URL=https://breeze.mydomain.comCADDY_LOCAL_CERTS=local_certsLet’s Encrypt cannot validate a name it cannot reach, so
CADDY_LOCAL_CERTS=local_certsswitches Caddy to its own internal CA instead. The guided installer asks this for you — “Can Let’s Encrypt reach this domain?” — whenever you give it a domain that is notlocalhostor an IP.Browsers warn on every visit until that CA root is trusted on each client machine. Export it with:
Terminal window docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt -
Real domain — the production path. See Production Deploy.
Whichever you choose, PUBLIC_APP_URL must be the URL users actually visit:
it is what agent installers and portal links are built from.
Troubleshooting
Section titled “Troubleshooting”You are sent back to the login page with “session expired” right after signing in
- Cause: the address in your browser’s address bar is not an origin this server
accepts.
CORS_ALLOWED_ORIGINSdoesn’t list it, orPUBLIC_APP_URLis set to a different address than the one you are visiting. Reaching aBREEZE_DOMAIN=localhostinstall through an SSH tunnel on a different port is the usual way to land here — the password was fine, the sign-in worked, and the token refresh that follows it is what got rejected. - Confirm: open your browser devtools, go to the Network tab, and sign in
again.
POST /api/v1/auth/refreshreturns 403 with the body{"error":"Invalid request origin"}. Newer Breeze builds also say so on the login page itself, naming the exact origin that was refused. - Fix: browse to the address in
PUBLIC_APP_URL, or add the address you are actually using toCORS_ALLOWED_ORIGINSin.env(comma-separated, no spaces, no trailing paths) and restart the API withdocker compose up -d api.
The browser shows ERR_SSL_PROTOCOL_ERROR
Section titled “The browser shows ERR_SSL_PROTOCOL_ERROR”Chrome reports ERR_SSL_PROTOCOL_ERROR and Firefox SSL_ERROR_INTERNAL_ERROR_ALERT,
with no certificate warning you can click through. That is Caddy aborting the TLS
handshake with alert 80 (internal_error) because it holds no certificate for the
name you asked for. Confirm it from the Caddy log:
docker logs breeze-caddy 2>&1 | grep tls.obtaincould not get certificate from issuer there means the Let’s Encrypt order for
BREEZE_DOMAIN failed. Let’s Encrypt needs public DNS pointing at this host and
ports 80 and 443 open from the internet — an internal-only name, or a host behind
a firewall, can never pass validation.
Two ways out:
-
Make the domain publicly resolvable and open 80/443 inbound, then restart Caddy so it retries the order.
-
Or keep it internal and switch to Caddy’s own CA — set
CADDY_LOCAL_CERTS=local_certsin.env(see Internal domain above) and restart Caddy:Terminal window docker compose up -d caddy
What’s Running
Section titled “What’s Running”| Container | Port | Purpose |
|---|---|---|
breeze-caddy |
80, 443 | Reverse proxy + auto-TLS |
breeze-api |
3001 (internal) | Hono API server |
breeze-web |
4321 (internal) | Astro SSR dashboard |
breeze-portal |
4322 (internal) | Customer portal, served under /portal |
breeze-postgres |
5432 (internal) | PostgreSQL 16 database |
breeze-redis |
6379 (internal) | Redis 7 (BullMQ + caching) |
breeze-coturn |
3478 (host) | TURN relay for WebRTC remote desktop (only with --profile turn) |
Next: Enroll Your First Agent
Section titled “Next: Enroll Your First Agent”Download and install the Breeze agent on a device. BREEZE_SERVER must be the
URL of your Breeze server as the target device can reach it — not
localhost, which on the target device means the target device itself:
# On the target device:# The enrollment token is REQUIRED — grab it from the Add Device dialog.curl -fsSL https://breeze.example.com/api/v1/agents/install.sh | \ BREEZE_SERVER=https://breeze.example.com \ BREEZE_ENROLL_TOKEN=<your-enrollment-token> \ bashIf your server is configured with an org enrollment secret, pass it as an optional extra gate alongside the token (never instead of it):
curl -fsSL https://breeze.example.com/api/v1/agents/install.sh | \ BREEZE_SERVER=https://breeze.example.com \ BREEZE_ENROLL_TOKEN=<your-enrollment-token> \ BREEZE_ENROLLMENT_SECRET=<your-enrollment-secret> \ bashSee Agent Installation for detailed instructions per platform.
For production deployment with a real domain and monitoring, see Production Deploy.