mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
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:
326
justfile
326
justfile
@@ -62,7 +62,7 @@ deploy:
|
||||
app/ xamxam:/var/www/xamxam/
|
||||
# Upload deploy-server.sh for post-deploy permission fix
|
||||
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 "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\"; }'"
|
||||
@@ -73,42 +73,45 @@ deploy:
|
||||
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
|
||||
@just deploy-env
|
||||
@just deploy-verify-permissions
|
||||
@echo ""
|
||||
@echo "ℹ️ First deploy? Also run: just deploy-backup"
|
||||
@echo ""
|
||||
|
||||
[group('deploy')]
|
||||
deploy-env:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY.
|
||||
if [ ! -f app/.env ]; then
|
||||
echo "WARNING: app/.env not found locally — skipping."
|
||||
exit 0
|
||||
fi
|
||||
if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then
|
||||
echo "Remote .env already exists — skipping to avoid overwriting key."
|
||||
echo "Run 'just reencrypt-password' if you rotated APP_KEY."
|
||||
else
|
||||
rsync -v --progress app/.env xamxam:/var/www/xamxam/.env
|
||||
ssh xamxam "chmod 640 /var/www/xamxam/.env && chown www-data:xamxam /var/www/xamxam/.env"
|
||||
echo ".env uploaded."
|
||||
fi
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY.
|
||||
if [ ! -f app/.env ]; then
|
||||
echo "WARNING: app/.env not found locally — skipping."
|
||||
exit 0
|
||||
fi
|
||||
if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then
|
||||
echo "Remote .env already exists — skipping to avoid overwriting key."
|
||||
echo "Run 'just reencrypt-password' if you rotated APP_KEY."
|
||||
else
|
||||
rsync -v --progress app/.env xamxam:/var/www/xamxam/.env
|
||||
ssh xamxam "chmod 640 /var/www/xamxam/.env && chown www-data:xamxam /var/www/xamxam/.env"
|
||||
echo ".env uploaded."
|
||||
fi
|
||||
|
||||
[group('deploy')]
|
||||
reencrypt-password new_key_b64="":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Re-encrypt the SMTP password in the remote DB after rotating APP_KEY.
|
||||
# Usage:
|
||||
# 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));"
|
||||
# 2. Run: just reencrypt-password <new_base64_key>
|
||||
# 3. Update app/.env locally with the new key, then run: just deploy-env
|
||||
if [ -z "{{new_key_b64}}" ]; then
|
||||
echo "Usage: just reencrypt-password <new_base64_key>"
|
||||
echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\""
|
||||
exit 1
|
||||
fi
|
||||
# Run the re-encryption script on the server using the current key (from remote .env)
|
||||
# and the supplied new key.
|
||||
ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Re-encrypt the SMTP password in the remote DB after rotating APP_KEY.
|
||||
# Usage:
|
||||
# 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));"
|
||||
# 2. Run: just reencrypt-password <new_base64_key>
|
||||
# 3. Update app/.env locally with the new key, then run: just deploy-env
|
||||
if [ -z "{{new_key_b64}}" ]; then
|
||||
echo "Usage: just reencrypt-password <new_base64_key>"
|
||||
echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\""
|
||||
exit 1
|
||||
fi
|
||||
# Run the re-encryption script on the server using the current key (from remote .env)
|
||||
# and the supplied new key.
|
||||
ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db"
|
||||
|
||||
[group('deploy')]
|
||||
deploy-db:
|
||||
@@ -118,115 +121,115 @@ deploy-db:
|
||||
|
||||
[group('deploy')]
|
||||
deploy-verify-permissions:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
APP_DIR="/var/www/xamxam"
|
||||
WEB_USER="www-data"
|
||||
APP_GROUP="xamxam"
|
||||
ERRORS=0
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
APP_DIR="/var/www/xamxam"
|
||||
WEB_USER="www-data"
|
||||
APP_GROUP="xamxam"
|
||||
ERRORS=0
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
||||
err() { printf "${RED}✗${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); }
|
||||
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
|
||||
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
||||
err() { printf "${RED}✗${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); }
|
||||
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
|
||||
|
||||
printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR"
|
||||
printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR"
|
||||
|
||||
# ── Ownership ──────────────────────────────────────────────────────────────────
|
||||
echo "── Ownership ───────────────────────────────────"
|
||||
while IFS= read -r line; do
|
||||
owner=$(echo "$line" | awk '{print $3}')
|
||||
group=$(echo "$line" | awk '{print $4}')
|
||||
path=$(echo "$line" | awk '{print $NF}')
|
||||
if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then
|
||||
err "$path → $owner:$group (expected $WEB_USER:$APP_GROUP)"
|
||||
else
|
||||
ok "$path → $owner:$group"
|
||||
fi
|
||||
done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null")
|
||||
# ── Ownership ──────────────────────────────────────────────────────────────────
|
||||
echo "── Ownership ───────────────────────────────────"
|
||||
while IFS= read -r line; do
|
||||
owner=$(echo "$line" | awk '{print $3}')
|
||||
group=$(echo "$line" | awk '{print $4}')
|
||||
path=$(echo "$line" | awk '{print $NF}')
|
||||
if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then
|
||||
err "$path → $owner:$group (expected $WEB_USER:$APP_GROUP)"
|
||||
else
|
||||
ok "$path → $owner:$group"
|
||||
fi
|
||||
done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null")
|
||||
|
||||
# ── Key directories: 2775 ─────────────────────────────────────────────────────
|
||||
echo "── Directory permissions (expected 2775) ───────"
|
||||
while IFS= read -r line; do
|
||||
perms=$(echo "$line" | awk '{print $1}')
|
||||
path=$(echo "$line" | awk '{print $NF}')
|
||||
if [ "$perms" != "drwxrwsr-x" ]; then
|
||||
err "$path → $perms (expected drwxrwsr-x / 2775)"
|
||||
else
|
||||
ok "$path → $perms"
|
||||
fi
|
||||
done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort")
|
||||
# ── Key directories: 2775 ─────────────────────────────────────────────────────
|
||||
echo "── Directory permissions (expected 2775) ───────"
|
||||
while IFS= read -r line; do
|
||||
perms=$(echo "$line" | awk '{print $1}')
|
||||
path=$(echo "$line" | awk '{print $NF}')
|
||||
if [ "$perms" != "drwxrwsr-x" ]; then
|
||||
err "$path → $perms (expected drwxrwsr-x / 2775)"
|
||||
else
|
||||
ok "$path → $perms"
|
||||
fi
|
||||
done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort")
|
||||
|
||||
# ── Key files: 664 ────────────────────────────────────────────────────────────
|
||||
echo "── File permissions (expected 664 / 660) ───────"
|
||||
# Spot-check a few critical files
|
||||
while IFS= read -r path; do
|
||||
perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING")
|
||||
if [ "$perms" = "MISSING" ]; then
|
||||
err "$path → FILE MISSING"
|
||||
else
|
||||
perm_num=$(echo "$perms" | awk '{print $1}')
|
||||
owner=$(echo "$perms" | awk '{print $2}')
|
||||
group=$(echo "$perms" | awk '{print $3}')
|
||||
case "$path" in
|
||||
*/storage/xamxam.db|*/storage/*.db)
|
||||
expected_perm="660" ;;
|
||||
*)
|
||||
expected_perm="664" ;;
|
||||
esac
|
||||
if [ "$perm_num" != "$expected_perm" ]; then
|
||||
err "$path → $perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP"
|
||||
elif [ "$owner" != "$WEB_USER" ]; then
|
||||
err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)"
|
||||
else
|
||||
ok "$path → $perm_num $owner:$group"
|
||||
fi
|
||||
fi
|
||||
done < <(printf '%s\n' \
|
||||
"$APP_DIR/.env" \
|
||||
"$APP_DIR/app/router.php" \
|
||||
"$APP_DIR/storage/xamxam.db")
|
||||
# ── Key files: 664 ────────────────────────────────────────────────────────────
|
||||
echo "── File permissions (expected 664 / 660) ───────"
|
||||
# Spot-check a few critical files
|
||||
while IFS= read -r path; do
|
||||
perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING")
|
||||
if [ "$perms" = "MISSING" ]; then
|
||||
err "$path → FILE MISSING"
|
||||
else
|
||||
perm_num=$(echo "$perms" | awk '{print $1}')
|
||||
owner=$(echo "$perms" | awk '{print $2}')
|
||||
group=$(echo "$perms" | awk '{print $3}')
|
||||
case "$path" in
|
||||
*/storage/xamxam.db|*/storage/*.db)
|
||||
expected_perm="660" ;;
|
||||
*)
|
||||
expected_perm="664" ;;
|
||||
esac
|
||||
if [ "$perm_num" != "$expected_perm" ]; then
|
||||
err "$path → $perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP"
|
||||
elif [ "$owner" != "$WEB_USER" ]; then
|
||||
err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)"
|
||||
else
|
||||
ok "$path → $perm_num $owner:$group"
|
||||
fi
|
||||
fi
|
||||
done < <(printf '%s\n' \
|
||||
"$APP_DIR/.env" \
|
||||
"$APP_DIR/app/router.php" \
|
||||
"$APP_DIR/storage/xamxam.db")
|
||||
|
||||
# ── var/ subdirectories must be writable ──────────────────────────────────────
|
||||
echo "── var/ writability ────────────────────────────"
|
||||
for subdir in cache logs tmp; do
|
||||
if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then
|
||||
ok "var/$subdir → writable"
|
||||
else
|
||||
err "var/$subdir → NOT WRITABLE"
|
||||
fi
|
||||
done
|
||||
# ── var/ subdirectories must be writable ──────────────────────────────────────
|
||||
echo "── var/ writability ────────────────────────────"
|
||||
for subdir in cache logs tmp; do
|
||||
if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then
|
||||
ok "var/$subdir → writable"
|
||||
else
|
||||
err "var/$subdir → NOT WRITABLE"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── storage/cache/rate_limit writable ─────────────────────────────────────────
|
||||
if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then
|
||||
ok "storage/cache/rate_limit → writable"
|
||||
else
|
||||
err "storage/cache/rate_limit → NOT WRITABLE"
|
||||
fi
|
||||
# ── storage/cache/rate_limit writable ─────────────────────────────────────────
|
||||
if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then
|
||||
ok "storage/cache/rate_limit → writable"
|
||||
else
|
||||
err "storage/cache/rate_limit → NOT WRITABLE"
|
||||
fi
|
||||
|
||||
# ── .env must be 640 ──────────────────────────────────────────────────────────
|
||||
env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
|
||||
if [ "$env_perm" = "640" ]; then
|
||||
ok ".env → 640"
|
||||
elif [ -z "$env_perm" ]; then
|
||||
warn ".env → MISSING"
|
||||
else
|
||||
err ".env → $env_perm (expected 640)"
|
||||
fi
|
||||
# ── .env must be 640 ──────────────────────────────────────────────────────────
|
||||
env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
|
||||
if [ "$env_perm" = "640" ]; then
|
||||
ok ".env → 640"
|
||||
elif [ -z "$env_perm" ]; then
|
||||
warn ".env → MISSING"
|
||||
else
|
||||
err ".env → $env_perm (expected 640)"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
printf "${GREEN}✅ All permissions OK${NC}\n"
|
||||
else
|
||||
printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS"
|
||||
printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
printf "${GREEN}✅ All permissions OK${NC}\n"
|
||||
else
|
||||
printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS"
|
||||
printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[group('deploy')]
|
||||
deploy-nginx:
|
||||
@@ -236,7 +239,7 @@ deploy-nginx:
|
||||
@echo "📋 Deploying nginx configuration…"
|
||||
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
|
||||
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"
|
||||
|
||||
[group('deploy')]
|
||||
@@ -249,6 +252,67 @@ deploy-script script_name:
|
||||
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
|
||||
@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
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user