Initial commit

This commit is contained in:
Thorsten committed 2026-05-12 15:50:50 +02:00
commit ccaff116ff
3 files changed
+258

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# Portainer Stack Backup Script
Dieses Skript sichert **alle Docker-Compose-Stacks aus Portainer** über die Portainer-API.
Es exportiert **immer die aktuell aktive Compose-Version** jedes Stacks und speichert sie
übersichtlich nach Stack-Namen.
Optional kann das Ergebnis zusätzlich als `tar.gz` archiviert werden.
---
## Features
- Backup **aller Portainer Stacks**
- Export der **aktuellen** `docker-compose.yml` je Stack
- Optionales `tar.gz`-Archiv
- Geeignet für:
- manuelle Nutzung
- Cronjobs
- Server / Docker-Hosts
---
## Voraussetzungen
Auf dem System müssen folgende Tools vorhanden sein:
| Tool | Benötigt |
|----|----|
| `bash` | ja |
| `curl` | ja |
| `jq` | ja |
| `tar` | **nur wenn** `CREATE_ARCHIVE=1` |
---
## Installation
Skript ausführbar machen:
```bash
chmod +x shell_portainer_stack_backup.sh
```
---
## Konfiguration (`settings.env`)
Die Datei `settings.env` muss **im selben Verzeichnis** wie das Skript liegen.
### Beispiel:
```bash
# Portainer connection
PORTAINER_URL="https://192.168.178.25:9443"
# Portainer API key
API_KEY="ptr_xxxxxxxxxxxxxxxxxxxxxx"
# Base output directory
OUT_DIR="/home/thorsten/Portainer_Backup"
# Create archive?
# 1 = create tar.gz and remove directory afterwards
# 0 = keep directory only
CREATE_ARCHIVE=1
```
## Hinweise & Einschränkungen
- Das Skript sichert **nur die Stack-Definitionen**, **keine Volumes / Daten**
- Git-basierte Stacks werden als **aktuelle, aufgelöste Compose-Datei** gesichert
- Der API-Key benötigt mindestens:
- Zugriff auf Stacks
- Zugriff auf das jeweilige Endpoint
---
+17
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
+164
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."