This commit is contained in:
2026-01-25 15:06:20 +01:00
parent 62402014c1
commit 996148410d
2 changed files with 181 additions and 0 deletions

17
settings.env Normal file
View File

@@ -0,0 +1,17 @@
# ==========================================
# Portainer Stack Backup Settings
# ==========================================
# Portainer Verbindung
PORTAINER_URL="https://192.168.178.25:9443"
# Portainer API Key
API_KEY="ptr_QX6nhHj2A9wF/XD4wEQ6z/b+udOkIC3Ocao+bC1PP7M="
# Zielverzeichnis für Backups
OUT_DIR="/home/thorsten/Schreibtisch/Backup"
# Archiv erstellen?
# 1 = tar.gz erzeugen
# 0 = nur Ordner behalten
CREATE_ARCHIVE=1

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
set -Eeuo pipefail
############################################
# Portainer Stack Backup (quiet/basic logging)
# Settings file: settings.env (same dir as script)
# Required settings:
# PORTAINER_URL
# API_KEY
# OUT_DIR
# CREATE_ARCHIVE (0/1)
#
# Behavior:
# - Always writes a log file in the run directory while running
# - If CREATE_ARCHIVE=1: creates RUN_DIR.tar.gz and then removes RUN_DIR
# - If CREATE_ARCHIVE=0: keeps RUN_DIR (and the log inside it)
############################################
# ---------- load settings ----------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SETTINGS_FILE="$SCRIPT_DIR/settings.env"
if [[ -f "$SETTINGS_FILE" ]]; then
# shellcheck disable=SC1090
source "$SETTINGS_FILE"
else
echo "ERROR: settings.env not found in: $SCRIPT_DIR" >&2
exit 1
fi
: "${PORTAINER_URL:?PORTAINER_URL not set in settings.env}"
: "${API_KEY:?API_KEY not set in settings.env}"
: "${OUT_DIR:?OUT_DIR not set in settings.env}"
: "${CREATE_ARCHIVE:?CREATE_ARCHIVE not set in settings.env}"
if [[ "$CREATE_ARCHIVE" != "0" && "$CREATE_ARCHIVE" != "1" ]]; then
echo "ERROR: CREATE_ARCHIVE must be 0 or 1 (got: '$CREATE_ARCHIVE')" >&2
exit 1
fi
command -v curl >/dev/null 2>&1 || { echo "ERROR: curl missing"; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq missing"; exit 1; }
# tar is only required when CREATE_ARCHIVE=1
if [[ "$CREATE_ARCHIVE" == "1" ]]; then
command -v tar >/dev/null 2>&1 || { echo "ERROR: tar missing (required because CREATE_ARCHIVE=1)"; exit 1; }
fi
# ---------- output + logging ----------
BASE_OUT="$OUT_DIR"
RUN_NAME="portainer-stack-backup-$(date +%F_%H%M%S)"
RUN_DIR="$BASE_OUT/$RUN_NAME"
STACKS_DIR="$RUN_DIR/stacks"
mkdir -p "$STACKS_DIR"
LOG_FILE="$RUN_DIR/backup.log"
# Mirror ALL output (stdout+stderr) to terminal + logfile
exec > >(tee -a "$LOG_FILE") 2>&1
# ---------- minimal log helpers ----------
say() { echo "$*"; }
ok() { echo "[OK] $*"; }
fail() { echo "[FAIL] $*"; }
# ---------- curl defaults ----------
# -k to tolerate self-signed certs on 9443
CURL_BASE=(curl -skS)
HDR=(-H "X-API-Key: ${API_KEY}")
############################################
# Header
############################################
say "Portainer Stack Backup started"
say "Target: $RUN_DIR"
say
############################################
# Preflight
############################################
if "${CURL_BASE[@]}" "$PORTAINER_URL/api/status" >/dev/null; then
say "Portainer reachable"
else
say "Portainer not reachable"
exit 1
fi
if "${CURL_BASE[@]}" "${HDR[@]}" "$PORTAINER_URL/api/users/me" >/dev/null; then
say "API key valid"
else
say "API key invalid"
exit 1
fi
stacks_json="$("${CURL_BASE[@]}" "${HDR[@]}" "$PORTAINER_URL/api/stacks")"
if ! echo "$stacks_json" | jq -e 'type=="array"' >/dev/null 2>&1; then
say "Unexpected response while fetching stacks (not a JSON array)."
exit 1
fi
stack_count="$(echo "$stacks_json" | jq 'length')"
say "Stacks found: $stack_count"
say
say "Starting backup"
say
############################################
# Export stacks (one line per stack)
############################################
ok_count=0
fail_count=0
while IFS= read -r s; do
id="$(echo "$s" | jq -r '.Id')"
name_raw="$(echo "$s" | jq -r '.Name')"
endpoint="$(echo "$s" | jq -r '.EndpointId')"
# sanitize name for filesystem
name_fs="$(echo "$name_raw" | tr '/ ' '__' | tr -cd '[:alnum:]_.-')"
[[ -n "$name_fs" ]] || name_fs="stack_${id}"
stack_dir="$STACKS_DIR/$name_fs"
mkdir -p "$stack_dir"
# minimal meta (helps mapping)
echo "$s" | jq '.' > "$stack_dir/meta.json"
# fetch stack file and extract StackFileContent
file_json="$("${CURL_BASE[@]}" "${HDR[@]}" \
"$PORTAINER_URL/api/stacks/$id/file?endpointId=$endpoint" 2>/dev/null || true)"
content="$(echo "$file_json" | jq -r '.StackFileContent // empty' 2>/dev/null || true)"
if [[ -n "$content" && "$content" != "null" ]]; then
printf "%s\n" "$content" > "$stack_dir/docker-compose.yml"
ok "$name_raw"
ok_count=$((ok_count+1))
else
fail "$name_raw"
fail_count=$((fail_count+1))
# keep raw response for troubleshooting (quiet, but useful)
printf "%s\n" "$file_json" > "$stack_dir/_file_response.json" 2>/dev/null || true
fi
done < <(echo "$stacks_json" | jq -c '.[]')
say
say "Backup finished: OK=$ok_count FAIL=$fail_count"
############################################
# Optional archive + cleanup
############################################
if [[ "$CREATE_ARCHIVE" == "1" ]]; then
tarball="${RUN_DIR}.tar.gz"
tar -C "$RUN_DIR" -czf "$tarball" .
say "Archive created: $(basename "$tarball")"
# Cleanup: remove the run directory after successful archive creation
rm -rf "$RUN_DIR"
say "Cleanup done: removed $RUN_DIR"
else
say "Keeping directory: $RUN_DIR"
say "Log: $LOG_FILE"
fi
say "Done."