Station 01 · Einrichten/Phase 5
Nachsehen, ob alles gesund ist
Ein Befehl, eine Antwort. Fünfzehn bis zwanzig Punkte, jedes Problem mit dem nächsten Schritt daneben.
auf dem Server
sudo bash automatisierung/n8n-check.sh --domain ablauf.example.de
Die Ausgabe hat vier Abschnitte und am Ende eine Bilanz. Der Rückgabewert ist 0, wenn nichts kaputt ist — damit taugt der Befehl auch für die Überwachung.
so sieht eine gesunde Anlage aus
Serverprüfung — n8n — ablauf.example.de — 03.09.2026 09:14 Betrieb ✓ Installation /opt/n8n ✓ Container n8n läuft ✓ Container db läuft ✓ Dauerneustarts 0 ✓ n8n-Fassung 1.114.0 ✓ Port nach außen keiner — nur über Traefik ✓ Datenbank nach außen nicht erreichbar Schlüssel und Sicherung ✓ Umgebungsdatei nur für root lesbar ✓ Verschlüsselungsschlüssel gesetzt (64 Zeichen) ✓ Notfallumschlag 2 Tage alt ✓ Fingerabdruck Umschlag passt zum Schlüssel ✓ Sicherung 0 Tage alt Läufe ✓ Aufräumen der Läufe an (336 Stunden) ✓ Letzter Lauf vor 1 Stunden ✓ Abgebrochene Läufe (24 h) 0 Erreichbarkeit ✓ Oberfläche antwortet (200) ✓ Zertifikat gültig, noch 74 Tage Alles in Ordnung — 17 Punkte geprüft
Der wichtigste Punkt ist „Letzter Lauf". Er ist der einzige, der den leisen Stillstand findet: Alles läuft, alles ist grün — nur kommt seit zwei Tagen nichts mehr an. Station 05 baut daraus eine Meldung, die von selbst kommt.
Das Prüfskript zum Mitlesen n8n-check.sh 227 Zeilen — Kopierknopf oben rechts
n8n-check.sh
#!/usr/bin/env bash
#
# n8n-check.sh — "Laufen meine Abläufe, und sind sie sicher?"
#
# Ein Befehl, eine Antwort. Läuft in ein paar Sekunden, ändert nichts.
# Jede Zeile ist entweder in Ordnung, ein Hinweis oder ein Problem —
# und jedes Problem nennt den nächsten Schritt.
#
# sudo bash n8n-check.sh
# sudo bash n8n-check.sh --domain ablauf.example.de (prüft zusätzlich das Zertifikat)
#
# Rückgabewert: 0 = alles in Ordnung, 1 = mindestens ein Problem.
# Damit taugt der Befehl auch für die Überwachung — und die Ausgabe lässt
# sich in der Akademie in die Phase einfügen, die die Nachweise abhakt.
set -uo pipefail
# Umlaute sollen die Spaltenbreite nicht verschieben: eine UTF-8-Umgebung wählen,
# damit die Shell Zeichen zählt und nicht Bytes.
if [[ "${LC_ALL:-${LANG:-}}" != *UTF-8* && "${LC_ALL:-${LANG:-}}" != *utf8* ]]; then
VERFUEGBAR="$(locale -a 2>/dev/null || true)"
for L in C.UTF-8 de_DE.UTF-8 en_US.UTF-8; do
case $'\n'"$VERFUEGBAR"$'\n' in *$'\n'"$L"$'\n'*) export LC_ALL="$L"; break ;; esac
done
fi
DOMAIN=""
VERZEICHNIS="/opt/n8n"
UMSCHLAG="/root/n8n-notfallumschlag.tar.gz"
STILLSTAND_STUNDEN=24 # so lange darf der letzte Lauf her sein
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) DOMAIN="${2:-}"; shift 2 ;;
--verzeichnis) VERZEICHNIS="${2:-}"; shift 2 ;;
--umschlag) UMSCHLAG="${2:-}"; shift 2 ;;
-h|--hilfe|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) shift ;;
esac
done
if [[ -t 1 ]]; then
F_TITEL=$'\e[1m'; F_OK=$'\e[32m'; F_WARN=$'\e[33m'; F_FEHLER=$'\e[31m'; F_AUS=$'\e[0m'
else
F_TITEL=""; F_OK=""; F_WARN=""; F_FEHLER=""; F_AUS=""
fi
GEPRUEFT=0; HINWEISE=0; PROBLEME=0
declare -a SCHRITTE=()
titel() { printf '\n%s%s%s\n' "$F_TITEL" "$1" "$F_AUS"; }
# Auf 34 ZEICHEN auffüllen (nicht auf Bytes) — Umlaute sollen die Spalte nicht verschieben.
breit() { local x="$1" n; n=$(( 34 - ${#x} )); (( n < 0 )) && n=0; printf '%s%*s' "$x" "$n" ''; }
gut() { GEPRUEFT=$((GEPRUEFT+1)); printf ' %s✓%s %s %s\n' "$F_OK" "$F_AUS" "$(breit "$1")" "${2:-}"; }
hinweis() { GEPRUEFT=$((GEPRUEFT+1)); HINWEISE=$((HINWEISE+1)); printf ' %s!%s %s %s\n' "$F_WARN" "$F_AUS" "$(breit "$1")" "${2:-}"; [[ -n "${3:-}" ]] && SCHRITTE+=("$3"); }
schlecht(){ GEPRUEFT=$((GEPRUEFT+1)); PROBLEME=$((PROBLEME+1)); printf ' %s✗%s %s %s\n' "$F_FEHLER" "$F_AUS" "$(breit "$1")" "${2:-}"; [[ -n "${3:-}" ]] && SCHRITTE+=("$3"); }
printf '%s\n' "${F_TITEL}Serverprüfung — n8n — $(hostname) — $(date '+%d.%m.%Y %H:%M')${F_AUS}"
COMPOSE="${VERZEICHNIS}/docker-compose.yml"
UMGEBUNG="${VERZEICHNIS}/.env"
dc() { docker compose -f "$COMPOSE" "$@" 2>/dev/null; }
umg() { [[ -r "$UMGEBUNG" ]] && grep -E "^${1}=" "$UMGEBUNG" | head -1 | cut -d= -f2- || true; }
# ---------------------------------------------------------------- Betrieb
titel "Betrieb"
if ! command -v docker >/dev/null; then
schlecht "Docker" "nicht vorhanden" "Erst bootstrap.sh aus Kurs 1 laufen lassen"
elif [[ ! -f "$COMPOSE" ]]; then
schlecht "Installation" "${COMPOSE} fehlt" "n8n-aufsetzen.sh laufen lassen — oder --verzeichnis angeben"
else
gut "Installation" "$VERZEICHNIS"
ZUSTAND="$(dc ps --format '{{.Service}} {{.State}}')"
for DIENST in n8n db; do
ZEILE="$(grep -E "^${DIENST} " <<<"$ZUSTAND" | head -1)"
NAME="Container ${DIENST}"
if [[ -z "$ZEILE" ]]; then
schlecht "$NAME" "läuft nicht" "cd ${VERZEICHNIS} && docker compose up -d"
elif grep -q 'running' <<<"$ZEILE"; then
gut "$NAME" "läuft"
else
schlecht "$NAME" "${ZEILE#* }" "cd ${VERZEICHNIS} && docker compose logs --tail=50 ${DIENST}"
fi
done
CID="$(dc ps -q n8n | head -1)"
if [[ -n "$CID" ]]; then
NEUSTARTS="$(docker inspect -f '{{.RestartCount}}' "$CID" 2>/dev/null || echo 0)"
if (( NEUSTARTS > 5 )); then
schlecht "Dauerneustarts" "${NEUSTARTS} Neustarts" "docker logs --tail=80 ${CID} — meist fehlt eine Umgebungsvariable"
else
gut "Dauerneustarts" "${NEUSTARTS}"
fi
FASSUNG="$(docker inspect -f '{{.Config.Image}}' "$CID" 2>/dev/null)"
if [[ "$FASSUNG" == *:latest ]]; then
hinweis "n8n-Fassung" "latest" "Feste Fassung eintragen — sonst ändert sich beim nächsten Neustart unangekündigt alles"
else
gut "n8n-Fassung" "${FASSUNG##*:}"
fi
OFFEN="$(docker inspect -f '{{json .NetworkSettings.Ports}}' "$CID" 2>/dev/null)"
if grep -q 'HostPort' <<<"$OFFEN"; then
schlecht "Port nach außen" "5678 ist veröffentlicht" "In docker-compose.yml den ports-Abschnitt entfernen — Traefik erreicht n8n über das Docker-Netz"
else
gut "Port nach außen" "keiner — nur über Traefik"
fi
fi
DBID="$(dc ps -q db | head -1)"
if [[ -n "$DBID" ]]; then
if docker inspect -f '{{json .NetworkSettings.Ports}}' "$DBID" 2>/dev/null | grep -q 'HostPort'; then
schlecht "Datenbank nach außen" "veröffentlicht" "ports beim Dienst db entfernen — die Datenbank gehört ins interne Netz"
else
gut "Datenbank nach außen" "nicht erreichbar"
fi
fi
fi
# ------------------------------------------------------ Schlüssel und Sicherung
titel "Schlüssel und Sicherung"
if [[ ! -r "$UMGEBUNG" ]]; then
schlecht "Umgebungsdatei" "nicht lesbar" "Mit sudo ausführen — oder n8n-aufsetzen.sh laufen lassen"
else
RECHTE="$(stat -c '%a' "$UMGEBUNG" 2>/dev/null || stat -f '%OLp' "$UMGEBUNG" 2>/dev/null)"
if [[ "$RECHTE" == "600" ]]; then gut "Umgebungsdatei" "nur für root lesbar"
else schlecht "Umgebungsdatei" "Rechte ${RECHTE}" "chmod 600 ${UMGEBUNG}"; fi
SCHLUESSEL="$(umg N8N_ENCRYPTION_KEY)"
if [[ -z "$SCHLUESSEL" ]]; then
schlecht "Verschlüsselungsschlüssel" "nicht gesetzt" "N8N_ENCRYPTION_KEY setzen — sonst würfelt n8n bei jedem Start einen neuen"
elif (( ${#SCHLUESSEL} < 32 )); then
hinweis "Verschlüsselungsschlüssel" "kurz (${#SCHLUESSEL} Zeichen)" "Länger wählen: openssl rand -hex 32 — Achtung, ein Wechsel macht bestehende Zugänge unlesbar"
else
gut "Verschlüsselungsschlüssel" "gesetzt (${#SCHLUESSEL} Zeichen)"
fi
if [[ -f "$UMSCHLAG" ]]; then
ALTER=$(( ( $(date +%s) - $(stat -c %Y "$UMSCHLAG" 2>/dev/null || stat -f %m "$UMSCHLAG") ) / 86400 ))
if (( ALTER > 30 )); then
hinweis "Notfallumschlag" "${ALTER} Tage alt" "sudo bash n8n-sichern.sh — und die neue Datei vom Server holen"
else
gut "Notfallumschlag" "${ALTER} Tage alt"
fi
if [[ -n "$SCHLUESSEL" ]] && tar -xzOf "$UMSCHLAG" 2>/dev/null | grep -q "$SCHLUESSEL"; then
gut "Fingerabdruck" "Umschlag passt zum Schlüssel"
else
hinweis "Fingerabdruck" "nicht überprüfbar" "Umschlag neu erzeugen: sudo bash n8n-sichern.sh"
fi
else
schlecht "Notfallumschlag" "fehlt" "sudo bash n8n-sichern.sh — ohne den Schlüssel ist jede Sicherung wertlos"
fi
LETZTE="$(ls -1t "${VERZEICHNIS}"/sicherung/*.tar.gz 2>/dev/null | head -1)"
if [[ -n "$LETZTE" ]]; then
TAGE=$(( ( $(date +%s) - $(stat -c %Y "$LETZTE" 2>/dev/null || stat -f %m "$LETZTE") ) / 86400 ))
if (( TAGE > 2 )); then hinweis "Sicherung" "${TAGE} Tage alt" "Tägliche Sicherung einrichten (Coolify Scheduled Task oder cron)"
else gut "Sicherung" "${TAGE} Tage alt"; fi
else
hinweis "Sicherung" "keine gefunden" "sudo bash n8n-sichern.sh täglich laufen lassen — und den Rückweg einmal üben"
fi
fi
# ---------------------------------------------------------------- Läufe
titel "Läufe"
PRUNE="$(umg EXECUTIONS_DATA_PRUNE)"
FRIST="$(umg EXECUTIONS_DATA_MAX_AGE)"
if [[ "$PRUNE" == "true" ]]; then
gut "Aufräumen der Läufe" "an${FRIST:+ (${FRIST} Stunden)}"
else
hinweis "Aufräumen der Läufe" "aus" "EXECUTIONS_DATA_PRUNE=true setzen — sonst wachsen die Daten jedes Laufs unbegrenzt"
fi
if [[ -n "${DBID:-}" ]]; then
DB_USER="$(umg POSTGRES_USER)"; DB_NAME="$(umg POSTGRES_DB)"
psql_() { docker exec -i "$DBID" psql -U "${DB_USER:-n8n}" -d "${DB_NAME:-n8n}" -tA -c "$1" 2>/dev/null; }
LETZTER="$(psql_ "select extract(epoch from now() - max(\"startedAt\"))::int from execution_entity;")"
if [[ -n "$LETZTER" && "$LETZTER" =~ ^[0-9]+$ ]]; then
STD=$(( LETZTER / 3600 ))
if (( STD > STILLSTAND_STUNDEN )); then
schlecht "Letzter Lauf" "vor ${STD} Stunden" "Das ist der leise Stillstand. Auslöser prüfen — Station 05"
else
gut "Letzter Lauf" "vor ${STD} Stunden"
fi
else
hinweis "Letzter Lauf" "nicht ablesbar" "Noch kein Lauf, oder die Datenbank antwortet nicht"
fi
FEHLER="$(psql_ "select count(*) from execution_entity where status='error' and \"startedAt\" > now() - interval '24 hours';")"
if [[ -n "$FEHLER" && "$FEHLER" =~ ^[0-9]+$ ]]; then
if (( FEHLER > 0 )); then hinweis "Abgebrochene Läufe (24 h)" "${FEHLER}" "In n8n unter Ausführungen ansehen — und prüfen, ob der Fehler-Ablauf gemeldet hat"
else gut "Abgebrochene Läufe (24 h)" "0"; fi
fi
fi
# ---------------------------------------------------------- Erreichbarkeit
if [[ -n "$DOMAIN" ]]; then
titel "Erreichbarkeit"
CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "https://${DOMAIN}/" 2>/dev/null)"
case "$CODE" in
200|401|302) gut "Oberfläche" "antwortet (${CODE})" ;;
000) schlecht "Oberfläche" "keine Antwort" "DNS, Traefik-Label und Container prüfen" ;;
*) hinweis "Oberfläche" "antwortet mit ${CODE}" "In Traefik nachsehen, ob der Router greift" ;;
esac
ENDE_ROH="$(echo | timeout 8 openssl s_client -servername "$DOMAIN" -connect "${DOMAIN}:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)"
if [[ -n "$ENDE_ROH" ]]; then
TAGE=$(( ( $(date -d "$ENDE_ROH" +%s 2>/dev/null || echo 0) - $(date +%s) ) / 86400 ))
if (( TAGE < 0 )); then schlecht "Zertifikat" "abgelaufen" "Domain in Coolify neu speichern — Traefik holt ein neues"
elif (( TAGE < 14 )); then hinweis "Zertifikat" "läuft in ${TAGE} Tagen ab" "Sollte sich selbst erneuern. Wenn nicht: Domain neu speichern"
else gut "Zertifikat" "gültig, noch ${TAGE} Tage"; fi
else
hinweis "Zertifikat" "keines abrufbar" "Kommt erst, wenn Traefik die Domain kennt"
fi
fi
# ---------------------------------------------------------------- Ergebnis
printf '\n%s' "$F_TITEL"
if (( PROBLEME > 0 )); then printf '%s%d Problem(e), %d Hinweis(e) — %d Punkte geprüft%s\n' "$F_FEHLER" "$PROBLEME" "$HINWEISE" "$GEPRUEFT" "$F_AUS"
elif (( HINWEISE > 0 )); then printf '%sKeine Probleme, %d Hinweis(e) — %d Punkte geprüft%s\n' "$F_WARN" "$HINWEISE" "$GEPRUEFT" "$F_AUS"
else printf '%sAlles in Ordnung — %d Punkte geprüft%s\n' "$F_OK" "$GEPRUEFT" "$F_AUS"; fi
if (( ${#SCHRITTE[@]} > 0 )); then
printf '\n%sNächste Schritte%s\n' "$F_TITEL" "$F_AUS"
N=1; for S in "${SCHRITTE[@]}"; do printf ' %d. %s\n' "$N" "$S"; N=$((N+1)); done
fi
printf '\n'
(( PROBLEME == 0 ))
- Prüfliste Inbetriebnahme
In der Akademie können Sie die Ausgabe dieses Befehls in dieser Phase einfügen — die drei Punkte oben werden dann aus dem Ergebnis abgehakt statt von Hand. Was das Skript nicht wissen kann, bleibt Selbstauskunft.
SelbstcheckErst überlegen, dann aufklappen — es zählt niemand mit.
1Sie spielen eine Sicherung auf einen neuen Server zurück. Alles läuft, aber jeder Ablauf meldet „credentials could not be decrypted". Was ist passiert?
Es wurde ein anderer Verschlüsselungsschlüssel gesetzt als der, mit dem die Zugänge verschlüsselt wurden. Die Datenbank ist vollständig, die Zugänge sind darin — und unlesbar. Der Schlüssel gehört deshalb in die Sicherung und in den Passwortspeicher; er darf nie „neu erzeugt" werden, solange Zugänge bestehen.
2Warum steht in der docker-compose.yml eine Fassungsnummer und nicht latest?
Weil sich sonst beim nächsten Neustart unangekündigt alles ändern kann — einschließlich der Knoten, aus denen Ihre Abläufe bestehen. Mit einer Nummer ist die Aktualisierung eine Entscheidung mit Sicherung davor und Probelauf danach, kein Nebeneffekt eines Neustarts.
3Welcher Punkt in der Ausgabe von n8n-check.sh findet den leisen Stillstand — und warum finden die anderen ihn nicht?
„Letzter Lauf". Alle anderen Punkte prüfen, ob etwas kaputt ist: Container, Zertifikat, Rechte, Sicherung. Beim leisen Stillstand ist nichts kaputt — es kommt nur nichts mehr an. Das sieht man ausschließlich am Alter des letzten Laufs.
Zugang erhalten
Fortschritt merken?Mit Zugang bleiben Häkchen und erledigte Phasen am Konto — auf jedem Gerät.