mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 06:55:40 +00:00
ea139272ce
Two coupled defects in the push path, fixed as one unit because 2699's honest-acceptance signature is the substrate 2698's fix reports through. BUG-2699 — Bus.Publish reports acceptance. The endpoint returned 200 pushed:true for a publish that was dropped, because Publish returned nothing and swallowed every failure. An error is two outcomes and they are kept apart: ErrBusClosed proves nothing was published (503 unavailable, safe to resend), while any other error means UNCONFIRMED — go-redis retries a command whose reply was lost, which is why the publish script already carries a dedupe token — and gets 502 push_unconfirmed, deliberately off the web client's safe-to-resend list. MemoryBus was the worse case, not the exempt one: neither implementation checked `closed`, and the in-process one dropped silently with no log at all. Seven production call sites, not the six the item named; the six best-effort producers discard through one named helper, and an AST-based test fails when a new producer publishes directly. BUG-2698 — RedisSessionPresence. A session-targeted push was resolved against the answering replica's presence registry, and the handler skips the publish when the target is absent, so a POST landing on A for a session held on B dropped the instruction and answered delivered_sessions:0. Fixed at the REGISTRY rather than the gate: a shared registry makes the snapshot right, which makes the picker complete and restores the gate's original premise, so the existing skip becomes correct for the reason it was written. Entry and index are written atomically under a TTL renewed by a goroutine that lives exactly as long as the connection; a crashed process stops renewing and Redis clears it. Staleness is unchanged and now stated in full: ~30s for a dropped client, ~90s for a dead instance. delivered_sessions becomes nullable — null means published-but-uncountable, never zero — documented as three states at every consumer. 35 Codex review rounds. Notable: a per-user registry cap was added and then removed after three consecutive rounds found defects inside it and a fourth was asked whether it belonged in this PR at all; a context bound was documented, disproved by its own test (go-redis does not apply a command context to connection establishment — 5.0s measured against a 150ms ctx), and rewritten to say what is true. Every fix was mutation-checked; one instrument was deleted for passing on broken code and one for not asserting its own premise. Filed rather than folded in: BUG-2724 (Redis keyspace namespacing + Cluster), BUG-2725 (delivered_sessions is an estimate with error in both directions), BUG-2726 (no concurrent-connection limit on the watch stream), BUG-2727 (Redis absent from readiness/metrics; silent subscriber loss), BUG-2728 (epoch-reset resume lead). Gates: build · make lint 0 issues · go test ./... (25 pkgs) · svelte-check 0 errors · vitest 1738 passed · CI 7/7 including Go (PostgreSQL) and Nix.
122 lines
5.3 KiB
YAML
122 lines
5.3 KiB
YAML
# Pad — local production setup with PostgreSQL + Redis
|
|
# Usage:
|
|
# 1. cp .env.example .env # generates a POSTGRES_PASSWORD if missing
|
|
# 2. docker compose up -d
|
|
#
|
|
# Starts Pad with PostgreSQL for storage and Redis for real-time events,
|
|
# notifications, and the shared session-presence registry.
|
|
# The web UI binds to 127.0.0.1:7777 on the host by default — LAN/internet
|
|
# access requires explicit opt-in (see PAD_BIND_ADDR below or the prod override).
|
|
# First-time setup: visit the UI or run `pad auth setup` from a local CLI.
|
|
|
|
services:
|
|
pad:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
ports:
|
|
# Bind to loopback only by default. A fresh install exposes the bootstrap
|
|
# endpoint until the first admin is created, so defaulting to 0.0.0.0
|
|
# would hand control of the instance to anyone who can route to the host.
|
|
# Override by setting PAD_BIND_ADDR=0.0.0.0 (or a specific LAN IP) in .env.
|
|
- "${PAD_BIND_ADDR:-127.0.0.1}:7777:7777"
|
|
environment:
|
|
# Container UID/GID for the pad process. Default to 1000/1000 for
|
|
# backward compat with existing compose deploys whose volumes were
|
|
# created under the previous USER pad (uid 1000) image. Override to
|
|
# match your host volume's ownership if different.
|
|
#
|
|
# ${VAR-default} (no colon), not ${VAR:-default}. Compose follows
|
|
# shell-like semantics — colon form would silently convert PUID=
|
|
# or PGID= in .env to 1000, masking operator typos. Colon-less lets
|
|
# explicit-empty values reach the entrypoint, which rejects them
|
|
# with a clear error (matches docker-entrypoint.sh's own
|
|
# ${VAR-99} convention).
|
|
PUID: "${PUID-1000}"
|
|
PGID: "${PGID-1000}"
|
|
PAD_HOST: "0.0.0.0"
|
|
PAD_PORT: "7777"
|
|
PAD_DB_DRIVER: "postgres"
|
|
# POSTGRES_PASSWORD is REQUIRED — docker compose refuses to start when unset.
|
|
# Use libpq's keyword=value DSN (not the URI form) so passwords with
|
|
# reserved URI characters like '/', '+', ':', '@' — common in
|
|
# `openssl rand -base64` output — don't need percent-encoding and
|
|
# can't break the connection string by accident.
|
|
PAD_DATABASE_URL: "host=postgres port=5432 user=pad password=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required — see .env.example} dbname=pad sslmode=disable"
|
|
# PAD_ENCRYPTION_KEY is REQUIRED on Postgres — Pad won't auto-generate
|
|
# one because a multi-replica deploy would generate a different key
|
|
# per replica and fail cross-instance decryption. Generate with
|
|
# `openssl rand -hex 32` and keep it stable across restarts.
|
|
PAD_ENCRYPTION_KEY: "${PAD_ENCRYPTION_KEY:?PAD_ENCRYPTION_KEY is required — see .env.example}"
|
|
PAD_REDIS_URL: "redis://redis:6379"
|
|
PAD_DATA_DIR: "/data"
|
|
PAD_LOG_LEVEL: "info"
|
|
# Cloud-mode wiring — safe to keep enabled on self-host deploys
|
|
# because every var defaults to empty and pad treats empty as
|
|
# "not in cloud mode". Populate from .env when co-deploying with
|
|
# pad-cloud so the reverse sidecar can cascade Stripe cancels on
|
|
# account delete (TASK-690).
|
|
PAD_CLOUD: "${PAD_CLOUD:-}"
|
|
PAD_CLOUD_SECRET: "${PAD_CLOUD_SECRET:-}"
|
|
PAD_CLOUD_SIDECAR_URL: "${PAD_CLOUD_SIDECAR_URL:-}"
|
|
PAD_CLOUD_OUTBOUND_SECRET: "${PAD_CLOUD_OUTBOUND_SECRET:-}"
|
|
volumes:
|
|
- pad-data:/data
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
# Conditional: root → su-exec to pad; non-root → direct wget.
|
|
# Mirrors the Dockerfile HEALTHCHECK so `docker run --user 1234`
|
|
# pass-through still reports healthy.
|
|
#
|
|
# NOTE: $$(id -u), not $(id -u) — Compose performs $-interpolation
|
|
# on YAML strings BEFORE the value reaches the container. Single
|
|
# `$` would be parsed as a Compose variable reference; `$$` escapes
|
|
# to a literal `$` so the shell inside the container sees `$(id -u)`.
|
|
test: ["CMD-SHELL", "if [ \"$$(id -u)\" = \"0\" ]; then exec su-exec pad wget -q --spider http://localhost:7777/api/v1/health; fi; exec wget -q --spider http://localhost:7777/api/v1/health"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 3
|
|
# Bumped 10s → 60s. The always-chown-R policy in the entrypoint
|
|
# means a user restoring a backup with a large attachment store
|
|
# can spend tens of seconds before pad starts listening — 10s
|
|
# would mark the container unhealthy mid-startup.
|
|
start_period: 60s
|
|
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
environment:
|
|
POSTGRES_USER: pad
|
|
# Required. Fail-fast with the helpful error below when unset.
|
|
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required — see .env.example}"
|
|
POSTGRES_DB: pad
|
|
volumes:
|
|
- pg-data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U pad"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
restart: unless-stopped
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru
|
|
volumes:
|
|
- redis-data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
pad-data:
|
|
pg-data:
|
|
redis-data:
|