From 2de85ea30ead9b2d32fa05f059c19c7a4f49a7bd Mon Sep 17 00:00:00 2001 From: jklein Date: Sun, 23 Aug 2026 19:32:44 +0200 Subject: [PATCH] Mount an emptyDir for the Next.js cache in rallly (5.0.0+up4.12.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chart mounted no volumes at all while running with readOnlyRootFilesystem: true, so every Next.js write to /app/apps/web/.next/cache failed. The image optimizer's failure even surfaced as an unhandledRejection, so this was more than log noise. The cache path is now a writable emptyDir; path and an optional sizeLimit are configurable under rallly.cache. The hardening is untouched — the pod's existing fsGroup (1000) is what makes the mounted volume writable for uid 20000. Verified in a throwaway k3d cluster which paths the app actually writes: with a fully writable root filesystem and the cache mounted, the only path the container touches is the cache directory itself. /tmp, $HOME (=/app) and the rest of the root filesystem stay untouched, so no second mount is needed. Also drops the vestigial "capabilities.add: []": Trivy's AVD-KSV-0022 for rallly (#84) comes from the older chart version deployed in the cluster, which rendered a null list entry under add:. Reproduced with trivy config against both renderings; the app needs no capabilities. Major bump: runtime behavior changes, a volume is added. Fixes #60 --- rallly/Chart.yaml | 2 +- rallly/templates/rallly.deployment.yaml | 14 +++++++++++++ rallly/values.yaml | 27 ++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/rallly/Chart.yaml b/rallly/Chart.yaml index 6f856dc..43aff9b 100644 --- a/rallly/Chart.yaml +++ b/rallly/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi type: application -version: 4.0.0+up4.12.3 +version: 5.0.0+up4.12.3 appVersion: "4.12.3" maintainers: diff --git a/rallly/templates/rallly.deployment.yaml b/rallly/templates/rallly.deployment.yaml index 339a070..8a7eaf3 100644 --- a/rallly/templates/rallly.deployment.yaml +++ b/rallly/templates/rallly.deployment.yaml @@ -150,4 +150,18 @@ spec: {{- include "rallly.resources" .Values.rallly.resources | nindent 12 }} securityContext: {{- toYaml .Values.rallly.securityContext.container | nindent 12 }} + volumeMounts: + # Next.js writes prerendered pages and optimized images into its + # cache directory; with a read-only root filesystem this must be a + # writable volume (see rallly.cache in values.yaml). + - name: next-cache + mountPath: {{ required "A cache path must be provided (rallly.cache.path)" .Values.rallly.cache.path | quote }} + volumes: + - name: next-cache + {{- if .Values.rallly.cache.sizeLimit }} + emptyDir: + sizeLimit: {{ .Values.rallly.cache.sizeLimit | quote }} + {{- else }} + emptyDir: {} + {{- end }} restartPolicy: Always diff --git a/rallly/values.yaml b/rallly/values.yaml index a6c3405..088287d 100644 --- a/rallly/values.yaml +++ b/rallly/values.yaml @@ -23,6 +23,27 @@ rallly: pictureClaimPath: 'picture' emailClaimPath: 'email' nameClaimPath: 'name' + # Next.js writes its cache below the app directory (prerendered/ISR pages and + # the optimized images of /_next/image). The container runs as uid 20000 + # while /app belongs to the image's own "nextjs" user, so that path is not + # writable for the app even without readOnlyRootFilesystem — the chart mounts + # a writable volume over it. The pod's fsGroup (1000) is what makes the + # mounted volume group-writable for the container. + # + # Deliberately an emptyDir, not a PVC: the cache is volatile. After every pod + # restart it starts out empty and Next.js rebuilds it on demand, which costs + # a little time on the first request after a restart. That is correct for a + # cache — its entries are derived data tied to the build id of the image, so + # a persistent volume would only carry stale entries across app updates. + cache: + # Mount path of the cache volume. Matches the Next.js cache directory of + # the upstream image; only needs changing if the image layout changes. + path: /app/apps/web/.next/cache + # Optional upper bound for the emptyDir, e.g. 512Mi. Unset means the volume + # is only bounded by the container's ephemeral-storage limit (computed as + # limitFactor x request, see resources below) — note that emptyDir usage + # counts against that limit, and exceeding either bound evicts the pod. + sizeLimit: null securityContext: pod: runAsNonRoot: true @@ -34,10 +55,14 @@ rallly: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true + # No capabilities are added: the app is a plain Node process listening on + # the unprivileged port 3000, verified to run with all capabilities + # dropped. Do not reintroduce an "add:" key here — an empty list item + # under it renders as a null entry, which is what Trivy reported as + # AVD-KSV-0022 for the older chart version deployed in the cluster (#84). capabilities: drop: - ALL - add: [] # Extra environment variables, appended to the ones the chart always sets. # Values are rendered through tpl; value, secretKeyRef and configMapKeyRef # forms are supported.