Skip to content

Docker

This guide deploys HappyView from the prebuilt images published to GitHub Container Registry, using one of the two production Compose files in the repository. Nothing is compiled locally: the image already contains the server and the dashboard.

If you want to run the repository’s development stack with hot reloading, see Running the development stack in HappyView’s CONTRIBUTING.md instead. To run the server directly with cargo run, see From source.

  • Docker and Docker Compose
  • A domain and a reverse proxy that terminates TLS, which HappyView doesn’t do itself. See step 4
ghcr.io/gamesgamesgamesgamesgames/happyview

Multi-arch manifests are published for linux/amd64 and linux/arm64 on every release.

TagMovesUse for
3.0.0-alpha.<n>Neverv3, which is what these docs describe
sha-abc1234NeverPinning to an exact commit
latestEvery stable releasev2, documented separately
2.13.0NeverPinning a v2 release
2.13Every stable patch in that minorAutomatic v2 patch updates
2Every stable minor in that majorAutomatic v2 minor updates

v3 is in alpha, so every v3 image is a prerelease, published under its full version and its commit SHA only. A prerelease moves none of latest, 2.13 or 2. Find the newest 3.0.0-alpha.<n> on the releases page. The beta line (such as 2.17.0-beta.1) is v2.

Pick one Compose file. Both live at the repository root:

FileDatabaseBest for
docker-compose.prod.sqlite.ymlSQLiteSmall to medium instances. One container, one volume, no database to operate
docker-compose.prod.postgres.ymlPostgresMultiple HappyView replicas sharing a database, larger-than-memory working sets, or external tools reading the records table directly

See the database setup guide for the full comparison. Migrations run automatically on startup on either backend, and both directions are migratable later.

Make a directory for the deployment and download the one you picked as docker-compose.yml, so docker compose finds it without a -f flag:

Terminal window
mkdir -p happyview && cd happyview
Terminal window
# SQLite
curl -o docker-compose.yml https://raw.githubusercontent.com/gamesgamesgamesgamesgames/happyview/alpha/docker-compose.prod.sqlite.yml
Terminal window
# Postgres
curl -o docker-compose.yml https://raw.githubusercontent.com/gamesgamesgamesgamesgames/happyview/alpha/docker-compose.prod.postgres.yml

Create a .env next to it, with <n> replaced by the newest alpha. Compose auto-loads that filename, so these need no flag either:

Terminal window
cat > .env <<EOF
PUBLIC_URL=https://happyview.example.com
SESSION_SECRET=$(openssl rand -base64 48)
TOKEN_ENCRYPTION_KEY=$(openssl rand -base64 32)
HAPPYVIEW_VERSION=3.0.0-alpha.<n>
EOF

For the Postgres stack, add a database password as well:

Terminal window
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .env

Compose refuses to start without these:

  • PUBLIC_URL: the public HTTPS URL people actually reach, scheme included. It’s used to build OAuth redirect URIs, so a mismatch breaks login. Do not include BASE_PATH here.
  • SESSION_SECRET: signs the dashboard session cookie. An unset or too-short value doesn’t stop the server booting. It disables cookie login, which is why the Compose files require it explicitly.
  • TOKEN_ENCRYPTION_KEY: the AES-256-GCM key protecting OAuth tokens, DPoP private keys, and plugin secrets at rest. Without it, app sign-in, spaces, and service identity don’t work. Rotating it makes everything already encrypted unreadable.
Terminal window
docker compose up -d

HappyView runs its migrations on first boot, then starts serving on port 3000 inside the container.

Watch it come up:

Terminal window
docker compose logs -f

HappyView does not terminate TLS, and both Compose files publish to loopback only by default (127.0.0.1:3000). The proxy in front is what should be exposed.

Each Compose file ends with a commented-out caddy service that handles TLS with certificates obtained and renewed automatically. Uncomment it, along with the caddy-data and caddy-config entries in the volumes block at the bottom, then create a Caddyfile beside the Compose file:

{$CADDY_DOMAIN} {
reverse_proxy happyview:3000
}

Then, in .env and the Compose file:

  • Add CADDY_DOMAIN to .env: the hostname from PUBLIC_URL with the scheme stripped, so happyview.example.com for https://happyview.example.com. PUBLIC_URL itself stays the full URL.
  • Delete the ports block from the happyview service. Caddy reaches it over the Compose network, so publishing it to the host as well only widens the exposure. Leave HTTP_BIND unset.

Point DNS at the host before the first up. Caddy requests a certificate on startup, and a challenge for a hostname that doesn’t resolve to this host fails and retries with backoff.

  • A proxy on the host: leave the default ports block and point nginx, Caddy, or Traefik at 127.0.0.1:3000
  • Cloudflare Tunnel: no inbound port needs opening at all. See Running the development stack in HappyView’s CONTRIBUTING.md for how the tunnel is wired up, then set PUBLIC_URL to the tunnel’s hostname
  • HTTP_BIND=0.0.0.0:3000: publishes on all interfaces. Only do this if something else is firewalling the port, since ports bypasses the host firewall on most Docker installs

Whatever you use, PUBLIC_URL must match the resulting public URL exactly. To serve HappyView at a subpath alongside other services, see Reverse proxy subpath. BASE_PATH is applied at container start, so prebuilt images work at any subpath without a rebuild.

Open PUBLIC_URL in a browser. The image serves the dashboard at the root, and a fresh instance opens its setup wizard. The first handle to authenticate becomes an operator with the superadmin privilege and every permission, so use the handle you want to run the instance.

An instance ships with no interpreter, so install one before saving a script. Then deploy a project to it.

Compose hands the container only the variables named in its environment block, so a line in .env that the file doesn’t name has no effect. For the ones it names, override them in .env. The Configuration reference lists every variable with its default. These are the ones the Compose files add or default differently:

VariableStackDefaultNotes
HAPPYVIEW_VERSIONBothlatestImage tag to deploy
HTTP_BINDBoth127.0.0.1:3000Host address the container publishes on
RUST_LOGBothhappyview=info,tower_http=info,sqlx=warnQuieter than the server’s own default
POSTGRES_VERSIONPostgres17Image tag for the postgres service
POSTGRES_USERPostgreshappyview
POSTGRES_DBPostgreshappyview
POSTGRES_MAX_CONNECTIONSPostgres200Raised from the stock 100; see the note below

To pass another variable, add it to the environment block. The SQLite file has a commented-out line for ATTESTATION_PRIVATE_KEY. The Postgres file has them for that, APP_NAME, LOGO_URI, TOS_URI, POLICY_URI, PLUGIN_URLS and the BACKFILL_CONCURRENT_* settings.

Bump HAPPYVIEW_VERSION in .env, then:

Terminal window
docker compose pull
docker compose up -d

Migrations run automatically on the new container’s first boot. Back up the volume first; see Backups. If you’re coming from v2, read Upgrading to v3 first.

Both stacks ship a container healthcheck. The runtime image carries no curl or wget, so it probes /health over bash’s /dev/tcp. The SQLite stack allows a 120-second start period, because a VACUUM scheduled from the dashboard runs at boot before the server binds. The Postgres stack uses 60 seconds, which covers migrations on a first boot. Scheduling a vacuum is rejected there, so there’s nothing longer to wait out.

For an external probe, GET /health returns 200 ok once HappyView can bind its listener, and stays at the domain root even when BASE_PATH is set. It doesn’t touch the database.

Both stacks set init: true. The entrypoint execs the server, making it PID 1, where the kernel drops signals that have only their default disposition, and the server installs no SIGTERM handler. Without an init, every stop, restart, and down would hang for the full grace period and end in SIGKILL (exit 137). init: true puts tini at PID 1 to forward the signal so the server exits promptly.

This is a prompt exit, not a graceful one: there is no graceful-shutdown handler, so in-flight requests are dropped either way, and an interrupted job is re-queued on the next boot.

The Postgres service additionally uses stop_signal: SIGINT. Docker’s default SIGTERM is a Postgres smart shutdown, which waits for every client to disconnect, so it would hang for the full grace period and take a SIGKILL, leaving the next boot to run crash recovery. SIGINT is the fast shutdown: roll back open transactions, checkpoint, exit.

Container stdout is the only log sink, capped at 5 × 10 MB per service so json-file logs can’t grow unbounded. Ship stdout to your usual aggregator if you need retention.

The SQLite stack keeps everything in the happyview-data volume; the Postgres stack in pgdata. Those are the only stateful paths. See Backups for what is and isn’t recoverable from the network: most records can be re-indexed via backfill, but operators, permissions, API keys, scripts and plugin secrets cannot.