feat: complete n8n stack setup

Docker Compose stack (n8n, PostgreSQL 16, Redis 7, 1 worker) with:
- nginx local proxy on port 8088, YunoHost TLS termination config
- helper scripts: backup/restore/import/export/update
- .env.example, README with architecture, ops commands, to-do list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-05-05 17:31:59 +02:00
commit 87cd005352
12 changed files with 586 additions and 2 deletions

35
.env.example Normal file
View file

@ -0,0 +1,35 @@
# =============================================================================
# n8n Stack — Konfigurationsvorlage
# Kopieren nach .env und alle Werte ausfüllen.
# =============================================================================
# --- Domain & URL ---
N8N_HOST=n8n.linix.de
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.linix.de/
# --- Datenbank ---
POSTGRES_DB=n8n
POSTGRES_USER=n8n
POSTGRES_PASSWORD=HIER_STARKES_PASSWORT_EINTRAGEN
# --- Redis ---
REDIS_PASSWORD=HIER_STARKES_PASSWORT_EINTRAGEN
# --- n8n Encryption Key ---
# KRITISCH: Einmalig generieren (z.B. openssl rand -base64 32) und nie mehr ändern!
# Änderung macht alle gespeicherten Credentials unbrauchbar.
N8N_ENCRYPTION_KEY=HIER_ZUFALLSKEY_EINTRAGEN
# --- SMTP ---
SMTP_HOST=linix.de
SMTP_PORT=587
SMTP_USER=nutzer@linix.de
SMTP_PASSWORD=SMTP_PASSWORT
SMTP_SENDER=n8n@linix.de
# --- Worker ---
N8N_WORKER_REPLICAS=1
# --- Zeitzone ---
TZ=Europe/Berlin

142
README.md
View file

@ -1,3 +1,141 @@
# n8n_stack # n8n Stack
Das Projekt installiert und konfiguriert einen vollständigen, produktionsnahen n8n-Stack auf einem Ubuntu-Linux-System. Produktionsnaher n8n-Stack auf Docker Compose mit PostgreSQL, Redis und Queue-Mode.
## Architektur
```
Internet → n8n.linix.de:443
→ YunoHost (192.168.179.10) — TLS-Terminierung (Let's Encrypt)
→ nginx lokal :8088 (192.168.179.124)
→ n8n Docker :5678
↔ PostgreSQL (n8n-stack-postgres-1)
↔ Redis (n8n-stack-redis-1)
← n8n-worker (n8n-stack-n8n-worker-1)
```
## Verzeichnisstruktur
```
n8n_stack/
├── compose/docker-compose.yml # Stack-Definition
├── .env # Secrets (600, nie committen!)
├── .env.example # Vorlage
├── scripts/ # Hilfsskripte (700)
├── data/ # Docker-Volumes (gitignoriert)
│ ├── n8n/
│ ├── postgres/
│ └── redis/
├── backups/ # Backups, max. 2 (gitignoriert)
├── imports/workflows/ # Workflow-JSONs zum Importieren
├── imports/credentials/ # Credentials-Exporte (unverschlüsselt!)
├── local-files/ # Dateien für n8n-Container (/files)
└── docs/ # nginx-Configs, Runbooks
```
## Start / Stop / Status
```bash
# Start
docker compose -f compose/docker-compose.yml up -d
# Stop
docker compose -f compose/docker-compose.yml down
# Status
docker compose -f compose/docker-compose.yml ps
# Logs
docker compose -f compose/docker-compose.yml logs -f n8n
# Worker skalieren (z.B. auf 2)
docker compose -f compose/docker-compose.yml up -d --scale n8n-worker=2
```
## Update
```bash
bash scripts/update-n8n.sh
```
Führt automatisch ein Backup aus, zieht neue Images und startet den Stack neu.
## Backup / Restore
```bash
# Backup erstellen (behält immer die letzten 2)
bash scripts/backup-n8n.sh
# Restore aus Backup-Verzeichnis
bash scripts/restore-n8n.sh backups/backup_20240101_120000
```
## Import / Export von Workflows
```bash
# Einzelne Workflow-JSON importieren
bash scripts/import-workflow.sh imports/workflows/mein-workflow.json
# Ganzes Verzeichnis importieren
bash scripts/import-workflow.sh imports/workflows/
# Alle Workflows exportieren
bash scripts/export-workflows.sh
```
## Import / Export von Credentials
```bash
# Credentials importieren
bash scripts/import-credentials.sh imports/credentials/creds.json
# Credentials exportieren (UNVERSCHLÜSSELT — sicher aufbewahren!)
bash scripts/export-credentials.sh
```
**Wichtig:** Credential-Exporte enthalten alle Secrets im Klartext. Export-Dateien nach Gebrauch löschen.
## Übernahme fremder n8n-Projekte
1. Workflow-JSON-Dateien nach `imports/workflows/` kopieren
2. `bash scripts/import-workflow.sh imports/workflows/<datei>.json`
3. Falls Credentials mitgeliefert: `bash scripts/import-credentials.sh <datei.json>`
4. In der n8n-UI: Credentials der importierten Workflows prüfen und neu verknüpfen
Bei Voll-Exporten (`.zip` mit mehreren JSONs): entpacken, dann Verzeichnis übergeben.
## Deployment auf neuem Host
1. Repo klonen
2. `.env.example``.env` kopieren, alle Werte ausfüllen
3. Neuen `N8N_ENCRYPTION_KEY` generieren: `openssl rand -base64 32`
4. `N8N_HOST` und `WEBHOOK_URL` auf die neue Domain anpassen
5. nginx-Config aus `docs/nginx-local-n8n.conf` anpassen und aktivieren
6. YunoHost-Config aus `docs/yunohost-nginx-n8n.conf` anpassen und installieren
7. `docker compose -f compose/docker-compose.yml up -d`
## Kritische Hinweise
- **`N8N_ENCRYPTION_KEY`** darf nach dem ersten Start **nie geändert** werden — eine Änderung macht alle gespeicherten Credentials unbrauchbar. Key steht in `.env`.
- **`.env`** nie committen (steht in `.gitignore`).
- Backup vor jedem Update läuft automatisch via `update-n8n.sh`.
## URLs und Zugangspfade
| Was | Wert |
|---|---|
| n8n Web-UI | https://n8n.linix.de |
| Erster Login | Beim ersten Aufruf Owner-Account anlegen |
| Secrets-Datei | `./.env` (Rechte 600) |
| Backup-Verzeichnis | `./backups/` |
| nginx lokal | `/etc/nginx/sites-available/n8n` (Port 8088) |
| YunoHost nginx | `/etc/nginx/conf.d/n8n.linix.de.conf` |
## To-do (manuell auf YunoHost)
- [ ] DNS: `n8n.linix.de``92.50.108.218` (A-Record beim DNS-Provider)
- [ ] YunoHost: Domain hinzufügen: `sudo yunohost domain add n8n.linix.de`
- [ ] YunoHost: Let's Encrypt: `sudo yunohost domain cert install n8n.linix.de`
- [ ] YunoHost: `docs/yunohost-nginx-n8n.conf``/etc/nginx/conf.d/n8n.linix.de.conf` kopieren, dann `sudo nginx -t && sudo systemctl reload nginx`
- [ ] SMTP-Credentials in `.env` eintragen (`SMTP_USER`, `SMTP_PASSWORD`)
- [ ] Ersten Owner-Account in n8n anlegen (https://n8n.linix.de beim ersten Aufruf)

118
compose/docker-compose.yml Normal file
View file

@ -0,0 +1,118 @@
name: n8n-stack
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ../data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- n8n-internal
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- ../data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- n8n-internal
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
# URL & Protokoll
N8N_HOST: ${N8N_HOST}
N8N_PORT: 5678
N8N_PROTOCOL: ${N8N_PROTOCOL}
WEBHOOK_URL: ${WEBHOOK_URL}
# Datenbank
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
# Encryption
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
# Queue / Redis
EXECUTIONS_MODE: queue
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PORT: 6379
QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
# SMTP
N8N_EMAIL_MODE: smtp
N8N_SMTP_HOST: ${SMTP_HOST}
N8N_SMTP_PORT: ${SMTP_PORT}
N8N_SMTP_USER: ${SMTP_USER}
N8N_SMTP_PASS: ${SMTP_PASSWORD}
N8N_SMTP_SENDER: ${SMTP_SENDER}
N8N_SMTP_SSL: "false"
# User Management
N8N_USER_MANAGEMENT_DISABLED: "false"
# Zeitzone
GENERIC_TIMEZONE: ${TZ}
TZ: ${TZ}
volumes:
- ../data/n8n:/home/node/.n8n
- ../local-files:/files
- ../imports:/imports
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- n8n-internal
n8n-worker:
image: n8nio/n8n:latest
restart: unless-stopped
command: worker
environment:
# Datenbank
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
# Encryption
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
# Queue / Redis
EXECUTIONS_MODE: queue
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PORT: 6379
QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
# Zeitzone
GENERIC_TIMEZONE: ${TZ}
TZ: ${TZ}
volumes:
- ../data/n8n:/home/node/.n8n
- ../local-files:/files
depends_on:
- n8n
networks:
- n8n-internal
networks:
n8n-internal:
driver: bridge

28
docs/nginx-local-n8n.conf Normal file
View file

@ -0,0 +1,28 @@
# Lokale nginx-Konfiguration auf 192.168.179.124
# Ablegen als: /etc/nginx/sites-available/n8n
# Aktivieren mit: sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/n8n
# Danach: sudo nginx -t && sudo systemctl reload nginx
server {
listen 8088;
server_name n8n.linix.de;
# n8n-Proxy
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
# WebSocket-Support (n8n Editor + Push-Benachrichtigungen)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
client_max_body_size 50m;
}
}

View file

@ -0,0 +1,49 @@
# YunoHost nginx-Konfiguration (auf dem YunoHost-Server 192.168.179.10)
# Ablegen als: /etc/nginx/conf.d/n8n.linix.de.conf
# Danach: sudo nginx -t && sudo systemctl reload nginx
#
# Voraussetzungen auf YunoHost:
# 1. Domain n8n.linix.de in YunoHost hinzufügen:
# sudo yunohost domain add n8n.linix.de
# 2. Let's Encrypt Zertifikat ausstellen:
# sudo yunohost domain cert install n8n.linix.de
# 3. Diese Datei ablegen und nginx neu laden.
#
# Cert-Pfad prüfen mit: ls /etc/yunohost/certs/
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name n8n.linix.de;
ssl_certificate /etc/yunohost/certs/n8n.linix.de/crt.pem;
ssl_certificate_key /etc/yunohost/certs/n8n.linix.de/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://192.168.179.124:8088;
proxy_http_version 1.1;
# WebSocket-Support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
client_max_body_size 50m;
}
}
server {
listen 80;
listen [::]:80;
server_name n8n.linix.de;
return 301 https://$host$request_uri;
}

48
scripts/backup-n8n.sh Executable file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
ENV="$ROOT/.env"
BACKUP_DIR="$ROOT/backups"
MAX_BACKUPS=2
# shellcheck source=/dev/null
source "$ENV"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
TARGET="$BACKUP_DIR/backup_$TIMESTAMP"
mkdir -p "$TARGET"
echo "[backup] Starte Backup → $TARGET"
# PostgreSQL dump
echo "[backup] Datenbank-Dump..."
docker compose -f "$COMPOSE" exec -T postgres \
pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" \
> "$TARGET/postgres.sql"
# n8n data
echo "[backup] n8n Datendirectory..."
tar -czf "$TARGET/n8n-data.tar.gz" -C "$ROOT/data" n8n
# Compose-Konfiguration
echo "[backup] Compose + .env..."
cp "$COMPOSE" "$TARGET/docker-compose.yml"
cp "$ENV" "$TARGET/.env"
echo "[backup] Backup abgeschlossen: $TARGET"
# Retention: nur die letzten MAX_BACKUPS behalten
EXISTING=$(ls -dt "$BACKUP_DIR"/backup_* 2>/dev/null)
COUNT=$(echo "$EXISTING" | wc -l)
if [ "$COUNT" -gt "$MAX_BACKUPS" ]; then
TO_DELETE=$(echo "$EXISTING" | tail -n +"$((MAX_BACKUPS + 1))")
echo "[backup] Lösche alte Backups:"
echo "$TO_DELETE"
echo "$TO_DELETE" | xargs rm -rf
fi
echo "[backup] Fertig. Aktuelle Backups:"
ls -lht "$BACKUP_DIR"

19
scripts/export-credentials.sh Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
OUTPUT_DIR="$ROOT/imports/credentials/export_$TIMESTAMP"
mkdir -p "$OUTPUT_DIR"
echo "[export-credentials] WARNUNG: Export enthält unverschlüsselte Credentials!"
echo "[export-credentials] Exportiere → $OUTPUT_DIR"
docker compose -f "$COMPOSE" exec n8n \
n8n export:credentials --all --output="/imports/credentials/export_$TIMESTAMP/"
echo "[export-credentials] Fertig."
echo "[export-credentials] WICHTIG: Export-Verzeichnis sichern und nach Gebrauch löschen!"
ls -lh "$OUTPUT_DIR"

17
scripts/export-workflows.sh Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
OUTPUT_DIR="$ROOT/imports/workflows/export_$TIMESTAMP"
mkdir -p "$OUTPUT_DIR"
echo "[export-workflows] Exportiere alle Workflows → $OUTPUT_DIR"
docker compose -f "$COMPOSE" exec n8n \
n8n export:workflow --all --output="/imports/workflows/export_$TIMESTAMP/"
echo "[export-workflows] Fertig."
ls -lh "$OUTPUT_DIR"

24
scripts/import-credentials.sh Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
if [ -z "${1:-}" ]; then
echo "Verwendung: $0 <credentials.json>"
echo "WARNUNG: Credentials-Exporte sind unverschlüsselt. Datei nach Import löschen!"
exit 1
fi
SOURCE="$1"
DEST="$ROOT/imports/credentials/$(basename "$SOURCE")"
cp "$SOURCE" "$DEST"
echo "[import-credentials] Importiere: $(basename "$SOURCE")"
echo "[import-credentials] WARNUNG: Diese Datei enthält unverschlüsselte Credentials!"
docker compose -f "$COMPOSE" exec n8n \
n8n import:credentials --input="/imports/credentials/$(basename "$SOURCE")"
echo "[import-credentials] Fertig."
echo "[import-credentials] Empfehlung: Quelldatei jetzt löschen: rm '$SOURCE'"

35
scripts/import-workflow.sh Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
if [ -z "${1:-}" ]; then
echo "Verwendung: $0 <workflow.json | verzeichnis/>"
exit 1
fi
SOURCE="$1"
if [ -d "$SOURCE" ]; then
# Verzeichnis: alle JSON-Dateien importieren
echo "[import-workflow] Importiere alle Workflows aus: $SOURCE"
for f in "$SOURCE"/*.json; do
[ -f "$f" ] || continue
DEST="$ROOT/imports/workflows/$(basename "$f")"
cp "$f" "$DEST"
echo "[import-workflow] → $(basename "$f")"
docker compose -f "$COMPOSE" exec n8n \
n8n import:workflow --input="/imports/workflows/$(basename "$f")"
done
else
# Einzelne Datei
DEST="$ROOT/imports/workflows/$(basename "$SOURCE")"
cp "$SOURCE" "$DEST"
echo "[import-workflow] Importiere: $(basename "$SOURCE")"
docker compose -f "$COMPOSE" exec n8n \
n8n import:workflow --input="/imports/workflows/$(basename "$SOURCE")"
fi
echo "[import-workflow] Fertig."

50
scripts/restore-n8n.sh Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
ENV="$ROOT/.env"
if [ -z "${1:-}" ]; then
echo "Verwendung: $0 <backup-verzeichnis>"
echo "Verfügbare Backups:"
ls -lht "$ROOT/backups/"
exit 1
fi
BACKUP="$1"
if [ ! -d "$BACKUP" ]; then
echo "Fehler: Verzeichnis '$BACKUP' nicht gefunden."
exit 1
fi
# shellcheck source=/dev/null
source "$ENV"
echo "[restore] Stelle wieder her aus: $BACKUP"
echo "[restore] Stack wird gestoppt..."
docker compose -f "$COMPOSE" down
# n8n data
echo "[restore] n8n Daten..."
rm -rf "$ROOT/data/n8n"
mkdir -p "$ROOT/data/n8n"
tar -xzf "$BACKUP/n8n-data.tar.gz" -C "$ROOT/data"
# Postgres
echo "[restore] Datenbank wird gestartet..."
docker compose -f "$COMPOSE" up -d postgres
echo "[restore] Warte auf PostgreSQL..."
sleep 10
echo "[restore] Datenbank-Restore..."
docker compose -f "$COMPOSE" exec -T postgres \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
< "$BACKUP/postgres.sql"
echo "[restore] Stack wird neu gestartet..."
docker compose -f "$COMPOSE" up -d
echo "[restore] Fertig. Stack-Status:"
docker compose -f "$COMPOSE" ps

23
scripts/update-n8n.sh Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$SCRIPT_DIR")"
COMPOSE="$ROOT/compose/docker-compose.yml"
echo "[update] Starte Pre-Update-Backup..."
bash "$SCRIPT_DIR/backup-n8n.sh"
echo "[update] Neue Images ziehen..."
docker compose -f "$COMPOSE" pull
echo "[update] Stack neu starten..."
docker compose -f "$COMPOSE" up -d
echo "[update] Warte 15s auf Start..."
sleep 15
echo "[update] Stack-Status nach Update:"
docker compose -f "$COMPOSE" ps
echo "[update] Update abgeschlossen."