Mein Versuch zu fixen

This commit is contained in:
Patrick Asmus
2026-06-07 19:58:07 +02:00
parent aa817f5dff
commit 15b67ab1ee
4 changed files with 336 additions and 54 deletions
+2
View File
@@ -0,0 +1,2 @@
*.sh text eol=lf
*.md text eol=lf
+41
View File
@@ -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).
+223 -54
View File
@@ -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 <<EOF
Backup fehlgeschlagen.
mail -s "$SUBJECT" "$MAIL_TO" < "$LOGFILE"
Projekt:
${DIR_NAME}
Datum:
$(date)
Exit-Code:
${exit_code}
Log-Datei:
${LOGFILE}
Log:
$(cat "$LOGFILE" 2>/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 <<EOF
Backup erfolgreich abgeschlossen.
Projekt:
@@ -67,38 +144,137 @@ $(date)
Backup:
${BACKUP_FILE}
Größe:
${SIZE}
Groesse:
${size}
Remote-Ziel:
${SFTP_HOST}:${SFTP_REMOTE_DIR}/${BACKUP_FILE}
Log:
$(cat "$LOGFILE")
EOF
)"
send_mail "Backup erfolgreich - ${DIR_NAME} (${size})" "$body"
}
trap 'send_error_mail; exit 1' ERR
cleanup_local_backup() {
if [[ "$KEEP_LOCAL_BACKUP" == "true" ]]; then
log "Lokales Backup bleibt erhalten: $BACKUP_PATH"
return 0
fi
if [[ -f "$BACKUP_PATH" ]]; then
log "Entferne lokales Backup: $BACKUP_PATH"
rm -f "$BACKUP_PATH"
fi
}
start_docker_if_needed() {
if [[ "$DOCKER_STOPPED" == "true" ]]; then
log "Starte Docker..."
docker compose up -d
DOCKER_STOPPED=false
fi
}
on_error() {
local exit_code="$?"
log "FEHLER: Backup fehlgeschlagen (Exit-Code: ${exit_code})."
if [[ "$DOCKER_STOPPED" == "true" ]]; then
log "Versuche Docker nach Fehler wieder zu starten..."
if docker compose up -d; then
DOCKER_STOPPED=false
log "Docker wurde nach Fehler wieder gestartet."
else
log "WARNUNG: Docker konnte nach Fehler nicht gestartet werden."
fi
fi
send_error_mail "$exit_code"
cleanup_local_backup
exit "$exit_code"
}
on_exit() {
local exit_code="$?"
if [[ "$SUCCESS" == "true" ]]; then
cleanup_local_backup
fi
exit "$exit_code"
}
check_free_space() {
local source_kb
local required_kb
local free_kb
local min_free_kb
source_kb="$(du -sk "$SCRIPT_DIR" | awk '{print $1}')"
min_free_kb=$((MIN_FREE_MB * 1024))
required_kb=$((source_kb + (source_kb / 10) + min_free_kb))
free_kb="$(df -Pk "$BACKUP_DIR" | awk 'NR == 2 {print $4}')"
log "Quellgroesse: $((source_kb / 1024)) MB"
log "Freier Speicher am Backup-Ziel: $((free_kb / 1024)) MB"
log "Geschaetzter Mindestbedarf: $((required_kb / 1024)) MB"
if (( free_kb < required_kb )); then
log "FEHLER: Zu wenig freier Speicher in $BACKUP_DIR."
return 1
fi
}
check_backup_dir_location() {
case "$BACKUP_DIR" in
"$SCRIPT_DIR"|"$SCRIPT_DIR"/*)
log "FEHLER: BACKUP_DIR darf nicht innerhalb des Projektverzeichnisses liegen."
log "Projektverzeichnis: $SCRIPT_DIR"
log "Backup-Verzeichnis: $BACKUP_DIR"
return 1
;;
esac
}
trap on_error ERR
trap on_exit EXIT
########################################
# Vorbedingungen
########################################
cd "$SCRIPT_DIR"
require_command docker
require_command tar
require_command gzip
require_command sshpass
require_command sftp
check_backup_dir_location
check_free_space
########################################
# Docker stoppen
########################################
cd "$SCRIPT_DIR"
echo "Stoppe Docker..."
log "Stoppe Docker..."
docker compose down
DOCKER_STOPPED=true
########################################
# Archiv erstellen
########################################
echo "Erstelle Backup..."
log "Erstelle Backup..."
tar \
--same-owner \
--preserve-permissions \
-czpf "/tmp/${BACKUP_FILE}" \
-czpf "$BACKUP_PATH" \
-C "$(dirname "$SCRIPT_DIR")" \
"$DIR_NAME"
@@ -106,27 +282,28 @@ tar \
# Docker starten
########################################
echo "Starte Docker..."
docker compose up -d
start_docker_if_needed
########################################
# Upload
########################################
echo "Upload nach ${SFTP_HOST}..."
log "Upload nach ${SFTP_HOST}:${SFTP_REMOTE_DIR}..."
sshpass -p "$SFTP_PASS" sftp \
-oPort="$SFTP_PORT" \
-P "$SFTP_PORT" \
-oBatchMode=no \
-oStrictHostKeyChecking=no \
-oServerAliveInterval=30 \
-oServerAliveCountMax=10 \
"${SFTP_USER}@${SFTP_HOST}" << EOF
cd ${SFTP_REMOTE_DIR}
put /tmp/${BACKUP_FILE}
cd "${SFTP_REMOTE_DIR}"
put "${BACKUP_PATH}" "${BACKUP_FILE}.part"
rename "${BACKUP_FILE}.part" "${BACKUP_FILE}"
bye
EOF
echo "Upload erfolgreich."
log "Upload erfolgreich."
########################################
# Erfolgsmail
@@ -134,13 +311,5 @@ echo "Upload erfolgreich."
send_success_mail
########################################
# Aufräumen
########################################
rm -f "/tmp/${BACKUP_FILE}"
echo "Backup beendet: $(date)"
exec 1>&-
exec 2>&-
exit 0
SUCCESS=true
log "Backup beendet: $(date)"
+70
View File
@@ -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
```