From aa5846b282284ff6f6d2efb98382389c7df063d8 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 16 Mar 2026 19:52:14 +0800 Subject: [PATCH] fix(docker): resolve /app/data permission denied on volume mounts Docker named volumes and host bind-mounts may be owned by root, causing "open data/model_pricing.sha256: permission denied" when the container runs as the non-root sub2api user. Add an entrypoint script that fixes /app/data ownership before dropping to sub2api via su-exec. Replace USER directive with the entrypoint approach across all three Dockerfiles and update both GoReleaser configs to include the script in Docker build contexts. --- .goreleaser.simple.yaml | 2 ++ .goreleaser.yaml | 8 ++++++++ Dockerfile | 11 +++++++---- Dockerfile.goreleaser | 9 +++++++-- deploy/Dockerfile | 11 +++++++---- deploy/docker-entrypoint.sh | 22 ++++++++++++++++++++++ 6 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 deploy/docker-entrypoint.sh diff --git a/.goreleaser.simple.yaml b/.goreleaser.simple.yaml index 2155ed9d..14f67fd1 100644 --- a/.goreleaser.simple.yaml +++ b/.goreleaser.simple.yaml @@ -47,6 +47,8 @@ dockers: - "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:latest" dockerfile: Dockerfile.goreleaser use: buildx + extra_files: + - deploy/docker-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.version={{ .Version }}" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index da2f9aa5..41f9a555 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -63,6 +63,8 @@ dockers: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" dockerfile: Dockerfile.goreleaser use: buildx + extra_files: + - deploy/docker-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.version={{ .Version }}" @@ -76,6 +78,8 @@ dockers: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" dockerfile: Dockerfile.goreleaser use: buildx + extra_files: + - deploy/docker-entrypoint.sh build_flag_templates: - "--platform=linux/arm64" - "--label=org.opencontainers.image.version={{ .Version }}" @@ -89,6 +93,8 @@ dockers: - "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" dockerfile: Dockerfile.goreleaser use: buildx + extra_files: + - deploy/docker-entrypoint.sh build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.version={{ .Version }}" @@ -102,6 +108,8 @@ dockers: - "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-arm64" dockerfile: Dockerfile.goreleaser use: buildx + extra_files: + - deploy/docker-entrypoint.sh build_flag_templates: - "--platform=linux/arm64" - "--label=org.opencontainers.image.version={{ .Version }}" diff --git a/Dockerfile b/Dockerfile index 8fd48cc2..a16eb958 100644 --- a/Dockerfile +++ b/Dockerfile @@ -92,6 +92,7 @@ LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" RUN apk add --no-cache \ ca-certificates \ tzdata \ + su-exec \ libpq \ zstd-libs \ lz4-libs \ @@ -120,8 +121,9 @@ COPY --from=backend-builder --chown=sub2api:sub2api /app/backend/resources /app/ # Create data directory RUN mkdir -p /app/data && chown sub2api:sub2api /app/data -# Switch to non-root user -USER sub2api +# Copy entrypoint script (fixes volume permissions then drops to sub2api) +COPY deploy/docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh # Expose port (can be overridden by SERVER_PORT env var) EXPOSE 8080 @@ -130,5 +132,6 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD wget -q -T 5 -O /dev/null http://localhost:${SERVER_PORT:-8080}/health || exit 1 -# Run the application -ENTRYPOINT ["/app/sub2api"] +# Run the application (entrypoint fixes /app/data ownership then execs as sub2api) +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["/app/sub2api"] diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index 419994b9..f251d154 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -21,6 +21,7 @@ RUN apk add --no-cache \ ca-certificates \ tzdata \ curl \ + su-exec \ libpq \ zstd-libs \ lz4-libs \ @@ -47,11 +48,15 @@ COPY sub2api /app/sub2api # Create data directory RUN mkdir -p /app/data && chown -R sub2api:sub2api /app -USER sub2api +# Copy entrypoint script (fixes volume permissions then drops to sub2api) +COPY deploy/docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD curl -f http://localhost:${SERVER_PORT:-8080}/health || exit 1 -ENTRYPOINT ["/app/sub2api"] +# Run the application (entrypoint fixes /app/data ownership then execs as sub2api) +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["/app/sub2api"] diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 0f4f1de9..7caa5ca6 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -82,6 +82,7 @@ RUN apk add --no-cache \ ca-certificates \ tzdata \ curl \ + su-exec \ && rm -rf /var/cache/apk/* # Create non-root user @@ -97,8 +98,9 @@ COPY --from=backend-builder /app/sub2api /app/sub2api # Create data directory RUN mkdir -p /app/data && chown -R sub2api:sub2api /app -# Switch to non-root user -USER sub2api +# Copy entrypoint script (fixes volume permissions then drops to sub2api) +COPY deploy/docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh # Expose port (can be overridden by SERVER_PORT env var) EXPOSE 8080 @@ -107,5 +109,6 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ CMD wget -q -T 5 -O /dev/null http://localhost:${SERVER_PORT:-8080}/health || exit 1 -# Run the application -ENTRYPOINT ["/app/sub2api"] +# Run the application (entrypoint fixes /app/data ownership then execs as sub2api) +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["/app/sub2api"] diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh new file mode 100644 index 00000000..344429bd --- /dev/null +++ b/deploy/docker-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +# Fix data directory permissions when running as root. +# Docker named volumes / host bind-mounts may be owned by root, +# preventing the non-root sub2api user from writing files. +if [ "$(id -u)" = "0" ]; then + mkdir -p /app/data + chown -R sub2api:sub2api /app/data + # Re-invoke this script as sub2api so the flag-detection below + # also runs under the correct user. + exec su-exec sub2api "$0" "$@" +fi + +# Compatibility: if the first arg looks like a flag (e.g. --help), +# prepend the default binary so it behaves the same as the old +# ENTRYPOINT ["/app/sub2api"] style. +if [ "${1#-}" != "$1" ]; then + set -- /app/sub2api "$@" +fi + +exec "$@"