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).
Quickest: pizza web
Section titled “Quickest: pizza web”The fastest way to self-host is with a single command. No cloning, no config files — just Docker.
pizzapi webThis automatically:
- Clones the PizzaPi repo (if not already present) to
~/.pizzapi/web/repo - Generates required secrets (including
BETTER_AUTH_SECRETand VAPID keys) and a Docker Compose file in~/.pizzapi/web/ - 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-uifrom a local checkout (UI on port 5173) - Builds and starts Redis + the relay server
- 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.
# 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 foregroundpizzapi web --foreground
# View / change config without startingpizzapi web configpizzapi web config set port 9000pizzapi web config set extraOrigins "https://example.com"
# Managementpizzapi web logs # Tail container logspizzapi web status # Show running containerspizzapi web stop # Stop the hubAll 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/.
Manual Docker Compose
Section titled “Manual Docker Compose”-
Clone the repository
Terminal window git clone https://github.com/Pizzaface/PizzaPi.gitcd PizzaPi -
Generate required secrets
The manual
docker/compose.ymldoes not setBETTER_AUTH_SECRETor 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 Pushexport VAPID_PUBLIC_KEY=...export VAPID_PRIVATE_KEY=...export VAPID_SUBJECT=mailto:you@example.com -
Start the stack
Terminal window docker compose -f docker/compose.yml up -dThis starts:
Service Port Description redis— (internal) Session state / pub-sub (host port 6379 is commented out) ui— Sidecar that copies the GHCR web UI assets into a shared volume ntfy— Self-hosted push relay; optional, configured in Mobile Push server7492 Relay API + Web UI -
Open the web UI
Navigate to http://localhost:7492 — you’ll see the PizzaPi interface.
-
Create your account
The first time you visit, register an account. Then run
pizzapi setupon your dev machine pointing tohttp://localhost:7492.
Environment Variables
Section titled “Environment Variables”Configure the server by setting environment variables in Docker Compose or your shell:
| Variable | Default | Description |
|---|---|---|
PORT | 7492 | HTTP/WebSocket listen port |
BETTER_AUTH_SECRET | — | Required for production. Session-signing secret (min 32 chars). Generate with openssl rand -hex 32. Without it, sessions are invalidated on every restart. |
VAPID_PUBLIC_KEY | — | Web 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_KEY | — | Web Push private key. |
VAPID_SUBJECT | — | Web Push subject (mailto: or https: URL). |
PIZZAPI_REDIS_URL | — | Redis connection URL, e.g. redis://localhost:6379 |
PIZZAPI_BASE_URL | http://localhost:5173 | Public URL of the UI, added to CORS trusted origins |
PIZZAPI_EXTRA_ORIGINS | — | Comma-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.
Custom docker/compose.yml overrides
Section titled “Custom docker/compose.yml overrides”# docker/compose.override.yml (not committed — create locally)services: server: environment: PORT: "8080" PIZZAPI_REDIS_URL: redis://redis:6379 ports: - "8080:8080"Development Stack
Section titled “Development Stack”Hot-reloading dev server for contributors:
# Start both the API server and Vite UI dev serverdocker compose -f docker/compose.yml --profile dev up| Service | Port | Description |
|---|---|---|
redis | 6379 | Redis |
dev | 7492, 5173 | Bun dev server (API + Vite UI via concurrently) |
Or run without Docker:
# Prerequisites: Bun, Redis running locallybun installbun run dev# API → http://localhost:7492# UI → http://localhost:5173Running Without Docker
Section titled “Running Without Docker”If you prefer not to use Docker:
-
Install Bun
Terminal window curl -fsSL https://bun.sh/install | bash -
Start Redis
Terminal window # macOS via Homebrewbrew install redis && brew services start redis# Ubuntu/Debiansudo apt install redis-server && sudo systemctl start redis -
Install dependencies and build
Terminal window bun installbun run build -
Run the server
Terminal window cd packages/serverexport BETTER_AUTH_SECRET=$(openssl rand -hex 32)export PIZZAPI_REDIS_URL=redis://localhost:6379bun run start
Database
Section titled “Database”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).
Running Migrations
Section titled “Running Migrations”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.
Backup
Section titled “Backup”# Simple backup — safe to copy while the server is stoppedcp packages/server/auth.db packages/server/auth.db.backupReverse Proxy Configuration
Section titled “Reverse Proxy Configuration”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.
Loopback Proxies (Auto-Detected)
Section titled “Loopback Proxies (Auto-Detected)”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.
Non-Loopback Proxies
Section titled “Non-Loopback Proxies”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-Fordirectly.
# Cloud LB with a fixed CIDRPIZZAPI_TRUSTED_PROXY_CIDRS="203.0.113.0/24" pizzapi web
# Single-host LBPIZZAPI_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 webLoopback (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.
PIZZAPI_TRUST_PROXY=true pizzapi webOr in docker/compose.override.yml (pass both compose files explicitly when using -f):
services: server: environment: PIZZAPI_TRUST_PROXY: "true"docker compose -f docker/compose.yml -f docker/compose.override.yml up -dMulti-Proxy Chains
Section titled “Multi-Proxy Chains”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:
# CDN + local reverse proxy = 1 intermediate hopPIZZAPI_TRUST_PROXY=true PIZZAPI_PROXY_DEPTH=1 pizzapi webOr with both compose files explicitly (required when using -f):
# docker/compose.override.yml — CDN + local proxy = 1 intermediate hopservices: server: environment: PIZZAPI_TRUST_PROXY: "true" PIZZAPI_PROXY_DEPTH: "1"docker compose -f docker/compose.yml -f docker/compose.override.yml up -d| Depth | Topology | XFF entry used |
|---|---|---|
0 (default) | Single proxy (Caddy, nginx, Traefik) | Rightmost |
1 | CDN + local proxy (2 total hops) | Second from right |
N | N+1 total trusted hops | N positions from the right |
HTTPS with Caddy
Section titled “HTTPS with Caddy”Caddy automatically provisions TLS certificates via Let’s Encrypt:
relay.example.com { reverse_proxy localhost:7492}sudo systemctl reload caddyThen update your CLI config to use the secure URL:
pizzapi setup# Relay server URL: https://relay.example.comUpdating
Section titled “Updating”cd PizzaPigit pulldocker compose -f docker/compose.yml builddocker compose -f docker/compose.yml up -dMigrations run automatically when the server starts, so no separate migration step is required.
Security Hardening
Section titled “Security Hardening”PizzaPi includes several built-in security measures for production deployments.
Response Headers
Section titled “Response Headers”All server responses include security headers automatically:
X-Content-Type-Options: nosniff— prevents MIME-type sniffingX-Frame-Options: DENY— prevents clickjacking (tunnel responses useSAMEORIGINto 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 leakagePermissions-Policy— restricts browser feature access
No configuration needed — these are applied to every response.
Request Body Size Limits
Section titled “Request Body Size Limits”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.
Redis Health Monitoring
Section titled “Redis Health Monitoring”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.
Web Push Notifications
Section titled “Web Push Notifications”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.