169 lines
5.4 KiB
Bash
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 =====" |