Skip to content

Commit 274c289

Browse files
committed
fix: entrypoint runs as root to fix bind-mount perms before dropping privs
The old GHCR image had USER chronicle baked in, so the entrypoint ran as non-root and couldn't write to root-owned bind mounts. Now: - Dockerfile has no USER directive; container starts as root - Entrypoint creates /app/data/media, chowns to chronicle, then drops to chronicle via su-exec - Entrypoint handles non-root fallback gracefully with a helpful warning - chronicle user has fixed UID/GID 1000 for predictable bind-mount ownership - All compose environment vars are configurable (no hardcoded values) https://claude.ai/code/session_0153nX7vQSjEFPpZTgZdDmu5
1 parent 5749ed2 commit 274c289

3 files changed

Lines changed: 36 additions & 22 deletions

File tree

Dockerfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ FROM alpine:3.20
4646
# dropping privileges in the entrypoint.
4747
RUN apk add --no-cache ca-certificates tzdata su-exec
4848

49-
# Create non-root user for runtime security.
50-
RUN adduser -D -H -s /sbin/nologin chronicle
49+
# Create non-root user with a fixed UID/GID for predictable bind-mount
50+
# ownership. Host dirs must be owned by this UID for non-root operation.
51+
RUN addgroup -g 1000 chronicle \
52+
&& adduser -D -H -s /sbin/nologin -G chronicle -u 1000 chronicle
5153

5254
# Copy the compiled binary.
5355
COPY --from=builder /chronicle /usr/local/bin/chronicle
@@ -58,7 +60,7 @@ COPY --from=builder /src/static /app/static
5860
# Copy database migrations for auto-migration on startup.
5961
COPY --from=builder /src/db/migrations /app/db/migrations
6062

61-
# Create persistent data directory owned by the non-root user.
63+
# Create persistent data directory owned by the chronicle user.
6264
# Media uploads go under /app/data/media (matches MEDIA_PATH default "./data/media").
6365
# Mount a volume at /app/data to persist media across container rebuilds.
6466
RUN mkdir -p /app/data/media && chown -R chronicle:chronicle /app/data

docker-compose.yml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,18 @@ services:
2424
- "8080:8080"
2525
restart: unless-stopped
2626
environment:
27-
# -- SECURITY: You MUST change these before deploying --
28-
- ENV=production
29-
- PORT=8080
30-
- BASE_URL=http://localhost:8080
27+
- ENV=${ENV:-production}
28+
- PORT=${PORT:-8080}
29+
- BASE_URL=${BASE_URL:-http://localhost:8080}
3130
# REQUIRED: Generate a unique key: openssl rand -base64 32
3231
- SECRET_KEY=${SECRET_KEY:?SECRET_KEY must be set. Run openssl rand -base64 32}
3332
# -- Database (DB_PASSWORD must match MYSQL_PASSWORD below) --
34-
- DB_HOST=chronicle-db:3306
35-
- DB_USER=chronicle
36-
# REQUIRED: Change this password and match MYSQL_PASSWORD below.
33+
- DB_HOST=${DB_HOST:-chronicle-db:3306}
34+
- DB_USER=${DB_USER:-chronicle}
3735
- DB_PASSWORD=${DB_PASSWORD:-chronicle}
38-
- DB_NAME=chronicle
39-
- REDIS_URL=redis://chronicle-redis:6379
36+
- DB_NAME=${DB_NAME:-chronicle}
37+
- REDIS_URL=${REDIS_URL:-redis://chronicle-redis:6379}
38+
- MEDIA_PATH=${MEDIA_PATH:-./data/media}
4039
volumes:
4140
- chronicle-data:/app/data
4241
depends_on:

docker-entrypoint.sh

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
#!/bin/sh
2-
# docker-entrypoint.sh -- fix bind-mount permissions, then drop to non-root.
2+
# docker-entrypoint.sh -- fix bind-mount permissions, then run the server.
33
#
4-
# When /app/data is a bind mount, the host directory's ownership may not match
5-
# the container's "chronicle" user. This script runs as root to ensure the data
6-
# directory is writable, then exec's the server as the unprivileged user.
4+
# When running as root (default), this script fixes /app/data ownership and
5+
# drops to the "chronicle" user via su-exec.
6+
# When running as non-root (e.g. Cosmos Cloud sets user: chronicle), it
7+
# creates subdirectories if writable and runs the server directly.
8+
#
9+
# For bind mounts with non-root user, ensure the host directory is owned by
10+
# the container's UID: chown -R $(id -u chronicle):$(id -g chronicle) /path/to/data
711

812
set -e
913

10-
# Ensure the media subdirectory exists and is owned by chronicle.
11-
mkdir -p /app/data/media
12-
chown -R chronicle:chronicle /app/data
13-
14-
# Drop privileges and exec the main process.
15-
exec su-exec chronicle "$@"
14+
if [ "$(id -u)" = "0" ]; then
15+
# Running as root: ensure dirs exist, fix ownership, drop privileges.
16+
mkdir -p /app/data/media
17+
chown -R chronicle:chronicle /app/data
18+
exec su-exec chronicle "$@"
19+
else
20+
# Running as non-root (platform-enforced user).
21+
# Try to create media dir; if it fails, the bind mount host dir needs
22+
# its ownership fixed (see comment above).
23+
if ! mkdir -p /app/data/media 2>/dev/null; then
24+
echo "WARNING: Cannot create /app/data/media -- bind mount not writable by UID $(id -u)." >&2
25+
echo "Fix: chown -R $(id -u):$(id -g) <host-data-dir>" >&2
26+
fi
27+
exec "$@"
28+
fi

0 commit comments

Comments
 (0)