Files
pad/docker-compose.prod.yml
T
xarmian c50f0585ad fix(collab): WebSocket dials get their own per-user bucket and a per-user socket cap, and the editor jitters its reconnects (BUG-1308) (#1499)
* fix(collab): WebSocket dials get their own per-user bucket and a per-user socket cap; the web provider jitters its reconnects (BUG-1308)

A collab dial drew on the general per-user API bucket (burst 60), so 100
dials from one user got 60 through, and a user's own dials and REST
calls refused each other. Dials now draw on a CollabDial bucket (5/s,
burst 50), and how many sockets a principal may hold is bounded
separately by PAD_COLLAB_MAX_PER_USER (default 50), refused with 429
collab_limit_exceeded and Retry-After. Both defaults are sized from the
reconnect-storm measurement on BUG-1308's trail (checkpoint 2).

A browser cannot read a refused handshake's status, so the provider's
backoff is what spaces a refused herd out: each step is now jittered to
between half and all of itself.

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(collab): PAD_COLLAB_MAX_PER_USER and the collab dial bucket, in the deployment docs, samples and load-test usage (BUG-1308)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(collab): only a WebSocket upgrade is a dial; per-user cap pinned within one workspace; dial-bucket keying stated correctly (BUG-1308, codex round 1)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(collab): a PAD_COLLAB_MAX_PER_USER below 1 means unlimited, as the SSE knobs do (BUG-1308, codex round 2)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(collab): the struct field agrees, 0 or less is unlimited (BUG-1308, codex round 3)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N
2026-09-24 04:51:57 -04:00

97 lines
3.5 KiB
YAML

# Pad — production Docker Compose override
# Usage: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
#
# Extends the base docker-compose.yml with:
# - Resource limits
# - Secure cookie settings
# - CORS configuration
# - Email (Maileroo) support
# - Named network for reverse proxy integration
services:
pad:
environment:
PAD_SECURE_COOKIES: "true"
# Override Redis URL to include password when REDIS_PASSWORD is set.
# Without this, the pad container inherits the passwordless URL from
# docker-compose.yml and fails to connect when Redis AUTH is enabled.
PAD_REDIS_URL: "redis://:${REDIS_PASSWORD:-}@redis:6379"
# Set your public-facing URL for correct invitation links:
# PAD_URL: "https://pad.example.com"
# CORS origins (comma-separated):
# PAD_CORS_ORIGINS: "https://pad.example.com"
# Email (Maileroo):
# PAD_MAILEROO_API_KEY: "your-sending-key"
# PAD_EMAIL_FROM: "noreply@example.com"
# PAD_EMAIL_FROM_NAME: "Pad"
# SSE limits:
# PAD_SSE_MAX_CONNECTIONS covers BOTH SSE endpoints (BUG-2726).
# PAD_SSE_MAX_CONNECTIONS: "1000"
# PAD_SSE_MAX_PER_WORKSPACE: "100"
# PAD_SSE_MAX_PER_USER: "50"
# Open collab WebSockets per user, per instance (BUG-1308).
# PAD_COLLAB_MAX_PER_USER: "50"
# Set only when this Redis is shared with another Pad installation.
# PAD_REDIS_NAMESPACE: "prod"
deploy:
resources:
limits:
cpus: "2.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 128M
networks:
- pad-net
- default
postgres:
environment:
# Base compose already requires POSTGRES_PASSWORD. We keep the same
# required-reference here (no placeholder default) so a production
# deploy can never silently boot with a known-weak credential.
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required — see .env.example}"
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.25"
memory: 256M
# In production, consider using a managed PostgreSQL service instead.
redis:
# noeviction, NOT allkeys-lru (BUG-2698). Pad's Redis holds the
# session-presence registry as well as pub/sub, and evicting a live
# session's entry is indistinguishable from its TTL lapsing: the
# session disappears from the "push to agent" picker and a push
# targeted at it reports delivered_sessions: 0 while it is still
# connected. It self-repairs on the next 30s renewal, but the
# keyspace is small — a few hundred bytes per connected session plus
# two counters — so there is nothing to gain by evicting it. See
# docs/deployment.md's Redis configuration notes.
command: redis-server --maxmemory 128mb --maxmemory-policy noeviction --requirepass "${REDIS_PASSWORD:-}"
healthcheck:
# Override the base healthcheck to authenticate when REDIS_PASSWORD is set.
# redis-cli reads REDISCLI_AUTH automatically for authentication.
test: ["CMD-SHELL", "REDISCLI_AUTH=$${REDIS_PASSWORD:-} redis-cli ping | grep -q PONG"]
interval: 5s
timeout: 3s
retries: 5
environment:
REDIS_PASSWORD: "${REDIS_PASSWORD:-}"
deploy:
resources:
limits:
cpus: "1.0"
memory: 256M
reservations:
cpus: "0.1"
memory: 64M
networks:
pad-net:
name: pad-net
# Attach your reverse proxy (Caddy, nginx) to this network.