#!/usr/bin/env bash # ~$ selfhostedops - bring the whole stack back on an empty Debian box (episode 7) # # Usage on a fresh Debian 12/13 server, as root: # mkdir -p /opt/selfhostedops-stack/backup # (from the vault) # curl -fsSLO https:///selfhostedops-stack/raw/branch/main/backup/restore.sh # bash restore.sh install # docker, compose plugin, restic # bash restore.sh restore # docker volumes + restic restore latest --target / # bash restore.sh up # docker compose up -d for the stack and NetBird # bash restore.sh all # the three in a row # # Each phase prints its own wall-clock time. Nothing here touches DNS: point # the records at the new box when the services answer (test first with # curl --resolve wiki.example.com:443: https://wiki.example.com/). set -Eeuo pipefail STACK_DIR=${STACK_DIR:-/opt/selfhostedops-stack} NETBIRD_DIR=${NETBIRD_DIR:-/opt/netbird} ENV_FILE="$STACK_DIR/backup/.restic-env" SNAPSHOT=${SNAPSHOT:-latest} log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; } phase_start() { PHASE_T0=$(date +%s%N); log "=== phase $1 ==="; } phase_end() { local ms=$(( ($(date +%s%N) - PHASE_T0) / 1000000 )) log "=== phase $1 done in $((ms / 1000)).$(printf '%03d' $((ms % 1000)))s ===" } load_env() { [ -f "$ENV_FILE" ] || { echo "missing $ENV_FILE (copy it from your vault first)"; exit 1; } set -a # shellcheck disable=SC1090 . "$ENV_FILE" set +a } phase_install() { phase_start install export DEBIAN_FRONTEND=noninteractive apt-get update -q apt-get install -y -q ca-certificates curl gnupg restic # (coreutils install, not a function of this script) /usr/bin/install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc # shellcheck disable=SC1091 . /etc/os-release echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $VERSION_CODENAME stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -q apt-get install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker docker --version; docker compose version; restic version phase_end install } phase_restore() { load_env phase_start restore log "snapshots in the repository:" restic snapshots --compact # Named volumes must exist in docker's registry before the files land in # /var/lib/docker/volumes//_data, otherwise compose creates fresh # empty ones on top. The names come straight out of the snapshot. log "creating docker volumes" restic ls "$SNAPSHOT" /var/lib/docker/volumes \ | grep '^/var/lib/docker/volumes/[^/]*_' \ | while read -r dir; do docker volume create "$(basename "$dir")"; done log "restic restore $SNAPSHOT --target /" restic restore "$SNAPSHOT" --target / log "restored:" du -sh "$STACK_DIR" "$NETBIRD_DIR" /var/lib/docker/volumes/*_*/_data phase_end restore } phase_up() { phase_start up cd "$STACK_DIR" docker compose up -d # NetBird joins the stack's proxy network, so it comes second. if [ -d "$NETBIRD_DIR" ]; then cd "$NETBIRD_DIR" docker compose up -d fi docker ps --format 'table {{.Names}}\t{{.Status}}' phase_end up } case "${1:-}" in install) phase_install ;; restore) phase_restore ;; up) phase_up ;; all) phase_install; phase_restore; phase_up ;; *) echo "usage: $0 install|restore|up|all"; exit 1 ;; esac