Docker
Deploy the Happier Server using Docker / Docker Compose.
This page explains how to deploy your own Happier Server with Docker. It focuses on the server only.
Use /apps/server/.env.example as your base env template and load the same values in Docker/Compose.
Prebuilt images (recommended)
We publish preview Docker images on Docker Hub (recommended):
docker pull happierdev/relay-server:previewWe also publish the same images to GitHub Container Registry (GHCR):
docker pull ghcr.io/happier-dev/relay-server:previewPublished images:
relay-server: Happier Server + embedded web UI (this page)dev-box: Happier CLI + daemon in Docker (see: Dev box)
Tags:
:preview(floating preview tag):preview-<short-sha>(immutable preview build):stable/:latest(stable channel, when enabled)
The relay-server image defaults to the light flavor + SQLite, with all data stored under /data. Mount it as a volume so upgrades and restarts are safe.
docker run --rm -p 3005:3005 \
-v happier-server-data:/data \
happierdev/relay-server:previewConfiguration docs:
- Server env var reference: Environment variables
- Server auth policy (GitHub/OIDC/anonymous, etc): Server Auth
- If you’re running a headless machine and want to pair/authenticate without a browser: Daemon auth (headless)
Notes:
- This Docker image runs the Happier Server directly (a self-host-friendly default config).
- It embeds the web UI bundle and serves it at
/by default.- Disable UI serving: set
HAPPIER_SERVER_UI_DIR=(empty). - Serve UI under
/ui: setHAPPIER_SERVER_UI_PREFIX=/ui.
- Disable UI serving: set
- It does not install a managed host service. Upgrades happen when the container is restarted after pulling a new image tag.
Prebuilt image: use Postgres (full flavor)
The relay-server image is built from the same server code and can run full flavor too — you just override the defaults.
Example docker-compose.yml (Postgres + local files backend):
services:
db:
image: postgres:17
environment:
POSTGRES_DB: happier
POSTGRES_USER: happier
POSTGRES_PASSWORD: change-me
volumes:
- pgdata:/var/lib/postgresql/data
relay:
image: happierdev/relay-server:preview
ports:
- "3005:3005"
environment:
PORT: "3005"
HAPPIER_SERVER_FLAVOR: full
HAPPIER_DB_PROVIDER: postgres
DATABASE_URL: postgresql://happier:change-me@db:5432/happier
HANDY_MASTER_SECRET: change-me-to-a-long-random-string
HAPPIER_FILES_BACKEND: local
HAPPIER_SERVER_LIGHT_DATA_DIR: /data/happier
volumes:
- happier-relay-data:/data/happier
depends_on:
- db
volumes:
pgdata:
happier-relay-data:For S3/Minio instead of local files, set HAPPIER_FILES_BACKEND=s3 and the S3_* env vars (see Environment variables).
Optional: expose a relay securely with Tailscale
If you want to access your self-hosted relay from a phone or remote machine without opening inbound ports, we recommend using Tailscale as a sidecar and publishing the server via Tailscale Serve.
This approach:
- avoids public ingress and TLS setup
- gives you a stable HTTPS URL inside your tailnet
- keeps the Happier server image unprivileged (Tailscale runs in its own container)
Minimal docker-compose.yml example (preview channel):
services:
relay:
image: happierdev/relay-server:preview
environment:
PORT: "3005"
# Optional:
# Disable UI serving:
# HAPPIER_SERVER_UI_DIR: ""
volumes:
- happier-relay-data:/data
tailscale:
image: tailscale/tailscale:stable
network_mode: service:relay
cap_add:
- NET_ADMIN
- SYS_MODULE
devices:
- /dev/net/tun:/dev/net/tun
environment:
TS_AUTHKEY: ${TS_AUTHKEY}
TS_HOSTNAME: happier-relay
TS_STATE_DIR: /var/lib/tailscale
volumes:
- tailscale-state:/var/lib/tailscale
command: >
sh -lc '
tailscaled --state=${TS_STATE_DIR}/tailscaled.state &
sleep 2
tailscale up --authkey="${TS_AUTHKEY}" --hostname="${TS_HOSTNAME}" --accept-dns=false
tailscale serve --bg --https=443 http://127.0.0.1:3005
tail -f /dev/null
'
volumes:
happier-relay-data:
tailscale-state:Notes:
- Use an ephemeral, one-time pre-auth key for
TS_AUTHKEYwhen possible. tailscale servepublishes the Happier UI + API (including WebSockets) behind the Tailscale HTTPS endpoint.- If your environment can’t use
/dev/net/tun(some restricted hosts), fall back to a normal reverse proxy (Caddy/Traefik/nginx) or run Tailscale on the host OS and proxy to the container.
Build images
This repo includes a top-level Dockerfile with multiple targets.
# Server image (defaults to the full flavor; role defaults to "all" unless SERVER_ROLE is set at runtime)
docker build -t happier-server --target server .
# Worker image (sets SERVER_ROLE=worker in the image)
docker build -t happier-server-worker --target server-worker .
# Relay server image (defaults to light flavor + sqlite; stores state under /data)
docker build -t happier-relay-server --target relay-server .Optional: build only selected DB providers
By default, the server image includes generated Prisma clients for all supported DB providers.
If you want a smaller, provider-specific image, you can limit which Prisma clients are generated at build time:
# Build with only Postgres (includes pglite support since it uses the Postgres client)
docker build -t happier-server --target server \
--build-arg HAPPIER_BUILD_DB_PROVIDERS='postgres' \
.
# Build with Postgres + MySQL (no SQLite client)
docker build -t happier-server --target server \
--build-arg HAPPIER_BUILD_DB_PROVIDERS='postgres|mysql' \
.Notes:
pgliteuses the Postgres Prisma client; includepostgres(orpglite) to run the light flavor default.- If you build without
mysql(orsqlite) and later run withHAPPIER_DB_PROVIDER=mysql(orsqlite), the server will fail at startup with a clear error telling you to rebuild with the missing provider included.
Full vs light flavor
Happier Server supports two “flavors” that share the same API + internal logic. The difference is which storage backends are used.
Full flavor (recommended for production)
Use this if you want a production setup: an external database, and optional Redis for multi-replica Socket.IO. By default, full flavor uses S3/Minio for public files, but you can opt into local disk storage with HAPPIER_FILES_BACKEND=local.
You will need:
- A database (
DATABASE_URL) - Public files backend (choose one):
- S3/Minio-compatible storage (the
S3_*env vars) withHAPPIER_FILES_BACKEND=s3(default) - Local disk with
HAPPIER_FILES_BACKEND=local(stores files underHAPPIER_SERVER_LIGHT_DATA_DIR)
- S3/Minio-compatible storage (the
- A stable secret (
HANDY_MASTER_SECRET) - Optional: Redis for scaling realtime across multiple API replicas (
REDIS_URL+HAPPIER_SOCKET_ADAPTER=redis-streams)
Light flavor (single-node / testing)
Use this if you want the simplest self-hosted setup: a single-node DB stored on disk (embedded Postgres via PGlite by default, or SQLite) and local file storage served by the server at GET /files/*.
You will need:
- Persistent disk for the light data directory (DB + files + persisted secrets)
- Optional
HANDY_MASTER_SECRETfor a stable secret (otherwise one is generated and persisted)
Database providers
The DB provider is selected via:
HAPPIER_DB_PROVIDER(preferred) orHAPPY_DB_PROVIDER
Supported values:
- Full flavor:
postgres(default),mysql(MySQL 8+) - Light flavor:
pglite(default),sqlite
Quick start (single container)
This is useful for quick experiments. For production, prefer Docker Compose (next section) or set a restart policy and use an env file.
Full flavor (Postgres)
docker run --rm -p 3005:3005 \
-e PORT=3005 \
-e DATABASE_URL='postgresql://user:pass@db:5432/happier?sslmode=require' \
-e HANDY_MASTER_SECRET='change-me-to-a-long-random-string' \
-e HAPPIER_FILES_BACKEND=s3 \
-e S3_HOST='minio' \
-e S3_PORT='9000' \
-e S3_USE_SSL='false' \
-e S3_BUCKET='happier-public' \
-e S3_PUBLIC_URL='https://files.example.com/happier-public' \
-e S3_ACCESS_KEY='...' \
-e S3_SECRET_KEY='...' \
happier-serverFull flavor (MySQL 8+)
docker run --rm -p 3005:3005 \
-e PORT=3005 \
-e HAPPIER_DB_PROVIDER=mysql \
-e DATABASE_URL='mysql://user:pass@db:3306/happier' \
-e HANDY_MASTER_SECRET='change-me-to-a-long-random-string' \
-e HAPPIER_FILES_BACKEND=s3 \
-e S3_HOST='minio' \
-e S3_PORT='9000' \
-e S3_USE_SSL='false' \
-e S3_BUCKET='happier-public' \
-e S3_PUBLIC_URL='https://files.example.com/happier-public' \
-e S3_ACCESS_KEY='...' \
-e S3_SECRET_KEY='...' \
happier-serverProduction note: remove --rm and add --restart unless-stopped (or your platform’s equivalent), and prefer --env-file over long -e ... lists.
Example:
cp apps/server/.env.example .env
docker run --env-file .env -p 3005:3005 happier-serverFull flavor (local files backend)
If you don’t want S3/Minio, you can store public files on local disk:
docker run --rm -p 3005:3005 \
-e PORT=3005 \
-e DATABASE_URL='postgresql://user:pass@db:5432/happier?sslmode=require' \
-e HANDY_MASTER_SECRET='change-me-to-a-long-random-string' \
-e HAPPIER_FILES_BACKEND=local \
-e HAPPIER_SERVER_LIGHT_DATA_DIR=/data/happier \
-v happier-server-data:/data/happier \
happier-serverLight flavor (PGlite, default)
The default server image starts the full flavor. To run light in Docker, you can either:
- use the
relay-servertarget (recommended for self-host), or - run the
servertarget withHAPPIER_SERVER_FLAVOR=light.
For PGlite specifically, you still need to run the light migrations helper script (PGlite’s connection string is created at runtime).
docker run --rm -p 3005:3005 \
-e PORT=3005 \
-e HAPPIER_SERVER_LIGHT_DATA_DIR=/data/server-light \
-v happier-server-light:/data/server-light \
happier-server \
sh -lc 'yarn --cwd apps/server migrate:light:deploy && yarn --cwd apps/server start:light'Light flavor (SQLite)
SQLite is the recommended light-mode database for Docker because it is single-process and easy to back up. The server image entrypoint automatically runs prisma migrate deploy for SQLite when RUN_MIGRATIONS=1 (default).
docker run --rm -p 3005:3005 \
-e PORT=3005 \
-e HAPPIER_SERVER_FLAVOR=light \
-e HAPPIER_DB_PROVIDER=sqlite \
-e HAPPIER_SERVER_LIGHT_DATA_DIR=/data/server-light \
-v happier-server-light:/data/server-light \
happier-serverRecommended: Docker Compose
For most self-hosters, Docker Compose is the most reliable and reproducible way to run the server: it keeps env vars and volumes in one place and makes upgrades easier.
You can either:
- copy values from
/apps/server/.env.exampleintoenvironment:, or - use
env_file:with a managed copy of that file.
Full flavor (single process)
This example uses Postgres + Minio. Redis is optional and only needed for multi-replica API deployments.
services:
db:
image: postgres:17
environment:
POSTGRES_DB: happier
POSTGRES_USER: happier
POSTGRES_PASSWORD: change-me
volumes:
- pgdata:/var/lib/postgresql/data
minio:
image: minio/minio
command: server /data --console-address :9001
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio:/data
ports:
- "9000:9000"
- "9001:9001"
server:
build:
context: .
target: server
ports:
- "3005:3005"
environment:
PORT: "3005"
DATABASE_URL: postgresql://happier:change-me@db:5432/happier
HANDY_MASTER_SECRET: change-me-to-a-long-random-string
HAPPIER_FILES_BACKEND: s3
S3_HOST: minio
S3_PORT: "9000"
S3_USE_SSL: "false"
S3_BUCKET: happier-public
# For Minio behind a reverse proxy, set this to your public files URL.
S3_PUBLIC_URL: https://files.example.com/happier-public
S3_ACCESS_KEY: minioadmin
S3_SECRET_KEY: minioadmin
# Single-container mode: leave SERVER_ROLE unset (defaults to "all")
depends_on:
- db
- minio
volumes:
pgdata:
minio:Full flavor (single process, MySQL 8+)
This example uses MySQL 8+ + Minio.
services:
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: happier
MYSQL_ROOT_PASSWORD: change-me
volumes:
- mysqldata:/var/lib/mysql
minio:
image: minio/minio
command: server /data --console-address :9001
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio:/data
ports:
- "9000:9000"
- "9001:9001"
server:
build:
context: .
target: server
ports:
- "3005:3005"
environment:
PORT: "3005"
HAPPIER_DB_PROVIDER: mysql
DATABASE_URL: mysql://root:change-me@db:3306/happier
HANDY_MASTER_SECRET: change-me-to-a-long-random-string
HAPPIER_FILES_BACKEND: s3
S3_HOST: minio
S3_PORT: "9000"
S3_USE_SSL: "false"
S3_BUCKET: happier-public
S3_PUBLIC_URL: https://files.example.com/happier-public
S3_ACCESS_KEY: minioadmin
S3_SECRET_KEY: minioadmin
depends_on:
- db
- minio
volumes:
mysqldata:
minio:Light flavor (Compose, PGlite or SQLite)
The light flavor runs a single process and stores everything on disk.
services:
server:
build:
context: .
target: server
ports:
- "3005:3005"
environment:
PORT: "3005"
HAPPIER_SERVER_LIGHT_DATA_DIR: /data/server-light
# default: embedded Postgres via PGlite
# HAPPIER_DB_PROVIDER: pglite
# alternative:
# HAPPIER_DB_PROVIDER: sqlite
volumes:
- happier-server-light:/data/server-light
command: >
sh -lc '
if [ "${HAPPIER_DB_PROVIDER:-pglite}" = "sqlite" ]; then
yarn --cwd apps/server migrate:sqlite:deploy &&
HAPPIER_DB_PROVIDER=sqlite yarn --cwd apps/server start:light;
else
yarn --cwd apps/server migrate:light:deploy &&
yarn --cwd apps/server start:light;
fi
'
volumes:
happier-server-light:Full flavor (API + worker)
If you split roles, run:
- API container(s) with
SERVER_ROLE=api - Worker container with
SERVER_ROLE=worker
With more than one API replica, enable Redis fanout (REDIS_URL + HAPPIER_SOCKET_ADAPTER=redis-streams) and configure sticky sessions at your load balancer.
Minimal example (add this on top of the full-flavor compose above):
services:
redis:
image: redis:7
api:
build:
context: .
target: server
environment:
SERVER_ROLE: api
REDIS_URL: redis://redis:6379
HAPPIER_SOCKET_ADAPTER: redis-streams
depends_on:
- db
- minio
- redis
worker:
build:
context: .
target: server-worker
environment:
REDIS_URL: redis://redis:6379
HAPPIER_SOCKET_ADAPTER: redis-streams
depends_on:
- db
- minio
- redisMigrations
The server Docker target runs DB migrations by default before starting:
- Full flavor:
- Provider is selected by
HAPPIER_DB_PROVIDER(defaultpostgres). - The container runs
prisma migrate deployagainst the matching schema on startup.
- Provider is selected by
- Light flavor:
- The default image entrypoint starts full flavor, so for light you run migrations explicitly in your overridden command (examples above).
Notes:
- Disable auto-migrations in full flavor with
RUN_MIGRATIONS=0. - In Postgres multi-replica setups, it’s OK if more than one replica tries to migrate at startup (the DB serializes via locks).
Reverse proxy checklist
- Terminate TLS at the proxy (recommended).
- Forward WebSocket upgrade headers.
- Increase timeouts for long-lived connections.
- Keep secrets in your deployment platform (env vars), not in git.
Development notes (contributors)
If you are developing Happier Server itself (not just deploying it), keep in mind:
- There are two runtime flavors: full (
yarn --cwd apps/server start) and light (yarn --cwd apps/server start:light). - The DB provider is selected via
HAPPIER_DB_PROVIDER:- Full:
postgres(default) ormysql - Light:
pglite(default) orsqlite
- Full:
- Prisma model source of truth:
apps/server/prisma/schema.prisma. - Provider schemas are generated from it:
yarn --cwd apps/server schema:sync(and tests enforceschema:sync:check)
- Migrations are provider-specific:
- Postgres/PGlite:
apps/server/prisma/migrations/* - SQLite:
apps/server/prisma/sqlite/migrations/* - MySQL:
apps/server/prisma/mysql/migrations/*
- Postgres/PGlite:
When you change the data model, make sure you:
- Update
apps/server/prisma/schema.prisma - Run
yarn --cwd apps/server schema:sync - Create migrations for each supported provider (at least Postgres + MySQL; and SQLite if you want light/sqlite supported for that change)