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 provided
docker/compose.ymlgenerates a randomBETTER_AUTH_SECRETon first start when it is unset and persists it in thepizzapi-datavolume, so it survives container restarts. Direct server execution requires a stable explicit secret (at least 32 characters); multi-replica deployments must also configure the same stable secret for every replica. Configure stable VAPID keys if you use Web Push; Compose does not generate or persist them.pizzapi webgenerates and persists these automatically. To set a secret explicitly, or configure VAPID keys before first start: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 for native Android 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 |
Generated and persisted by the provided Docker Compose stack when unset | Signs sessions and API tokens; explicit values must be at least 32 characters. Direct server execution requires a stable explicitly configured secret, and multi-replica deployments must share the same stable secret. Generate with openssl rand -hex 32. |
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 |
PIZZAPI_API_KEY_RATE_LIMIT_ENABLED |
false |
Enable better-auth API-key rate limiting. |
PIZZAPI_API_KEY_RATE_LIMIT_MAX_REQUESTS |
10 |
API-key requests allowed per rate-limit window when enabled. Invalid or non-positive values fall back to the default. |
PIZZAPI_API_KEY_RATE_LIMIT_TIME_WINDOW_MS |
86400000 |
API-key rate-limit window in milliseconds (24 hours). Invalid or non-positive values fall back to the default. |
PIZZAPI_ALLOW_ATTACHMENT_QUERY_API_KEY |
false |
Temporary compatibility escape hatch: accept a durable API key in ?apiKey= on GET /api/attachments/{id} for old mobile builds. Leaves the key in URLs and proxy logs; current clients use short-lived ?token= URLs. |
PIZZAPI_WEBHOOK_ALLOW_LEGACY_SIGNATURES |
false |
Temporary migration escape hatch: accept deprecated body-only webhook signatures (no timestamp/nonce). Identical legacy bodies are rejected for 24 hours, but legacy requests have no freshness guarantee. See Webhooks. |
PIZZAPI_EPHEMERAL_TTL_MS |
600000 |
Idle TTL for ephemeral relay sessions and their Redis event data (10 minutes). Invalid or non-positive values fall back to the default. |
PIZZAPI_EPHEMERAL_SWEEP_MS |
60000 |
Interval for pruning expired ephemeral relay sessions (1 minute). Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_EVENT_TTL_MS |
86400000 |
Redis event TTL for non-ephemeral sessions (24 hours). Ephemeral sessions use PIZZAPI_EPHEMERAL_TTL_MS instead. |
PIZZAPI_RELAY_EVENT_BUFFER_SIZE |
1000 |
Maximum Redis replay-cache rows per session. Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_EVENT_CACHE_MAX_BYTES |
8388608 |
Maximum serialized replay-cache data per session (8 MiB, excluding Redis overhead and a small gap marker). Full snapshots replace older rows. Oversize events, including snapshots, become gap markers; viewers request a fresh snapshot rather than replay incomplete history. This is not a global Redis memory limit. Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_SNAPSHOT_SCAN_CHUNK_SIZE |
64 |
Redis scan chunk size for relay snapshot cleanup. Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_SNAPSHOT_MAX_CHUNKS |
10000 |
Maximum totalChunks for a chunked session snapshot sent by a runner. A chunk whose index is not an integer in 0 ≤ chunkIndex < totalChunks ≤ limit, or whose totalChunks changes mid-stream, abandons that snapshot; the previously stored snapshot stays in place and the session is marked so that viewers who hydrate or resync from it also ask the runner for a fresh snapshot (the marker clears when a complete snapshot arrives; if the marker cannot be written after retries, the relay asks the runner for a fresh snapshot immediately). Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_SNAPSHOT_MAX_MESSAGES |
1000000 |
Maximum messages accumulated across one chunked snapshot before it is abandoned. Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_SNAPSHOT_MAX_BYTES |
536870912 |
Maximum serialized size (JSON characters, after image extraction) accumulated across one chunked snapshot (512 MiB) before it is abandoned. Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_SNAPSHOT_MAX_DEFERRED_EVENTS |
10000 |
Maximum transcript events held back while a chunked snapshot is assembling. When exceeded, the snapshot is abandoned and held events are published in order. Invalid or non-positive values fall back to the default. |
PIZZAPI_ATTACHMENT_DIR |
<cwd>/.pizzapi/uploads |
Directory for uploaded session attachments. Resolved to an absolute path at server startup. |
PIZZAPI_ATTACHMENT_TTL_MS |
900000 |
TTL for regular uploaded session attachments (15 minutes). Invalid or non-positive values fall back to the default; extracted session images use a separate 24-hour TTL. |
PIZZAPI_RELAY_IMAGE_MAX_PER_EVENT |
500 |
Maximum distinct inline images (≥ 10 KB) the relay extracts to attachment storage from one session event. Additional images have their inline data removed and are marked omitted (omittedReason: "too_many_images"). Each extracted image is also capped by PIZZAPI_ATTACHMENT_MAX_FILE_SIZE_BYTES (too_large). Invalid or non-positive values fall back to the default. |
PIZZAPI_RELAY_IMAGE_MAX_EVENT_BYTES |
134217728 |
Maximum aggregate decoded bytes of images extracted from one session event (128 MiB); images beyond it are marked omitted (event_budget_exceeded). Invalid or non-positive values fall back to the default. |
PIZZAPI_EXTRACTED_IMAGE_USER_QUOTA_BYTES |
2147483648 |
Per-user cap on stored extracted-image bytes (2 GiB), including images kept alive because a pinned or durable session references them. Usage is tracked in the SQLite attachment metadata with an atomic reservation per write, so relay instances sharing one database share one budget. Reservations are keyed by image, so relays storing the same image at once count it once. An image whose metadata cannot be recorded is not kept. New images over the quota are marked omitted (quota_exceeded) instead of being written; space frees as images expire or sessions are pruned. Only these size, count, quota, and invalid-data policies remove image data: an unexpected storage error (disk or database) keeps the image inline and a later relay stage retries extraction. Invalid or non-positive values fall back to the default. |
PIZZAPI_ROUTE_RECOVERY_USER_IDS |
— | Comma-separated user IDs allowed to list and delete legacy event routes whose owner cannot be resolved from any record. Unset means nobody can manage such quarantined routes through the API (they never match events). Recovery is delete-only. |
PIZZAPI_TUNNEL_MAX_REQUEST_BODY_BYTES |
104857600 |
Maximum request body per tunnelled HTTP request (100 MiB), enforced against Content-Length and the streamed byte count. Oversized requests get 413. 0 disables the limit. Invalid values fall back to the default. |
PIZZAPI_TUNNEL_MAX_RESPONSE_BODY_BYTES |
0 (unlimited) |
Maximum response body per tunnelled HTTP request; the stream is terminated once exceeded. Unlimited by default so large downloads and long-lived streams (SSE, logs) keep working — memory is bounded by the buffer limit below. |
PIZZAPI_TUNNEL_MAX_INFLIGHT_PER_RUNNER |
256 |
Maximum concurrent tunnelled HTTP requests plus WebSockets per runner. Excess requests get 503. 0 disables the limit. |
PIZZAPI_TUNNEL_MAX_BUFFERED_BYTES |
67108864 |
Hard ceiling (64 MiB) on bytes queued for any one slow tunnel consumer (runner link, viewer response stream, viewer WebSocket). Producers are paused at about 1 MiB; a stream that still exceeds this ceiling is terminated. For WebSockets and tunnelled HTTP request bodies the check includes the frame being forwarded (with its encoding overhead), so one oversized frame closes the socket (code 1013) or fails the request (502, Tunnel buffer limit exceeded) instead of being queued, and a single viewer frame larger than the ceiling is refused. It also caps one frame from a runner on the relay’s /_tunnel link: a larger frame closes that runner’s tunnel link (code 1009), and the runner reconnects. 0 disables the ceiling (backpressure still applies). Also read by the runner daemon for its side of the tunnel. |
PIZZAPI_DISABLE_SIGNUP_AFTER_FIRST_USER |
true |
Disable new signups after the first user exists. Set to false to keep signups open. |
PIZZAPI_TRUST_MOBILE_ORIGINS |
trusted unless set to false |
Trust Capacitor mobile origins (capacitor://localhost, https://localhost) for auth/CORS. |
PIZZAPI_MOBILE_OTA_DIR |
— | Directory containing mobile OTA manifest.json and .zip bundles. Unset or missing directory disables OTA routes with 404s. |
PIZZAPI_UI_DIR |
auto-detected UI dist | Directory for the web UI static bundle and build-info.json. Server checks this first, then monorepo UI dist fallbacks. |
PIZZAPI_PREBUILD_UI |
true |
pizza web host-side setting: set to false to skip the local UI pre-build step before Docker build. |
Ephemeral sessions are not just hidden after the idle TTL: expired, unpinned relay_session rows and matching relay_session_state rows are hard-deleted by the sweep. If you need long-lived history, use non-ephemeral sessions or pin the session before it expires.
See Configuration & Environment Variables for the runner settings and server source for server options.
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.
Mixed-version upgrades
Section titled “Mixed-version upgrades”Two protections need upgraded components on both sides. There is no version negotiation, so while old and new components are mixed these protections are not enforced. Requests still succeed; nothing fails closed.
| Protection | Enforced by | Mixed-version behavior |
|---|---|---|
| Tunnel links bound to one port exposure (details) | Relay sends each signed link’s age; the runner refuses links minted before the port was last closed | An old runner ignores the age, and an old relay never sends it. Either way, a link minted before a port was closed still reaches a later service on the same port. |
Config-route session binding (target.ownerUserId, Routes) and refusal of unbound session targets in PIZZAPI_ROUTES_FILE |
Every relay instance | An old relay instance loads target.ownerUserId but ignores it, accepts unbound session targets, and delivers to whichever session registers the ID. With several relay replicas, every old replica delivers without the check. |
Upgrade order:
- Before upgrading, bind every session target in
PIZZAPI_ROUTES_FILE. Settarget.ownerUserIdor the route’sownerUserId. A new relay refuses to load a file with an unbound session target. - Upgrade every relay instance, and do not keep old and new replicas behind the same load balancer.
- Upgrade the runners and restart their daemons. Restarting a daemon starts
new port exposures, so signed links minted before the restart stop working.
Remint them with Reload or
create_tunnel.
Do not rely on either protection until both steps 2 and 3 are complete. Rolling the relay back removes both protections; rolling a runner back removes the exposure binding for that runner.
Remote access with Tailscale HTTPS
Section titled “Remote access with Tailscale HTTPS”For private access, install and sign in to Tailscale on the relay host, enable MagicDNS, then proxy the local relay with Tailscale Serve:
tailscale serve --bg http://localhost:7492tailscale serve statusUse the HTTPS hostname shown by tailscale serve status. Add that exact origin (without a trailing slash) to the web hub’s extra origins setting, then restart the hub. For pizzapi web:
pizzapi web config set extraOrigins "https://your-hostname.tailnet.ts.net"pizzapi web stop && pizzapi webStop serving with tailscale serve off. Tailscale Funnel makes the service public; treat it as internet-facing and retain strong account authentication. See Tailscale Serve documentation.
HTTPS for tunnelled development servers
Section titled “HTTPS for tunnelled development servers”A tunnel lets a runner expose a local development port through the relay. By default, it uses a path-prefix URL; client-side single-page apps may not route correctly under that prefix. Set PIZZAPI_TUNNEL_DOMAIN on the relay to use a dedicated wildcard origin (https://<opaque-label>.<domain>/) with verbatim paths. Local development can use t.localhost:7492; a real hostname needs wildcard DNS (*.<domain>) pointing to the relay.
pizzapi web can add Caddy for wildcard TLS. For a private LAN/tailnet, PIZZAPI_CADDY=1 uses a local CA that each viewing device must trust. To use a publicly trusted certificate, configure PIZZAPI_CADDY_DNS_PROVIDER and a narrowly scoped PIZZAPI_CADDY_DNS_TOKEN; Caddy uses DNS-01, so inbound port 80 is not required. Example:
PIZZAPI_TUNNEL_DOMAIN=https://t.example.com:8444 \\PIZZAPI_CADDY=1 \\PIZZAPI_CADDY_DNS_PROVIDER=cloudflare \\PIZZAPI_CADDY_DNS_TOKEN="$TOKEN" pizzapi webThe tunnel URL itself is a bearer credential; do not publish it. Use a separate domain from the relay’s cookie domain. Tunnel TLS configuration applies to tunnel origins, not the relay’s own HTTPS endpoint. See Tunnel Tools for URL behavior and limitations.
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)Content-Security-Policy: sandbox …on path-prefix tunnel responses — runner-controlled content gets an opaque origin, so it can never act as the relay origin, even when opened in its own tab (see Tunnel origin isolation)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 - Tunnel requests: streamed to the runner rather than buffered, with their own 100 MiB per-request limit (
PIZZAPI_TUNNEL_MAX_REQUEST_BODY_BYTES), a per-runner concurrency cap, and buffer limits (see the tunnel variables above)
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, set stable VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT values so subscriptions survive restarts.
Native Android background push (ntfy)
Section titled “Native Android background push (ntfy)”This is separate from browser Web Push and is Android-only; iOS does not receive background push through this integration. The bundled Compose stack includes ntfy, but you must expose a dedicated HTTPS hostname (for example push.example.com) through your reverse proxy and configure the server’s PIZZAPI_NTFY_PUBLIC_URL and PIZZAPI_NTFY_PUBLISH_TOKEN. The internal URL remains http://ntfy.
The bundled ntfy service defaults to deny-all. Provision an admin publisher and a token, then allow anonymous read-only access to the unguessable device topics:
# Repository Compose deployment:COMPOSE=docker/compose.yml# For `pizzapi web`, use instead: COMPOSE="$HOME/.pizzapi/web/compose.yml"docker compose -f "$COMPOSE" exec ntfy ntfy user add --role=admin pizzapi-publishdocker compose -f "$COMPOSE" exec ntfy ntfy token add pizzapi-publish# Copy the returned tk_... token into PIZZAPI_NTFY_PUBLISH_TOKENdocker compose -f "$COMPOSE" exec ntfy ntfy access everyone 'pizzapi-*' read-onlySet PIZZAPI_NTFY_PUBLIC_URL=https://push.example.com and the publish token in the server environment (for the repository Compose file, its .env; for pizzapi web, export them before starting the hub). pizzapi web reads these values when it regenerates Compose, so provide them on each invocation or persist them in the environment that launches it. Proxy streaming requests to ntfy without buffering. Never publish device topic names: the topic is the read credential. If the ntfy settings are absent, browser push still works, but native Android push does not. See Preferences & Notifications for the client behavior.