From 0b0b1bb2d529dbaae8e49891d8d5e8019b971838 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 14 May 2026 20:31:25 +0200 Subject: [PATCH 1/2] fix(install): silence pg healthcheck noise + drop legacy workers container (#484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three install-experience bugs that compounded into MrGabri's "fresh install fails" report: 1. **postgres healthcheck noise.** `pg_isready -U ` without -d defaults to probing a database whose name matches the user. Since DB_NAME defaults to picpeak_prod (not picpeak), every healthcheck interval logged FATAL: database "picpeak" does not exist into postgres logs even though the install was working correctly. Reporter saw the FATAL, assumed broken, restarted with DB_NAME=picpeak, hit a tainted-state migration error on the second try, filed a bug. Fixed in both docker-compose.production.yml and the inline compose generated by scripts/picpeak-setup.sh — pin -d to ${DB_NAME} so the probe hits the real database. 2. **backend container shows perpetually `unhealthy`.** Both compose files used `curl -f` for the backend healthcheck, but backend/Dockerfile only installs dumb-init + postgresql-client + ffmpeg — no curl. Switch to wget --no-verbose --tries=1 --spider to match what backend/Dockerfile's own HEALTHCHECK already does. Now docker ps, docker compose ps, and the backend image's built-in healthcheck all agree. 3. **stale separate `workers` container.** scripts/picpeak-setup.sh still generated a second container running `npm run workers` alongside the backend, but workers (fileWatcher, expirationChecker, emailQueueProcessor, backgroundProcessor, webhookWorker) have been started by server.js in-process for a while — see the comment at line ~895 of the same script for the systemd-side cleanup. The duplicate container caused two file watchers and two expiration checkers to compete for the same DB rows. Removed from the generated compose; install + upgrade paths now stop and rm any pre-existing picpeak-workers container. Doesn't address issue B's bigger architecture mismatch (the script generates a build-from-source compose with no frontend container, while docker-compose.production.yml uses prebuilt images with a separate frontend container). That deserves its own design pass to pick a canonical install path and align — out of scope here. --- docker-compose.production.yml | 17 ++++++++-- scripts/picpeak-setup.sh | 61 ++++++++++++++++++++++++----------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 1bbfe487..27fcd9e5 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -15,7 +15,13 @@ services: - picpeak-network restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak}"] + # `pg_isready -U ` without -d defaults to probing a database + # whose name matches the user — postgres then logs constant + # `FATAL: database "picpeak" does not exist` even though the + # actual DB is `picpeak_prod`. Pinning -d to DB_NAME makes the + # probe hit the real database and silences the log noise that + # made #484's reporter think the install was broken. + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-picpeak} -d ${DB_NAME:-picpeak}"] interval: 10s timeout: 5s retries: 5 @@ -64,8 +70,13 @@ services: condition: service_healthy restart: unless-stopped healthcheck: - # Backend exposes /health on internal port 3000 - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + # Backend exposes /health on internal port 3000. + # The backend image only ships wget (Alpine base) — using curl + # here makes `docker ps` show the container as `unhealthy` + # indefinitely even when /health responds. Mirrors the wget-based + # HEALTHCHECK already declared in backend/Dockerfile so docker + # compose, plain `docker run`, and `docker ps` all agree. + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 diff --git a/scripts/picpeak-setup.sh b/scripts/picpeak-setup.sh index 6d84131c..3b0d071c 100755 --- a/scripts/picpeak-setup.sh +++ b/scripts/picpeak-setup.sh @@ -518,6 +518,17 @@ EOF setup_ssl_docker "$app_dir" fi + # Clean up the legacy picpeak-workers container from prior installs + # (workers are now in-process — see create_docker_compose_file). + # Otherwise the leftover container keeps running its own + # fileWatcher / expirationChecker against the same DB rows the new + # backend container processes. + if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^picpeak-workers$'; then + log_step "Removing legacy picpeak-workers container (workers now run in-process)..." + docker stop picpeak-workers >/dev/null 2>&1 || true + docker rm picpeak-workers >/dev/null 2>&1 || true + fi + # Start services log_step "Starting services..." cd "$app_dir" @@ -583,7 +594,12 @@ services: - picpeak-network restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + # `pg_isready -U ` without -d defaults to probing a database + # whose name matches the user — postgres logs constant `FATAL: + # database "" does not exist` even though the actual DB + # is `${DB_NAME}`. Pinning -d to DB_NAME silences the noise + # that made #484's reporter think the install was broken. + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 10s timeout: 5s retries: 5 @@ -621,24 +637,23 @@ services: condition: service_healthy restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] + # backend image only ships wget (Alpine base) — using curl + # makes `docker ps` show the container as `unhealthy` + # indefinitely even when /api/health responds. Mirrors the + # wget-based HEALTHCHECK in backend/Dockerfile. + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/api/health"] interval: 30s timeout: 10s retries: 3 - workers: - build: ./backend - container_name: picpeak-workers - command: npm run workers - env_file: .env - volumes: - - ./storage:/app/storage - - ./logs:/app/logs - networks: - - picpeak-network - depends_on: - - backend - restart: unless-stopped + # The legacy separate `workers` container has been removed: as of + # the in-process worker consolidation noted in the native systemd + # installer below ("Backend service includes workers — fileWatcher, + # expirationChecker are started by server.js"), running a second + # `npm run workers` container caused two file watchers + two + # expiration checkers to compete for the same DB rows. Cleaned up + # on existing installs by `picpeak-setup.sh upgrade`, which stops + # and removes the picpeak-workers container if found. volumes: postgres-data: @@ -1165,15 +1180,25 @@ update_docker_installation() { # Pull latest code git pull - + # Rebuild and restart containers docker compose down docker compose build --no-cache + + # Clean up the legacy picpeak-workers container if it exists + # (workers are now in-process — see comment in + # create_docker_compose_file). Best-effort: ignore if absent. + if docker ps -a --format '{{.Names}}' | grep -q '^picpeak-workers$'; then + log_step "Removing legacy picpeak-workers container (workers now run in-process)..." + docker stop picpeak-workers >/dev/null 2>&1 || true + docker rm picpeak-workers >/dev/null 2>&1 || true + fi + docker compose up -d - + # Run migrations docker compose exec -T backend npm run migrate - + log_success "Docker installation updated successfully!" } From d4155c46117eb1db6255ebac0ea47e6fc3e99801 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 14 May 2026 20:35:54 +0200 Subject: [PATCH 2/2] fix(install): drop racy migration step + add missing frontend container (#484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up fixes inside the same install-experience surface as the previous commit: 1. **Removed `docker compose exec -T backend npm run migrate`** in both install_docker and update_docker_installation. The backend container's wait-for-db.sh already runs `npm run migrate:safe` on startup; the script was racing it with a separate (and non-safe) `npm run migrate`. That race is the most likely actual mechanism behind #484's "relation 'photos' does not exist" error on the second install attempt — partial schema visible to one of the two parallel migrators. Replaced with a bounded wait for the backend container to become healthy (Docker healthcheck reports green only after wait-for-db.sh finishes its migration pass). 2. **Added the missing frontend container** to the script-generated compose. The script previously generated a postgres + redis + backend stack with no frontend at all (backend on host port 3001), while the documented production install (docker-compose.production.yml) ships postgres + redis + backend + frontend (nginx /api proxy on host port 3000). That shape divergence is half of issue B in #484 — script-installed admins had no frontend container and were left wondering where the UI lived. Aligning both compose files on the same shape eliminates the divergence; the frontend uses curl in its healthcheck (frontend/Dockerfile explicitly `apk add curl`) unlike the backend. The remaining piece of issue B — picking ONE canonical install path (build-from-source script vs. prebuilt-image production compose) and deprecating the other — is a deployment-strategy call that deserves its own design pass. Both paths now produce architecturally-equivalent stacks. --- scripts/picpeak-setup.sh | 65 ++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/scripts/picpeak-setup.sh b/scripts/picpeak-setup.sh index 3b0d071c..215643d2 100755 --- a/scripts/picpeak-setup.sh +++ b/scripts/picpeak-setup.sh @@ -538,9 +538,21 @@ EOF log_step "Waiting for services to initialize..." sleep 10 - # Run database migrations - log_step "Running database migrations..." - docker compose exec -T backend npm run migrate + # Migrations are run automatically by backend/wait-for-db.sh on + # container startup (`npm run migrate:safe`). Running migrate here + # in parallel — as we used to — could race against the in-container + # migration and leave the schema half-applied (the actual mechanism + # behind the "relation 'photos' does not exist" symptom in #484 + # when re-installing on top of partial state). Wait briefly for the + # backend to finish its migrate:safe pass instead. + log_step "Waiting for backend to finish migrations and become healthy..." + for _ in $(seq 1 30); do + if docker inspect -f '{{.State.Health.Status}}' picpeak-backend 2>/dev/null | grep -q '^healthy$'; then + log_success "Backend healthy." + break + fi + sleep 2 + done if [[ "$FORCE_ADMIN_PASSWORD_RESET" == "true" ]]; then log_step "Resetting admin credentials..." @@ -646,14 +658,32 @@ services: timeout: 10s retries: 3 - # The legacy separate `workers` container has been removed: as of - # the in-process worker consolidation noted in the native systemd - # installer below ("Backend service includes workers — fileWatcher, - # expirationChecker are started by server.js"), running a second - # `npm run workers` container caused two file watchers + two - # expiration checkers to compete for the same DB rows. Cleaned up - # on existing installs by `picpeak-setup.sh upgrade`, which stops - # and removes the picpeak-workers container if found. + # Frontend container (#484). Previously absent from the + # script-generated compose, which left the script's docker + # architecture (backend on host port 3001, no separate frontend) + # different from the documented production install + # (docker-compose.production.yml: backend internal-only + + # frontend nginx serving /api proxy on host port 3000). Aligning + # both shapes on the same 3-service shape — postgres + redis + + # backend, plus a frontend nginx — eliminates the doc/script + # divergence MrGabri flagged. + frontend: + build: ./frontend + container_name: picpeak-frontend + ports: + - "${FRONTEND_PORT:-3000}:80" + networks: + - picpeak-network + depends_on: + - backend + restart: unless-stopped + healthcheck: + # frontend Dockerfile installs curl (apk add --no-cache curl) + # — safe to use here unlike the backend. + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 volumes: postgres-data: @@ -1196,8 +1226,17 @@ update_docker_installation() { docker compose up -d - # Run migrations - docker compose exec -T backend npm run migrate + # Migrations are run automatically by backend/wait-for-db.sh on + # container startup. Wait for the backend to become healthy + # rather than racing it with a manual `npm run migrate`. + log_step "Waiting for backend to finish migrations and become healthy..." + for _ in $(seq 1 30); do + if docker inspect -f '{{.State.Health.Status}}' picpeak-backend 2>/dev/null | grep -q '^healthy$'; then + log_success "Backend healthy." + break + fi + sleep 2 + done log_success "Docker installation updated successfully!" }