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:
+2
-2
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user