From e03477108782821c9ef272922a9e6dcef959ddf8 Mon Sep 17 00:00:00 2001 From: Malin Date: Tue, 10 Mar 2026 08:23:29 +0100 Subject: [PATCH] fix: handle EACCES on data/settings.json in Docker containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - settings-store: auto-detect writable path at startup — tries /data/settings.json first, falls back to /tmp/lingvai-settings.json if the directory is not writable. Logs a warning when fallback is used. Also supports SETTINGS_PATH env var for explicit override. - Dockerfile: switch from yarn to npm, explicitly create /app/data with chown nextjs:nodejs so the directory is writable at runtime without needing a privileged volume mount. Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 20 +++++++++++--------- utils/settings-store.ts | 42 +++++++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index eac892c..530d04b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,31 +3,33 @@ FROM node:lts-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app -COPY package.json yarn.lock ./ -RUN yarn install --frozen-lockfile +COPY package.json package-lock.json* ./ +RUN npm install --legacy-peer-deps FROM node:lts-alpine AS builder RUN apk add --no-cache curl WORKDIR /app -RUN addgroup -g 1001 -S nodejs -RUN adduser -S nextjs -u 1001 +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nextjs -u 1001 + COPY --chown=nextjs:nodejs . . -COPY --from=deps /app/node_modules ./node_modules -RUN chown nextjs:nodejs . +COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules + +# Ensure the data directory exists and is writable by the nextjs user +RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data && chmod 755 /app/data USER nextjs EXPOSE 3000 ENV NODE_ENV production - ENV NEXT_TELEMETRY_DISABLED 1 HEALTHCHECK --interval=1m --timeout=3s CMD curl -f http://localhost:3000/ || exit 1 -CMD NEXT_PUBLIC_SITE_DOMAIN=$site_domain\ +CMD NEXT_PUBLIC_SITE_DOMAIN=$site_domain \ NEXT_PUBLIC_FORCE_DEFAULT_THEME=$force_default_theme \ NEXT_PUBLIC_DEFAULT_SOURCE_LANG=$default_source_lang \ NEXT_PUBLIC_DEFAULT_TARGET_LANG=$default_target_lang \ - yarn build && yarn start + npm run build && npm start diff --git a/utils/settings-store.ts b/utils/settings-store.ts index 75210aa..afea1cb 100644 --- a/utils/settings-store.ts +++ b/utils/settings-store.ts @@ -17,18 +17,43 @@ const DEFAULT_SETTINGS: Settings = { adminPasswordHash: process.env["ADMIN_PASSWORD"] ?? "admin" }; -const SETTINGS_PATH = path.join(process.cwd(), "data", "settings.json"); - -function ensureDataDir() { - const dir = path.dirname(SETTINGS_PATH); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); +/** + * Resolve a writable path for settings.json. + * Priority: + * 1. SETTINGS_PATH env var (explicit override) + * 2. /data/settings.json (default, works when data/ is writable) + * 3. /tmp/lingvai-settings.json (fallback for read-only containers) + */ +function resolveSettingsPath(): string { + if (process.env["SETTINGS_PATH"]) { + return process.env["SETTINGS_PATH"]; } + const primary = path.join(process.cwd(), "data", "settings.json"); + try { + const dir = path.dirname(primary); + fs.mkdirSync(dir, { recursive: true }); + // Test write access by opening with 'a' (append/create without truncating) + const fd = fs.openSync(primary, "a"); + fs.closeSync(fd); + return primary; + } catch { + return path.join("/tmp", "lingvai-settings.json"); + } +} + +// Resolve once at module load so every call uses the same path +const SETTINGS_PATH = resolveSettingsPath(); + +if (SETTINGS_PATH.startsWith("/tmp")) { + console.warn( + `[lingvai] data/settings.json is not writable. ` + + `Settings will be stored at ${SETTINGS_PATH}. ` + + `Mount a writable volume at /app/data or set SETTINGS_PATH to persist across restarts.` + ); } export function readSettings(): Settings { try { - ensureDataDir(); if (!fs.existsSync(SETTINGS_PATH)) { return { ...DEFAULT_SETTINGS }; } @@ -40,9 +65,10 @@ export function readSettings(): Settings { } export function writeSettings(updates: Partial): Settings { - ensureDataDir(); const current = readSettings(); const next = { ...current, ...updates }; + const dir = path.dirname(SETTINGS_PATH); + fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(SETTINGS_PATH, JSON.stringify(next, null, 2), "utf-8"); return next; }