n8n self-hosted sous Docker : le pourquoi du comment d’une stack de production
Avant tout : c’est quoi n8n ?
Si tu tombes sur cet article sans connaître n8n, voilà en deux mots. n8n c’est une plateforme d’automation open source — un peu comme Zapier ou Make, mais que tu héberges toi-même. Tu connectes des services entre eux, tu crées des workflows, tu automatises des tâches répétitives. Et depuis la version 2, n8n a sérieusement monté en gamme : support natif des agents IA, des sub-workflows, une gestion des erreurs bien plus fine, et surtout une architecture qui tient la route en production.
Le self-hosting c’est l’intérêt principal. Pas de limite d’exécutions, pas de données qui partent sur des serveurs tiers, et surtout la possibilité d’utiliser des librairies Python ou JavaScript dans tes workflows — ce que les versions cloud ne permettent pas. Mais justement, pour que ça tourne bien en production, il faut pas se contenter du docker run basique. Et c’est exactement ce qu’on va voir ici.
On trouve des tas de tutos qui t’expliquent comment faire tourner n8n avec un simple docker run. Et effectivement, en deux minutes tu as quelque chose qui tourne. Le problème c’est que dès que tu commences à l’utiliser vraiment — des webhooks qui arrivent en parallèle, des workflows un peu lourds, du code Python — tout commence à ramer, à freezer, parfois à planter.
Dans cet article on va pas juste te coller un docker-compose et te dire bonne chance. On va t’expliquer pourquoi chaque brique est là, ce qui se passe si tu l’enlèves, et comment on est arrivé à cette architecture après l’avoir affinée dans le temps.
Ce qui se passe avec un n8n « basique »
Par défaut, n8n tourne dans un seul process, mono-thread, avec SQLite comme base de données. C’est parfait pour tester, découvrir l’outil, faire ses premiers workflows.
Mais très rapidement tu vas tomber sur des problèmes concrets :
👉 Tu lances deux workflows en même temps → l’un attend que l’autre finisse
👉 Tu exécutes un workflow un peu lourd → l’interface devient inutilisable pendant ce temps
👉 Un webhook arrive pendant qu’un autre tourne → il peut tout simplement être perdu
👉 Tu veux scaler → impossible, SQLite ne supporte pas l’accès concurrent depuis plusieurs process
C’est pas un bug de n8n. C’est juste que l’architecture par défaut n’est pas faite pour la production. Alors on va changer ça.
La stack n8n de production : les 6 briques
Dans notre docker-compose, les conteneurs qui font tourner n8n sont au nombre de 6. Voilà pourquoi on en a besoin de chacun.
1. PostgreSQL — pas un luxe, une nécessité
Le premier réflexe c’est de se dire « SQLite ça marche bien, pourquoi changer ». Le problème c’est que SQLite verrouille l’intégralité du fichier à chaque écriture. Un seul workflow qui écrit ses logs bloque tous les autres au même moment.
PostgreSQL lui travaille avec des verrous au niveau des lignes. Plusieurs workers peuvent écrire en parallèle sans se marcher dessus. Et surtout — et c’est la raison principale — le queue mode ne fonctionne pas avec SQLite. Tu peux pas scaler. Point.
postgres:
image: postgres:16-alpine
container_name: postgres-n8n
environment:
- POSTGRES_USER=${POSTGRES_USER:-n8n}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB:-n8n}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
Note le healthcheck — c’est important. n8n ne démarrera pas tant que PostgreSQL n’est pas prêt à accepter des connexions. On reviendra sur ça.
Dans le .env :
POSTGRES_USER=n8n
POSTGRES_PASSWORD=<mot_de_passe_genere>
POSTGRES_DB=n8n
Et côté n8n, la configuration qui bascule du SQLite vers PostgreSQL :
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
DB_POSTGRESDB_USER=${POSTGRES_USER:-n8n}
DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
⚠️ La valeur correcte est
postgresdbet paspostgres. Cette erreur fait que n8n retombe silencieusement sur SQLite sans te prévenir. Beaucoup de gens passent des heures à chercher pourquoi leur queue mode ne fonctionne pas alors que c’est juste ça.
2. Redis — le système nerveux du queue mode
Redis c’est pas un cache ici. C’est le broker de messages entre le process principal et les workers.
Le fonctionnement est simple : quand un workflow doit s’exécuter, n8n le pousse comme un job dans une queue Redis. Les workers surveillent cette queue, prennent le prochain job disponible et l’exécutent. C’est ce mécanisme qui permet à plusieurs workflows de tourner vraiment en parallèle.
Sans Redis, le queue mode est impossible. Sans queue mode, tu resteras toujours sur une architecture mono-process.
redis:
image: redis:7-alpine
container_name: redis-n8n
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--appendonly yes
--maxmemory 512mb
--maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 5s
retries: 5
Quelques points importants dans cette config :
--requirepass: on sécurise Redis avec un mot de passe, il n’y en a pas par défaut--appendonly yes: persistance des données sur disque--maxmemory 512mbavecallkeys-lru: Redis sait quoi supprimer si la mémoire est pleine
Dans le .env :
REDIS_PASSWORD=<mot_de_passe_genere>
Et côté n8n :
QUEUE_BULL_REDIS_HOST=redis
QUEUE_BULL_REDIS_PORT=6379
QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
EXECUTIONS_MODE=queue
EXECUTIONS_MODE=queue — c’est la ligne qui bascule tout. Sans elle, PostgreSQL et Redis sont là pour rien.
3. n8n (main) — l’interface, les webhooks, le chef d’orchestre
Le process principal n8n a un rôle très précis dans une architecture queue : il gère l’UI, reçoit les webhooks, déclenche les triggers, et distribue les jobs dans Redis. Il n’exécute pas lui-même les workflows de production.
n8n:
image: n8nio/n8n:latest
container_name: n8n-main
ports:
- "${N8N_PORT:-5678}:5678"
- "5679:5679" # port broker pour les runners
environment:
- EXECUTIONS_MODE=queue
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_USER_MANAGEMENT_JWT_SECRET=${N8N_JWT_SECRET}
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678/}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
Deux variables critiques ici :
N8N_ENCRYPTION_KEY — c’est la clé qui chiffre tous tes credentials stockés en base. Si tu la perds, tous tes credentials deviennent irrécupérables. Génère-la une fois, stocke-la précieusement, ne la change jamais.
N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0 — par défaut le broker écoute uniquement sur localhost. Comme les runners tournent dans leurs propres conteneurs, ils ne peuvent pas joindre localhost du conteneur n8n. Ce paramètre ouvre le broker sur toutes les interfaces du réseau Docker. Sans ça, les runners ne peuvent pas se connecter au broker et les Code nodes restent bloqués indéfiniment.
Le port 5679 est le port de communication entre le broker (n8n main) et les runners. Il faut l’exposer pour que les conteneurs sidecar puissent l’atteindre.
4. n8n-worker — l’exécuteur de workflows
Le worker c’est une instance n8n qui tourne en mode worker. Il ne fait qu’une chose : surveiller la queue Redis, prendre les jobs et les exécuter.
n8n-worker:
image: n8nio/n8n:latest
container_name: n8n-worker-1
command: worker
environment:
- EXECUTIONS_MODE=queue
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
Note que le worker pointe vers n8n-main:5679 pour le broker des runners. C’est le process principal qui joue le rôle de broker central — les runners des workers s’y connectent aussi.
Tu peux scaler horizontalement en ajoutant des workers :
docker compose up -d --scale n8n-worker=3
Chaque worker supplémentaire augmente le nombre de workflows pouvant tourner en parallèle.
5. Les Task Runners — la brique que personne n’explique
C’est là que ça devient vraiment intéressant, et c’est aussi la partie la plus mal documentée sur le net.
Le problème sans runners :
Quand tu utilises un Code node dans n8n — que ce soit du JavaScript ou du Python — ce code s’exécute. La question c’est : où et comment ?
Sans runners en mode externe, le code s’exécute directement dans le process n8n. Ça veut dire qu’un script Python mal écrit, une lib qui plante, un code qui boucle indéfiniment… peut faire tomber l’ensemble de ton instance n8n. Tout le monde dehors.
Pire : ce code tourne avec les mêmes droits que n8n lui-même. Il peut en théorie accéder à tes variables d’environnement, tes credentials, ta connexion base de données.
La solution : les runners en mode externe
Les runners sont des conteneurs séparés dont l’unique rôle est d’exécuter le code des Code nodes. Ils communiquent avec n8n via WebSocket, reçoivent le code à exécuter, le font tourner dans leur environnement isolé, et renvoient le résultat.
Si un runner plante → seul le runner meurt. n8n continue de tourner.
runners:
build:
context: .
dockerfile: Dockerfile.runners
container_name: n8n-runners
environment:
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_NATIVE_PYTHON_RUNNER=true
- N8N_RUNNERS_TASK_TIMEOUT=600
- N8N_RUNNERS_STDLIB_ALLOW=re,hashlib,math,datetime,json,logging,typing,asyncio,http,urllib,base64
- N8N_RUNNERS_EXTERNAL_ALLOW=pandas,numpy,spacy,sklearn,requests
Un runner par worker
Et là c’est le détail que même la doc officielle enterre dans une note : chaque worker a besoin de son propre runner sidecar. Ce n’est pas un runner global pour tout le monde.
C’est pourquoi on a un n8n-worker-runners dédié qui pointe spécifiquement vers n8n-worker-1:5679 :
n8n-worker-runners:
image: n8n-runners
container_name: n8n-worker-runners
environment:
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-worker-1:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
Le N8N_RUNNERS_AUTH_TOKEN est un secret partagé entre le broker et les runners pour s’authentifier. Sans ça, les runners sont rejetés à la connexion.
6. Le Dockerfile.runners custom — quand l’image officielle ne suffit pas
L’image officielle n8nio/runners embarque les runners JavaScript et Python de base. Mais si tu veux utiliser pandas, spacy, pdfplumber, pytesseract dans tes Code nodes, il faut les installer dans l’environnement Python du runner, pas dans n8n.
C’est là qu’on crée notre propre image :
FROM n8nio/runners:latest
USER root
RUN cd /opt/runners/task-runner-python && uv pip install \
markitdown \
langchain \
yt-dlp \
numpy \
pandas \
spacy \
requests \
pdfplumber \
PyPDF2 \
tika \
pytesseract \
pillow \
pdf2image \
charset-normalizer
COPY n8n-task-runners.json /etc/n8n-task-runners.json
USER runner
Deux points à noter :
- On utilise
uv pipet paspipclassique — c’est le gestionnaire de paquets Python ultra-rapide intégré dans l’image des runners. C’est volontaire. - On revient sur l’user
runneraprès l’installation. Ne pas oublier cette ligne sinon le conteneur tourne en root, ce qui va à l’encontre de toute la logique de sécurité des runners.
Le fichier n8n-task-runners.json
Ce fichier configure le launcher — le process qui démarre les runners à la demande et gère leur cycle de vie. C’est lui qui définit comment lancer le runner JS et le runner Python, quelles variables d’environnement ils peuvent voir, et les flags de sécurité :
{
"task-runners": [
{
"runner-type": "javascript",
"command": "/usr/local/bin/node",
"args": [
"--disallow-code-generation-from-strings",
"--disable-proto=delete",
"/opt/runners/task-runner-javascript/dist/start.js"
],
"health-check-server-port": "5681",
"env-overrides": {
"NODE_FUNCTION_ALLOW_BUILTIN": "*",
"NODE_FUNCTION_ALLOW_EXTERNAL": "*",
"N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST": "0.0.0.0"
}
},
{
"runner-type": "python",
"command": "/opt/runners/task-runner-python/.venv/bin/python",
"args": ["-m", "src.main"],
"health-check-server-port": "5682",
"env-overrides": {
"PYTHONPATH": "/opt/runners/task-runner-python",
"N8N_RUNNERS_STDLIB_ALLOW": "*",
"N8N_RUNNERS_EXTERNAL_ALLOW": "*"
}
}
]
}
Les flags --disallow-code-generation-from-strings et --disable-proto=delete sur le runner JS sont des mesures de sécurité qui empêchent certaines techniques d’injection de code. Les runners sont conçus pour exécuter du code utilisateur — autant les blinder.
Le docker-compose complet
Voici le docker-compose complet pour la partie n8n et toutes ses dépendances. Le reste du stack (Ollama, Supabase, Langfuse…) est volontairement exclu pour rester focalisé sur n8n.
services:
# ═══════════════════════════════════════════════════════════════════
# POSTGRESQL - Base de données pour n8n
# ═══════════════════════════════════════════════════════════════════
postgres:
image: postgres:16-alpine
container_name: postgres-n8n
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER:-n8n}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB:-n8n}
restart: unless-stopped
networks:
- ai-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
# ═══════════════════════════════════════════════════════════════════
# REDIS - Queue pour scalabilité n8n
# ═══════════════════════════════════════════════════════════════════
redis:
image: redis:7-alpine
container_name: redis-n8n
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--appendonly yes
--maxmemory 512mb
--maxmemory-policy allkeys-lru
restart: unless-stopped
networks:
- ai-network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ═══════════════════════════════════════════════════════════════════
# N8N - Plateforme d'automation (interface principale)
# ═══════════════════════════════════════════════════════════════════
n8n:
image: n8nio/n8n:latest
container_name: n8n-main
ports:
- "${N8N_PORT:-5678}:5678"
- "5679:5679"
volumes:
- n8n_data:/home/node/.n8n
- ./n8n-custom:/home/node/.n8n/custom
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
- DB_POSTGRESDB_USER=${POSTGRES_USER:-n8n}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
- EXECUTIONS_MODE=queue
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_USER_MANAGEMENT_JWT_SECRET=${N8N_JWT_SECRET}
- N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678/}
- N8N_SECURE_COOKIE=${N8N_SECURE_COOKIE:-false}
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_TIMEOUT=600
- N8N_RUNNERS_STDLIB_ALLOW=*
- N8N_RUNNERS_EXTERNAL_ALLOW=pandas,numpy,spacy,sklearn,requests,langchain,pdfplumber,PyPDF2,tika,charset-normalizer
- N8N_METRICS=true
- N8N_LOG_LEVEL=info
- TZ=${TZ:-Europe/Paris}
- EXECUTIONS_TIMEOUT=-1
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- ai-network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5678/healthz"]
interval: 30s
timeout: 10s
retries: 3
# ═══════════════════════════════════════════════════════════════════
# N8N WORKER - Exécution des workflows
# ═══════════════════════════════════════════════════════════════════
n8n-worker:
image: n8nio/n8n:latest
container_name: n8n-worker-1
command: worker
volumes:
- n8n_data:/home/node/.n8n
- ./n8n-custom:/home/node/.n8n/custom
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
- DB_POSTGRESDB_USER=${POSTGRES_USER:-n8n}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
- EXECUTIONS_MODE=queue
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- TZ=${TZ:-Europe/Paris}
- EXECUTIONS_TIMEOUT=-1
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_TIMEOUT=600
- N8N_RUNNERS_STDLIB_ALLOW=*
- N8N_RUNNERS_EXTERNAL_ALLOW=pandas,numpy,spacy,sklearn,requests,langchain,pdfplumber,PyPDF2,tika,charset-normalizer
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
n8n:
condition: service_healthy
restart: unless-stopped
networks:
- ai-network
# ═══════════════════════════════════════════════════════════════════
# N8N WORKER RUNNERS - Sidecar runner dédié au worker
# Un runner par worker — c'est la règle, pas une option
# ═══════════════════════════════════════════════════════════════════
n8n-worker-runners:
image: n8n-runners
container_name: n8n-worker-runners
environment:
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-worker-1:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_MAX_OLD_SPACE_SIZE=4096
- N8N_RUNNERS_TASK_TIMEOUT=600
- N8N_NATIVE_PYTHON_RUNNER=true
- N8N_RUNNERS_STDLIB_ALLOW=*
- N8N_RUNNERS_EXTERNAL_ALLOW=pandas,numpy,spacy,sklearn,requests
depends_on:
- n8n-worker
networks:
- ai-network
restart: unless-stopped
# ═══════════════════════════════════════════════════════════════════
# RUNNERS - Sidecar pour le main (exécutions manuelles UI)
# ═══════════════════════════════════════════════════════════════════
runners:
build:
context: .
dockerfile: Dockerfile.runners
container_name: n8n-runners
volumes:
- ./n8n-task-runners.json:/etc/n8n-task-runners.json:ro
environment:
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_NATIVE_PYTHON_RUNNER=true
- N8N_RUNNERS_TASK_TIMEOUT=600
- N8N_RUNNERS_STDLIB_ALLOW=re,hashlib,math,datetime,json,logging,typing,asyncio,http,urllib,base64
- N8N_RUNNERS_EXTERNAL_ALLOW=pandas,numpy,spacy,sklearn,requests
- N8N_RUNNERS_MAX_OLD_SPACE_SIZE=${N8N_RUNNERS_MAX_OLD_SPACE_SIZE:-4096}
restart: unless-stopped
networks:
- ai-network
volumes:
postgres_data:
name: postgres_data
redis_data:
name: redis_data
n8n_data:
name: n8n_data
networks:
ai-network:
name: ai-network
driver: bridge
Le fichier .env et la gestion des secrets
Un fichier .env bien structuré c’est la base. Toutes les valeurs sensibles doivent y être et ne jamais se retrouver en clair dans le docker-compose.
Voici les variables spécifiques à n8n :
# ═══════════════════════════════════════════════════════
# POSTGRESQL (n8n)
# ═══════════════════════════════════════════════════════
POSTGRES_PORT=5432
POSTGRES_USER=n8n
POSTGRES_PASSWORD=CHANGEZ_MOI
POSTGRES_DB=n8n
# ═══════════════════════════════════════════════════════
# REDIS (queue n8n)
# ═══════════════════════════════════════════════════════
REDIS_PORT=6379
REDIS_PASSWORD=CHANGEZ_MOI
# ═══════════════════════════════════════════════════════
# N8N
# ═══════════════════════════════════════════════════════
N8N_PORT=5678
# NE JAMAIS PERDRE CETTE CLÉ — tous tes credentials en dépendent
N8N_ENCRYPTION_KEY=CHANGEZ_MOI
# Secret JWT pour la gestion des sessions
N8N_JWT_SECRET=CHANGEZ_MOI
# URL publique pour que les webhooks soient joignables de l'extérieur
N8N_WEBHOOK_URL=https://n8n.ton-domaine.com/
# false si tu es en HTTP (LAN / accès local)
N8N_SECURE_COOKIE=false
TZ=Europe/Paris
# Token partagé entre le broker et les runners
N8N_RUNNERS_AUTH_TOKEN=CHANGEZ_MOI
Cybersécurité — les points à ne pas négliger
Parce qu’un n8n mal sécurisé c’est une porte d’entrée sur tout ton réseau. Voici les points critiques à avoir en tête.
La N8N_ENCRYPTION_KEY : ta responsabilité numéro 1
Cette clé chiffre tous les credentials que tu stockes dans n8n — tokens API, mots de passe, clés SSH, tout. Elle n’est pas stockée dans PostgreSQL, elle est sur le volume n8n.
👉 Règle absolue : génère-la une fois au démarrage, stocke-la dans un gestionnaire de mots de passe (Bitwarden, KeePass…), et ne la change jamais. Si tu la changes ou si tu la perds, tous tes credentials deviennent des données chiffrées illisibles. Tu dois tout re-saisir à la main.
Redis et PostgreSQL non exposés
Si tu n’as pas besoin d’accéder à Redis ou PostgreSQL depuis l’extérieur des conteneurs, retire carrément les lignes ports dans le docker-compose. Moins de surface exposée, c’est moins de risques. Les conteneurs Docker communiquent entre eux via le réseau interne sans avoir besoin d’exposer les ports sur l’hôte.
N8N_SECURE_COOKIE et HTTPS
Si tu exposes n8n sur internet derrière un reverse proxy (Nginx, Traefik, Caddy), passe N8N_SECURE_COOKIE=true. Ce flag force les cookies de session à être transmis uniquement en HTTPS. En HTTP local, tu peux le laisser à false.
Les runners : le mode externe c’est pas juste du confort, c’est une vraie barrière de sécurité
Quand quelqu’un écrit du code dans un Code node n8n — que ce soit toi ou un utilisateur de ton instance — ce code ne sait pas qu’il tourne dans n8n. Il s’exécute dans un conteneur séparé, sans accès à ta base de données, sans accès à tes variables d’environnement n8n, sans accès au réseau hôte.
Si tu laisses les runners en mode interne (le mode par défaut), le code utilisateur tourne dans le même process que n8n. C’est exactement comme si tu laissais un inconnu ouvrir un terminal sur ton serveur. Le mode externe crée une vraie frontière d’isolation — pas une option de confort, une décision de sécurité.
Le N8N_RUNNERS_AUTH_TOKEN est le secret partagé qui permet aux runners de s’authentifier auprès du broker. Sans lui, n’importe quel process qui essaierait de se connecter au broker serait rejeté.
Le .env ne va jamais dans Git
Ton .env contient des secrets en clair. Il doit impérativement être dans ton .gitignore. Si tu pousses ce fichier dans un dépôt public — même une seconde — considère tous tes secrets compromis et régénère-les immédiatement.
🔒 Les flags
--disallow-code-generation-from-stringset--disable-proto=deletedans len8n-task-runners.jsonbloquent des techniques d’injection de code connues. Les runners exécutent du code utilisateur — autant les blinder au maximum.
Le script de démarrage — génération automatique des secrets
Plutôt que de générer les clés à la main et de les coller dans le .env, voici un script bash qui s’en charge automatiquement. Il détecte les variables vides ou marquées CHANGEZ_MOI et génère une valeur sécurisée avec openssl rand.
#!/bin/bash
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
clear
echo -e "${BLUE}══════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} N8N STACK - Initialisation et démarrage ${NC}"
echo -e "${BLUE}══════════════════════════════════════════════════════════${NC}"
echo ""
# Vérifications des prérequis
info "Vérification des prérequis..."
command -v docker >/dev/null 2>&1 || { error "Docker requis"; exit 1; }
docker compose version >/dev/null 2>&1 || { error "Docker Compose requis"; exit 1; }
info "✓ Prérequis OK"
# Génération des secrets si nécessaire
declare -A secrets=(
[N8N_ENCRYPTION_KEY]=64
[N8N_JWT_SECRET]=64
[POSTGRES_PASSWORD]=32
[REDIS_PASSWORD]=32
[N8N_RUNNERS_AUTH_TOKEN]=64
)
for key in "${!secrets[@]}"; do
val=$(grep -E "^$key=" .env 2>/dev/null | cut -d'=' -f2-)
if [ -z "$val" ] || [ "$val" == "CHANGEZ_MOI" ]; then
length=${secrets[$key]}
newval=$(openssl rand -hex $((length/2)))
sed -i "s|^$key=.*|$key=$newval|" .env
info "✓ $key généré automatiquement"
fi
done
# Build de l'image custom des runners
info "Build de l'image n8n-runners custom..."
docker compose build runners
info "✓ Image runners construite"
# Démarrage de la stack
info "Démarrage de la stack..."
docker compose up -d
sleep 15
# Vérification de l'état des conteneurs
info "État des conteneurs :"
docker compose ps
echo ""
info "✓ Stack démarrée !"
echo ""
info "Accès aux services :"
echo " - n8n : http://localhost:5678"
echo ""
info "Pour scaler les workers :"
echo " docker compose up -d --scale n8n-worker=3"
echo ""
info "Commandes utiles :"
echo " - Logs : docker compose logs -f n8n"
echo " - Arrêter : docker compose down"
Le script est idempotent — tu peux le lancer plusieurs fois sans problème. S’il voit que la valeur est déjà définie, il ne la touche pas.
Mettre à jour les librairies du runner — sans tout casser
C’est l’une des opérations les plus courantes une fois la stack en place. Tu découvres une nouvelle lib Python que tu veux utiliser dans tes Code nodes — httpx, beautifulsoup4, openai, peu importe. Voilà la procédure complète.
Étape 1 : modifier le Dockerfile.runners
Ouvre ton Dockerfile.runners et ajoute ta librairie dans le bloc uv pip install :
RUN cd /opt/runners/task-runner-python && uv pip install \
markitdown langchain yt-dlp numpy pandas \
spacy requests pdfplumber PyPDF2 tika \
pytesseract pillow pdf2image charset-normalizer \
httpx \ # ← nouvelle lib ajoutée ici
beautifulsoup4 # ← et une autre
Étape 2 : mettre à jour la whitelist si nécessaire
Si tu veux que ta lib soit utilisable dans les Code nodes, elle doit aussi être dans la whitelist N8N_RUNNERS_EXTERNAL_ALLOW côté n8n et n8n-worker dans le docker-compose. Dans notre config on a mis * — tout est autorisé. En production tu peux restreindre à la liste exacte des libs dont tu as besoin.
Étape 3 : rebuilder l’image avec –no-cache
Le --no-cache est important ici. Sans lui, Docker peut réutiliser des couches cachées et ne pas réinstaller les nouvelles libs. Avec des Dockerfiles qui font du pip install, le cache peut jouer des tours.
docker compose build --no-cache runners
Étape 4 : redémarrer les runners sans tout couper
Bonne nouvelle, tu n’as pas besoin de couper n8n ou les workers pour ça. Les runners sont des sidecars indépendants. Tu peux les redémarrer seuls :
# --force-recreate garantit que la nouvelle image est utilisée
docker compose up -d --force-recreate runners n8n-worker-runners
# Vérification : la lib est bien dans le runner ?
docker exec n8n-runners \
/opt/runners/task-runner-python/.venv/bin/pip list | grep httpx
Si tu vois ta lib dans la liste → c’est bon. Tu peux maintenant l’utiliser dans n’importe quel Code node Python de tes workflows, sans redémarrer n8n.
Résumé de l’architecture
Pour bien visualiser comment tout ça s’articule :
Chaque flèche représente une dépendance réelle. Si un maillon manque, quelque chose ne fonctionne pas — et souvent en silence, ce qui rend le diagnostic difficile.
Conclusion
Un n8n de production sous Docker c’est 6 conteneurs avec des rôles bien distincts. Pas parce qu’on aime la complexité, mais parce que chaque brique répond à un problème réel qu’on rencontre dès qu’on dépasse le stade « test sur le coin du bureau ».
La logique c’est :
- PostgreSQL → pour la concurrence et le queue mode
- Redis → pour distribuer le travail entre workers
- n8n main → uniquement l’orchestration, pas l’exécution
- n8n-worker → uniquement l’exécution
- Runners → isoler le code utilisateur du reste
- Dockerfile custom → avoir les librairies Python dont tu as besoin
Une fois que t’as compris le rôle de chaque brique, le docker-compose devient beaucoup plus lisible et surtout beaucoup plus facile à maintenir et à faire évoluer.



Laisser un commentaire