Skip to content

Commit 41fc65f

Browse files
feat(dx): add automated database backup scripts
- Add backup-db.sh for daily SQLite backups with 30-day retention - Add restore-db.sh for safe database restoration - Add download-backup.sh to fetch backups to local machine - Add backup-setup.md documentation - Add prisma/backups/ to .gitignore Closes rb-4ca.7 Amp-Thread-ID: https://ampcode.com/threads/T-019ba70f-5528-749c-bab6-85ffe771776f Co-authored-by: Amp <amp@ampcode.com>
1 parent 9e13ca8 commit 41fc65f

5 files changed

Lines changed: 283 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ bv-pages/
4242

4343
# Kamal deployment secrets
4444
.kamal/secrets
45+
46+
# Local backup files
47+
prisma/backups/

docs/backup-setup.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Database Backup Setup
2+
3+
This document describes how to set up automated backups for the SQLite database.
4+
5+
## Overview
6+
7+
- **Database location**: `/data/kelas/db/prod.db` (Docker volume mount)
8+
- **Backup location**: `/var/backups/kelas-db/`
9+
- **Retention**: 30 days
10+
- **Schedule**: Daily at 3:00 AM
11+
12+
## Setup Instructions (on VPS)
13+
14+
Get the VPS host from `config/deploy.yml` or set it:
15+
16+
```bash
17+
VPS_HOST="root@$(grep -A2 'hosts:' config/deploy.yml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+')"
18+
```
19+
20+
### 1. Copy scripts to server
21+
22+
```bash
23+
scp scripts/backup-db.sh scripts/restore-db.sh ${VPS_HOST}:/usr/local/bin/
24+
```
25+
26+
### 2. Make scripts executable
27+
28+
```bash
29+
ssh ${VPS_HOST} "chmod +x /usr/local/bin/backup-db.sh /usr/local/bin/restore-db.sh"
30+
```
31+
32+
### 3. Install sqlite3 (recommended for safe backups)
33+
34+
```bash
35+
ssh ${VPS_HOST} "apt-get update && apt-get install -y sqlite3"
36+
```
37+
38+
### 4. Set up cron job
39+
40+
```bash
41+
ssh ${VPS_HOST} 'echo "0 3 * * * /usr/local/bin/backup-db.sh >> /var/log/kelas-backup.log 2>&1" | crontab -'
42+
```
43+
44+
Or manually add to crontab:
45+
46+
```bash
47+
ssh ${VPS_HOST} "crontab -e"
48+
```
49+
50+
Add this line:
51+
52+
```
53+
0 3 * * * /usr/local/bin/backup-db.sh >> /var/log/kelas-backup.log 2>&1
54+
```
55+
56+
### 5. Create backup directory
57+
58+
```bash
59+
ssh ${VPS_HOST} "mkdir -p /var/backups/kelas-db"
60+
```
61+
62+
### 6. Test backup manually
63+
64+
```bash
65+
ssh ${VPS_HOST} "/usr/local/bin/backup-db.sh"
66+
```
67+
68+
## Manual Operations
69+
70+
### Create a backup now
71+
72+
```bash
73+
ssh ${VPS_HOST} "/usr/local/bin/backup-db.sh"
74+
```
75+
76+
### List backups
77+
78+
```bash
79+
ssh ${VPS_HOST} "ls -lh /var/backups/kelas-db/"
80+
```
81+
82+
### Restore from backup
83+
84+
```bash
85+
# Restore most recent backup
86+
ssh ${VPS_HOST} "/usr/local/bin/restore-db.sh"
87+
88+
# Restore specific backup
89+
ssh ${VPS_HOST} "/usr/local/bin/restore-db.sh /var/backups/kelas-db/prod_20260110_030000.db"
90+
```
91+
92+
### Check backup logs
93+
94+
```bash
95+
ssh ${VPS_HOST} "tail -50 /var/log/kelas-backup.log"
96+
```
97+
98+
## Future Improvements
99+
100+
- **Litestream**: Real-time replication to S3 (see epic rb-8xp)
101+
- **Off-site backups**: rsync to secondary location
102+
- **Monitoring**: Alert on backup failures

scripts/backup-db.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash
2+
# Database backup script for Kelas Rumah Berbagi
3+
# Backs up SQLite database from Docker volume with 30-day retention
4+
#
5+
# Usage: ./scripts/backup-db.sh
6+
# Setup: Run on VPS via cron for automated daily backups
7+
8+
set -euo pipefail
9+
10+
# Configuration
11+
DB_PATH="/data/kelas/db/prod.db"
12+
BACKUP_DIR="/var/backups/kelas-db"
13+
RETENTION_DAYS=30
14+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
15+
BACKUP_FILE="${BACKUP_DIR}/prod_${TIMESTAMP}.db"
16+
17+
# Ensure backup directory exists
18+
mkdir -p "${BACKUP_DIR}"
19+
20+
# Check if database exists
21+
if [ ! -f "${DB_PATH}" ]; then
22+
echo "ERROR: Database not found at ${DB_PATH}"
23+
exit 1
24+
fi
25+
26+
# Create backup using SQLite backup command (safe for running database)
27+
# This creates a consistent backup even if the database is in use
28+
if command -v sqlite3 &> /dev/null; then
29+
echo "Creating backup with sqlite3 .backup command..."
30+
sqlite3 "${DB_PATH}" ".backup '${BACKUP_FILE}'"
31+
else
32+
echo "sqlite3 not found, using file copy..."
33+
cp "${DB_PATH}" "${BACKUP_FILE}"
34+
fi
35+
36+
# Verify backup was created
37+
if [ ! -f "${BACKUP_FILE}" ]; then
38+
echo "ERROR: Backup file was not created"
39+
exit 1
40+
fi
41+
42+
BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
43+
echo "Backup created: ${BACKUP_FILE} (${BACKUP_SIZE})"
44+
45+
# Remove backups older than retention period
46+
echo "Cleaning up backups older than ${RETENTION_DAYS} days..."
47+
find "${BACKUP_DIR}" -name "prod_*.db" -type f -mtime +${RETENTION_DAYS} -delete
48+
49+
# List remaining backups
50+
BACKUP_COUNT=$(find "${BACKUP_DIR}" -name "prod_*.db" -type f | wc -l)
51+
echo "Total backups: ${BACKUP_COUNT}"
52+
53+
echo "Backup completed successfully"

scripts/download-backup.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
# Download database backups from VPS to local machine
3+
#
4+
# Usage: ./scripts/download-backup.sh [latest|all|<filename>]
5+
# latest - Download most recent backup (default)
6+
# all - Download all backups
7+
# <file> - Download specific backup file
8+
9+
set -euo pipefail
10+
11+
SCRIPT_DIR="$(dirname "$0")"
12+
DEPLOY_CONFIG="${SCRIPT_DIR}/../config/deploy.yml"
13+
14+
# Extract VPS IP from Kamal config
15+
VPS_IP=$(grep -A2 'hosts:' "${DEPLOY_CONFIG}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+')
16+
VPS_HOST="root@${VPS_IP}"
17+
REMOTE_BACKUP_DIR="/var/backups/kelas-db"
18+
LOCAL_BACKUP_DIR="$(dirname "$0")/../prisma/backups"
19+
20+
# Ensure local backup directory exists
21+
mkdir -p "${LOCAL_BACKUP_DIR}"
22+
23+
MODE="${1:-latest}"
24+
25+
case "${MODE}" in
26+
latest)
27+
echo "Fetching latest backup..."
28+
LATEST=$(ssh "${VPS_HOST}" "ls -t ${REMOTE_BACKUP_DIR}/prod_*.db 2>/dev/null | head -1")
29+
if [ -z "${LATEST}" ]; then
30+
echo "ERROR: No backups found on server"
31+
exit 1
32+
fi
33+
FILENAME=$(basename "${LATEST}")
34+
echo "Downloading ${FILENAME}..."
35+
scp "${VPS_HOST}:${LATEST}" "${LOCAL_BACKUP_DIR}/"
36+
echo "Downloaded to ${LOCAL_BACKUP_DIR}/${FILENAME}"
37+
;;
38+
all)
39+
echo "Downloading all backups..."
40+
scp "${VPS_HOST}:${REMOTE_BACKUP_DIR}/prod_*.db" "${LOCAL_BACKUP_DIR}/"
41+
echo "Downloaded to ${LOCAL_BACKUP_DIR}/"
42+
ls -lh "${LOCAL_BACKUP_DIR}"
43+
;;
44+
*)
45+
echo "Downloading ${MODE}..."
46+
scp "${VPS_HOST}:${REMOTE_BACKUP_DIR}/${MODE}" "${LOCAL_BACKUP_DIR}/"
47+
echo "Downloaded to ${LOCAL_BACKUP_DIR}/${MODE}"
48+
;;
49+
esac
50+
51+
echo "Done"

scripts/restore-db.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/bin/bash
2+
# Database restore script for Kelas Rumah Berbagi
3+
# Restores SQLite database from backup
4+
#
5+
# Usage: ./scripts/restore-db.sh [backup_file]
6+
# If no backup file specified, uses the most recent backup
7+
8+
set -euo pipefail
9+
10+
# Configuration
11+
DB_PATH="/data/kelas/db/prod.db"
12+
BACKUP_DIR="/var/backups/kelas-db"
13+
14+
# Get backup file (from argument or most recent)
15+
if [ $# -ge 1 ]; then
16+
BACKUP_FILE="$1"
17+
else
18+
BACKUP_FILE=$(ls -t "${BACKUP_DIR}"/prod_*.db 2>/dev/null | head -1)
19+
if [ -z "${BACKUP_FILE}" ]; then
20+
echo "ERROR: No backup files found in ${BACKUP_DIR}"
21+
exit 1
22+
fi
23+
echo "Using most recent backup: ${BACKUP_FILE}"
24+
fi
25+
26+
# Verify backup file exists
27+
if [ ! -f "${BACKUP_FILE}" ]; then
28+
echo "ERROR: Backup file not found: ${BACKUP_FILE}"
29+
exit 1
30+
fi
31+
32+
# Confirm restore
33+
echo "WARNING: This will replace the current database!"
34+
echo " Source: ${BACKUP_FILE}"
35+
echo " Target: ${DB_PATH}"
36+
read -p "Are you sure you want to continue? (yes/no): " CONFIRM
37+
38+
if [ "${CONFIRM}" != "yes" ]; then
39+
echo "Restore cancelled"
40+
exit 0
41+
fi
42+
43+
# Stop the application container to prevent writes during restore
44+
echo "Stopping application container..."
45+
if docker ps --format '{{.Names}}' | grep -q "kelas-web"; then
46+
docker stop kelas-web || true
47+
fi
48+
49+
# Create backup of current database before restore
50+
if [ -f "${DB_PATH}" ]; then
51+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
52+
PRERESTORE_BACKUP="${BACKUP_DIR}/prod_prerestore_${TIMESTAMP}.db"
53+
echo "Backing up current database to ${PRERESTORE_BACKUP}..."
54+
cp "${DB_PATH}" "${PRERESTORE_BACKUP}"
55+
fi
56+
57+
# Restore the backup
58+
echo "Restoring database..."
59+
cp "${BACKUP_FILE}" "${DB_PATH}"
60+
61+
# Restart the application
62+
echo "Starting application container..."
63+
docker start kelas-web || true
64+
65+
# Verify restore
66+
if [ -f "${DB_PATH}" ]; then
67+
RESTORED_SIZE=$(du -h "${DB_PATH}" | cut -f1)
68+
echo "Database restored successfully (${RESTORED_SIZE})"
69+
else
70+
echo "ERROR: Restore failed - database file not found"
71+
exit 1
72+
fi
73+
74+
echo "Restore completed"

0 commit comments

Comments
 (0)