Files
Docker-Backup-Script/docker-backup.sh
T
2026-05-09 14:23:03 +00:00

169 lines
5.4 KiB
Bash

#!/bin/bash
#
# Docker services backup script
# Backs up /opt/<service> directories to Internxt via rclone
#
# Special handling for Netbird: the Management service database lives inside
# a Docker named volume mounted at /var/lib/netbird/ in the container, NOT
# in the bind-mounted /opt/netbird/ directory. We extract it via
# `docker compose cp` per the official Netbird backup docs.
#
set -euo pipefail
# ---- Configuration ----
SERVICES=("netbird" "pocket-id" "caddy" "vaultwarden")
SOURCE_DIR="/opt"
RCLONE_REMOTE="internxt" # name of your configured rclone remote
RCLONE_PATH="vps-backups" # path/folder on the remote
LOCAL_TMP="/tmp/docker-backups" # staging directory for archives
RETENTION_DAYS=30 # delete backups older than this (local + remote)
LOG_FILE="/var/log/docker-backup.log"
# Netbird-specific: which container holds the management DB?
# Newer single-container setup uses "netbird-server"; older multi-container
# setup uses "management". We auto-detect below.
NETBIRD_DIR="${SOURCE_DIR}/netbird"
NETBIRD_DB_PATH_IN_CONTAINER="/var/lib/netbird"
# ---- Setup ----
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
ARCHIVE_NAME="docker-backup_${TIMESTAMP}.tar.gz"
ARCHIVE_PATH="${LOCAL_TMP}/${ARCHIVE_NAME}"
STAGING_DIR="${LOCAL_TMP}/staging_${TIMESTAMP}"
mkdir -p "$LOCAL_TMP"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# Make sure containers come back up and staging is cleaned even if something fails
STOPPED_SERVICES=()
cleanup() {
local exit_code=$?
for service in "${STOPPED_SERVICES[@]}"; do
log "Restarting ${service}..."
(cd "${SOURCE_DIR}/${service}" && docker compose start) || \
log "WARNING: failed to restart ${service}"
done
rm -rf "$STAGING_DIR"
exit $exit_code
}
trap cleanup EXIT INT TERM
# ---- Pre-flight checks ----
log "===== Backup started ====="
if ! command -v rclone &>/dev/null; then
log "ERROR: rclone not found"
exit 1
fi
if ! rclone listremotes | grep -q "^${RCLONE_REMOTE}:$"; then
log "ERROR: rclone remote '${RCLONE_REMOTE}' not configured"
exit 1
fi
# ---- Detect Netbird setup style ----
# Returns the container name for the management service, or empty if not found.
detect_netbird_container() {
if [[ ! -d "$NETBIRD_DIR" ]]; then
echo ""
return
fi
cd "$NETBIRD_DIR"
# Try newer setup first, then older
if docker compose config --services 2>/dev/null | grep -qx "netbird-server"; then
echo "netbird-server"
elif docker compose config --services 2>/dev/null | grep -qx "management"; then
echo "management"
else
echo ""
fi
}
NETBIRD_DB_CONTAINER=$(detect_netbird_container)
if [[ -n "$NETBIRD_DB_CONTAINER" ]]; then
log "Detected Netbird management container: ${NETBIRD_DB_CONTAINER}"
else
log "WARNING: could not detect Netbird management container; DB will not be backed up"
fi
# ---- Stop containers ----
# Netbird must be stopped to get a consistent DB copy. We stop all services
# for SQLite consistency (pocket-id, vaultwarden) too.
for service in "${SERVICES[@]}"; do
service_dir="${SOURCE_DIR}/${service}"
if [[ ! -d "$service_dir" ]]; then
log "WARNING: ${service_dir} does not exist, skipping"
continue
fi
log "Stopping ${service}..."
(cd "$service_dir" && docker compose stop)
STOPPED_SERVICES+=("$service")
done
# ---- Stage data for archiving ----
log "Staging files in ${STAGING_DIR}"
mkdir -p "$STAGING_DIR"
# Copy all bind-mounted /opt/<service> directories
for service in "${SERVICES[@]}"; do
service_dir="${SOURCE_DIR}/${service}"
[[ -d "$service_dir" ]] || continue
log "Staging ${service_dir}"
cp -a "$service_dir" "$STAGING_DIR/"
done
# Extract Netbird's management database from inside the container.
# The container is stopped, but `docker compose cp` works on stopped containers.
if [[ -n "$NETBIRD_DB_CONTAINER" ]]; then
log "Extracting Netbird database from container ${NETBIRD_DB_CONTAINER}"
mkdir -p "${STAGING_DIR}/netbird/_netbird-db"
(
cd "$NETBIRD_DIR"
docker compose cp -a \
"${NETBIRD_DB_CONTAINER}:${NETBIRD_DB_PATH_IN_CONTAINER}" \
"${STAGING_DIR}/netbird/_netbird-db/"
)
fi
# ---- Restart containers (before upload, to minimize downtime) ----
for service in "${STOPPED_SERVICES[@]}"; do
log "Starting ${service}..."
(cd "${SOURCE_DIR}/${service}" && docker compose start)
done
STOPPED_SERVICES=() # clear so cleanup trap doesn't double-start
# ---- Create archive ----
log "Creating archive: ${ARCHIVE_NAME}"
tar -czf "$ARCHIVE_PATH" -C "$STAGING_DIR" .
ARCHIVE_SIZE=$(du -h "$ARCHIVE_PATH" | cut -f1)
log "Archive created: ${ARCHIVE_SIZE}"
# Free staging now that the archive exists
rm -rf "$STAGING_DIR"
# ---- Upload to Internxt ----
log "Uploading to ${RCLONE_REMOTE}:${RCLONE_PATH}/"
rclone copy "$ARCHIVE_PATH" "${RCLONE_REMOTE}:${RCLONE_PATH}/" \
--log-file="$LOG_FILE" \
--log-level INFO \
--stats=30s
log "Upload complete"
# ---- Cleanup old backups ----
log "Cleaning up local backups older than ${RETENTION_DAYS} days"
find "$LOCAL_TMP" -name "docker-backup_*.tar.gz" -mtime +${RETENTION_DAYS} -delete
log "Cleaning up remote backups older than ${RETENTION_DAYS} days"
rclone delete "${RCLONE_REMOTE}:${RCLONE_PATH}/" \
--min-age "${RETENTION_DAYS}d" \
--include "docker-backup_*.tar.gz" \
--log-file="$LOG_FILE" \
--log-level INFO
log "===== Backup complete ====="