#!/usr/bin/env bash # Runs ON the data node (BHS128) — by the dci-backup systemd timer (daily 03:30) or deploy/bin/backup.sh. # 1. pg_dump -Fc dci → $BACKUP_DIR/pg/dci-.dump # 2. connector configs + parsers → $BACKUP_DIR/config/dci-config-.tar.zst (+ git rev) # 3. ClickHouse BACKUP DATABASE dci → $BACKUP_DIR/clickhouse/dci-ch-.zip (best effort) # 4. mc mirror dci-raw → $BACKUP_DIR/minio/dci-raw (incremental mirror, not versioned) # 5. retention: keep 14 daily + 8 weekly (Sunday) for 1–3 # 6. rsync $BACKUP_DIR → ubuntu@10.68.0.2:/srv/dci-backups/ over the private link (key ~/.ssh/dci-backup) set -euo pipefail APP_DIR="${DCI_APP_DIR:-/srv/dci/app}" BACKUP_DIR="${DCI_BACKUP_DIR:-/srv/dci/backups}" OFFSITE_HOST="${DCI_OFFSITE_HOST:-10.68.0.2}" OFFSITE_DIR="${DCI_OFFSITE_DIR:-/srv/dci-backups}" OFFSITE="${DCI_OFFSITE:-1}" KEEP_DAILY="${DCI_KEEP_DAILY:-14}" KEEP_WEEKLY="${DCI_KEEP_WEEKLY:-8}" STAMP="$(date +%Y%m%d-%H%M%S)" LOCK=/tmp/dci-backup.lock exec 9>"$LOCK"; flock -n 9 || { echo "backup already running"; exit 0; } say() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; } cd "$APP_DIR" if docker info >/dev/null 2>&1; then DOCKER=docker; else DOCKER="sudo docker"; fi dc() { $DOCKER compose -f deploy/compose.data.yml --env-file deploy/.env.data "$@"; } mkdir -p "$BACKUP_DIR"/{pg,config,clickhouse,minio} # 1. Postgres say "pg_dump → pg/dci-$STAMP.dump" # streamed to the host (written as the invoking user; the container's postgres uid cannot write the bind mount) dc exec -T postgres pg_dump -U dci -d dci -Fc --no-owner --compress=zstd > "$BACKUP_DIR/pg/dci-$STAMP.dump.part" mv "$BACKUP_DIR/pg/dci-$STAMP.dump.part" "$BACKUP_DIR/pg/dci-$STAMP.dump" say " $(du -h "$BACKUP_DIR/pg/dci-$STAMP.dump" | cut -f1)" # 2. Config + parser sources say "config → config/dci-config-$STAMP.tar.zst" { git -C "$APP_DIR" rev-parse HEAD 2>/dev/null || echo "no-git ($(date -Iseconds))"; } > "$BACKUP_DIR/config/GIT_REV-$STAMP.txt" tar --zstd -cf "$BACKUP_DIR/config/dci-config-$STAMP.tar.zst" -C "$APP_DIR" \ --ignore-failed-read \ config/connectors packages/connectors/src apps/worker/src/parsers apps/worker/src/connectors packages/db/migrations \ deploy/compose.data.yml deploy/compose.crawl.yml deploy/edge deploy/clickhouse deploy/monitoring \ -C "$BACKUP_DIR/config" "GIT_REV-$STAMP.txt" 2>/dev/null || true rm -f "$BACKUP_DIR/config/GIT_REV-$STAMP.txt" # 3. ClickHouse (best effort — analytics are derived) say "clickhouse BACKUP DATABASE dci → clickhouse/dci-ch-$STAMP.zip" CH_PW="$(sed -nE 's/^CLICKHOUSE_PASSWORD=(.*)$/\1/p' deploy/.env.data | tr -d '"')" CH_DB="$(sed -nE 's/^CLICKHOUSE_DB=(.*)$/\1/p' deploy/.env.data | tr -d '"')"; CH_DB="${CH_DB:-dci}" if ! dc exec -T clickhouse clickhouse-client --user dci --password "$CH_PW" \ --query "BACKUP DATABASE $CH_DB TO File('/backups/dci-ch-$STAMP.zip') SETTINGS compression_method='zstd'" >/dev/null; then say " clickhouse backup failed (non-fatal)" fi # the zip is written by the clickhouse container user (uid 101, mode 640) → make it readable for the offsite rsync sudo chown -R "$(id -u):$(id -g)" "$BACKUP_DIR/clickhouse" 2>/dev/null || true chmod -R u+rwX,go+rX "$BACKUP_DIR/clickhouse" 2>/dev/null || true # 4. MinIO raw archive mirror say "mc mirror dci-raw → minio/dci-raw" dc run --rm --no-deps --entrypoint /bin/sh minio-init -c \ 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc mirror --overwrite --quiet local/'"${S3_BUCKET:-dci-raw}"' /backup/dci-raw' \ || say " mc mirror failed (non-fatal)" # 5. Retention (14 daily + 8 weekly on Sundays) prune() { local dir="$1" f d age dow now; now=$(date +%s) for f in "$dir"/dci-*; do [[ -e $f ]] || continue d="$(basename "$f" | grep -oE '[0-9]{8}' | head -1)" || continue [[ -n $d ]] || continue age=$(( (now - $(date -d "$d" +%s)) / 86400 )) dow=$(date -d "$d" +%u) # 7 = Sunday if (( age <= KEEP_DAILY )); then continue; fi if (( dow == 7 && age <= KEEP_DAILY + 7 * KEEP_WEEKLY )); then continue; fi say " prune $(basename "$f")"; rm -rf -- "$f" done } say "retention: $KEEP_DAILY daily + $KEEP_WEEKLY weekly" prune "$BACKUP_DIR/pg"; prune "$BACKUP_DIR/config"; prune "$BACKUP_DIR/clickhouse" # 6. Off-node copy over the private link if [[ $OFFSITE == 1 ]]; then say "rsync → ubuntu@$OFFSITE_HOST:$OFFSITE_DIR" rsync -a --delete --info=stats1 \ -e "ssh -i $HOME/.ssh/dci-backup -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new" \ "$BACKUP_DIR/" "ubuntu@$OFFSITE_HOST:$OFFSITE_DIR/" \ || say " offsite rsync failed (check ~/.ssh/dci-backup is authorized on $OFFSITE_HOST)" fi say "done — $(du -sh "$BACKUP_DIR" | cut -f1) in $BACKUP_DIR; latest: $(ls -1t "$BACKUP_DIR/pg" | head -1)"