diff --git a/docker/unified/app-start.sh b/docker/unified/app-start.sh index 9e4f0f1..c2270fa 100644 --- a/docker/unified/app-start.sh +++ b/docker/unified/app-start.sh @@ -1,22 +1,59 @@ #!/bin/bash # App startup wrapper for unified container -# Starts Next.js server and initializes services +# Uses gosu to ensure correct PUID:PGID for file operations + +set -e + +# Load environment from /etc/environment (set by entrypoint) +if [ -f /etc/environment ]; then + set -a + source /etc/environment + set +a +fi + +# Get PUID/PGID (default to node user's current IDs if not set) +PUID=${PUID:-$(id -u node)} +PGID=${PGID:-$(id -g node)} echo "[App] Starting Next.js server..." +echo "[App] Process will run as UID:GID = $PUID:$PGID" + cd /app -# Start server in background -node server.js & -SERVER_PID=$! +# Use gosu to switch to correct UID:GID and start server +# This bypasses username resolution issues when PUID collides with existing users +if [ "$(id -u)" = "0" ]; then + # Running as root - use gosu to switch to PUID:PGID + echo "[App] Switching to UID:GID $PUID:$PGID via gosu..." -echo "[App] Waiting for server to be ready..." -sleep 5 + # Start server in background with gosu + gosu "$PUID:$PGID" node server.js & + SERVER_PID=$! -# Initialize application services (creates default scheduled jobs) -echo "[App] Initializing application services..." -curl -f http://localhost:3030/api/init || echo "[App] ⚠️ Warning: Failed to initialize services" + echo "[App] Waiting for server to be ready..." + sleep 5 -echo "[App] Server ready with PID $SERVER_PID" + # Initialize application services (creates default scheduled jobs) + echo "[App] Initializing application services..." + curl -sf http://localhost:3030/api/init || echo "[App] Warning: Failed to initialize services (may already be initialized)" -# Wait for server process -wait $SERVER_PID + echo "[App] Server ready with PID $SERVER_PID (running as $PUID:$PGID)" + + # Verify the process is running with correct UID:GID + if [ -f "/proc/$SERVER_PID/status" ]; then + ACTUAL_UID=$(grep '^Uid:' /proc/$SERVER_PID/status | awk '{print $2}') + ACTUAL_GID=$(grep '^Gid:' /proc/$SERVER_PID/status | awk '{print $2}') + echo "[App] Verified process credentials: UID=$ACTUAL_UID GID=$ACTUAL_GID" + + if [ "$ACTUAL_UID" != "$PUID" ] || [ "$ACTUAL_GID" != "$PGID" ]; then + echo "[App] WARNING: Process UID:GID ($ACTUAL_UID:$ACTUAL_GID) does not match expected ($PUID:$PGID)" + fi + fi + + # Wait for server process + wait $SERVER_PID +else + # Not running as root - just run directly (fallback) + echo "[App] Warning: Not running as root, cannot use gosu. Running as current user." + exec node server.js +fi diff --git a/docker/unified/entrypoint.sh b/docker/unified/entrypoint.sh index caa5a9e..63334e9 100644 --- a/docker/unified/entrypoint.sh +++ b/docker/unified/entrypoint.sh @@ -4,12 +4,16 @@ set -e echo "🚀 ReadMeABook Unified Container Starting..." # ============================================================================ -# PUID/PGID USER REMAPPING (Hybrid approach) +# PUID/PGID USER REMAPPING (Hybrid approach with gosu) # ============================================================================ # Hybrid approach to support user file ownership while maintaining PostgreSQL compatibility: # - postgres user: Keep UID 103 (required by PostgreSQL), remap GID → PGID -# - redis user: Remap UID → PUID, GID → PGID -# - node user: Remap UID → PUID, GID → PGID +# - redis user: Remap UID → PUID, GID → PGID (also uses gosu at runtime) +# - node user: Remap UID → PUID, GID → PGID (also uses gosu at runtime) +# +# NOTE: We use gosu in app-start.sh and redis-start.sh to ensure the process +# actually runs with the correct UID:GID. This fixes issues where PUID collides +# with existing system users (e.g., PUID=65534 collides with 'nobody'). # # Result: # - PostgreSQL data (103:PGID) - postgres user with shared group @@ -309,7 +313,8 @@ export NODE_ENV="production" export PORT="3030" export HOSTNAME="0.0.0.0" -# Persist environment variables for supervisord +# Persist environment variables for supervisord and child processes +# PUID/PGID are critical for gosu-based user switching in app-start.sh and redis-start.sh cat > /etc/environment < /etc/apt/sources.list.d/postgresql.list -# Install PostgreSQL, Redis, and supervisord +# Install PostgreSQL, Redis, supervisord, and gosu (for reliable user switching) RUN apt-get update && apt-get install -y \ postgresql-16 \ postgresql-client-16 \ @@ -25,6 +25,7 @@ RUN apt-get update && apt-get install -y \ openssl \ ffmpeg \ locales \ + gosu \ && sed -i 's/^# \(en_US.UTF-8 UTF-8\)/\1/' /etc/locale.gen \ && locale-gen en_US.UTF-8 \ && update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 \ @@ -106,6 +107,11 @@ COPY --chown=root:root docker/unified/app-start.sh /app/app-start.sh # Convert line endings and make executable RUN sed -i 's/\r$//' /app/app-start.sh && chmod +x /app/app-start.sh +# Copy redis startup wrapper +COPY --chown=root:root docker/unified/redis-start.sh /app/redis-start.sh +# Convert line endings and make executable +RUN sed -i 's/\r$//' /app/redis-start.sh && chmod +x /app/redis-start.sh + # Expose app port EXPOSE 3030 diff --git a/documentation/deployment/unified.md b/documentation/deployment/unified.md index 3c1a629..1eca1ce 100644 --- a/documentation/deployment/unified.md +++ b/documentation/deployment/unified.md @@ -691,6 +691,20 @@ docker volume rm readmeabook-pgdata readmeabook-redis readmeabook-cache - Let Docker create directories on first run (they'll have correct ownership) - Note: Works fine on WSL2 when using Docker volumes or letting container create directories +**19. PUID collision causes wrong GID (EACCES: permission denied, mkdir)** +- Issue: File organization fails with `EACCES: permission denied, mkdir '/media/...'` even though PUID/PGID are set correctly +- Symptoms: `docker exec -u $PUID:$PGID container mkdir /media/test` works, but the app fails +- Root cause: When PUID collides with an existing system user (e.g., PUID=65534 collides with `nobody`), the `usermod` command creates two users with the same UID. When supervisord resolves `user=node` from /etc/passwd, it may resolve to the wrong user's GID +- Example: User sets PUID=65534 (nobody), PGID=321601206 (AD group). App runs with UID=65534 but GID=65534 (nogroup) instead of GID=321601206 +- Diagnosis: Run `docker exec container ps ax -o user,pid,group,gid,comm` - if GID column shows wrong value, this is the issue +- Fix: Use `gosu` for reliable UID:GID switching that bypasses username resolution + - Added `gosu` package to Dockerfile + - Created `app-start.sh` and `redis-start.sh` wrapper scripts that use `gosu $PUID:$PGID` to switch users + - Supervisord now starts these wrappers as root, and gosu switches to the exact UID:GID + - PUID/PGID are passed via /etc/environment to the wrapper scripts +- Verification: After fix, `ps ax -o user,pid,group,gid,comm` will show correct GID for app and redis processes +- Note: This primarily affects users with complex setups (NFS mounts, AD/SSSD groups, PUID=65534) + ## Related - [Multi-container deployment](docker.md)