mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 15:05:40 +00:00
0877b260c1
Every Redis key and channel Pad uses was flat — pad:events:, pad:event_seq, pad:watchevents*, pad:session:* — so two Pad installations pointed at one Redis endpoint cross-feed each other's notifications and merge each other's session-presence registries. Different logical DB numbers do not help: Redis pub/sub is not namespaced by DB at all. The exposure is narrow but real. Delivery is filtered per caller on user id, and user ids are per-installation UUIDs, so cross-feed needs the same id in both installations — a CLONED database, such as a staging environment restored from a production dump. For that case it is a genuine cross-tenant leak: foreign sessions listed in the picker, and a private push deliverable across installations. Fixed the way internal/watchevents' existing ruling demanded: not by one package growing a prefix the others lack, but through internal/redisns — one value parsed in cmd/pad/cmd_server.go and passed into all three constructors. The three cannot drift because there is nothing to drift from, and the operator rule is stateable in one sentence for every keyspace. PAD_REDIS_NAMESPACE defaults to empty, which reproduces the historical names byte for byte, so an existing deployment keeps addressing its own replay buffers, counters and presence entries across the upgrade. Tests assert both directions per keyspace — present under the namespace AND absent under the historical names — because an implementation that wrote both would still cross-feed while passing a one-directional test. Namespaces are validated at startup, and a colon is rejected specifically: it is Pad's own separator, so namespace "a:events" would build pad:a:events:<ws> and collide with installation "a"'s channel — reintroducing the cross-feed through the mechanism meant to fix it. Names are built through a function rather than assembled from a literal at each site, and redisns' doc says why: "pad:" also begins Pad's OAuth SCOPE values (pad:read / pad:write / pad:admin) in four files, so a grep-driven prefix sweep would break authorization. Not included, deliberately: hash tags for Redis Cluster. BUG-2724's trail recommended shipping them alongside on cost-sharing grounds; that premise is falsified by publishScript, which spans four keys in one EVAL and fails CROSSSLOT exactly as presence's MGET does. There is no cheap half, and no cluster client here to exercise tagged keys against, so they would ship untested by construction. Cluster stays documented as unsupported and the future unit is named on the trail. Renaming is a CUTOVER for the buses (the seq and epoch keys carry Last-Event-ID meaning, so connected clients resync) and free for presence (90s TTL). Both stated in docs/deployment.md and at the constructors. Claude-Session: https://claude.ai/code/session_01JVDBKbgn3Xt7ndW1YoYd8X
95 lines
3.4 KiB
YAML
95 lines
3.4 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"
|
|
# 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.
|