mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 15:05:40 +00:00
92a4931f44
Tiny /bin/sh entrypoint shim that, if invoked as root, reads PUID/PGID
env vars (defaulting to 99/100 — Unraid's nobody:users), remaps the
in-image pad user, chowns /data, and execs the binary via su-exec.
If invoked as non-root (caller passed --user), it just execs directly
— caller knows what they want.
Solves the classic Unraid appdata-ownership-mismatch first-run failure
where the in-image pad user (uid 1000) couldn't write to a host volume
owned by nobody:users (uid 99, gid 100). Reusable on Synology / QNAP /
TrueNAS where the host's appdata user is similarly non-1000.
Behavior changes:
- Container starts as root (USER directive removed). Entrypoint drops
privileges via su-exec before exec'ing pad — standard PUID/PGID
pattern. Healthcheck adapts: root → su-exec to pad; non-root →
direct wget.
- chown -R is always-run (warn-and-continue on per-file failures). A
shallow stat-only check would silently break pad on a restored
backup with mixed-ownership inner files.
- Healthcheck start-period bumped 10s → 60s to absorb slow chown -R
on large attachment stores.
- Compose default 1000/1000 for backward compat with existing deploys
whose volumes were created under the previous USER pad image.
- Raw `docker run` defaults to 99/100 (Unraid convention).
Validation rejects PUID=0 / PGID=0 (would defeat the unprivileged-user
invariant), empty values, and non-numeric values with clear errors.
Goes through 11 rounds of codex pre-implementation design review,
catching:
- gid bug where groupmod alone leaves /etc/passwd's primary-gid stale
- compose $-interpolation gotcha (needs $$( ) not $())
- getent missing from default alpine BusyBox
- shell ${VAR:-} silently masking explicit empty values
- healthcheck running as root after USER drop
- su-exec failing for --user non-root pass-through
Part of PLAN-1166 (Pad on Unraid — Community Apps launch). Unblocks
TASK-1169 (XML template authoring).
104 lines
4.4 KiB
Bash
Executable File
104 lines
4.4 KiB
Bash
Executable File
#!/bin/sh
|
|
# docker-entrypoint.sh — UID/GID remapping shim for the pad container.
|
|
#
|
|
# Allows operators to run pad as their host's preferred uid/gid by setting
|
|
# PUID and PGID env vars at container start. Mainly for Unraid users
|
|
# (default 99/100 = nobody:users) but useful on Synology / TrueNAS / any
|
|
# Docker host where the appdata volume is owned by something other than
|
|
# uid 1000.
|
|
#
|
|
# Pattern adopted from LinuxServer.io. See TASK-1168 / PLAN-1166.
|
|
#
|
|
# Compatibility: /bin/sh (BusyBox-compatible). No bash-isms.
|
|
|
|
set -e
|
|
|
|
# Pass-through path: caller specified a uid via `docker run --user`. They
|
|
# know what they want; we don't try to outsmart them. No chown, no
|
|
# privilege drop — just exec the target program directly.
|
|
if [ "$(id -u)" != "0" ]; then
|
|
exec "$@"
|
|
fi
|
|
|
|
# Remap path: container started as root (the default).
|
|
#
|
|
# Use ${VAR-default} (no colon), not ${VAR:-default}. The colon form
|
|
# substitutes the default when VAR is unset OR empty, which would silently
|
|
# accept `docker run -e PUID= -e PGID=` and mask a real operator typo.
|
|
# The colon-less form only substitutes when VAR is unset, so an explicit
|
|
# empty value falls through to validation and is rejected.
|
|
PUID="${PUID-99}"
|
|
PGID="${PGID-100}"
|
|
|
|
# Validate: must be positive integers. Rejecting 0 explicitly is critical
|
|
# — PUID=0 (root) or PGID=0 (root group) would defeat the unprivileged-
|
|
# user invariant the whole shim exists to enforce. Empty / non-numeric
|
|
# also fail loudly so an operator typo doesn't silently default.
|
|
case "$PUID" in
|
|
''|*[!0-9]*)
|
|
echo "docker-entrypoint: PUID must be a positive integer, got '$PUID'" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
case "$PGID" in
|
|
''|*[!0-9]*)
|
|
echo "docker-entrypoint: PGID must be a positive integer, got '$PGID'" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [ "$PUID" = "0" ]; then
|
|
echo "docker-entrypoint: PUID=0 (root) is rejected — set a non-zero uid to keep pad unprivileged" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$PGID" = "0" ]; then
|
|
echo "docker-entrypoint: PGID=0 (root group) is rejected — set a non-zero gid to keep pad unprivileged" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Remap the in-image `pad` user/group only when values differ — skips
|
|
# the noisy "id changed" log entry on every warm restart in the steady
|
|
# state.
|
|
#
|
|
# Use `id -u/-g pad` (BusyBox supports both flags) rather than
|
|
# `getent group pad`: getent lives in the optional `musl-utils` package
|
|
# on alpine, NOT in BusyBox's default applet list, so the entrypoint
|
|
# would crash before chown/exec on a fresh `apk add` install that
|
|
# didn't include it.
|
|
current_uid="$(id -u pad)"
|
|
current_gid="$(id -g pad)"
|
|
if [ "$current_gid" != "$PGID" ]; then
|
|
groupmod -o -g "$PGID" pad
|
|
fi
|
|
if [ "$current_uid" != "$PUID" ] || [ "$current_gid" != "$PGID" ]; then
|
|
# Combined -u + -g ensures the user's /etc/passwd primary-gid field
|
|
# is updated to PGID even when only the group renumber happened.
|
|
# `groupmod` alone changes the group's gid but leaves the user's
|
|
# /etc/passwd line referencing the OLD gid number — su-exec would
|
|
# then launch pad with the wrong process gid.
|
|
usermod -o -u "$PUID" -g "$PGID" pad
|
|
fi
|
|
|
|
# Always chown -R the data directory.
|
|
#
|
|
# Statting only `/data` itself is unsafe: a user who restored a backup
|
|
# tarball might have /data owned by the target uid but /data/pad.db,
|
|
# /data/encryption.key, or /data/attachments/* owned differently, and a
|
|
# shallow check would skip the recursive chown — leaving pad unable to
|
|
# read its own DB. The few seconds of chown cost on warm restart with
|
|
# large attachment stores is the correct trade for guaranteed correctness.
|
|
#
|
|
# Warn-and-continue (not fail-fast). chown -R can fail on individual
|
|
# files for non-fatal reasons (immutable bits, root-squashed NFS,
|
|
# partial permission gaps). Refusing to start pad over any single
|
|
# failed file would lock the operator out of an otherwise-fine /data.
|
|
# chown's per-file errors hit stderr already (visible in `docker logs`);
|
|
# the trailing aggregated warning surfaces a single clear "investigate
|
|
# this" line. The `||` form is set-e-safe.
|
|
chown -R "$PUID:$PGID" /data \
|
|
|| echo "docker-entrypoint: warning: chown -R /data had errors (see above). pad will start anyway; some files may not be writable." >&2
|
|
|
|
# Drop privileges and exec. su-exec replaces the shell process so signals
|
|
# (SIGTERM, SIGINT) propagate to the pad process correctly — without
|
|
# this, docker stop's SIGTERM would hit the shell instead of pad.
|
|
exec su-exec pad "$@"
|