From 15b67ab1eeaa1a72ae52bf830a84bfae1e4982a5 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Sun, 7 Jun 2026 19:58:07 +0200 Subject: [PATCH 1/2] Mein Versuch zu fixen --- .gitattributes | 2 + README.md | 41 +++++ docker-backup-sftp-uploader.sh | 277 ++++++++++++++++++++++++++------- docs/README.md | 70 +++++++++ 4 files changed, 336 insertions(+), 54 deletions(-) create mode 100644 .gitattributes create mode 100644 README.md create mode 100644 docs/README.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e535307 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +*.md text eol=lf diff --git a/README.md b/README.md new file mode 100644 index 0000000..3653d1e --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Docker Backup SFTP Uploader + +Dieses Repository enthält ein Bash-Script, das ein Docker-Compose-Projekt kurz stoppt, das Projektverzeichnis als `tar.gz` archiviert, das Archiv per SFTP hochlädt und optional eine Status-Mail verschickt. + +## Nutzung + +Voraussetzungen auf dem Server: + +- `docker` mit Compose-Plugin +- `tar` und `gzip` +- `sshpass` und `sftp` +- optional `mail` für Status-Mails + +```bash +chmod +x docker-backup-sftp-uploader.sh +./docker-backup-sftp-uploader.sh +``` + +Die wichtigsten Einstellungen können direkt im Script angepasst oder als Umgebungsvariablen gesetzt werden: + +```bash +export SFTP_HOST="backup.example.com" +export SFTP_PORT="22" +export SFTP_USER="backup-user" +export SFTP_PASS="secret" +export SFTP_REMOTE_DIR="/uploads" +export MAIL_TO="admin@example.com" +export MAIL_FROM="backup@example.com" +export BACKUP_DIR="/srv/backups/docker-backup-sftp-uploader" +./docker-backup-sftp-uploader.sh +``` + +## Wichtige Hinweise + +- Archive und Logs werden nicht mehr in `/tmp` geschrieben. Standard ist ein Backup-Ordner neben dem Projektverzeichnis. +- Vor dem Stoppen von Docker prüft das Script, ob am Backup-Ziel genug Speicher frei ist. +- Bei Fehlern versucht das Script, Docker wieder zu starten. +- Der Upload erfolgt zuerst als `.part`-Datei und wird nach erfolgreichem Transfer auf den finalen Namen umbenannt. +- Mailversand ist nur aktiv, wenn `MAIL_TO` gesetzt ist und das System ein funktionierendes `mail`-Kommando besitzt. + +Weitere Details stehen in [docs/README.md](docs/README.md). diff --git a/docker-backup-sftp-uploader.sh b/docker-backup-sftp-uploader.sh index b66b8b2..2249b7d 100644 --- a/docker-backup-sftp-uploader.sh +++ b/docker-backup-sftp-uploader.sh @@ -1,31 +1,44 @@ -#!/bin/bash +#!/usr/bin/env bash -set -o pipefail +set -Eeuo pipefail ######################################## # Konfiguration ######################################## -SFTP_HOST="sftp.example.com" -SFTP_PORT="22" -SFTP_USER="user" -SFTP_PASS="Tpassw0rd" -SFTP_REMOTE_DIR="/uploads" +SFTP_HOST="${SFTP_HOST:-sftp.example.com}" +SFTP_PORT="${SFTP_PORT:-22}" +SFTP_USER="${SFTP_USER:-user}" +SFTP_PASS="${SFTP_PASS:-Tpassw0rd}" +SFTP_REMOTE_DIR="${SFTP_REMOTE_DIR:-/uploads}" -MAIL_TO="" -MAIL_FROM="" +MAIL_TO="${MAIL_TO:-}" +MAIL_FROM="${MAIL_FROM:-}" + +# Nicht /tmp verwenden: viele Systeme mounten /tmp klein oder als tmpfs. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIR_NAME="$(basename "$SCRIPT_DIR")" +BACKUP_DIR="${BACKUP_DIR:-$(dirname "$SCRIPT_DIR")/${DIR_NAME}_backups}" +LOG_DIR="${LOG_DIR:-${BACKUP_DIR}/logs}" +KEEP_LOCAL_BACKUP="${KEEP_LOCAL_BACKUP:-false}" +MIN_FREE_MB="${MIN_FREE_MB:-1024}" ######################################## # Variablen ######################################## -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -DIR_NAME="$(basename "$SCRIPT_DIR")" - -DATE=$(date +"%Y-%m-%d_%H-%M-%S") +DATE="$(date +"%Y-%m-%d_%H-%M-%S")" BACKUP_FILE="${DIR_NAME}_${DATE}.tar.gz" -LOGFILE="/tmp/${DIR_NAME}_backup.log" +mkdir -p "$BACKUP_DIR" "$LOG_DIR" +BACKUP_DIR="$(cd "$BACKUP_DIR" && pwd)" +LOG_DIR="$(cd "$LOG_DIR" && pwd)" + +BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}" +LOGFILE="${LOG_DIR}/${DIR_NAME}_backup_${DATE}.log" + +DOCKER_STOPPED=false +SUCCESS=false ######################################## # Logging @@ -34,28 +47,92 @@ LOGFILE="/tmp/${DIR_NAME}_backup.log" exec > >(tee -a "$LOGFILE") exec 2>&1 -echo "=========================================" -echo "Backup gestartet: $(date)" -echo "Projekt: $DIR_NAME" -echo "=========================================" +log() { + echo "[$(date +"%Y-%m-%d %H:%M:%S")] $*" +} + +log "=========================================" +log "Backup gestartet" +log "Projekt: $DIR_NAME" +log "Backup-Datei: $BACKUP_PATH" +log "Log-Datei: $LOGFILE" +log "=========================================" ######################################## -# Fehlerbehandlung +# Hilfsfunktionen ######################################## +require_command() { + local command_name="$1" + + if ! command -v "$command_name" >/dev/null 2>&1; then + log "FEHLER: Benötigtes Kommando fehlt: $command_name" + return 1 + fi +} + +send_mail() { + local subject="$1" + local body="$2" + + if [[ -z "$MAIL_TO" ]]; then + log "Hinweis: MAIL_TO ist leer, Mailversand wird übersprungen." + return 0 + fi + + if ! command -v mail >/dev/null 2>&1; then + log "WARNUNG: Kommando 'mail' wurde nicht gefunden, Mailversand nicht möglich." + return 0 + fi + + if [[ -n "$MAIL_FROM" ]]; then + if printf '%s\n' "$body" | mail -r "$MAIL_FROM" -s "$subject" "$MAIL_TO"; then + return 0 + fi + + log "WARNUNG: Mailversand mit MAIL_FROM fehlgeschlagen, versuche ohne Absenderoption." + fi + + if ! printf '%s\n' "$body" | mail -s "$subject" "$MAIL_TO"; then + log "WARNUNG: Mailversand fehlgeschlagen." + fi +} + send_error_mail() { + local exit_code="$1" + local body - SUBJECT="❌ Backup FEHLGESCHLAGEN - ${DIR_NAME}" + body="$(cat </dev/null || true) +EOF +)" + + send_mail "Backup FEHLGESCHLAGEN - ${DIR_NAME}" "$body" } send_success_mail() { + local size + local body - SIZE=$(du -h "/tmp/${BACKUP_FILE}" | awk '{print $1}') - - mail -s "✅ Backup erfolgreich - ${DIR_NAME} (${SIZE})" "$MAIL_TO" << EOF + size="$(du -h "$BACKUP_PATH" | awk '{print $1}')" + body="$(cat <&- -exec 2>&- -exit 0 +SUCCESS=true +log "Backup beendet: $(date)" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5b30e14 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,70 @@ +# Dokumentation + +## Ablauf + +1. Das Script ermittelt das Projektverzeichnis. +2. Es legt Backup- und Log-Verzeichnis an. +3. Es prüft benötigte Kommandos und freien Speicher. +4. Es stoppt Docker mit `docker compose down`. +5. Es erstellt ein `tar.gz`-Archiv des Projektordners. +6. Es startet Docker mit `docker compose up -d`. +7. Es lädt das Archiv per SFTP hoch. +8. Es verschickt bei Erfolg oder Fehler eine Mail, sofern Mailversand konfiguriert ist. +9. Es löscht das lokale Archiv, außer `KEEP_LOCAL_BACKUP=true` ist gesetzt. + +## Konfiguration + +| Variable | Standard | Beschreibung | +| --- | --- | --- | +| `SFTP_HOST` | `sftp.example.com` | Hostname des SFTP-Servers | +| `SFTP_PORT` | `22` | Port des SFTP-Servers | +| `SFTP_USER` | `user` | SFTP-Benutzer | +| `SFTP_PASS` | `Tpassw0rd` | SFTP-Passwort | +| `SFTP_REMOTE_DIR` | `/uploads` | Zielverzeichnis auf dem SFTP-Server | +| `MAIL_TO` | leer | Empfängeradresse; leer deaktiviert Mailversand | +| `MAIL_FROM` | leer | Optionaler Absender für `mail -r` | +| `BACKUP_DIR` | Ordner neben dem Projekt | Lokales Ziel für Archive | +| `LOG_DIR` | `${BACKUP_DIR}/logs` | Lokales Ziel für Logs | +| `KEEP_LOCAL_BACKUP` | `false` | Lokales Archiv nach Lauf behalten | +| `MIN_FREE_MB` | `1024` | Zusätzlicher freier Speicher, der neben der Quellgröße vorhanden sein muss | + +## Voraussetzungen + +Auf dem Server müssen diese Kommandos verfügbar sein: + +- `docker` +- `tar` +- `gzip` +- `sshpass` +- `sftp` +- optional `mail` + +## Mailversand + +Das Script nutzt das lokale Kommando `mail`. Auf Debian/Ubuntu kann dafür zum Beispiel `mailutils`, `bsd-mailx` oder ein passendes Mail-Relay wie `msmtp` eingerichtet sein. + +Wenn keine Mail ankommt: + +- prüfen, ob `MAIL_TO` gesetzt ist +- prüfen, ob `mail` installiert ist: `command -v mail` +- einen manuellen Test ausführen: `echo test | mail -s "Test" empfaenger@example.com` +- lokale Mail-Logs prüfen, zum Beispiel `/var/log/mail.log` +- falls `MAIL_FROM` Probleme macht, testweise leer lassen + +## Speicherplatz und große Dateien + +Das Archiv wird bewusst nicht in `/tmp` erstellt, weil `/tmp` auf vielen Systemen als kleiner tmpfs-Mount eingerichtet ist. Für große Projekte sollte `BACKUP_DIR` auf ein Dateisystem mit ausreichend freiem Speicher zeigen, zum Beispiel: + +```bash +BACKUP_DIR=/srv/backups/mein-projekt ./docker-backup-sftp-uploader.sh +``` + +Das Script prüft vor dem Docker-Stopp grob, ob genug Platz vorhanden ist. Der Mindestbedarf entspricht ungefähr der Quellgröße plus 10 Prozent Reserve plus `MIN_FREE_MB`. + +Der SFTP-Upload schreibt zuerst `${BACKUP_FILE}.part` und benennt die Datei nach erfolgreichem Upload um. Dadurch bleibt auf dem Zielserver erkennbar, ob ein Upload vollständig abgeschlossen wurde. + +## Cron-Beispiel + +```cron +0 3 * * * SFTP_HOST=backup.example.com SFTP_USER=backup SFTP_PASS='secret' SFTP_REMOTE_DIR=/uploads MAIL_TO=admin@example.com BACKUP_DIR=/srv/backups/mein-projekt /pfad/zum/docker-backup-sftp-uploader.sh +``` -- 2.52.0 From c45bf6473882504ddb4029e57801cc28508811ae Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Sun, 7 Jun 2026 20:02:33 +0200 Subject: [PATCH 2/2] Variablen ausgelagert, weil sauberer --- .gitattributes | 2 ++ README.md | 32 ++++++++++++++------ docker-backup-sftp-uploader.conf | 24 +++++++++++++++ docker-backup-sftp-uploader.sh | 51 ++++++++++++++++++++++---------- docs/README.md | 37 +++++++++++++---------- 5 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 docker-backup-sftp-uploader.conf diff --git a/.gitattributes b/.gitattributes index e535307..99f83d2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ +.gitattributes text eol=lf *.sh text eol=lf *.md text eol=lf +*.conf text eol=lf diff --git a/README.md b/README.md index 3653d1e..03c1206 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,34 @@ chmod +x docker-backup-sftp-uploader.sh ./docker-backup-sftp-uploader.sh ``` -Die wichtigsten Einstellungen können direkt im Script angepasst oder als Umgebungsvariablen gesetzt werden: +Die Konfiguration liegt in `docker-backup-sftp-uploader.conf` neben dem Script: + +```bash +SFTP_HOST="backup.example.com" +SFTP_PORT="22" +SFTP_USER="backup-user" +SFTP_PASS="secret" +SFTP_REMOTE_DIR="/uploads" +MAIL_TO="admin@example.com" +MAIL_FROM="backup@example.com" +BACKUP_DIR="/srv/backups/docker-backup-sftp-uploader" +LOG_DIR="/srv/backups/docker-backup-sftp-uploader/logs" +KEEP_LOCAL_BACKUP="false" +MIN_FREE_MB="1024" +``` + +Danach reicht: ```bash -export SFTP_HOST="backup.example.com" -export SFTP_PORT="22" -export SFTP_USER="backup-user" -export SFTP_PASS="secret" -export SFTP_REMOTE_DIR="/uploads" -export MAIL_TO="admin@example.com" -export MAIL_FROM="backup@example.com" -export BACKUP_DIR="/srv/backups/docker-backup-sftp-uploader" ./docker-backup-sftp-uploader.sh ``` +Falls eine andere Konfigurationsdatei genutzt werden soll: + +```bash +CONFIG_FILE="/etc/docker-backup-sftp-uploader.conf" ./docker-backup-sftp-uploader.sh +``` + ## Wichtige Hinweise - Archive und Logs werden nicht mehr in `/tmp` geschrieben. Standard ist ein Backup-Ordner neben dem Projektverzeichnis. diff --git a/docker-backup-sftp-uploader.conf b/docker-backup-sftp-uploader.conf new file mode 100644 index 0000000..c86a742 --- /dev/null +++ b/docker-backup-sftp-uploader.conf @@ -0,0 +1,24 @@ +######################################## +# Docker Backup SFTP Uploader +# Konfiguration +######################################## + +SFTP_HOST="sftp.example.com" +SFTP_PORT="22" +SFTP_USER="user" +SFTP_PASS="Tpassw0rd" +SFTP_REMOTE_DIR="/uploads" + +MAIL_TO="" +MAIL_FROM="" + +# Nicht /tmp verwenden: viele Systeme mounten /tmp klein oder als tmpfs. +# Der Standard legt den Backup-Ordner neben dem Projektverzeichnis ab. +BACKUP_DIR="" +LOG_DIR="" + +# true = lokales Archiv nach erfolgreichem Upload behalten +KEEP_LOCAL_BACKUP="false" + +# Zusätzlicher freier Speicher in MB, der neben der Quellgröße vorhanden sein muss. +MIN_FREE_MB="1024" diff --git a/docker-backup-sftp-uploader.sh b/docker-backup-sftp-uploader.sh index 2249b7d..fae2af5 100644 --- a/docker-backup-sftp-uploader.sh +++ b/docker-backup-sftp-uploader.sh @@ -3,25 +3,32 @@ set -Eeuo pipefail ######################################## -# Konfiguration +# Script- und Konfigurationspfade ######################################## -SFTP_HOST="${SFTP_HOST:-sftp.example.com}" -SFTP_PORT="${SFTP_PORT:-22}" -SFTP_USER="${SFTP_USER:-user}" -SFTP_PASS="${SFTP_PASS:-Tpassw0rd}" -SFTP_REMOTE_DIR="${SFTP_REMOTE_DIR:-/uploads}" - -MAIL_TO="${MAIL_TO:-}" -MAIL_FROM="${MAIL_FROM:-}" - -# Nicht /tmp verwenden: viele Systeme mounten /tmp klein oder als tmpfs. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIR_NAME="$(basename "$SCRIPT_DIR")" -BACKUP_DIR="${BACKUP_DIR:-$(dirname "$SCRIPT_DIR")/${DIR_NAME}_backups}" -LOG_DIR="${LOG_DIR:-${BACKUP_DIR}/logs}" -KEEP_LOCAL_BACKUP="${KEEP_LOCAL_BACKUP:-false}" -MIN_FREE_MB="${MIN_FREE_MB:-1024}" +CONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/docker-backup-sftp-uploader.conf}" + +if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/dev/null + source "$CONFIG_FILE" +else + echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2 + exit 1 +fi + +######################################## +# Optionale Fallbacks +######################################## + +: "${SFTP_PORT:=22}" +: "${MAIL_TO:=}" +: "${MAIL_FROM:=}" +: "${BACKUP_DIR:=$(dirname "$SCRIPT_DIR")/${DIR_NAME}_backups}" +: "${LOG_DIR:=${BACKUP_DIR}/logs}" +: "${KEEP_LOCAL_BACKUP:=false}" +: "${MIN_FREE_MB:=1024}" ######################################## # Variablen @@ -54,6 +61,7 @@ log() { log "=========================================" log "Backup gestartet" log "Projekt: $DIR_NAME" +log "Konfiguration: $CONFIG_FILE" log "Backup-Datei: $BACKUP_PATH" log "Log-Datei: $LOGFILE" log "=========================================" @@ -71,6 +79,15 @@ require_command() { fi } +require_config_value() { + local variable_name="$1" + + if [[ -z "${!variable_name:-}" ]]; then + log "FEHLER: Pflichtwert fehlt in der Konfiguration: $variable_name" + return 1 + fi +} + send_mail() { local subject="$1" local body="$2" @@ -255,6 +272,10 @@ require_command tar require_command gzip require_command sshpass require_command sftp +require_config_value SFTP_HOST +require_config_value SFTP_USER +require_config_value SFTP_PASS +require_config_value SFTP_REMOTE_DIR check_backup_dir_location check_free_space diff --git a/docs/README.md b/docs/README.md index 5b30e14..a163682 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,27 +3,34 @@ ## Ablauf 1. Das Script ermittelt das Projektverzeichnis. -2. Es legt Backup- und Log-Verzeichnis an. -3. Es prüft benötigte Kommandos und freien Speicher. -4. Es stoppt Docker mit `docker compose down`. -5. Es erstellt ein `tar.gz`-Archiv des Projektordners. -6. Es startet Docker mit `docker compose up -d`. -7. Es lädt das Archiv per SFTP hoch. -8. Es verschickt bei Erfolg oder Fehler eine Mail, sofern Mailversand konfiguriert ist. -9. Es löscht das lokale Archiv, außer `KEEP_LOCAL_BACKUP=true` ist gesetzt. +2. Es lädt `docker-backup-sftp-uploader.conf` aus demselben Verzeichnis. +3. Es legt Backup- und Log-Verzeichnis an. +4. Es prüft benötigte Kommandos, Pflichtwerte und freien Speicher. +5. Es stoppt Docker mit `docker compose down`. +6. Es erstellt ein `tar.gz`-Archiv des Projektordners. +7. Es startet Docker mit `docker compose up -d`. +8. Es lädt das Archiv per SFTP hoch. +9. Es verschickt bei Erfolg oder Fehler eine Mail, sofern Mailversand konfiguriert ist. +10. Es löscht das lokale Archiv, außer `KEEP_LOCAL_BACKUP=true` ist gesetzt. ## Konfiguration +Die Konfiguration liegt standardmäßig in `docker-backup-sftp-uploader.conf` neben dem Script. Alternativ kann ein anderer Pfad über `CONFIG_FILE` angegeben werden: + +```bash +CONFIG_FILE="/etc/docker-backup-sftp-uploader.conf" ./docker-backup-sftp-uploader.sh +``` + | Variable | Standard | Beschreibung | | --- | --- | --- | -| `SFTP_HOST` | `sftp.example.com` | Hostname des SFTP-Servers | +| `SFTP_HOST` | Pflichtwert | Hostname des SFTP-Servers | | `SFTP_PORT` | `22` | Port des SFTP-Servers | -| `SFTP_USER` | `user` | SFTP-Benutzer | -| `SFTP_PASS` | `Tpassw0rd` | SFTP-Passwort | -| `SFTP_REMOTE_DIR` | `/uploads` | Zielverzeichnis auf dem SFTP-Server | +| `SFTP_USER` | Pflichtwert | SFTP-Benutzer | +| `SFTP_PASS` | Pflichtwert | SFTP-Passwort | +| `SFTP_REMOTE_DIR` | Pflichtwert | Zielverzeichnis auf dem SFTP-Server | | `MAIL_TO` | leer | Empfängeradresse; leer deaktiviert Mailversand | | `MAIL_FROM` | leer | Optionaler Absender für `mail -r` | -| `BACKUP_DIR` | Ordner neben dem Projekt | Lokales Ziel für Archive | +| `BACKUP_DIR` | leer = Ordner neben dem Projekt | Lokales Ziel für Archive | | `LOG_DIR` | `${BACKUP_DIR}/logs` | Lokales Ziel für Logs | | `KEEP_LOCAL_BACKUP` | `false` | Lokales Archiv nach Lauf behalten | | `MIN_FREE_MB` | `1024` | Zusätzlicher freier Speicher, der neben der Quellgröße vorhanden sein muss | @@ -56,7 +63,7 @@ Wenn keine Mail ankommt: Das Archiv wird bewusst nicht in `/tmp` erstellt, weil `/tmp` auf vielen Systemen als kleiner tmpfs-Mount eingerichtet ist. Für große Projekte sollte `BACKUP_DIR` auf ein Dateisystem mit ausreichend freiem Speicher zeigen, zum Beispiel: ```bash -BACKUP_DIR=/srv/backups/mein-projekt ./docker-backup-sftp-uploader.sh +BACKUP_DIR="/srv/backups/mein-projekt" ``` Das Script prüft vor dem Docker-Stopp grob, ob genug Platz vorhanden ist. Der Mindestbedarf entspricht ungefähr der Quellgröße plus 10 Prozent Reserve plus `MIN_FREE_MB`. @@ -66,5 +73,5 @@ Der SFTP-Upload schreibt zuerst `${BACKUP_FILE}.part` und benennt die Datei nach ## Cron-Beispiel ```cron -0 3 * * * SFTP_HOST=backup.example.com SFTP_USER=backup SFTP_PASS='secret' SFTP_REMOTE_DIR=/uploads MAIL_TO=admin@example.com BACKUP_DIR=/srv/backups/mein-projekt /pfad/zum/docker-backup-sftp-uploader.sh +0 3 * * * /pfad/zum/docker-backup-sftp-uploader.sh ``` -- 2.52.0