1505775678
The fresh-install restart loop reported by @MrGabri (and confirmed by
@AloePacci with the user:0:0 workaround) had a clear root cause:
- Dockerfile pinned USER nodejs (UID 1001) before the entrypoint
ran, so the existing chown branch in init-production.sh:13 was
dead code.
- wait-for-db.sh (the actual entrypoint, not init-production.sh)
silently swallowed mkdir/EACCES on bind mounts with || true,
then a downstream migration error surfaced as the visible failure.
- Net effect on a typical Linux host where the bind-mount dir is
owned by UID 1000: container can't write, exits non-zero,
restarts forever with no clear error.
Switch to the standard Docker drop-privileges pattern:
1. Install su-exec, drop `USER nodejs` from the Dockerfile —
container now starts as root.
2. wait-for-db.sh: if running as root, chown /app/storage,
/app/data, /app/logs to nodejs and re-exec self via
su-exec nodejs:nodejs. App still ends up running as UID 1001.
3. Preflight check for non-root invocations (compose `user:`
overrides): verify the bind mounts are actually writable
before continuing. If not, exit 1 immediately with an
actionable error pointing at the docs — no more silent
restart loops.
Also:
- Delete backend/init-production.sh. It was an orphan — no caller
in the Dockerfile, compose, or anywhere else. Its chown logic
looked authoritative enough that @MrGabri ran it manually trying
to debug, which is what finally surfaced the EACCES.
- docker-compose.yml: drop user: + PUID/PGID env. The pattern-B
UID-matching workaround they implemented is obsolete now that
pattern A (root-then-drop) is in place.
- .env.example + README: drop PUID/PGID documentation.
- Add fresh-install smoke test workflow. Boots backend + postgres
against bind mounts owned by UID 1000 (the GitHub runner UID,
and the common-mismatch case on Linux hosts) and verifies:
+ container reaches healthy without restart-looping
+ chown happened (dirs now owned by 1001 inside the container)
+ node runs as nodejs, not root (su-exec drop worked)
+ /health returns status:ok
+ with --user 5005:5005 + unwritable mounts, preflight exits
loud with the expected error string
Verified locally end-to-end against a fresh Postgres + UID-501-owned
bind mount: backend reaches healthy in ~20s, chown applied, node
runs as nodejs, no restart loop. Docs in picpeak-docs cover the new
behavior + a Troubleshooting section for the install-path bugs
fixed in #484/#494/#511/#488.
Refs: #484
145 lines
4.0 KiB
YAML
145 lines
4.0 KiB
YAML
services:
|
|
backend:
|
|
build:
|
|
context: ./backend
|
|
dockerfile: Dockerfile
|
|
container_name: picpeak-backend
|
|
restart: unless-stopped
|
|
environment:
|
|
- NODE_ENV=${NODE_ENV:-production}
|
|
- PORT=3000
|
|
- JWT_SECRET=${JWT_SECRET}
|
|
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
|
|
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
|
- DATABASE_CLIENT=pg
|
|
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
|
|
- DB_TYPE=postgresql
|
|
- DB_HOST=postgres
|
|
- DB_PORT=5432
|
|
- DB_USER=${DB_USER}
|
|
- DB_PASSWORD=${DB_PASSWORD}
|
|
- DB_NAME=${DB_NAME}
|
|
- EXTERNAL_MEDIA_ROOT=${EXTERNAL_MEDIA_ROOT:-/app/storage/external-media}
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_SECURE=${SMTP_SECURE:-false}
|
|
- SMTP_USER=${SMTP_USER}
|
|
- SMTP_PASS=${SMTP_PASS}
|
|
- EMAIL_FROM=${EMAIL_FROM:-noreply@picpeak.local}
|
|
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
|
|
- ADMIN_URL=${ADMIN_URL:-http://localhost:3001}
|
|
- TZ=${TZ:-UTC}
|
|
- STORAGE_PATH=/app/storage
|
|
# No `user:` directive — as of #484, the container starts as root,
|
|
# chowns the bind mounts to nodejs (UID 1001), then drops privileges
|
|
# via su-exec. PUID/PGID env vars are no longer read; if you need
|
|
# a different runtime UID, pre-chown the host dirs and pin
|
|
# `user: "<uid>:<gid>"` here.
|
|
volumes:
|
|
- ./events:/app/events
|
|
- ./data:/app/data
|
|
- ./logs:/app/logs
|
|
- ./backup:/backup
|
|
- ./storage:/app/storage
|
|
ports:
|
|
- "${BACKEND_PORT:-3001}:3000"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 40s
|
|
networks:
|
|
- picpeak-network
|
|
|
|
postgres:
|
|
image: postgres:15-alpine
|
|
container_name: picpeak-postgres
|
|
restart: unless-stopped
|
|
userns_mode: "host"
|
|
environment:
|
|
- POSTGRES_USER=${DB_USER}
|
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
- POSTGRES_DB=${DB_NAME}
|
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
|
- TZ=${TZ:-UTC}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
ports:
|
|
- "127.0.0.1:${DB_PORT:-5432}:5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
networks:
|
|
- picpeak-network
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: picpeak-redis
|
|
restart: unless-stopped
|
|
userns_mode: "host"
|
|
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-picpeak_redis_pass}
|
|
volumes:
|
|
- redis-data:/data
|
|
ports:
|
|
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 3
|
|
networks:
|
|
- picpeak-network
|
|
|
|
mailhog:
|
|
image: mailhog/mailhog:latest
|
|
container_name: picpeak-mailhog
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${MAILHOG_SMTP_PORT:-1025}:1025"
|
|
- "${MAILHOG_UI_PORT:-8025}:8025"
|
|
networks:
|
|
- picpeak-network
|
|
|
|
frontend:
|
|
build:
|
|
context: ./frontend
|
|
dockerfile: Dockerfile
|
|
args:
|
|
- VITE_API_URL=${VITE_API_URL:-/api}
|
|
- VITE_UMAMI_URL=${VITE_UMAMI_URL:-}
|
|
- VITE_UMAMI_WEBSITE_ID=${VITE_UMAMI_WEBSITE_ID:-}
|
|
- VITE_UMAMI_SHARE_URL=${VITE_UMAMI_SHARE_URL:-}
|
|
container_name: picpeak-frontend
|
|
restart: unless-stopped
|
|
environment:
|
|
- NODE_ENV=${NODE_ENV:-production}
|
|
ports:
|
|
- "${FRONTEND_PORT:-3000}:80"
|
|
depends_on:
|
|
- backend
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
networks:
|
|
- picpeak-network
|
|
|
|
volumes:
|
|
postgres-data:
|
|
driver: local
|
|
redis-data:
|
|
driver: local
|
|
|
|
networks:
|
|
picpeak-network:
|
|
driver: bridge
|