feat: add backup service 574e4e64
Steve · 2026-04-04 22:26 6 file(s) · +93 −2
.github/workflows/docker.yml +2 −2
25 25
      - name: Determine which apps to build
26 26
        id: filter
27 27
        run: |
28 -
          ALL='["cellar","sipp","feeds","parcels","jotts","og","shrink"]'
28 +
          ALL='["cellar","sipp","feeds","parcels","jotts","og","shrink","backup"]'
29 29
30 30
          # Map cargo package names to directory names
31 31
          pkg_to_dir() {
61 61
          fi
62 62
63 63
          apps=()
64 -
          for app in cellar sipp feeds parcels jotts og shrink; do
64 +
          for app in cellar sipp feeds parcels jotts og shrink backup; do
65 65
            if echo "$changed" | grep -q "^apps/${app}/"; then
66 66
              apps+=("\"${app}\"")
67 67
            fi
apps/backup/.env.example (added) +14 −0
1 +
# Cloudflare R2 credentials
2 +
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
3 +
AWS_ACCESS_KEY_ID=<your-r2-access-key>
4 +
AWS_SECRET_ACCESS_KEY=<your-r2-secret-key>
5 +
R2_BUCKET=andromeda-backups
6 +
7 +
# Optional: override external volume names if they differ on your host
8 +
# Run `docker volume ls` to check actual names
9 +
# JOTTS_VOLUME=jotts_jotts-data
10 +
# SIPP_VOLUME=sipp_sipp-data
11 +
# CELLAR_VOLUME=cellar_cellar-data
12 +
13 +
# Optional: days to keep backups (default: 30)
14 +
# RETENTION_DAYS=30
apps/backup/Dockerfile (added) +14 −0
1 +
FROM debian:bookworm-slim
2 +
3 +
RUN apt-get update && \
4 +
    apt-get install -y --no-install-recommends sqlite3 awscli cron && \
5 +
    rm -rf /var/lib/apt/lists/*
6 +
7 +
COPY backup.sh /usr/local/bin/backup.sh
8 +
RUN chmod +x /usr/local/bin/backup.sh
9 +
10 +
COPY crontab /etc/cron.d/backup-cron
11 +
RUN chmod 0644 /etc/cron.d/backup-cron && crontab /etc/cron.d/backup-cron
12 +
13 +
# Pass environment variables to cron jobs
14 +
CMD printenv | grep -E '^(R2_|AWS_|RETENTION)' >> /etc/environment && cron -f
apps/backup/backup.sh (added) +42 −0
1 +
#!/bin/sh
2 +
set -eu
3 +
4 +
TIMESTAMP=$(date -u +%Y-%m-%dT%H%M%SZ)
5 +
BUCKET="${R2_BUCKET:-andromeda-backups}"
6 +
RETENTION_DAYS="${RETENTION_DAYS:-30}"
7 +
8 +
DBS="jotts:/data/jotts/jotts.sqlite sipp:/data/sipp/sipp.sqlite cellar:/data/cellar/cellar.sqlite"
9 +
10 +
for entry in $DBS; do
11 +
  name="${entry%%:*}"
12 +
  path="${entry#*:}"
13 +
14 +
  if [ ! -f "$path" ]; then
15 +
    echo "WARN: $path not found, skipping $name"
16 +
    continue
17 +
  fi
18 +
19 +
  backup_file="/tmp/${name}-${TIMESTAMP}.sqlite"
20 +
  echo "$(date -u) Backing up $name..."
21 +
  sqlite3 "$path" ".backup '$backup_file'"
22 +
  gzip "$backup_file"
23 +
  aws s3 cp "${backup_file}.gz" "s3://${BUCKET}/${name}/${TIMESTAMP}.sqlite.gz" \
24 +
    --endpoint-url "${R2_ENDPOINT}"
25 +
  rm -f "${backup_file}.gz"
26 +
  echo "$(date -u) OK: $name uploaded"
27 +
done
28 +
29 +
# Prune old backups
30 +
cutoff=$(date -u -d "-${RETENTION_DAYS} days" +%Y-%m-%d 2>/dev/null || date -u -v-${RETENTION_DAYS}d +%Y-%m-%d)
31 +
for name in jotts sipp cellar; do
32 +
  aws s3 ls "s3://${BUCKET}/${name}/" --endpoint-url "${R2_ENDPOINT}" 2>/dev/null | while read -r line; do
33 +
    filedate=$(echo "$line" | awk '{print $1}')
34 +
    filename=$(echo "$line" | awk '{print $4}')
35 +
    if [ -n "$filename" ] && [ "$filedate" \< "$cutoff" ]; then
36 +
      aws s3 rm "s3://${BUCKET}/${name}/${filename}" --endpoint-url "${R2_ENDPOINT}"
37 +
      echo "$(date -u) Pruned: ${name}/${filename}"
38 +
    fi
39 +
  done
40 +
done
41 +
42 +
echo "$(date -u) Backup complete"
apps/backup/crontab (added) +1 −0
1 +
0 */6 * * * . /etc/environment && /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
apps/backup/docker-compose.yml (added) +20 −0
1 +
services:
2 +
  backup:
3 +
    build: .
4 +
    volumes:
5 +
      - jotts-data:/data/jotts:ro
6 +
      - sipp-data:/data/sipp:ro
7 +
      - cellar-data:/data/cellar:ro
8 +
    env_file: .env
9 +
    restart: unless-stopped
10 +
11 +
volumes:
12 +
  jotts-data:
13 +
    external: true
14 +
    name: ${JOTTS_VOLUME:-jotts_jotts-data}
15 +
  sipp-data:
16 +
    external: true
17 +
    name: ${SIPP_VOLUME:-sipp_sipp-data}
18 +
  cellar-data:
19 +
    external: true
20 +
    name: ${CELLAR_VOLUME:-cellar_cellar-data}