fix(docker): normalise data-volume ownership at startup via gosu entrypoint

Two related failure modes have been biting Docker users repeatedly,
  most recently in #1211:

    1. Docker named volumes are created by the daemon as root:root, and
       the previous `chmod 777 /app/data` Dockerfile workaround only
       covered the named-volume root — so subdirs Bambuddy creates at
       runtime (virtual_printer/uploads, virtual_printer/certs, etc.)
       inherited wrong ownership when the container ran as 1000:1000.

    2. The shipped docker-compose.yml ships
       `./virtual_printer:/app/data/virtual_printer` uncommented, and
       dockerd creates a missing bind-mount source on the host as root
       before the container starts — leaving the host directory
       unwritable by uid 1000 inside the container even though the named
       volume above it had the chmod-777 workaround.

  Symptom either way: [Errno 13] Permission denied:
  '/app/data/virtual_printer/uploads', no virtual printer ever starts,
  "VP doesn't work" support reports follow.

  Replace the chmod-777 hack with a proper entrypoint:

    - deploy/docker-entrypoint.sh runs as root, chowns /app/data and
      /app/logs (and /app/data/virtual_printer when bind-mounted) to
      PUID:PGID, then drops to that uid via gosu before exec'ing the
      app. The chown is gated behind a top-level ownership check so
      subsequent restarts skip the recursive traversal — no multi-
      second startup penalty on multi-GB archive directories.

    - A sentinel .bambuddy file in each data path prevents Docker from
      re-syncing image directory metadata on every mount (otherwise
      empty volumes have their ownership reverted from the image on
      each restart, defeating the idempotency).

    - When the container is started with an explicit `user:` directive
      or `--user` flag the entrypoint detects it isn't root and falls
      through to direct exec — preserving compatibility for users who
      pin a specific uid.

  Compose template changes:

    - Remove `user: "${PUID:-1000}:${PGID:-1000}"` (entrypoint owns
      privilege drop now).
    - Add PUID / PGID env vars with the same defaults.
    - Comment out the ./virtual_printer:/app/data/virtual_printer
      bind mount by default, with explicit "only needed if you also
      run a native install of Bambuddy on the same host and want both
      to share the VP CA cert" guidance. The entrypoint chowns the
      host-side dir through the bind mount the first time it sees
      wrong ownership, so existing uncomented installs continue to
      work and #1211 specifically gets fixed.
This commit is contained in:
maziggy
2026-05-05 17:22:08 +02:00
parent d5280ce21f
commit 3407afc0c7
4 changed files with 114 additions and 9 deletions
+25 -3
View File
@@ -24,6 +24,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ffmpeg \
gnupg \
gosu \
iproute2 \
libcap2-bin \
openssh-client \
@@ -66,9 +67,29 @@ COPY .git/HEAD ./.git/HEAD
# Copy built frontend from builder stage
COPY --from=frontend-builder /app/static ./static
# Create data directory for persistent storage
# chmod 777 allows running as non-root user (e.g., with docker compose user: directive)
RUN mkdir -p /app/data /app/logs && chmod 777 /app/data /app/logs
# Create data directories. Ownership is normalised at startup by the
# entrypoint (chowns to PUID:PGID and drops privileges via gosu before
# exec'ing the app), so we don't need a chmod 777 hack here — that was
# the workaround for the previous compose `user: "1000:1000"` model and
# only worked when the volume's perms happened to survive (named volume
# first-create case; bind-mount-source case bit users in #1211 / #668).
#
# The sentinel file is needed so a freshly-created Docker named volume
# isn't "empty" from Docker's POV. On empty volumes Docker resyncs the
# directory metadata (incl. ownership) from the image on every mount,
# which would mean our entrypoint chown gets reverted on every restart
# and re-fired on every start (slow on multi-GB archive dirs). With a
# sentinel inside the volume on first mount, Docker considers the
# volume populated and stops resyncing, so the chown is genuinely
# one-shot.
RUN mkdir -p /app/data /app/logs && \
: >/app/data/.bambuddy && \
: >/app/logs/.bambuddy
# Entrypoint script: handles PUID/PGID + ownership normalisation +
# privilege drop. See deploy/docker-entrypoint.sh for the full rationale.
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Environment variables
ENV PYTHONUNBUFFERED=1
@@ -103,4 +124,5 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
# Run the application
# Use standard asyncio loop (uvloop has permission issues in some Docker environments)
# Port is configurable via PORT environment variable (default: 8000)
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["sh", "-c", "uvicorn backend.app.main:app --host 0.0.0.0 --port ${PORT:-8000} --loop asyncio"]