diff --git a/backend/Dockerfile b/backend/Dockerfile index d84af42..b364eb6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,11 +1,12 @@ -FROM node:20-alpine +FROM node:24-alpine -# better-sqlite3 needs build tools +# better-sqlite3 is a native module and is compiled here against this exact +# Node version — the reason a Node upgrade needs a rebuild, not just a retag. RUN apk add --no-cache python3 make g++ WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm install --production +RUN npm install --omit=dev COPY . . RUN mkdir -p /app/data /app/uploads diff --git a/backend/package.json b/backend/package.json index aa53864..2a61fc2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "bcryptjs": "^2.4.3", - "better-sqlite3": "^11.0.0", + "better-sqlite3": "^12.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", diff --git a/backend/server.js b/backend/server.js index f84eb68..84fa5bf 100644 --- a/backend/server.js +++ b/backend/server.js @@ -67,8 +67,46 @@ const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || "") const DATA_DIR = path.join(__dirname, "data"); const UPLOAD_DIR = path.join(__dirname, "uploads"); -if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); -if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + +/** + * Both directories are bind mounts from the Docker host, and the container runs + * unprivileged. If the host directory belongs to someone else — usually because + * Docker created it as root when it did not exist — nothing here can work. + * + * Without this check the failure surfaces from deep inside better-sqlite3 as + * `SqliteError: unable to open database file`, which says nothing about the + * cause and sends people looking at the database instead of at `ls -ld`. + */ +function ensureWritable(dir, label) { + try { + fs.mkdirSync(dir, { recursive: true }); + const probe = path.join(dir, `.write-probe-${process.pid}`); + fs.writeFileSync(probe, ""); + fs.unlinkSync(probe); + } catch (err) { + const uid = typeof process.getuid === "function" ? process.getuid() : "?"; + const gid = typeof process.getgid === "function" ? process.getgid() : "?"; + let owner = "unknown"; + try { + const s = fs.statSync(dir); + owner = `uid ${s.uid}, gid ${s.gid}, mode ${(s.mode & 0o777).toString(8)}`; + } catch { + owner = "does not exist"; + } + console.error( + `\n❌ Cannot write to the ${label} directory (${dir}).\n` + + ` The server runs as uid ${uid}, gid ${gid}; the directory is ${owner}.\n` + + ` This is the host directory bind-mounted into the container. Fix it there:\n\n` + + ` mkdir -p /{db,uploads}\n` + + ` chown -R ${uid}:${gid} \n` + + ` # under /root also: chmod o+x /root\n\n` + + ` (${err.code || err.message})\n` + ); + process.exit(1); + } +} +ensureWritable(DATA_DIR, "data"); +ensureWritable(UPLOAD_DIR, "uploads"); /* ─────────── Database ─────────── */ const db = new Database(path.join(DATA_DIR, "inknote.db")); diff --git a/frontend/dockerfile b/frontend/dockerfile index 8af3d66..119ecac 100644 --- a/frontend/dockerfile +++ b/frontend/dockerfile @@ -1,7 +1,9 @@ -FROM node:20-alpine AS build +FROM node:24-alpine AS build WORKDIR /app -COPY package.json ./ -RUN npm install +# The lockfile comes along so the image is built from the same dependency +# versions that were tested, rather than whatever resolves on build day. +COPY package.json package-lock.json ./ +RUN npm ci COPY . . RUN npm run build