eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env bash
2# πŸ€– Fleet DB backup via Litestream (issue #550: continuous replication, not
3# snapshots β€” closes the "instance DBs have no off-box backup" gap).
4#
5# Runs ON the router box, as root, idempotent β€” re-run any time (e.g. after
6# add-instance) to pick up new databases; it regenerates the config and
7# restarts the replicator. Everything replicated is instance ciphertext plus
8# routing metadata the box already holds, so an off-box copy is
9# threat-model-free (the E2EE rule).
10#
11# Prerequisites:
12# /data/litestream.env β€” AWS creds for the scoped litestream-fleet IAM user
13# (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY; object-rw on the fleet
14# bucket only β€” minted per infra/prod/backup.tf, key NOT in tofu state).
15#
16# Usage:
17# fleet-backup.sh # replicate every /data/*.db
18# fleet-backup.sh test.luan.chat.db # canary: just the named DB file(s)
19#
20# Verify after first run:
21# /opt/lchat/litestream databases -config /etc/litestream.yml
22# journalctl -u lchat-litestream -n 20
23# Restore drill (to a scratch path, never over the live file):
24# AWS creds in env, then:
25# /opt/lchat/litestream restore -config /etc/litestream.yml -o /tmp/drill.db /data/<host>.db
26set -euo pipefail
27
28LITESTREAM_VERSION="0.5.14"
29BUCKET="eleven-prod-fleet-backup-427880590669"
30REGION="eu-west-1"
31BIN=/opt/lchat/litestream
32CONF=/etc/litestream.yml
33ENVFILE=/data/litestream.env
34UNIT=/etc/systemd/system/lchat-litestream.service
35
36[ -f "$ENVFILE" ] || { echo "missing $ENVFILE (AWS creds for litestream-fleet) β€” mint per infra/prod/backup.tf" >&2; exit 1; }
37
38# 1. The binary (skip when the right version is already installed).
39if ! "$BIN" version 2>/dev/null | grep -q "$LITESTREAM_VERSION"; then
40 echo "installing litestream v$LITESTREAM_VERSION"
41 curl -fsSL "https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" \
42 | tar -xzO litestream > "$BIN.tmp"
43 chmod 0755 "$BIN.tmp" && mv "$BIN.tmp" "$BIN"
44fi
45
46# 2. The config, regenerated from what's actually in /data. One replica prefix
47# per database file; trash and sidecar session dirs are not databases.
48shopt -s nullglob
49# Hibernating instances are excluded: the activator's embedded Litestream
50# owns their replication (to the prod-instances bucket, under the lease) β€”
51# a second replicator on the same WAL would fight it for checkpointing.
52hibernating=""
53if [ -f /data/registry.db ] && command -v sqlite3 >/dev/null; then
54 hibernating=$(sqlite3 /data/registry.db \
55 "SELECT host FROM routes WHERE hibernate = 1" 2>/dev/null || true)
56fi
57skip_hibernating() {
58 local base; base=$(basename "$1" .db)
59 for h in $hibernating; do [ "$h" = "$base" ] && return 0; done
60 return 1
61}
62dbs=()
63if [ $# -gt 0 ]; then
64 for name in "$@"; do dbs+=("/data/$name"); done
65else
66 for f in /data/*.db; do
67 skip_hibernating "$f" && { echo "skipping $f (hibernating β€” activator owns its replication)"; continue; }
68 dbs+=("$f")
69 done
70fi
71[ ${#dbs[@]} -gt 0 ] || { echo "no databases matched" >&2; exit 1; }
72
73{
74 echo "# generated by deploy/fleet-backup.sh β€” re-run the script, don't edit"
75 # Cadence: this replicator is a BACKUP, not failover (the activator owns
76 # failover for hibernating hosts), so every timer below trades recency for
77 # S3 request volume. Litestream's defaults (sync 1s, L1 compaction 30s, L0
78 # retention LISTs every 15s) burned ~113k Tier-1 requests in 14h across the
79 # fleet with near-zero writes β€” the retention/compaction monitors LIST per
80 # database per tick whether or not anything changed.
81 cat <<'YAML'
82# RPO: at most ~60s of a space's ciphertext is lost if the box dies β€” for a
83# backup of family-chat instances that is plenty; idle databases upload
84# nothing regardless (an idle sync tick makes no S3 calls).
85sync-interval: 60s
86# Compaction ladder, ~10x the 30s/5m/1h defaults with the same geometric
87# shape (restore read-amplification stays bounded). L1's tick is the request
88# driver β€” each level's monitor costs up to two LISTs per database per tick.
89levels:
90 - interval: 5m
91 - interval: 30m
92 - interval: 3h
93# Snapshots stay daily β€” the gentle direction here is LESS often, not the
94# hourly one might reach for: each snapshot uploads a full database image.
95# Retention 72h keeps ~3 daily snapshots so one bad newest snapshot never
96# strands the restore chain. v0.5 enforces snapshot retention on the snapshot
97# tick itself (there is no separate retention-check knob), so retention
98# sweeps are daily too.
99snapshot:
100 interval: 24h
101 retention: 72h
102# The L0 retention monitor is the worst idle offender: every tick LISTs L1
103# (and usually L0) per database even with zero writes, and its 15s default
104# exists for litestream's VFS read replicas, which we don't run. L0 files
105# only become reclaimable after an L1 compaction, so hourly sweeps delete
106# exactly the same files β€” just 240x fewer LISTs. l0-retention is the minimum
107# age before a compacted L0 file may go; 15m costs pennies of storage and
108# keeps recent increments around for point-in-time restores.
109l0-retention: 15m
110l0-retention-check-interval: 1h
111YAML
112 echo "dbs:"
113 for f in "${dbs[@]}"; do
114 [ -f "$f" ] || { echo "warning: $f missing, skipped" >&2; continue; }
115 printf ' - path: %s\n replica:\n type: s3\n bucket: %s\n path: fleet/%s\n region: %s\n' \
116 "$f" "$BUCKET" "$(basename "$f")" "$REGION"
117 done
118} > "$CONF"
119chmod 0644 "$CONF"
120echo "wrote $CONF (${#dbs[@]} databases)"
121
122# 3. The unit. Runs as the lchat user (owns /data), same hardening posture as
123# the other units; litestream is a pure outbound client.
124cat > "$UNIT" <<'EOF'
125[Unit]
126Description=Litestream fleet DB replication (eleven)
127After=network-online.target
128Wants=network-online.target
129
130[Service]
131User=lchat
132Group=lchat
133EnvironmentFile=/data/litestream.env
134ExecStart=/opt/lchat/litestream replicate -config /etc/litestream.yml
135Restart=always
136RestartSec=5
137NoNewPrivileges=true
138ProtectSystem=strict
139ReadWritePaths=/data
140ProtectHome=true
141PrivateTmp=true
142MemoryMax=512M
143
144[Install]
145WantedBy=multi-user.target
146EOF
147
148systemctl daemon-reload
149systemctl enable lchat-litestream
150systemctl restart lchat-litestream
151systemctl --no-pager --lines=0 status lchat-litestream || true
152echo "done β€” verify with: journalctl -u lchat-litestream -n 20"
153