fix(docker): run runtime as nextjs so self-host boots under cap_drop: ALL (#737)
Self-hosted Docker image now runs fully unprivileged (USER nextjs): the entrypoint populates the .next/public tmpfs mounts and substitutes NEXT_PUBLIC_* placeholders as nextjs, so the container boots under the hardened compose (cap_drop: ALL + read_only: true) with no capabilities. Also: substitute *.json (fixes CSP connect-src in routes-manifest.json), escape sed metacharacters in brand name, healthcheck via 127.0.0.1, and a fully-self-hosted Supabase docs section.
This commit is contained in:
+25
-24
@@ -46,44 +46,45 @@ RUN npm run build
|
||||
FROM node:22-alpine@sha256:968df39aedcea65eeb078fb336ed7191baf48f972b4479711397108be0966920 AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# su-exec drops privileges in the entrypoint after the placeholder-substitution
|
||||
# step. Healthcheck uses BusyBox wget (already present in alpine), so no curl.
|
||||
# `apk upgrade` patches OS packages (libssl3/libcrypto3, …) in the final image
|
||||
# that Trivy scans — the runner uses its own FROM, so it needs the upgrade too.
|
||||
RUN apk upgrade --no-cache && apk add --no-cache su-exec
|
||||
# Patch OS packages (libssl3/libcrypto3, …) with fixes published after the
|
||||
# pinned base digest, so CI's Trivy scan doesn't flag fixable Alpine CVEs. No
|
||||
# su-exec or curl needed: the entrypoint runs unprivileged as nextjs and the
|
||||
# healthcheck uses BusyBox wget.
|
||||
RUN apk upgrade --no-cache
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
adduser --system --uid 1001 -G nodejs nextjs
|
||||
|
||||
# /app at runtime is split across the read-only image layer and tmpfs mounts:
|
||||
# /app/server.js, /app/node_modules/, /app/package.json — image (read-only)
|
||||
# /app/.next/ — tmpfs (writable)
|
||||
# /app/public/ — tmpfs (writable)
|
||||
# The entrypoint copies templates from /opt/gnubok-template/ into the tmpfs
|
||||
# mounts at startup, runs placeholder substitution, then chmods read-only.
|
||||
# This lets us run with docker-compose `read_only: true`.
|
||||
# The entrypoint runs UNPRIVILEGED as nextjs: it copies the templates from
|
||||
# /opt/gnubok-template/ into the nextjs-owned tmpfs mounts, substitutes the
|
||||
# NEXT_PUBLIC_* placeholders, then drops the write bits. Because it never needs
|
||||
# to chown or setuid, the container runs under docker-compose's `cap_drop: ALL`
|
||||
# + `read_only: true` with no added capabilities.
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/server.js ./server.js
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/node_modules ./node_modules
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/package.json ./package.json
|
||||
|
||||
COPY --from=builder /app/.next/standalone/server.js ./server.js
|
||||
COPY --from=builder /app/.next/standalone/node_modules ./node_modules
|
||||
COPY --from=builder /app/.next/standalone/package.json ./package.json
|
||||
# Baked-in templates for runtime population of the tmpfs mounts.
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone/.next /opt/gnubok-template/.next
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static /opt/gnubok-template/.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public /opt/gnubok-template/public
|
||||
|
||||
# Baked-in templates for runtime population of tmpfs mounts.
|
||||
COPY --from=builder /app/.next/standalone/.next /opt/gnubok-template/.next
|
||||
COPY --from=builder /app/.next/static /opt/gnubok-template/.next/static
|
||||
COPY --from=builder /app/public /opt/gnubok-template/public
|
||||
# Pre-create the tmpfs mount points (empty in the image layer; the entrypoint
|
||||
# fills them at startup). Owned by nextjs so the unprivileged entrypoint can
|
||||
# write into the tmpfs mounted over them.
|
||||
RUN mkdir -p /app/.next/cache /app/public && \
|
||||
chown nextjs:nodejs /app /app/.next /app/.next/cache /app/public
|
||||
|
||||
# Pre-create mount points so tmpfs has somewhere to attach when running with
|
||||
# docker-compose's read_only:true. The directories are empty in the image
|
||||
# layer — content is copied in by the entrypoint.
|
||||
RUN mkdir -p /app/.next /app/.next/cache /app/public
|
||||
COPY --chmod=755 --chown=nextjs:nodejs docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
# No USER directive — entrypoint handles the privilege drop with su-exec
|
||||
# after the root-only setup steps complete.
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
|
||||
Reference in New Issue
Block a user