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)
The published relay-server image is built from signed server-runtime and UI-web release artifacts. It runs as the non-root happier user, embeds the UI bundle at image build time, and does not need a source checkout or hstack.
Prebuilt relay-server images support the default SQLite setup and the documented Postgres override. They intentionally do not include the generated MySQL Prisma client. To run Happier with MySQL, build a source-based server image from this repository with MySQL included at build time.
Tags:
:preview(floating preview tag):preview-<short-sha>(immutable preview build):stable/:latest(stable channel, when enabled)
Updates
Docker images do not update themselves. To upgrade, pull a newer tag and restart/recreate your container:
docker pull happierdev/relay-server:preview
docker compose up -d --force-recreateFor strict control, pin to an immutable tag (for example :preview-<short-sha>) or a digest.
Full matrix (all apps/assets): Updates.
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. Postgres is the external database supported by the prebuilt image. On startup, the image applies its bundled Postgres migrations before starting the server; set RUN_MIGRATIONS=0 only when migrations are managed separately.
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.- Make sure your proxy supports WebSocket upgrades for Socket.IO realtime at
/v1/updates(otherwise machines can stay offline). - See: Deployment → Environment variables → Networking / reverse proxy
- Make sure your proxy supports WebSocket upgrades for Socket.IO realtime at
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 .The server and server-worker targets build from the source tree. The relay-server target is the same artifact-based shape used by published Docker images, so it expects explicit release artifact inputs:
docker build -t happier-relay-server --target relay-server \
--build-arg HAPPIER_RELAY_SERVER_RELEASE_TAG=server-preview \
--build-arg HAPPIER_RELAY_SERVER_VERSION=<server-runtime-version> \
--build-arg HAPPIER_RELAY_UI_WEB_RELEASE_TAG=ui-web-preview \
--build-arg HAPPIER_RELAY_UI_WEB_VERSION=<ui-web-version> \
.The release pipeline resolves these build args from GitHub releases automatically. For local source iteration, build the server target instead.
Optional: build only selected DB providers
By default, source-built server and server-worker images include generated Prisma clients for all supported DB providers. The prebuilt relay-server image is artifact-based and prunes the generated MySQL Prisma client; use the server target below when you need MySQL.
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, so includepostgresif you intend to run light mode on PGlite. The light flavor's own default issqlite.- 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 presets
Happier Server supports two presets that share the same API and runtime. A preset supplies topology and backend defaults; HAPPIER_DB_PROVIDER independently selects the database under either preset.
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 (SQLite by default, or embedded Postgres via PGlite) 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 under either preset are postgres, mysql (MySQL 8.0.16+), sqlite, and pglite. Full defaults to Postgres; light defaults to SQLite.
Image support depends on how the image was built. Prebuilt relay-server images include SQLite and Postgres runtime support; MySQL requires a source-built server image with MySQL included.
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.0.16+)
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)
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.
The image entrypoint runs the packaged PGlite migration owner before startup, just as it does for external Postgres. Persist the local data directory.
docker run --rm -p 3005:3005 \
-e PORT=3005 \
-e HAPPIER_SERVER_FLAVOR=light \
-e HAPPIER_DB_PROVIDER=pglite \
-e HAPPIER_SERVER_LIGHT_DATA_DIR=/data/server-light \
-v happier-server-light:/data/server-light \
happier-serverLight flavor (SQLite, default)
SQLite is the light flavor's default and the recommended light-mode database for Docker because it is single-process and easy to back up. With RUN_MIGRATIONS=1 (default), the entrypoint enables the server's existing startup migration path; migrations run once inside normal server startup before the database client is initialized.
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.0.16+)
This example uses MySQL 8.0.16+ + 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: SQLite
# HAPPIER_DB_PROVIDER: sqlite
# alternative: embedded Postgres via PGlite
# HAPPIER_DB_PROVIDER: pglite
volumes:
- happier-server-light:/data/server-light
command: >
sh -lc '
if [ "${HAPPIER_DB_PROVIDER:-sqlite}" = "pglite" ]; then
yarn --cwd apps/server migrate:light:deploy &&
HAPPIER_DB_PROVIDER=pglite yarn --cwd apps/server start:light;
else
yarn --cwd apps/server migrate:sqlite:deploy &&
HAPPIER_DB_PROVIDER=sqlite 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 source-built server target and prebuilt relay-server image run DB migrations by default. Migration selection follows HAPPIER_DB_PROVIDER, independently of the preset: Postgres, MySQL, and PGlite use their packaged/provider migration owner; SQLite uses the server's canonical in-process startup migration path.
Notes:
- Disable automatic migrations with
RUN_MIGRATIONS=0. For SQLite, the entrypoint maps this to the server startup migration setting; it does not launch a second migration process. - 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 presets: full (
yarn --cwd apps/server start) and light (yarn --cwd apps/server start:light). - The DB provider is independently selected via
HAPPIER_DB_PROVIDER=postgres|mysql|sqlite|pglite; full defaults to Postgres and light defaults to SQLite. - 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)