- Operating
- Self-hosting
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.
Prerequisites
Section titled “Prerequisites”- Docker and Docker Compose
- A domain and a reverse proxy that terminates TLS, which HappyView doesn’t do itself. See step 4
The image
Section titled “The image”ghcr.io/gamesgamesgamesgamesgames/happyviewMulti-arch manifests are published for linux/amd64 and linux/arm64 on every release.
| Tag | Moves | Use for |
|---|---|---|
3.0.0-alpha.<n> | Never | v3, which is what these docs describe |
sha-abc1234 | Never | Pinning to an exact commit |
latest | Every stable release | v2, documented separately |
2.13.0 | Never | Pinning a v2 release |
2.13 | Every stable patch in that minor | Automatic v2 patch updates |
2 | Every stable minor in that major | Automatic 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.
1. Choose a database
Section titled “1. Choose a database”Pick one Compose file. Both live at the repository root:
| File | Database | Best for |
|---|---|---|
docker-compose.prod.sqlite.yml | SQLite | Small to medium instances. One container, one volume, no database to operate |
docker-compose.prod.postgres.yml | Postgres | Multiple 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:
mkdir -p happyview && cd happyview# SQLitecurl -o docker-compose.yml https://raw.githubusercontent.com/gamesgamesgamesgamesgames/happyview/alpha/docker-compose.prod.sqlite.yml# Postgrescurl -o docker-compose.yml https://raw.githubusercontent.com/gamesgamesgamesgamesgames/happyview/alpha/docker-compose.prod.postgres.yml2. Generate secrets
Section titled “2. Generate secrets”Create a .env next to it, with <n> replaced by the newest alpha. Compose auto-loads that filename, so these need no flag either:
cat > .env <<EOFPUBLIC_URL=https://happyview.example.comSESSION_SECRET=$(openssl rand -base64 48)TOKEN_ENCRYPTION_KEY=$(openssl rand -base64 32)HAPPYVIEW_VERSION=3.0.0-alpha.<n>EOFFor the Postgres stack, add a database password as well:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .envCompose 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 includeBASE_PATHhere.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.
3. Start the stack
Section titled “3. Start the stack”docker compose up -dHappyView runs its migrations on first boot, then starts serving on port 3000 inside the container.
Watch it come up:
docker compose logs -f4. Put a reverse proxy in front
Section titled “4. Put a reverse proxy in front”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.
Using the bundled Caddy service
Section titled “Using the bundled Caddy service”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_DOMAINto.env: the hostname fromPUBLIC_URLwith the scheme stripped, sohappyview.example.comforhttps://happyview.example.com.PUBLIC_URLitself stays the full URL. - Delete the
portsblock from thehappyviewservice. Caddy reaches it over the Compose network, so publishing it to the host as well only widens the exposure. LeaveHTTP_BINDunset.
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.
Other options
Section titled “Other options”- A proxy on the host: leave the default
portsblock and point nginx, Caddy, or Traefik at127.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_URLto 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, sinceportsbypasses 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.
5. Log in
Section titled “5. Log in”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.
Configuration
Section titled “Configuration”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:
| Variable | Stack | Default | Notes |
|---|---|---|---|
HAPPYVIEW_ | Both | latest | Image tag to deploy |
HTTP_ | Both | 127.0.0.1:3000 | Host address the container publishes on |
RUST_ | Both | happyview=info,tower_http=info,sqlx=warn | Quieter than the server’s own default |
POSTGRES_ | Postgres | 17 | Image tag for the postgres service |
POSTGRES_ | Postgres | happyview | |
POSTGRES_ | Postgres | happyview | |
POSTGRES_ | Postgres | 200 | Raised 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.
Operations
Section titled “Operations”Upgrading
Section titled “Upgrading”Bump HAPPYVIEW_VERSION in .env, then:
docker compose pulldocker compose up -dMigrations 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.
Health checks
Section titled “Health checks”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.
Stopping and restarting
Section titled “Stopping and restarting”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.
Backups
Section titled “Backups”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.
Next steps
Section titled “Next steps”- Production: what to change for production
- Hardening: locking down a public instance
- Configuring an instance: what to set first
- Configuration reference: every environment variable
- Deploying: deploy a project to your instance
© HappyProto

