From 3fe854a499965eb0c7ef63819d234cb571aace5f Mon Sep 17 00:00:00 2001 From: Andrei Lavrenov Date: Wed, 10 Jun 2026 20:56:50 +0200 Subject: [PATCH] fix(docker): chown runtime image by numeric uid to fix EACCES on .next/cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Chainguard runner (cgr.dev/chainguard/node:latest) has no `nonroot` entry in /etc/passwd — uid 65532 is named `node` — so the runner-stage `COPY --chown=nonroot:nonroot` lines silently fell back to root (0:0), leaving /app/.next root-owned. The container runs as uid 65532, so the Next.js image optimizer's mkdir('.next/cache/images') failed with EACCES and flooded logs with unhandledRejection on every remote-avatar optimization. Switch all three runner COPY lines to numeric --chown=65532:65532. Hotfix off main (v0.47.10); the same fix is already on develop as 0.52.1. The runner COPY change is identical to the develop fix, which was verified by building the real image and confirming /app/.next is uid=65532 with a successful cache write. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 ++++++ Dockerfile.app | 15 ++++++++++++--- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0565232d..c5fa5fae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## 0.47.10 + +### Fixes + +- **Fix the `EACCES: permission denied, mkdir '/app/.next/cache'` flood in the production container by chowning the runtime image by numeric uid instead of the unresolvable name `nonroot`.** Root cause: the Chainguard runtime (`cgr.dev/chainguard/node:latest`) has **no `nonroot` entry in `/etc/passwd`** — uid 65532 is named `node` — so the runner-stage `COPY --from=builder --chown=nonroot:nonroot …` lines silently fell back to root (`0:0`). That left `/app/.next` root-owned (mode 755), and since the container runs as uid 65532, the Next.js image optimizer's first remote-avatar optimization (`mkdir('.next/cache/images', { recursive: true })`, triggered by Discord/GitHub/Google/Gravatar avatars) failed with `EACCES` and rejected on every subsequent cacheable image request. `Dockerfile.app` now uses `--chown=65532:65532` (numeric IDs need no passwd lookup) on all three runner COPY lines, so the runtime user owns the standalone tree and creates `.next/cache` on demand. Verified by reproducing the production state (`/app/.next` `uid=0`, `mkdir FAILED:EACCES`) and confirming the numeric-chown image yields `uid=65532` and a successful write. Hotfix off `main`; the same fix is already on `develop` as 0.52.1. + ## 0.47.9 ### Documentation diff --git a/Dockerfile.app b/Dockerfile.app index 46a1569e..67d4542f 100644 --- a/Dockerfile.app +++ b/Dockerfile.app @@ -103,9 +103,18 @@ ARG NEXT_PUBLIC_DEPLOY_ENV ENV NEXT_PUBLIC_DEPLOY_ENV=$NEXT_PUBLIC_DEPLOY_ENV # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nonroot:nonroot /app/.next/standalone ./ -COPY --from=builder --chown=nonroot:nonroot /app/.next/static ./.next/static -COPY --from=builder --chown=nonroot:nonroot /app/public ./public +# +# Chown by NUMERIC uid:gid, NOT the name `nonroot`. This Chainguard runtime +# has no `nonroot` entry in /etc/passwd (uid 65532 is named `node`), so +# `--chown=nonroot:nonroot` silently falls back to root (0:0). That left +# /app/.next root-owned and made the Next.js image optimizer's runtime +# `mkdir('.next/cache/images')` fail with EACCES, flooding logs with +# unhandledRejection on every remote-avatar (Discord/GitHub/Google/Gravatar) +# optimization. Numeric IDs need no passwd lookup, so 65532 (the runtime +# user) owns the tree as intended and the cache dir is created on demand. +COPY --from=builder --chown=65532:65532 /app/.next/standalone ./ +COPY --from=builder --chown=65532:65532 /app/.next/static ./.next/static +COPY --from=builder --chown=65532:65532 /app/public ./public EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" diff --git a/package-lock.json b/package-lock.json index 277a9e8b..2b8cccf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "helldivers.bot", - "version": "0.47.4", + "version": "0.47.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "helldivers.bot", - "version": "0.47.4", + "version": "0.47.10", "dependencies": { "@asteasolutions/zod-to-openapi": "^8.5.0", "@mdx-js/loader": "^3.1.1", diff --git a/package.json b/package.json index 2845c48c..842d3675 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "helldivers.bot", - "version": "0.47.9", + "version": "0.47.10", "private": true, "type": "module", "scripts": {