Playbook: SupabaseBackupStale — Supabase Daily Backup Stale (>26h)
Alert: SupabaseBackupStale
Exporter: backup-exporter:9220
Severity: Critical
SLO: Backup must succeed daily; alert fires when supabase_backup_db_age_seconds > 93600 (26h)
What triggers this problem
The supabase-backup GitHub Actions workflow runs daily at 03:30 UTC on the self-hosted ionos runner (vps-i1). The workflow writes a metrics status file to Wasabi S3 (p24-infra bucket) on every run. The backup-exporter reads that file every scrape; if the file is absent or too old, the alert fires.
Common root causes (ranked by frequency as of 2026-06):
- IONOS runners OOM-killed — most likely. vps-i1 has 8GB RAM shared between the monitoring stack and runner jobs. Heavy jobs (Docker pg_dump, Python tests with large imports) can exhaust RAM, triggering OOM kills on both
ionos-localhostandionos-localhost-2runner services. Once killed they go offline and all queued jobs stay stuck. - Runners not restarted after OOM — the runner service exits cleanly (code 0) after OOM, so
Restart=on-failuredoes not auto-restart it. - Workflow runs stuck in queue — queued runs do not expire automatically; they wait indefinitely until a runner picks them up.
- Historical: GitHub-hosted runners out of minutes — resolved by PR #1045 (2026-06-22) which migrated to self-hosted ionos runner.
How to confirm it (exact commands)
# 1. Check backup-exporter metric directly (from local workstation)
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 `
"curl -s http://localhost:9220/metrics | grep supabase_backup"
# Expected when healthy (age < 93600):
# supabase_backup_db_age_seconds 12345.0
# Unhealthy: metric line present but NO VALUE (gauge with no sample)
# 2. Check recent workflow runs
gh run list --workflow supabase-backup.yml --limit 10 --repo radieu/p24-infra
# 3. Check runner status
gh api repos/radieu/p24-infra/actions/runners --jq '.runners[] | {name: .name, status: .status, busy: .busy}'
# 4. Check for OOM in system journal (run on vps-i1)
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 `
"journalctl -k --since '24 hours ago' | grep -i 'oom\|out of memory'"Step-by-step fix
Step 1: Confirm runners are offline
gh api repos/radieu/p24-infra/actions/runners --jq '.runners[] | {name: .name, status: .status}'If both show "status": "offline" proceed to Step 2.
Step 2: Restart runners on vps-i1
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 @'
systemctl start actions.runner.radieu-p24-infra.ionos-localhost.service
systemctl start actions.runner.radieu-p24-infra.ionos-localhost-2.service
sleep 5
systemctl is-active actions.runner.radieu-p24-infra.ionos-localhost.service
systemctl is-active actions.runner.radieu-p24-infra.ionos-localhost-2.service
'@Step 3: Cancel stale queued runs (if any)
# List queued runs
gh run list --workflow supabase-backup.yml --limit 10 --repo radieu/p24-infra
# Cancel each queued run ID
gh run cancel <RUN_ID> --repo radieu/p24-infraStep 4: Trigger a fresh backup
gh workflow run supabase-backup.yml --repo radieu/p24-infra --field backup_type=dailyStep 5: Verify backup succeeds
# Get the new run ID
gh run list --workflow supabase-backup.yml --limit 3 --repo radieu/p24-infra
# Watch the run (takes 5-15 minutes)
gh run watch <RUN_ID> --repo radieu/p24-infra
# After success, confirm metric is back
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 `
"curl -s 'http://localhost:9090/api/v1/query?query=supabase_backup_db_age_seconds'"If runners die again after restart (recurring OOM)
The runner services have MemoryAccounting=no + Restart=on-failure systemd overrides (applied 2026-06-23). If OOM recurs:
# Check current memory on vps-i1
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 "free -h && docker stats --no-stream"
# Kill any stuck Docker containers from failed jobs
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 `
"docker ps -a --filter status=exited -q | xargs docker rm -f"
# Verify overrides are still in place
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 `
"systemctl show actions.runner.radieu-p24-infra.ionos-localhost.service | grep -E 'MemoryAccounting|Restart='"If MemoryAccounting=yes (override missing — re-apply):
ssh -i "C:\Users\konar\.ssh\id_ed25519" root@217.154.82.162 @'
mkdir -p /etc/systemd/system/actions.runner.radieu-p24-infra.ionos-localhost.service.d
mkdir -p /etc/systemd/system/actions.runner.radieu-p24-infra.ionos-localhost-2.service.d
cat > /etc/systemd/system/actions.runner.radieu-p24-infra.ionos-localhost.service.d/override.conf << OVERRIDE
[Service]
MemoryAccounting=no
Restart=on-failure
RestartSec=30s
OVERRIDE
cat > /etc/systemd/system/actions.runner.radieu-p24-infra.ionos-localhost-2.service.d/override.conf << OVERRIDE
[Service]
MemoryAccounting=no
Restart=on-failure
RestartSec=30s
OVERRIDE
systemctl daemon-reload
'@Escalation path
If the workflow still fails after multiple restarts (non-runner cause):
- View workflow logs:
gh run view --log --job=<JOB_ID> --repo radieu/p24-infra - Check Supabase pooler connectivity from vps-i1:
nc -zv aws-0-eu-central-1.pooler.supabase.com 5432 - Check Wasabi write access: verify
P24_INFRA_WASABI_ACCESS_KEYsecret is valid in GitHub Secrets - If pooler is unreachable: check
status.supabase.com - Create a
human-actionGitHub issue if root cause requires account-level intervention
Prevention notes
- Systemd overrides applied 2026-06-23:
MemoryAccounting=noon both runner services prevents memcg-constrained OOM kills;Restart=on-failurehandles non-OOM exits. - MemoryAccounting=no note: this disables per-service cgroup memory accounting so the system-level OOM killer decides (uses all available RAM + swap before killing), giving the runner more headroom for heavy Docker jobs.
- Runner isolation: consider running only one runner at a time on vps-i1, or migrating heavy test jobs to bms-4 which has 32GB RAM.
- Monitoring: the
SupabaseBackupStalealert fires after 26h — respond within 2h to avoid a full backup gap day. - Wasabi backup is secondary: Supabase managed PITR backups remain active regardless of this workflow. This workflow provides an off-platform offsite copy for disaster recovery.
Audit Log — Log to infra_operations
After this operation completes, log it to the infra_operations audit table.
Python (Linux server — bms-4, vps-i1, vps-h1, or similar):
import sys
sys.path.insert(0, '/opt/p24-infra')
from scripts.lib.log_op import log_op
log_op(
actor="claude", # "radieu" for manual human ops, "claude" for agent
op_type="other",
resource="supabase-backup",
result="success", # "success" | "failed" | "skipped"
detail="Supabase backup staleness resolved — backup process manually triggered",
env="vps-i1",
gh_issue=2730,
)PowerShell (Windows dev machine):
$env:SUPABASE_URL = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
python -c "
import os, sys
sys.path.insert(0, 'C:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('claude', 'other', 'supabase-backup', 'success', 'Supabase backup staleness resolved — backup process manually triggered', 'vps-i1')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''