feat: add backup service
574e4e64
6 file(s) · +93 −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 |
|
| 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 |
| 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 |
| 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" |
| 1 | + | 0 */6 * * * . /etc/environment && /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 |
| 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} |