Skip to content

Self-Hosting

Self-hosting gives you full control over your data, no rate limits, and the ability to customize the server. The default Docker stack includes Redis (event buffer), a UI sidecar (GHCR UI assets), and the PizzaPi server (relay API + web UI).


The fastest way to self-host is with a single command. No cloning, no config files — just Docker.

Terminal window
pizzapi web

This automatically:

  1. Clones the PizzaPi repo (if not already present) to ~/.pizzapi/web/repo
  2. Generates required secrets (including BETTER_AUTH_SECRET and VAPID keys) and a Docker Compose file in ~/.pizzapi/web/
  3. Uses the GHCR UI image — the default tag matches the installed CLI version (falling back to latest) — or the local dev stack (bun run dev / dev:ui) when you run --dev-ui from a local checkout (UI on port 5173)
  4. Builds and starts Redis + the relay server
  5. Serves the web UI at http://localhost:7492

On subsequent runs, it pulls the latest repo changes, regenerates the compose file with the configured UI tag, and pulls the UI image before startup.

Terminal window
# Custom port (persisted for future runs)
pizzapi web --port 8080
# Set extra CORS origins (persisted)
pizzapi web --origins "https://example.com"
# Pull a specific GHCR UI tag (default is the installed CLI version)
pizzapi web --tag main
# Build the UI locally (fallback mode)
pizzapi web --build
# Run the local dev UI stack (repo checkout only)
pizzapi web --dev-ui
# Run in the foreground
pizzapi web --foreground
# View / change config without starting
pizzapi web config
pizzapi web config set port 9000
pizzapi web config set extraOrigins "https://example.com"
# Management
pizzapi web logs # Tail container logs
pizzapi web status # Show running containers
pizzapi web stop # Stop the hub

All settings are persisted in ~/.pizzapi/web/config.json and applied on every start. CLI flags like --port and --origins update the config automatically — you only need to pass them once.

Persistent data (SQLite database) is stored in ~/.pizzapi/web/data/.


  1. Clone the repository

    Terminal window
    git clone https://github.com/Pizzaface/PizzaPi.git
    cd PizzaPi
  2. Generate required secrets

    The manual docker/compose.yml does not set BETTER_AUTH_SECRET or VAPID keys. Without them, sessions and push subscriptions are invalidated on every restart. Generate them first:

    Terminal window
    export BETTER_AUTH_SECRET=$(openssl rand -hex 32)
    # Optional: only if you use Web Push
    export VAPID_PUBLIC_KEY=...
    export VAPID_PRIVATE_KEY=...
    export VAPID_SUBJECT=mailto:you@example.com
  3. Start the stack

    Terminal window
    docker compose -f docker/compose.yml up -d

    This starts:

    ServicePortDescription
    redis— (internal)Session state / pub-sub (host port 6379 is commented out)
    uiSidecar that copies the GHCR web UI assets into a shared volume
    ntfySelf-hosted push relay; optional, configured in Mobile Push
    server7492Relay API + Web UI
  4. Open the web UI

    Navigate to http://localhost:7492 — you’ll see the PizzaPi interface.

  5. Create your account

    The first time you visit, register an account. Then run pizzapi setup on your dev machine pointing to http://localhost:7492.


Configure the server by setting environment variables in Docker Compose or your shell:

VariableDefaultDescription
PORT7492HTTP/WebSocket listen port
BETTER_AUTH_SECRETRequired for production. Session-signing secret (min 32 chars). Generate with openssl rand -hex 32. Without it, sessions are invalidated on every restart.
VAPID_PUBLIC_KEYWeb Push public key (with VAPID_PRIVATE_KEY and VAPID_SUBJECT). Auto-generated by pizza web; must be set manually for compose/no-Docker or push subscriptions break on restart.
VAPID_PRIVATE_KEYWeb Push private key.
VAPID_SUBJECTWeb Push subject (mailto: or https: URL).
PIZZAPI_REDIS_URLRedis connection URL, e.g. redis://localhost:6379
PIZZAPI_BASE_URLhttp://localhost:5173Public URL of the UI, added to CORS trusted origins
PIZZAPI_EXTRA_ORIGINSComma-separated extra trusted origins (e.g. a Tailscale URL). Added to CORS and WebSocket auth without hardcoding hostnames. Example: https://myhost.ts.net,http://myhost.ts.net:5173

See the Environment Variables reference for the full list.

# docker/compose.override.yml (not committed — create locally)
services:
server:
environment:
PORT: "8080"
PIZZAPI_REDIS_URL: redis://redis:6379
ports:
- "8080:8080"

Hot-reloading dev server for contributors:

Terminal window
# Start both the API server and Vite UI dev server
docker compose -f docker/compose.yml --profile dev up
ServicePortDescription
redis6379Redis
dev7492, 5173Bun dev server (API + Vite UI via concurrently)

Or run without Docker:

Terminal window
# Prerequisites: Bun, Redis running locally
bun install
bun run dev
# API → http://localhost:7492
# UI → http://localhost:5173

If you prefer not to use Docker:

  1. Install Bun

    Terminal window
    curl -fsSL https://bun.sh/install | bash
  2. Start Redis

    Terminal window
    # macOS via Homebrew
    brew install redis && brew services start redis
    # Ubuntu/Debian
    sudo apt install redis-server && sudo systemctl start redis
  3. Install dependencies and build

    Terminal window
    bun install
    bun run build
  4. Run the server

    Terminal window
    cd packages/server
    export BETTER_AUTH_SECRET=$(openssl rand -hex 32)
    export PIZZAPI_REDIS_URL=redis://localhost:6379
    bun run start

The server uses SQLite (via Kysely) for user accounts and session metadata. The database file lives at packages/server/auth.db by default (non-Docker); Docker uses /app/data/auth.db (named volume pizzapi-data, or ~/.pizzapi/web/data/ for pizza web).

Migrations run automatically on server startup for both Docker and no-Docker paths. bun run migrate is optional if you want to run migrations manually or inspect the process.

Terminal window
# Simple backup — safe to copy while the server is stopped
cp packages/server/auth.db packages/server/auth.db.backup

When PizzaPi runs behind a reverse proxy (like Caddy, nginx, or Traefik in Docker), the server needs to trust proxy headers to correctly identify the real client IP for rate limiting and other security features.

If your reverse proxy is on the same host and connects to PizzaPi via loopback (127.0.0.1) — for example, a system-installed nginx or Caddy forwarding to localhost:7492 — no configuration is needed. The server auto-detects this and safely trusts X-Forwarded-For headers.

If your reverse proxy reaches PizzaPi via a non-loopback address — a Docker Compose bridge, a Docker overlay network, a cloud load balancer, or anything else that isn’t 127.0.0.1/::1 — you need to tell PizzaPi which peers are trusted to set X-Forwarded-For.

There are two settings, in order of preference:

Preferred: PIZZAPI_TRUSTED_PROXY_CIDRS (CIDR allowlist)

Section titled “Preferred: PIZZAPI_TRUSTED_PROXY_CIDRS (CIDR allowlist)”

A comma-separated list of CIDR ranges (IPv4 or IPv6) whose peers are trusted as proxies. This is the only safe option when:

  • The proxy has a public IP (cloud load balancers, CDNs).
  • The server shares its private network with untrusted hosts (multi-tenant VPC, shared LAN, multi-app Docker bridge) — any of which could otherwise spoof their IP by sending X-Forwarded-For directly.
Terminal window
# Cloud LB with a fixed CIDR
PIZZAPI_TRUSTED_PROXY_CIDRS="203.0.113.0/24" pizzapi web
# Single-host LB
PIZZAPI_TRUSTED_PROXY_CIDRS="203.0.113.50" pizzapi web
# Multiple proxies (mix of IPv4 / IPv6, bare IPs default to /32 or /128)
PIZZAPI_TRUSTED_PROXY_CIDRS="10.0.0.0/8, 2001:db8::/32, 203.0.113.50" pizzapi web

Loopback (127.0.0.1, ::1) is always trusted in addition to the allowlist — you don’t have to enumerate it.

Convenience: PIZZAPI_TRUST_PROXY=true (private/loopback only)

Section titled “Convenience: PIZZAPI_TRUST_PROXY=true (private/loopback only)”

Trusts X-Forwarded-For from any loopback or RFC1918/ULA private peer (10.x, 172.16–31.x, 192.168.x, fc00::/7, fe80::/10, 169.254.x). Convenient for single-tenant Docker Compose where you control every container on the bridge network.

Terminal window
PIZZAPI_TRUST_PROXY=true pizzapi web

Or in docker/compose.override.yml (pass both compose files explicitly when using -f):

docker/compose.override.yml
services:
server:
environment:
PIZZAPI_TRUST_PROXY: "true"
Terminal window
docker compose -f docker/compose.yml -f docker/compose.override.yml up -d

By default, PizzaPi uses the rightmost X-Forwarded-For entry — the IP appended by the directly-connected proxy. This is safe against client spoofing for single-proxy setups.

If you have multiple trusted proxy hops (e.g. CDN → nginx → PizzaPi), set PIZZAPI_PROXY_DEPTH to the number of intermediate hops between the outermost trusted proxy and the real client:

Terminal window
# CDN + local reverse proxy = 1 intermediate hop
PIZZAPI_TRUST_PROXY=true PIZZAPI_PROXY_DEPTH=1 pizzapi web

Or with both compose files explicitly (required when using -f):

# docker/compose.override.yml — CDN + local proxy = 1 intermediate hop
services:
server:
environment:
PIZZAPI_TRUST_PROXY: "true"
PIZZAPI_PROXY_DEPTH: "1"
Terminal window
docker compose -f docker/compose.yml -f docker/compose.override.yml up -d
DepthTopologyXFF entry used
0 (default)Single proxy (Caddy, nginx, Traefik)Rightmost
1CDN + local proxy (2 total hops)Second from right
NN+1 total trusted hopsN positions from the right

Caddy automatically provisions TLS certificates via Let’s Encrypt:

/etc/caddy/Caddyfile
relay.example.com {
reverse_proxy localhost:7492
}
Terminal window
sudo systemctl reload caddy

Then update your CLI config to use the secure URL:

Terminal window
pizzapi setup
# Relay server URL: https://relay.example.com

Terminal window
cd PizzaPi
git pull
docker compose -f docker/compose.yml build
docker compose -f docker/compose.yml up -d

Migrations run automatically when the server starts, so no separate migration step is required.


PizzaPi includes several built-in security measures for production deployments.

All server responses include security headers automatically:

  • X-Content-Type-Options: nosniff — prevents MIME-type sniffing
  • X-Frame-Options: DENY — prevents clickjacking (tunnel responses use SAMEORIGIN to allow same-origin iframing)
  • X-XSS-Protection: 0 — disables the legacy browser XSS filter (the recommended modern practice)
  • Referrer-Policy: strict-origin-when-cross-origin — limits referrer leakage
  • Permissions-Policy — restricts browser feature access

No configuration needed — these are applied to every response.

The server enforces body size limits to prevent denial-of-service:

  • API routes: 1 MB maximum
  • Attachment uploads: 30 MB per file by default (configurable via PIZZAPI_ATTACHMENT_MAX_FILE_SIZE_BYTES); multipart body ceiling 50 MB

Requests exceeding these limits receive a 413 Payload Too Large response.

The /health endpoint reports Redis and Socket.IO connectivity status. When Redis becomes unavailable, the web UI shows a dismissable degraded-mode banner with automatic retry. Sessions continue operating with reduced functionality (no cross-node relay, no event persistence) until Redis recovers.


The server supports browser push notifications for session events. Push is configured automatically with pizza web, which generates and persists VAPID keys. For manual Docker Compose or no-Docker deployments, you must set VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT yourself, or existing push subscriptions will break on every restart. Native Android background push via ntfy is configured separately in Mobile Push.