Station 01/Nachsehen, ob alles gesund ist·Vorschau
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.

Der Film zu dieser Phase1:32 — mit Untertiteln. Er ersetzt den Text nicht, er geht ihm voraus.
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.