mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
92a4931f44
Tiny /bin/sh entrypoint shim that, if invoked as root, reads PUID/PGID
env vars (defaulting to 99/100 — Unraid's nobody:users), remaps the
in-image pad user, chowns /data, and execs the binary via su-exec.
If invoked as non-root (caller passed --user), it just execs directly
— caller knows what they want.
Solves the classic Unraid appdata-ownership-mismatch first-run failure
where the in-image pad user (uid 1000) couldn't write to a host volume
owned by nobody:users (uid 99, gid 100). Reusable on Synology / QNAP /
TrueNAS where the host's appdata user is similarly non-1000.
Behavior changes:
- Container starts as root (USER directive removed). Entrypoint drops
privileges via su-exec before exec'ing pad — standard PUID/PGID
pattern. Healthcheck adapts: root → su-exec to pad; non-root →
direct wget.
- chown -R is always-run (warn-and-continue on per-file failures). A
shallow stat-only check would silently break pad on a restored
backup with mixed-ownership inner files.
- Healthcheck start-period bumped 10s → 60s to absorb slow chown -R
on large attachment stores.
- Compose default 1000/1000 for backward compat with existing deploys
whose volumes were created under the previous USER pad image.
- Raw `docker run` defaults to 99/100 (Unraid convention).
Validation rejects PUID=0 / PGID=0 (would defeat the unprivileged-user
invariant), empty values, and non-numeric values with clear errors.
Goes through 11 rounds of codex pre-implementation design review,
catching:
- gid bug where groupmod alone leaves /etc/passwd's primary-gid stale
- compose $-interpolation gotcha (needs $$( ) not $())
- getent missing from default alpine BusyBox
- shell ${VAR:-} silently masking explicit empty values
- healthcheck running as root after USER drop
- su-exec failing for --user non-root pass-through
Part of PLAN-1166 (Pad on Unraid — Community Apps launch). Unblocks
TASK-1169 (XML template authoring).
158 lines
8.1 KiB
Bash
158 lines
8.1 KiB
Bash
# Pad — Docker Compose environment example
|
|
#
|
|
# Copy to .env, fill in values, then run `docker compose up -d`.
|
|
#
|
|
# If you don't already have a POSTGRES_PASSWORD, generate one with:
|
|
# openssl rand -base64 32
|
|
# …and paste it below. Docker Compose WILL refuse to start with the password
|
|
# missing; this is deliberate so a fresh deploy can never inherit a known-weak
|
|
# default credential.
|
|
|
|
# REQUIRED — PostgreSQL password for the `pad` user.
|
|
POSTGRES_PASSWORD=
|
|
|
|
# REQUIRED on Postgres — AES-256 encryption key for TOTP seeds and other
|
|
# sensitive fields. 64-character hex string (32 bytes). Pad will auto-
|
|
# generate one on SQLite deployments but refuses to on Postgres because
|
|
# multi-replica deployments would generate divergent keys and fail cross-
|
|
# instance decryption. Generate with:
|
|
# openssl rand -hex 32
|
|
PAD_ENCRYPTION_KEY=
|
|
|
|
# OPTIONAL — Which host interface the Pad web UI publishes to.
|
|
# Default (loopback only) is the safest choice on a fresh install, because
|
|
# the bootstrap endpoint is reachable until the first admin is created.
|
|
# 127.0.0.1 → localhost only (default)
|
|
# 0.0.0.0 → every interface (LAN + internet if no firewall)
|
|
# 10.0.0.5 → bind to a specific LAN IP
|
|
# PAD_BIND_ADDR=127.0.0.1
|
|
|
|
# OPTIONAL — Container UID/GID for the pad process (Docker only).
|
|
# Default: 1000/1000 with docker-compose; 99/100 with raw `docker run`
|
|
# (Unraid `nobody:users` convention). Set these to match your host
|
|
# volume's ownership to avoid permission errors on /data writes.
|
|
# Must be POSITIVE integers — 0 (root) is rejected by the entrypoint.
|
|
# See docker-entrypoint.sh.
|
|
# PUID=1000
|
|
# PGID=1000
|
|
|
|
# OPTIONAL — Redis auth (docker-compose.prod.yml only).
|
|
# If set, Redis starts with `--requirepass` and Pad connects with the password.
|
|
# REDIS_PASSWORD=
|
|
|
|
# OPTIONAL — Pad Cloud sidecar shared secret (cloud mode only).
|
|
# Comma-separated list supports rotation for INBOUND calls: pad accepts
|
|
# any listed secret when pad-cloud calls pad's admin endpoints.
|
|
# PAD_CLOUD_SECRET=
|
|
|
|
# OPTIONAL — Base URL pad uses to call pad-cloud back (reverse direction,
|
|
# e.g. Stripe cancel-customer when a user deletes their account). Required
|
|
# in cloud mode to cascade account deletion through to Stripe billing;
|
|
# without it, pad deletes local rows but never cancels the subscription.
|
|
# PAD_CLOUD_SIDECAR_URL=http://pad-cloud:7778
|
|
|
|
# OPTIONAL — Exact outbound secret pad sends on reverse calls. Use this
|
|
# during a cloud-secret rotation when pad-cloud is still validating against
|
|
# the OLD value: pin the outbound here so the reverse call matches, then
|
|
# roll pad-cloud, then unpin. When unset, pad falls back to the LAST entry
|
|
# of PAD_CLOUD_SECRET (the older rotation value).
|
|
# PAD_CLOUD_OUTBOUND_SECRET=
|
|
|
|
# OPTIONAL — Encryption key for sensitive fields (32-byte hex string).
|
|
# If unset, Pad stores 2FA seeds in plaintext — set this for production.
|
|
# openssl rand -hex 32
|
|
# PAD_ENCRYPTION_KEY=
|
|
|
|
# OPTIONAL — Trusted reverse-proxy CIDRs. Only peers in these CIDRs may set
|
|
# X-Forwarded-For / X-Real-IP. Leave unset for direct-exposed deploys.
|
|
# PAD_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12
|
|
|
|
# OPTIONAL — Bearer token required to scrape /metrics. When unset, /metrics
|
|
# is reachable only from loopback (so a Prometheus on the same host keeps
|
|
# working without config); when set, every scrape must send
|
|
# "Authorization: Bearer $PAD_METRICS_TOKEN".
|
|
# openssl rand -hex 32
|
|
# PAD_METRICS_TOKEN=
|
|
|
|
# OPTIONAL — Session IP-change policy.
|
|
# Default (unset) logs an ActionSessionIPChanged audit entry when a session
|
|
# presents a new client IP and updates the stored IP. "strict" additionally
|
|
# destroys the session and forces re-login. Strict mode breaks legitimate
|
|
# mobility (mobile roaming, VPN toggles, carrier NAT); enable only for
|
|
# high-sensitivity deployments.
|
|
# PAD_IP_CHANGE_ENFORCE=strict
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Core server — see docs/deployment.md for full reference.
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
# Listen address. 0.0.0.0 for containers, 127.0.0.1 for loopback-only.
|
|
# PAD_HOST=0.0.0.0
|
|
|
|
# Listen port.
|
|
# PAD_PORT=7777
|
|
|
|
# Public-facing base URL. Used to build invitation + password-reset links.
|
|
# Required on deployments that bind to 0.0.0.0 — without it, emailed links
|
|
# point at "http://0.0.0.0:7777" which recipients cannot reach (BUG-899).
|
|
# PAD_URL=https://pad.example.com
|
|
|
|
# Alternative to PAD_URL using the generic env-var convention. Read by the
|
|
# server only (does not flip the CLI to remote mode, does not contaminate
|
|
# the CLI's BaseURL, and is intentionally NOT persisted to config.toml on
|
|
# `pad init` / `pad configure` saves), so it's safe to set on hosts that
|
|
# already use PUBLIC_URL for other tooling. Pad-cloud sets it on its
|
|
# sidecar and forwards it to the pad service in the same compose.
|
|
# Precedence: PAD_URL > PUBLIC_URL > constructed http://host:port.
|
|
# PUBLIC_URL=https://pad.example.com
|
|
|
|
# Writable data directory — SQLite DB, config, logs. Default: ~/.pad
|
|
# PAD_DATA_DIR=/data
|
|
|
|
# Log level: debug | info | warn | error
|
|
# PAD_LOG_LEVEL=info
|
|
|
|
# Operating mode: local | remote | cloud. Controls how the CLI
|
|
# discovers the daemon and which credential keying rules apply.
|
|
# PAD_MODE=local
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Database — SQLite default; set PAD_DB_DRIVER=postgres for Postgres.
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
# PAD_DB_DRIVER=postgres
|
|
# PAD_DB_PATH=/data/pad.db # SQLite path, ignored on Postgres
|
|
# PAD_DATABASE_URL=postgres://pad:secret@postgres:5432/pad?sslmode=disable
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Real-time events — cross-instance SSE fan-out via Redis pub/sub.
|
|
# Without this, SSE stays in-process (fine for single-node).
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
# PAD_REDIS_URL=redis://redis:6379
|
|
|
|
# Global cap on concurrent SSE connections (default: 1000).
|
|
# PAD_SSE_MAX_CONNECTIONS=1000
|
|
|
|
# Per-workspace cap on concurrent SSE connections (default: 100).
|
|
# PAD_SSE_MAX_PER_WORKSPACE=100
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Security hardening for production behind TLS.
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
# Set Secure flag on session cookies — requires TLS.
|
|
# PAD_SECURE_COOKIES=true
|
|
|
|
# Comma-separated list of allowed CORS origins. Leave unset to disable CORS.
|
|
# PAD_CORS_ORIGINS=https://pad.example.com,https://app.example.com
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
# Transactional email via Maileroo. Optional — without it, workspace
|
|
# invites fall back to CLI-copyable join codes.
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
# PAD_MAILEROO_API_KEY=your-sending-key
|
|
# PAD_EMAIL_FROM=noreply@example.com
|
|
# PAD_EMAIL_FROM_NAME=Pad
|