mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
9a7b38ed12
* build(deploy): docker compose runs the released, signed image; k8s pins a release (TASK-2034)
docker-compose.yml built this checkout, so every self-hoster ran an unpinned
source build that reports its version as "dev (unknown)". It now runs
ghcr.io/perpetualsoftware/pad:${PAD_VERSION:-latest}: the multi-arch image
the release pipeline builds and cosign-signs. `latest` moves only on stable
releases (goreleaser's tag template), so an unset PAD_VERSION never pulls an
RC. .env.example documents PAD_VERSION and recommends pinning it.
Contributors keep a source build through a new override,
docker-compose.build.yml, which tags the result pad:local so it never
shadows the released image.
deploy/k8s/deployment.yaml pins 0.17.2 instead of `latest`. With several
replicas (hpa.yaml), `latest` lets pods restarted after a release run a
different version from the rest against one database.
README and docs/deployment.md describe the pin, the upgrade
(`docker compose pull && up -d`) and the contributor build.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* build(deploy): the source-build override never pulls pad:local (TASK-2034, codex P3)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
128 lines
5.8 KiB
YAML
128 lines
5.8 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
|
|
#
|
|
# Runs the RELEASED image, ghcr.io/perpetualsoftware/pad (multi-arch, built by
|
|
# the release pipeline and cosign-signed), not a build of this checkout
|
|
# (TASK-2034). Set PAD_VERSION in .env to pin a release (e.g. 0.17.2); unset,
|
|
# it is `latest`, which only ever moves to a STABLE release, never an RC.
|
|
# Upgrade: change PAD_VERSION, then `docker compose pull && docker compose up -d`.
|
|
# Contributors building this checkout instead:
|
|
# docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
|
|
#
|
|
# 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:
|
|
image: ghcr.io/perpetualsoftware/pad:${PAD_VERSION:-latest}
|
|
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:
|