Deployment

Local Development

This guide runs the full HappyView stack locally with hot reloading, using the docker-compose.yml at the repository root. The Rust server rebuilds on save via cargo watch, and the dashboard runs the Next.js dev server.

This is not the production path — nothing here uses the published image. To deploy HappyView from prebuilt images, see Docker. To run the server directly on your machine without containers, see From Source.

Prerequisites

1. Clone and configure

git clone git@github.com:gamesgamesgamesgamesgames/happyview
cd happyview
cp .env.example .env

Set SESSION_SECRET in .env to a random value — openssl rand -base64 48 works. The defaults cover everything else. See Configuration for the full list.

2. Start the stack

docker compose up

The first run takes several minutes: it compiles the Rust workspace from scratch inside the container and installs cargo-watch. Subsequent runs reuse the cached cargo-target volume, so only changed crates rebuild.

Four services come up:

ServicePortDescription
caddy3080Unified origin — routes to the backend or the dashboard
happyview3000Rust API server, rebuilt on save by cargo watch
web3001Next.js dashboard dev server, hot-reloads on changes to web/
tunnel---cloudflared, giving the stack a public HTTPS URL

Port 3000 does serve a dashboard, but not a useful one in this stack. The dev happyview service sets no STATIC_DIR, so the server falls back to its default of ./web/out — which, under the source bind-mount, is whatever your last npm run build left on your host. That directory is gitignored and usually stale or missing. Use :3080 (or the tunnel URL) for the dashboard.

Exposing your instance with a Cloudflare Tunnel

The tunnel service runs cloudflared against http://caddy:80 and writes the resulting URL to a shared volume. scripts/entrypoint.sh in the happyview service reads that file and exports it as PUBLIC_URL, overriding whatever you set in .env.

This exists because atproto OAuth needs a publicly reachable HTTPS URL. With a tunnel, your local instance gets one, so you can sign in with a real account and test flows that a loopback URL can't reach.

Quick tunnel (default)

Leave CLOUDFLARE_TUNNEL_TOKEN blank and you get a free ephemeral tunnel — no Cloudflare account needed. Find the URL in the logs:

docker compose logs tunnel | grep trycloudflare

The hostname is random and changes on every restart, so anything that pins your instance's URL (an OAuth client registration, a lexicon's service entry) has to be updated each time.

Named tunnel (stable hostname)

For a hostname that survives restarts, create a tunnel in the Cloudflare dashboard and set both variables in .env:

CLOUDFLARE_TUNNEL_TOKEN=<token from the Cloudflare dashboard>
TUNNEL_HOSTNAME=happyview-dev.example.com

The token starts the named tunnel, but TUNNEL_HOSTNAME is what actually gets written to the shared URL file. Set the token alone and nothing is written, so the server waits the full 30 seconds and then falls back to the PUBLIC_URL in your .env. That works, but only if you've already set it to match the tunnel's hostname.

Running without a tunnel

Start only the three local services:

docker compose up happyview web caddy

The server still waits 30 seconds for a URL that never arrives, then falls back to your .env PUBLIC_URL. To skip the wait, comment out the TUNNEL_URL_FILE line in the happyview service.

Using Postgres instead of SQLite

SQLite is the default and needs no extra container. To use Postgres, uncomment the postgres service in docker-compose.yml, its pgdata volume, and the depends_on block on the happyview service, then point DATABASE_URL at it in .env:

DATABASE_URL=postgres://happyview:happyview@postgres/happyview
POSTGRES_USER=happyview
POSTGRES_PASSWORD=happyview
POSTGRES_DB=happyview

See the database setup guide for the differences between backends.

The other Compose files

The repository root holds five Compose files, each declaring its own project name so they can run simultaneously without sharing containers, networks, or volumes:

FileProject namePurpose
docker-compose.ymlhappyviewThis dev stack
docker-compose.test.ymlhappyview-testPostgres for the Rust integration tests
docker-compose.e2e.ymlhappyview-e2eFull stack plus a local PLC directory and PDS for Playwright
docker-compose.prod.sqlite.ymlhappyview-prod-sqliteProduction, SQLite
docker-compose.prod.postgres.ymlhappyview-prod-postgresProduction, Postgres

Next steps