---
id: task-013
title: 'Automated database backups and offsite storage'
status: Done
assignee: []
created_date: '2026-03-15 09:00'
labels:
  - dev-ops
  - enhancement
dependencies: []
priority: high
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
No critical database has automated backups. No data leaves this single server. A disk failure or compromise would lose all source code (Gitea), business data (ERPNext), photos (Immich), wiki/blog content (p2p-db), and email (Mailcow). This is the highest priority infrastructure gap.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Automated daily pg_dump for Gitea-DB (Postgres) — 3.1MB
- [x] #2 Automated daily mysqldump for ERPNext MariaDB — 2.3MB
- [x] #3 Automated daily mysqldump for p2p-db (MariaDB) — 707MB
- [x] #4 Automated daily pg_dump for Immich Postgres — 620MB
- [x] #5 Automated daily Mailcow backup (mariadb-dump) — 1.1MB
- [x] #6 Off-server storage — rclone sync to Cloudflare R2 (plex-media/db-backups/)
- [x] #7 Backup retention policy (7 days local, synced to R2)
- [x] #8 Backup monitoring — Uptime Kuma push monitor (ID 199, 48h heartbeat interval)
<!-- AC:END -->

## Notes

### Approach: Centralized backup container
Deploy an Alpine container with cron, database clients (pg_dump, mariadb-dump), and rclone. Runs scheduled dumps for all databases, compresses, and pushes to R2.

### Template
rphotos backup script at `/opt/apps/rphotos-online/backup-database.sh` has a good pattern (pg_dumpall with 30-day retention).

### Database credentials needed
- Gitea-DB: Postgres, accessible via `gitea-db` container
- ERPNext: MariaDB at `/opt/erpnext/` (host-only)
- p2p-db: MariaDB, root password in container env
- Immich: Postgres at `/opt/immich/postgres` (host bind mount)
- Mailcow: MySQL in `mailcowdockerized-mysql-mailcow-1`

### R2 storage
`r2-mount` container already has rclone configured for Cloudflare R2. Can reuse config for backup uploads.
