-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.sh
More file actions
executable file
·55 lines (46 loc) · 2.78 KB
/
Copy pathbackup.sh
File metadata and controls
executable file
·55 lines (46 loc) · 2.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env bash
# Nightly backup: a compressed custom-format dump of each database plus the
# storage volume.
#
# All three parts are needed. The project dump alone restores an instance whose
# storage rows point at files that are gone, which looks like a working restore
# right up until someone opens an image — and whose control database is empty,
# which means no Studio account and no way in without psql.
set -euo pipefail
cd "$(dirname "$0")/.."
# docker compose accepts unquoted values containing spaces and angle brackets,
# which `. ./.env` would treat as redirection. Pull out only what we need.
envget() { sed -n "s/^$1=//p" .env | head -1 | sed 's/^"//; s/"$//'; }
POSTGRES_USER="${POSTGRES_USER:-$(envget POSTGRES_USER)}"
POSTGRES_DB="${POSTGRES_DB:-$(envget POSTGRES_DB)}"
: "${POSTGRES_USER:=baselyra}"
: "${POSTGRES_DB:=baselyra}"
# Baselyra's own operating data lives in a second database, and the only Studio
# account is in it. Same default as CONTROL_DATABASE_URL derives in src/config.ts.
CONTROL_DB="${CONTROL_POSTGRES_DB:-baselyra_control}"
DEST="${BASELYRA_BACKUP_DIR:-/var/backups/baselyra}"
KEEP_DAYS="${BASELYRA_BACKUP_KEEP_DAYS:-14}"
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p "$DEST"
fail() { echo "[backup] FAILED: $*" >&2; exit 1; }
# -Fc is the custom format: compressed, and restorable table-by-table with
# pg_restore, which a plain SQL file cannot do.
docker compose exec -T db pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc \
> "$DEST/db-$STAMP.dump.part" || fail "pg_dump $POSTGRES_DB"
mv "$DEST/db-$STAMP.dump.part" "$DEST/db-$STAMP.dump"
docker compose exec -T db pg_dump -U "$POSTGRES_USER" -d "$CONTROL_DB" -Fc \
> "$DEST/control-$STAMP.dump.part" || fail "pg_dump $CONTROL_DB"
mv "$DEST/control-$STAMP.dump.part" "$DEST/control-$STAMP.dump"
# Read the volume through a throwaway container so the files are consistent
# with what the app sees, regardless of where Docker put the volume on disk.
docker run --rm -v baselyra_storage-data:/data:ro -v "$DEST:/out" alpine \
tar -czf "/out/storage-$STAMP.tar.gz.part" -C /data . || fail "storage tar"
mv "$DEST/storage-$STAMP.tar.gz.part" "$DEST/storage-$STAMP.tar.gz"
# A zero-byte dump is the classic silent backup failure — refuse to count it.
[ -s "$DEST/db-$STAMP.dump" ] || fail "project dump is empty"
[ -s "$DEST/control-$STAMP.dump" ] || fail "control dump is empty"
find "$DEST" -name 'db-*.dump' -mtime "+$KEEP_DAYS" -delete
find "$DEST" -name 'control-*.dump' -mtime "+$KEEP_DAYS" -delete
find "$DEST" -name 'storage-*.tar.gz' -mtime "+$KEEP_DAYS" -delete
find "$DEST" -name '*.part' -mtime +1 -delete
echo "[backup] $STAMP db=$(du -h "$DEST/db-$STAMP.dump" | cut -f1) control=$(du -h "$DEST/control-$STAMP.dump" | cut -f1) storage=$(du -h "$DEST/storage-$STAMP.tar.gz" | cut -f1)"