Docker quick start
Grab the stack files, fill in .env, and bring Airwave up with docker compose — first boot, the seeded admin, and reaching the panel.
This walks the deploy end to end with docker-compose.yml and .env.example from the repo root. It
works the same in a terminal (docker compose) or a UI like Dockge / Portainer — paste the
compose and the env, then deploy.
For what every variable means, see Configuration. For why the same image runs as more than one service, see Roles & the single image.
1. Grab the stack files
You need two files from the repo root: docker-compose.yml and .env.example. In Dockge,
create a stack and paste the compose in; in a terminal, drop both in a directory. The compose already
points at the published image — you don't build anything.
# docker-compose.yml — the three services (optional tvweb omitted)
services:
postgres: # postgres:16-alpine, healthchecked
server: # CG_ROLE=server — API + TV REST, applies migrations
web: # CG_ROLE=web — builds + serves the admin SPABoth server and web run the same image, defaulting to
ghcr.io/quixomatic/airwave:latest (override with CG_IMAGE).
2. Fill in .env
Copy .env.example to .env and set, at minimum:
# The addresses your BROWSER and TV actually use — LAN IP or domain + published ports.
# NOT localhost (unless you only browse from the host). Baked into the admin build.
SERVER_PUBLIC_URL=http://192.168.1.50:36020
WEB_PUBLIC_URL=http://192.168.1.50:36021
# Published host ports — must match the ports in the URLs above.
SERVER_PORT=36020
WEB_PORT=36021
# Postgres + auth
POSTGRES_PASSWORD=a-strong-password
BETTER_AUTH_SECRET=change-me-to-a-long-random-string-at-least-32-chars
# First admin, seeded on first boot
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=change-me
# Match your host — important on TrueNAS datasets
PUID=1000
PGID=1000
TZ=UTCGenerate the auth secret with:
openssl rand -base64 48Get the public URLs right.
SERVER_PUBLIC_URLandWEB_PUBLIC_URLare baked into the admin SPA at build time and drive auth + CORS, so they must be the addresses your browser and TV really reach — a LAN IP or a domain, with the published ports.localhostonly works from the host machine. Getting these wrong is the most common first-deploy snag (see Gotchas).
3. Bring it up
docker compose up -dWatch the logs on first boot — the sequence is:
- Postgres starts and passes its healthcheck.
serverrunsprisma migrate deploy(builds the schema), seeds the first admin fromADMIN_EMAIL/ADMIN_PASSWORD, then starts the API on container port3000. Its healthcheck hits/api/health.webrunsvite buildwithSERVER_PUBLIC_URLbaked in — this takes a minute or two on first start (the healthcheck has a generous start period) — then serves the SPA on container port3001.
The build-on-start for web is by design: each self-host lives at a different address, so the admin
SPA is compiled for your server URL rather than shipped pre-baked.
4. Sign in and connect Plex
- Open the admin at your
WEB_PUBLIC_URL(e.g.http://192.168.1.50:36021). - Sign in with the
ADMIN_EMAIL/ADMIN_PASSWORDyou set. That account is seeded on first boot and given theadminrole; it's your way in. Public sign-up is disabled — every other account is admin-provisioned. - From there, follow the Quick Start: connect your Plex source, run a metadata sync, and build your first channel.
The admin seed is optional but recommended. If you leave
ADMIN_EMAIL/ADMIN_PASSWORDunset (e.g. a pure Plex/OAuth deployment), no admin is created — the seed is a no-op. The seed is also idempotent: on later boots it just re-asserts theadminrole, it doesn't reset the password.
5. Watch
Open a TV app and point it at the server. The native apps scan your LAN automatically; if that misses,
enter SERVER_PUBLIC_URL by hand. Want a browser instead of a native app? Enable the optional
tvweb role — see Roles & the single image.
Gotchas that actually bite
These are the real snags from deploying on TrueNAS SCALE and plain-HTTP LANs:
- Wrong IP baked into the admin.
VITE_SERVER_URLis baked at thewebcontainer's build, which runs on every start. If you fix a typo inSERVER_PUBLIC_URL, a plain restart isn't enough — force a rebuild:docker compose up -d --force-recreate web, then hard-refresh the browser. - TrueNAS Postgres permissions. A
user: "1000:1000"on thepostgresservice can fail on a TrueNAS dataset (mkdir: can't create '…/pgdata': Permission denied— a dataset ACL overrides POSIX). The fix that works is to drop theuser:line so Postgres runs as its default and chowns itself. The stock compose doesn't setuser:on Postgres, so this only bites if you added one. - Plain-HTTP LAN login already handled. Over
http://on a LAN IP, the auth cookie is automatically issuedSameSite=Lax(notNone;Secure) — derived from theBETTER_AUTH_URLscheme — because admin and server share a host. So LAN-IP-over-HTTP login works without any extra config. (An HTTPS server keepsSameSite=None;Secure.) - The
tvwebservice stays dormant by default. It's gated behindprofiles: ["tvweb"], sodocker compose upwon't start it unless you setCOMPOSE_PROFILES=tvweb. That's intentional — see Roles.
Source map
| Concern | File |
|---|---|
| Compose stack | docker-compose.yml |
| Env reference | .env.example |
| Entrypoint (migrations, seed, role dispatch) | docker/entrypoint.sh |
| First-admin seed | packages/auth/src/lib/seed-admin.ts |
| Static SPA server (web / tvweb roles) | docker/serve-web.ts |
See also: Configuration · Roles & the single image · Updating · Quick Start
Self-hosting (Docker)
Run Airwave on your own hardware — one prebuilt image, a Postgres database, and docker compose. The deploy model, prerequisites, and where each piece lives.
Configuration
The full .env reference for self-hosting Airwave — every variable, what it does, its default or example, and whether it's required.
