Explorar el Código

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.
maziggy hace 3 semanas
padre
commit
3407afc0c7
Se han modificado 4 ficheros con 112 adiciones y 9 borrados
  1. 0 0
      CHANGELOG.md
  2. 25 3
      Dockerfile
  3. 68 0
      deploy/docker-entrypoint.sh
  4. 19 6
      docker-compose.yml

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
CHANGELOG.md


+ 25 - 3
Dockerfile

@@ -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"]

+ 68 - 0
deploy/docker-entrypoint.sh

@@ -0,0 +1,68 @@
+#!/bin/sh
+# Bambuddy container entrypoint.
+#
+# Runs as root (the image leaves USER unset, so containers start as
+# root by default), chowns /app/data and /app/logs to PUID:PGID, then
+# drops to PUID:PGID via gosu and execs the application. This fixes the
+# class of "Permission denied" errors that bit users when:
+#
+#   - a Docker named volume was first created with root ownership and
+#     the container was running with `user: 1000:1000` (named volumes
+#     created by the daemon take its ownership; Dockerfile chmod hacks
+#     cover the parent path but not subdirs created at runtime).
+#   - a bind-mount source path didn't exist on the host yet, so dockerd
+#     created it as root before the container started, leaving it
+#     unwritable by uid 1000 inside the container — see #1211 / #668
+#     for the virtual_printer bind-mount case the shipped compose
+#     template ships uncommented.
+#
+# If the container is started with an explicit `user:` directive
+# (compose `user:` or `docker run --user`), the entrypoint runs as that
+# user instead of root and chown isn't possible. The script falls
+# through to direct exec without modifying ownership — preserving the
+# previous behavior for users who pin a specific uid via compose.
+
+set -eu
+
+# Default to 1000:1000 to match the legacy `user: "1000:1000"` default
+# in our previously-shipped compose template; overridable via env so
+# users who run docker as a different uid can match their host without
+# editing the compose user: directive.
+PUID="${PUID:-1000}"
+PGID="${PGID:-1000}"
+
+# If we're not root, we can't chown anything. Exec the original command
+# and trust that the user has set up host-side ownership themselves.
+if [ "$(id -u)" -ne 0 ]; then
+    exec "$@"
+fi
+
+# `chown -R` is gated behind a top-level ownership check so a correctly-
+# owned directory isn't traversed on every container start. A user with
+# a multi-GB archive directory would otherwise pay seconds-to-minutes
+# of chown traversal at every restart.
+chown_if_needed() {
+    target="$1"
+    [ -d "$target" ] || mkdir -p "$target"
+    current="$(stat -c '%u:%g' "$target" 2>/dev/null || echo '')"
+    if [ "$current" != "$PUID:$PGID" ]; then
+        echo "[entrypoint] chown -R ${PUID}:${PGID} ${target}"
+        chown -R "${PUID}:${PGID}" "$target" || true
+    fi
+}
+
+chown_if_needed /app/data
+chown_if_needed /app/logs
+
+# Bind-mount-source path needs the same treatment when present. dockerd
+# creates missing bind-mount sources as root on the host before the
+# container starts; the chown here propagates through the bind mount to
+# the host-side directory and fixes the issue once and for all.
+if [ -d /app/data/virtual_printer ]; then
+    chown_if_needed /app/data/virtual_printer
+fi
+
+# Drop privileges and run the application. python's file capabilities
+# (cap_net_bind_service=+ep, set in the Dockerfile) survive the uid
+# switch, so binding to :322 / :990 still works post-drop.
+exec gosu "${PUID}:${PGID}" "$@"

+ 19 - 6
docker-compose.yml

@@ -6,9 +6,11 @@ services:
     #   docker compose up -d          → pulls pre-built image from ghcr.io
     #   docker compose up -d --build  → builds locally from source
     container_name: bambuddy
-    # Run as current user to avoid permission issues with mounted volumes
-    # Override with: PUID=$(id -u) PGID=$(id -g) docker compose up -d
-    user: "${PUID:-1000}:${PGID:-1000}"
+    # File ownership inside the data and logs volumes is normalised by the
+    # entrypoint at startup (chowns to PUID:PGID and drops privileges via
+    # gosu before running the app). Override PUID / PGID below to match
+    # your host user if needed — defaults to 1000:1000 to match the
+    # historical compose `user:` directive.
     #
     # Allow binding to privileged ports (322, 990) as non-root user — required
     # for FTPS in every VP mode and for the RTSPS camera proxy in proxy mode +
@@ -37,9 +39,14 @@ services:
       - bambuddy_data:/app/data
       - bambuddy_logs:/app/logs
       #
-      # Share virtual printer certs with native installation
-      # This ensures the slicer only needs to trust one CA certificate.
-      - ./virtual_printer:/app/data/virtual_printer
+      # OPTIONAL — only needed if you ALSO run a native install of Bambuddy
+      # on the same host and want both installs to share the same Virtual
+      # Printer CA certificate (so the slicer only has to trust one CA).
+      # Most Docker-only users should leave this commented out — the
+      # entrypoint will keep the VP data inside the named volume above.
+      # If uncommented, the entrypoint chowns the host directory to
+      # PUID:PGID on first start so the container user can write to it.
+      #- ./virtual_printer:/app/data/virtual_printer
       #
       # Mount scheduled backup output to NAS or external storage
       # Backups default to DATA_DIR/backups/ inside the data volume.
@@ -57,6 +64,12 @@ services:
       #- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
     environment:
       - TZ=${TZ:-Europe/Berlin}
+      # User/group the container drops to after the entrypoint normalises
+      # ownership on /app/data and /app/logs. Match your host user (run
+      # `id -u` / `id -g`) if you want files written by the container to
+      # show up as your user on the host. Defaults to 1000:1000.
+      - PUID=${PUID:-1000}
+      - PGID=${PGID:-1000}
       # Port BamBuddy runs on (default: 8000)
       # Usage: PORT=8080 docker compose up -d
       - PORT=${PORT:-8000}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio