Merge pull request #519 from the-luap/fix/install-permissions-484

fix(install): self-chowning entrypoint kills fresh-install restart loop (#484)
This commit is contained in:
Paul Nothaft
2026-05-18 20:59:33 +02:00
committed by GitHub
7 changed files with 281 additions and 72 deletions
+9 -4
View File
@@ -35,12 +35,15 @@ RUN apk upgrade --no-cache
RUN npm install -g npm@10
# Install dumb-init for proper signal handling, postgresql-client for database
# checks, and ffmpeg for video upload support. Alpine's ffmpeg package ships
# both `ffmpeg` and `ffprobe` built natively against musl libc — the npm
# checks, ffmpeg for video upload support, and su-exec for the root → nodejs
# privilege drop in wait-for-db.sh (see #484: container starts as root so it
# can chown bind-mounted host volumes to UID 1001, then re-execs as nodejs
# before running the app). Alpine's ffmpeg package ships both `ffmpeg` and
# `ffprobe` built natively against musl libc — the npm
# `@ffmpeg-installer/ffmpeg` binary is glibc-built and (a) doesn't reliably
# run on Alpine and (b) only includes ffmpeg, not ffprobe (which the video
# pipeline calls via fluent-ffmpeg.ffprobe()).
RUN apk add --no-cache dumb-init postgresql-client ffmpeg
RUN apk add --no-cache dumb-init postgresql-client ffmpeg su-exec
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
@@ -56,7 +59,9 @@ RUN chmod -R a+r /app && chmod +x wait-for-db.sh
RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \
chown -R nodejs:nodejs storage data logs
USER nodejs
# No USER directive — the container starts as root so wait-for-db.sh can
# chown bind-mounted host directories to UID 1001 before dropping privs
# via su-exec. See #484 for the fresh-install restart loop this avoids.
EXPOSE 3000
-50
View File
@@ -1,50 +0,0 @@
#!/bin/sh
# init-production.sh - Production initialization script
set -e
echo "🚀 Initializing PicPeak Production Environment..."
# Wait for services to be ready
echo "⏳ Waiting for database to be fully ready..."
sleep 3
# Fix permissions if running as root (shouldn't happen with proper Dockerfile)
if [ "$(id -u)" = "0" ]; then
echo "🔧 Fixing file permissions..."
chown -R nodejs:nodejs /app/storage /app/data /app/logs 2>/dev/null || true
fi
# Create required directories
echo "📁 Creating required directories..."
mkdir -p /app/storage/events/active \
/app/storage/events/archived \
/app/storage/thumbnails \
/app/storage/uploads/logos \
/app/storage/uploads/favicons \
/app/data \
/app/logs
# Run migrations with safe runner
echo "🗄️ Running database migrations (safe mode)..."
NODE_ENV=production npm run migrate:safe
# Create admin user if environment variables are set
if [ -n "$ADMIN_EMAIL" ] && [ -n "$ADMIN_PASSWORD" ]; then
echo "👤 Creating admin user..."
node scripts/create-admin.js \
--email "$ADMIN_EMAIL" \
--username "${ADMIN_USERNAME:-admin}" \
--password "$ADMIN_PASSWORD" || echo "Admin user might already exist"
fi
# Initialize email configuration if variables are set
if [ -n "$SMTP_HOST" ]; then
echo "📧 Email configuration detected via environment variables"
fi
echo "✅ Production initialization complete!"
echo "🌐 Starting application server..."
# Start the application
exec node server.js
+37
View File
@@ -3,6 +3,43 @@
set -e
# Permission handling (#484): the image starts as root so this script can
# chown bind-mounted host volumes to UID 1001 (nodejs) before dropping
# privileges via su-exec. This avoids the fresh-install restart loop where
# the host directory's UID (commonly 1000) didn't match the container's
# hard-coded nodejs user. Compose deployments that pin `user:` to something
# other than root skip this branch — they own permissions themselves and hit
# the preflight check below instead.
if [ "$(id -u)" = "0" ]; then
if ! chown -R nodejs:nodejs /app/storage /app/data /app/logs 2>/dev/null; then
echo "ERROR: failed to chown /app/storage, /app/data, /app/logs to nodejs (UID 1001)." >&2
echo " This usually means the host filesystem rejects chown (e.g. NFS without root squash" >&2
echo " disabled, or a SELinux/AppArmor policy blocking the operation)." >&2
echo " Workaround: pre-chown the host directories to 1001:1001 and pin 'user: \"1001:1001\"'" >&2
echo " in your compose file so this script never tries to chown them itself." >&2
echo " See https://docs.picpeak.app/deployment/docker#permissions" >&2
exit 1
fi
exec su-exec nodejs:nodejs "$0" "$@"
fi
# Belt-and-suspenders: if we got here as non-root (compose `user:` override),
# verify the bind mounts are actually writable before proceeding. Failing
# loud here beats the previous behavior — silent mkdir-||-true at line 69
# followed by a confusing migration error and a restart loop.
_uid="$(id -u)"
_gid="$(id -g)"
for _dir in /app/storage /app/data /app/logs; do
if [ ! -w "$_dir" ]; then
echo "ERROR: $_dir is not writable by UID $_uid." >&2
echo " Either drop the 'user:' override from your compose file so the container starts as" >&2
echo " root and can self-fix permissions, or run on the host:" >&2
echo " chown -R $_uid:$_gid <host-mount-for-$_dir>" >&2
echo " See https://docs.picpeak.app/deployment/docker#permissions" >&2
exit 1
fi
done
host="${DB_HOST:-postgres}"
port="${DB_PORT:-5432}"
user="${DB_USER:-picpeak}"