Backup: chown /app/backups in image + report uid/writability

The real backup run failed: pg_dump could not write to /app/backups —
'Permission denied'. Cause: the container runs as the non-root 'vyndr'
user (Dockerfile USER vyndr) and the Coolify-mounted volume is
root-owned, so the mount is present but unwritable.

- Dockerfile now creates AND chowns /app/backups to vyndr alongside the
  existing /app/data + /app/.pm2 line. Docker seeds ownership into a
  NAMED volume on first creation, so this fixes it for a fresh volume;
  a host bind-mount still needs a host-side chown, which is why the
  next change exists.
- GET /api/internal/backup/verify now reports process uid/gid,
  backup_dir_writable and the access errno, so the exact chown target is
  observable instead of guessed. A mounted-but-unwritable volume reads as
  'configured' everywhere else — this makes it loud.

Suite 278/3310 green, build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
Kev
2026-07-19 22:30:37 -04:00
parent ef7f17610f
commit 97e4dc72d5
2 changed files with 21 additions and 3 deletions
+2 -2
View File
@@ -60,8 +60,8 @@ COPY content ./content
# Persistent volume for JSONL training data (resolutions survive
# redeploys via the Coolify mount). PM2_HOME lives outside it so
# supervisor state is local to the container.
RUN mkdir -p /app/data/training /app/.pm2 \
&& chown -R vyndr:vyndr /app/data /app/.pm2 \
RUN mkdir -p /app/data/training /app/.pm2 /app/backups \
&& chown -R vyndr:vyndr /app/data /app/.pm2 /app/backups \
&& chmod +x /app/scripts/docker-entrypoint.sh
USER vyndr
+19 -1
View File
@@ -281,15 +281,33 @@ router.get('/backup/verify', async (req, res) => {
const { latestDump, countRowsInDump } = require('../backupScheduler');
try {
const dir = process.env.BACKUP_DIR || '/var/backups/vyndr';
// Report identity + writability: a mounted-but-unwritable volume is the
// exact failure we hit (Coolify mounts root-owned; the container runs as
// the non-root `vyndr` user), and the fix needs the real uid/gid.
const fs = require('fs');
let writable = false;
let dirErr = null;
try {
fs.accessSync(dir, fs.constants.W_OK);
writable = true;
} catch (e) { dirErr = e.code || e.message; }
const identity = {
uid: typeof process.getuid === 'function' ? process.getuid() : null,
gid: typeof process.getgid === 'function' ? process.getgid() : null,
backup_dir_writable: writable,
backup_dir_error: dirErr,
};
const dump = latestDump(dir);
if (!dump) {
return res.json({ ok: false, backup_dir: dir, error: 'no dump found in BACKUP_DIR' });
return res.json({ ok: false, backup_dir: dir, ...identity, error: 'no dump found in BACKUP_DIR' });
}
const table = String(req.query.table || 'ledger_entries');
const counted = await countRowsInDump(dump.path, table);
return res.json({
ok: counted.ok,
backup_dir: dir,
...identity,
dump: dump.file,
dump_bytes: dump.size,
table,