mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Use gosu for reliable UID:GID switching
Fix PUID/PGID collision issues by using gosu to run services with exact UID:GID. Changes include: - Added redis-start.sh and updated app-start.sh to load /etc/environment, determine PUID/PGID, and invoke gosu "$PUID:$PGID" to start Redis and the Next.js app (with verification and fallbacks). - Updated entrypoint.sh to persist PUID/PGID into /etc/environment, document the gosu approach, and adjust startup messaging. - Updated supervisord.conf to run the new startup wrappers as root (so they can use gosu) instead of running processes directly as specific users. - Dockerfile updated to install gosu and copy the redis-start.sh wrapper. - Documentation updated (deployment/unified.md) describing the PUID collision bug, the root cause, and the gosu-based fix. This resolves cases where PUID collides with existing system users (e.g., nobody) which previously caused processes to run with the wrong GID and produce EACCES errors.
This commit is contained in:
+49
-12
@@ -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
|
||||
|
||||
@@ -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 <<EOF
|
||||
DATABASE_URL=$DATABASE_URL
|
||||
REDIS_URL=$REDIS_URL
|
||||
@@ -322,6 +327,8 @@ LOG_LEVEL=$LOG_LEVEL
|
||||
NODE_ENV=$NODE_ENV
|
||||
PORT=$PORT
|
||||
HOSTNAME=$HOSTNAME
|
||||
PUID=${PUID:-}
|
||||
PGID=${PGID:-}
|
||||
EOF
|
||||
|
||||
echo "✅ Environment configured"
|
||||
@@ -353,9 +360,14 @@ if [ "$POSTGRES_PASSWORD" = "$(generate_secret)" ]; then
|
||||
fi
|
||||
echo ""
|
||||
echo "📊 Services starting:"
|
||||
echo " - PostgreSQL (internal)"
|
||||
echo " - Redis (internal)"
|
||||
echo " - Next.js App (port 3030)"
|
||||
echo " - PostgreSQL (internal, user=postgres)"
|
||||
echo " - Redis (internal, UID:GID=${PUID:-102}:${PGID:-102})"
|
||||
echo " - Next.js App (port 3030, UID:GID=${PUID:-1000}:${PGID:-1000})"
|
||||
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
echo ""
|
||||
echo "🔐 Using gosu for reliable UID:GID switching"
|
||||
echo " App and Redis will run as $PUID:$PGID"
|
||||
fi
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Redis startup wrapper for unified container
|
||||
# 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 redis user's current IDs if not set)
|
||||
PUID=${PUID:-$(id -u redis)}
|
||||
PGID=${PGID:-$(id -g redis)}
|
||||
|
||||
echo "[Redis] Starting Redis server..."
|
||||
echo "[Redis] Process will run as UID:GID = $PUID:$PGID"
|
||||
|
||||
# Use gosu to switch to correct UID:GID and start redis
|
||||
# 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 "[Redis] Switching to UID:GID $PUID:$PGID via gosu..."
|
||||
exec gosu "$PUID:$PGID" /usr/bin/redis-server --appendonly yes --dir /var/lib/redis --bind 127.0.0.1 --port 6379
|
||||
else
|
||||
# Not running as root - just run directly (fallback)
|
||||
echo "[Redis] Warning: Not running as root, cannot use gosu. Running as current user."
|
||||
exec /usr/bin/redis-server --appendonly yes --dir /var/lib/redis --bind 127.0.0.1 --port 6379
|
||||
fi
|
||||
@@ -20,8 +20,8 @@ stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
|
||||
[program:redis]
|
||||
command=/usr/bin/redis-server --appendonly yes --dir /var/lib/redis --bind 127.0.0.1 --port 6379
|
||||
user=redis
|
||||
command=/app/redis-start.sh
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=20
|
||||
@@ -35,7 +35,7 @@ stderr_events_enabled=true
|
||||
[program:app]
|
||||
command=/app/app-start.sh
|
||||
directory=/app
|
||||
user=node
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=30
|
||||
|
||||
+7
-1
@@ -15,7 +15,7 @@ RUN apt-get update && apt-get install -y curl gnupg && \
|
||||
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user