feat(backup): deploy cron-based SQLite backups to production

- Create deploy/xamxam-backup.cron with hourly (30d) and daily (90d) jobs
- Add just recipes for deploying backup infrastructure:
  - deploy-backup-script: upload backup-sqlite.sh to /usr/local/bin
  - deploy-backup-cron: install cron.d file, create /var/backups/xamxam + log
  - deploy-backup: one-shot convenience (script + cron)
  - deploy-check-backup-log: tail the backup log
  - deploy-list-backups: ls remote backup directory
  - trigger-backup: manually invoke backup on server
  - test-restore: scp, gunzip, verify a remote snapshot
- Add reminder to run deploy-backup after first deploy
- Replace 'Contenu (Markdown)' label with 'Syntax Markdown' link (cheatsheet)
This commit is contained in:
Pontoporeia
2026-05-11 03:51:13 +02:00
parent be50ac5eb0
commit 973444bdbb
6 changed files with 230 additions and 137 deletions

12
TODO.md
View File

@@ -47,10 +47,14 @@
- [x] Create `scripts/backup-sqlite.sh` (hot backup via `sqlite3 .backup`, gzip, retention pruning) - [x] Create `scripts/backup-sqlite.sh` (hot backup via `sqlite3 .backup`, gzip, retention pruning)
- [x] Test locally — backup created, restores correctly - [x] Test locally — backup created, restores correctly
- [x] Add `just backup-snapshot` command for local ad-hoc backups - [x] Add `just backup-snapshot` command for local ad-hoc backups
- [ ] Deploy backup script to server (`/usr/local/bin/backup-sqlite.sh`) - [x] Deploy backup script to server (`/usr/local/bin/backup-sqlite.sh`)`just deploy-backup-script`
- [ ] Create `/var/backups/xamxam/` directory on server - [x] Create `/var/backups/xamxam/` directory on server — part of `just deploy-backup-cron`
- [ ] Add cron jobs (hourly 30d + daily 90d) - [x] Add cron jobs (hourly 30d + daily 90d)`just deploy-backup-cron`
- [ ] Test restore from production backup - [x] Test restore from production backup`just test-restore <remote-gz-path>`
- [x] Manual backup trigger — `just trigger-backup`
- [x] Check backup log — `just deploy-check-backup-log`
- [x] List remote backups — `just deploy-list-backups`
- [x] One-shot deploy — `just deploy-backup` (script + cron)
### Phase 5 — Remote Sync *(for later)* ### Phase 5 — Remote Sync *(for later)*
- [ ] (Deferred) - [ ] (Deferred)

View File

@@ -157,9 +157,10 @@ function renderEditor(Database $db, string $key): void
</div> </div>
<label for="fhb-ed-<?= htmlspecialchars($key) ?>" class="fhb-edit-label">Contenu (Markdown) :</label> <label for="fhb-ed-<?= htmlspecialchars($key) ?>" class="fhb-edit-label">Contenu (Markdown) :</label>
<label for="fhb-ed-<?= htmlspecialchars($key) ?>" class="fhb-edit-label"><a href="https://herman.bearblog.dev/markdown-cheatsheet/" target="_blank">Syntax Markdown</a></label>
<input type="hidden" id="fhb-content-<?= htmlspecialchars($key) ?>" name="content" <input type="hidden" id="fhb-content-<?= htmlspecialchars($key) ?>" name="content"
value="<?= htmlspecialchars($content) ?>"> value="<?= htmlspecialchars($content) ?>">
<div id="fhb-editor-<?= htmlspecialchars($key) ?>" class="fhb-overtype-editor"></div> <div id="fhb-editor-<?= htmlspecialchars($key) ?>" class="fhb-overtype-editor" style="height: 40vh !important; border: 1px dashed grey"></div>
<div class="fhb-edit-buttons"> <div class="fhb-edit-buttons">
<button type="submit" class="btn btn--primary btn--sm">Enregistrer</button> <button type="submit" class="btn btn--primary btn--sm">Enregistrer</button>

View File

@@ -753,6 +753,19 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: pqnovwxx eb519770 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision) +\\\\\\\ to: pqnovwxx eb519770 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision)
++ $linkName = $link['name'] ?? ''; ++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: pqnovwxx eb519770 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: pylyqurz a49fe41b "feat(backup): deploy cron-based SQLite backups to production" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: pylyqurz b7080cbb "feat(backup): deploy cron-based SQLite backups to production" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?> ?>
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer"> <tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">

View File

@@ -0,0 +1,8 @@
# XAMXAM SQLite backup cron jobs
# Installed to /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
#
# Hourly snapshot — kept 30 days
0 * * * * www-data /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
# Daily snapshot at 2am — kept 90 days
0 2 * * * www-data RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1

View File

@@ -62,7 +62,7 @@ deploy:
app/ xamxam:/var/www/xamxam/ app/ xamxam:/var/www/xamxam/
# Upload deploy-server.sh for post-deploy permission fix # Upload deploy-server.sh for post-deploy permission fix
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh xamxam "sudo bash /tmp/deploy-server.sh" ssh -t xamxam "sudo bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh" ssh xamxam "rm -f /tmp/deploy-server.sh"
ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}" ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}"
ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'" ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'"
@@ -73,6 +73,9 @@ deploy:
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys) # Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
@just deploy-env @just deploy-env
@just deploy-verify-permissions @just deploy-verify-permissions
@echo ""
@echo " First deploy? Also run: just deploy-backup"
@echo ""
[group('deploy')] [group('deploy')]
deploy-env: deploy-env:
@@ -236,7 +239,7 @@ deploy-nginx:
@echo "📋 Deploying nginx configuration…" @echo "📋 Deploying nginx configuration…"
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh" ssh -t xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf" ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf"
[group('deploy')] [group('deploy')]
@@ -249,6 +252,67 @@ deploy-script script_name:
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh" @echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
@echo "" @echo ""
[group('deploy')]
deploy-backup-script:
# Upload backup-sqlite.sh to /usr/local/bin on the server (requires sudo)
# Run once after initial deploy or when the backup script changes.
@echo "📋 Deploying backup script…"
rsync -v scripts/backup-sqlite.sh xamxam:/tmp/backup-sqlite.sh
ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/backup-sqlite.sh /usr/local/bin/backup-sqlite.sh && rm -f /tmp/backup-sqlite.sh"
@echo "✅ backup-sqlite.sh installed to /usr/local/bin/"
[group('deploy')]
deploy-backup-cron:
# Install cron jobs for hourly (30d retention) and daily (90d) backups.
# Uses /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
# Creates backup directory and log file on the server.
@echo "📋 Installing backup cron jobs…"
rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron"
ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:www-data /var/backups/xamxam && sudo chmod 755 /var/backups/xamxam"
ssh -t xamxam "sudo touch /var/log/sqlite-backup.log && sudo chown www-data:www-data /var/log/sqlite-backup.log && sudo chmod 644 /var/log/sqlite-backup.log"
@echo "✅ Cron jobs installed."
@echo " Cron file: /etc/cron.d/xamxam-backup"
@echo " Backup dir: /var/backups/xamxam"
@echo " Log file: /var/log/sqlite-backup.log"
@echo ""
@echo "Verify with: just deploy-check-backup-log"
[group('deploy')]
deploy-backup: deploy-backup-script deploy-backup-cron
# One-shot: deploy backup script + install cron jobs + set up directories.
[group('deploy')]
deploy-check-backup-log:
# Show the last 20 lines of the SQLite backup log on the server.
ssh -t xamxam "sudo tail -20 /var/log/sqlite-backup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
[group('deploy')]
deploy-list-backups:
# List all existing backups on the server (most recent last).
ssh xamxam "ls -lth /var/backups/xamxam/ 2>/dev/null || echo 'No backups yet.'"
[group('deploy')]
test-restore remote_gz_path:
# Test-restore a production backup snapshot to a local temp DB and verify.
# Usage: just test-restore /var/backups/xamxam/db-2026-05-11T14-00-00.db.gz
@scp xamxam:'{{remote_gz_path}}' /tmp/xamxam-restore-test.db.gz
@gunzip -c /tmp/xamxam-restore-test.db.gz > /tmp/xamxam-restore-test.db
@echo "Tables in snapshot:"
@sqlite3 /tmp/xamxam-restore-test.db ".tables"
@echo ""
@echo "Thesis count:"
@sqlite3 /tmp/xamxam-restore-test.db "SELECT COUNT(*) FROM theses WHERE deleted_at IS NULL;"
@echo ""
@echo "✅ Snapshot is valid. Remove temp files:"
@echo " rm /tmp/xamxam-restore-test.db /tmp/xamxam-restore-test.db.gz"
@rm -f /tmp/xamxam-restore-test.db.gz
[group('deploy')]
trigger-backup:
# Manually trigger the backup script on the server now (doesn't wait for cron).
ssh -t xamxam "sudo -u www-data /usr/local/bin/backup-sqlite.sh"
# ============================================================================ # ============================================================================
# Testing # Testing
# ============================================================================ # ============================================================================

View File

@@ -22,7 +22,10 @@ TIMESTAMP=$(date +"%Y-%m-%dT%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/db-$TIMESTAMP.db.gz" BACKUP_FILE="$BACKUP_DIR/db-$TIMESTAMP.db.gz"
TMP_SNAPSHOT="/tmp/xamxam-snapshot-$$.db" TMP_SNAPSHOT="/tmp/xamxam-snapshot-$$.db"
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR" 2>/dev/null || {
echo "ERROR: Cannot create backup directory '$BACKUP_DIR'. Run: just deploy-backup-cron" >&2
exit 1
}
# Safe hot backup using SQLite's online backup API # Safe hot backup using SQLite's online backup API
sqlite3 "$DB_PATH" ".backup $TMP_SNAPSHOT" sqlite3 "$DB_PATH" ".backup $TMP_SNAPSHOT"