Auto-héberger n8n et sa stack d'automatisation sur VPS
Monte n8n, Typebot, DocuSeal et MinIO derrière Traefik sur un seul VPS : SSL automatique, supervision, mises à jour maîtrisées et sauvegardes testées.

Fais le compte de ce que tes automatisations coûtent chaque mois. Un abonnement pour l’outil de workflows. Un autre pour le formulaire conversationnel. Un troisième pour la signature électronique. Un quatrième pour stocker les fichiers. Quatre factures, quatre plafonds de plan, et tes données réparties chez quatre entreprises différentes.
L’auto-hébergement, c’est l’idée de faire tourner tout ça sur une seule machine que tu loues, un VPS, pour le prix d’un seul de ces abonnements. Les outils existent en version open source : n8n pour les workflows, Typebot pour les formulaires, DocuSeal pour la signature, MinIO pour les fichiers. Ce qui manque en général, c’est le mode d’emploi qui les fait tenir ensemble, proprement, avec du HTTPS partout et des sauvegardes qui marchent.
C’est exactement ce qu’on va construire pour auto-héberger n8n et ses compagnons : un VPS, Docker, un seul fichier de configuration, et à la fin une stack complète où chaque service a son sous-domaine, son certificat et sa place dans la sauvegarde nocturne. La documentation n8n prévient que l’auto-hébergement demande des connaissances techniques, et elle a raison : c’est le prix de la maîtrise. Cet article paie ce prix une bonne fois, étape par étape, sans rien supposer d’autre que quelques bases de terminal.
Un mot d’honnêteté avant de commencer : tu gagnes la souveraineté et la facture, tu récupères en échange la responsabilité. Les pannes, les mises à jour et les sauvegardes deviennent ton problème. C’est le mouvement du rappeur indépendant qui quitte sa maison de disques pour monter son propre studio : la liberté totale arrive avec la compta et les câbles à brancher soi-même. La seconde moitié de l’article est consacrée à rendre ce problème petit.
Ce qu’il te faut
Avant d’endosser cette responsabilité, vérifie que tu as le nécessaire sous la main :
- un VPS sous Ubuntu LTS, 4 Go de RAM, 2 vCPU, 60 Go de disque
- un nom de domaine dont tu contrôles les DNS
- une adresse email pour Let’s Encrypt
- un accès SSH et l’aisance terminal de base : éditer un fichier, lancer une commande
- 2 à 3 heures pour tout monter, sauvegardes comprises
Chaque élément de cette liste resservira dans les étapes qui suivent, à commencer par le premier, le VPS lui-même, dont le choix se raisonne.
Choisir le VPS et préparer la machine
Le dimensionnement d’abord, parce que c’est la première question et qu’elle se raisonne sans benchmark. Cette stack fait tourner une dizaine de conteneurs : sept services, une base de données, un stockage objet et la supervision. Au repos, chacun consomme entre quelques dizaines et quelques centaines de Mo de mémoire. C’est en exécution que ça se joue : un workflow n8n qui manipule des fichiers, une génération de PDF DocuSeal et une sauvegarde qui tourne en même temps, et une machine de 2 Go se met à échanger sur le disque. D’où le plancher confortable à 4 Go. Au-delà de 8 Go, pour cette stack, tu paies de la marge que tu n’utiliseras pas.
La taille répond à la question du combien, la localisation à celle du où, et elle mérite ses trente secondes de réflexion, surtout si tes utilisateurs ne sont pas tous en Europe. La latence suit la géographie : un datacenter à Paris sert très bien la France et honorablement l’Afrique de l’Ouest, l’inverse est plus rare faute d’offres locales comparables. Et si tes formulaires collectent des données personnelles, l’adresse du datacenter devient une information de conformité que tu inscriras dans ta politique de confidentialité : c’est une des choses que l’auto-hébergement te redonne, savoir précisément où vivent les données, et pouvoir répondre à la question quand on te la pose, sans transpirer.
Taille et emplacement choisis, tu peux commander la machine. Côté système, prends Ubuntu LTS, parce que c’est la distribution que tous les outils de cet article documentent en premier. La première connexion se fait en root, et les trois premiers gestes sécurisent la porte d’entrée :
# Un utilisateur dedie, ta cle SSH, et plus jamais root
adduser deploy
usermod -aG sudo deploy
rsync -a ~/.ssh /home/deploy/ && chown -R deploy:deploy /home/deploy/.ssh
# Pare-feu : SSH, HTTP, HTTPS, rien d'autre
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Une précision qui évitera une mauvaise surprise plus loin : Docker inscrit ses règles réseau directement dans iptables, en dessous d’UFW. Un conteneur qui publie un port est joignable de l’extérieur même si UFW croit le bloquer. La parade de cet article est architecturale plutôt que réglementaire : un seul conteneur publiera des ports, Traefik, et tous les autres n’en publieront aucun. Le pare-feu protège la machine, l’architecture protège les services.
Reste à installer Docker, avec le script officiel, puis à donner la main à ton utilisateur :
curl -fsSL https://get.docker.com | sh
usermod -aG docker deploy
Docker installé, deux compléments valent le quart d’heure qu’ils coûtent, parce qu’ils travailleront pour toi sans plus jamais te demander d’attention. Le premier est fail2ban, qui bannit les adresses IP après quelques tentatives SSH ratées : dès la première heure d’existence, un serveur public reçoit des essais de connexion automatisés en continu, c’est le bruit de fond d’Internet. Le second concerne le système lui-même : les conteneurs seront mis à jour à la main, on y viendra, mais les correctifs de sécurité d’Ubuntu peuvent s’installer seuls.
apt install -y fail2ban unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades # repondre Yes
La configuration par défaut de fail2ban surveille SSH, ce qui couvre le besoin. Un mois plus tard, fail2ban-client status sshd te montrera le nombre d’adresses bannies, et le chiffre suffit en général à convaincre que le quart d’heure était bien investi. Un videur qui ne dort jamais et ne réclame pas de salaire, ça se prend.
Déconnecte-toi, reviens en deploy, et vérifie que docker compose version répond. La machine est prête, on peut dessiner ce qu’on va y poser.
L’architecture cible avant la première ligne de YAML
Avant d’empiler du YAML, il faut une image claire de qui parle à qui, parce que chaque décision de configuration en découle. Le principe tient en trois phrases. Traefik est le seul service exposé sur Internet, sur les ports 80 et 443. Chaque application vit derrière lui, sur son propre sous-domaine, et ne publie aucun port. PostgreSQL et MinIO sont des services internes, partagés par les applications, invisibles de l’extérieur.
Un détail du schéma mérite explication : une seule instance PostgreSQL porte trois bases distinctes, une pour n8n, une pour Typebot, une pour DocuSeal. C’est plus léger que trois serveurs de base de données, et chaque application reste isolée dans sa base. MinIO apparaît derrière Traefik parce que Typebot en a besoin : quand un visiteur téléverse un fichier dans un formulaire, son navigateur parle directement au stockage, qui doit donc être joignable publiquement, en HTTPS.
Chaque flèche qui part de Traefik dans ce schéma correspond à un sous-domaine, et chaque sous-domaine doit exister dans ta zone DNS : pose dès maintenant six enregistrements A vers l’IP du VPS, parce que la propagation prend parfois son temps. Le DNS se commande en avance, comme une pizza un soir de match. Ce sont n8n, typebot, bot (le viewer, celui que verront tes visiteurs), sign, s3 et status. Dans tout l’article, remplace exemple.fr par ton domaine.
Dernier préparatif, le fichier .env qui centralise les secrets. Chaque valeur se génère avec openssl rand -hex 24 :
# /home/deploy/stack/.env
DOMAIN=exemple.fr
ACME_EMAIL=toi@exemple.fr
POSTGRES_PASSWORD=GENERE_MOI
N8N_ENCRYPTION_KEY=GENERE_MOI
TYPEBOT_ENCRYPTION_SECRET=GENERE_MOI_32_CARACTERES
MINIO_ROOT_USER=GENERE_MOI
MINIO_ROOT_PASSWORD=GENERE_MOI
Ce fichier est le coffre de la stack. Il ne se partage pas, il ne se commite nulle part, et on le retrouvera dans la sauvegarde, parce que le perdre coûte plus cher qu’un disque effacé. On a maintenant tout pour ouvrir le docker-compose.yml.
Traefik, la porte d’entrée et le SSL automatique
Tout entre par lui, donc il s’écrit en premier. Traefik fait trois métiers dans cette stack : il route chaque sous-domaine vers le bon conteneur en lisant leurs étiquettes, il redirige tout le HTTP vers le HTTPS, et il obtient puis renouvelle les certificats Let’s Encrypt sans intervention. La documentation Traefik décrit plusieurs types de challenge ACME, on utilise le challenge TLS, le plus simple quand les ports 80 et 443 pointent déjà vers la machine.
# /home/deploy/stack/docker-compose.yml
services:
traefik:
image: traefik:v3.3
restart: unless-stopped
command:
- "--providers.docker=true"
# Aucun conteneur n'est route sans etiquette explicite
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Tout le HTTP part en HTTPS
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
Deux choix méritent leur pourquoi. exposedbydefault=false inverse la logique par défaut : un conteneur n’est jamais accessible via Traefik tant qu’il ne le demande pas par étiquette, donc un service ajouté à la va-vite ne se retrouve pas exposé par accident. Et le socket Docker est monté en lecture seule (:ro), parce que Traefik a besoin de lire l’état des conteneurs, pas de les piloter.
Avant le premier démarrage, crée le fichier de certificats avec les bonnes permissions, Traefik refuse de démarrer sinon :
mkdir -p ~/stack/letsencrypt
touch ~/stack/letsencrypt/acme.json
chmod 600 ~/stack/letsencrypt/acme.json
La porte est posée. Il reste à mettre quelque chose derrière.
n8n et sa base PostgreSQL
Le premier locataire est le plus important, et il arrive avec sa base. La documentation d’installation n8n recommande elle-même ce montage Traefik + compose, on le suit en y ajoutant PostgreSQL, la seule base sérieuse pour une instance qui doit durer.
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
retries: 5
networks:
- interne
n8n:
image: docker.n8n.io/n8nio/n8n:TA_VERSION_ICI
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: n8n.${DOMAIN}
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.${DOMAIN}/
GENERIC_TIMEZONE: Europe/Paris
volumes:
- n8n_data:/home/node/.n8n
labels:
- "traefik.enable=true"
- "traefik.http.routers.n8n.rule=Host(`n8n.${DOMAIN}`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
networks:
- proxy
- interne
Trois variables portent tout le poids de cette configuration. N8N_ENCRYPTION_KEY chiffre tes credentials en base : fournie dès le premier démarrage, elle est connue, sauvegardable, et tu ne dépends pas d’un fichier généré en silence dans le volume. WEBHOOK_URL donne aux webhooks leur adresse publique, sans elle n8n fabrique des URLs en localhost inutilisables. Et le TA_VERSION_ICI de l’image n’est pas une coquetterie : la section sur les mises à jour expliquera pourquoi latest est interdit de séjour ici. Prends la dernière version stable du moment sur la page de releases et écris-la en toutes lettres.
Le depends_on conditionné au healthcheck évite le grand classique du premier démarrage : n8n qui s’élance avant que la base n’accepte les connexions, et qui tombe en erreur pour de mauvaises raisons.
Si tu arrives avec une instance n8n existante sous SQLite ou MySQL, ne recrée pas tout à la main : la procédure complète pour déménager n8n vers PostgreSQL sans perdre les credentials s’applique telle quelle ici, y compris le transport de la clé de chiffrement.
Un docker compose up -d à ce stade te donne déjà un n8n public en HTTPS. Du lourd, pour un copier-coller soigné. Le premier certificat prend quelques dizaines de secondes, le temps du challenge.
MinIO, le S3 de la maison
n8n tourne, mais la stack manipule des fichiers : pièces jointes de formulaires, documents signés, exports. Les stocker dans les volumes des applications les éparpille, et aucun service tiers n’est censé entrer dans l’équation. MinIO répond aux deux besoins : c’est un stockage objet compatible S3, le dépôt officiel fournit l’image, et tout ce qui parle le protocole S3 saura s’en servir, Typebot le premier.
minio:
image: minio/minio:TA_VERSION_ICI
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- minio_data:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.minio.rule=Host(`s3.${DOMAIN}`)"
- "traefik.http.routers.minio.entrypoints=websecure"
- "traefik.http.routers.minio.tls.certresolver=letsencrypt"
- "traefik.http.services.minio.loadbalancer.server.port=9000"
networks:
- proxy
- interne
Note ce qui est exposé et ce qui ne l’est pas. L’API S3, port 9000 interne, sort sur s3.exemple.fr parce que les navigateurs des visiteurs devront y téléverser des fichiers. La console d’administration, port 9001, ne sort pas du tout : pas d’étiquette, pas de port publié, elle n’existe que depuis la machine. Pour l’utiliser ponctuellement, un tunnel SSH suffit, on verra la commande avec Portainer, c’est la même gymnastique.
Après le démarrage, crée le bucket de Typebot depuis le conteneur :
docker compose exec minio mc alias set local http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
docker compose exec minio mc mb local/typebot
docker compose exec minio mc anonymous set download local/typebot
La dernière commande autorise la lecture publique des fichiers du bucket, nécessaire pour que les fichiers téléversés dans un formulaire soient affichables. L’écriture, elle, reste soumise aux clés. Le stockage est prêt, le prochain locataire en a justement besoin.
Typebot, le duo builder et viewer
Typebot arrive en deux conteneurs, et la distinction compte : le builder est ton atelier, là où tu construis les formulaires, le viewer est la vitrine, l’adresse que tes visiteurs verront. La documentation d’auto-hébergement Typebot liste les variables, en voici la traduction dans notre architecture :
typebot-builder:
image: baptistearno/typebot-builder:TA_VERSION_ICI
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://n8n:${POSTGRES_PASSWORD}@postgres:5432/typebot
ENCRYPTION_SECRET: ${TYPEBOT_ENCRYPTION_SECRET}
NEXTAUTH_URL: https://typebot.${DOMAIN}
NEXT_PUBLIC_VIEWER_URL: https://bot.${DOMAIN}
S3_ACCESS_KEY: ${MINIO_ROOT_USER}
S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
S3_BUCKET: typebot
S3_ENDPOINT: s3.${DOMAIN}
labels:
- "traefik.enable=true"
- "traefik.http.routers.typebot.rule=Host(`typebot.${DOMAIN}`)"
- "traefik.http.routers.typebot.entrypoints=websecure"
- "traefik.http.routers.typebot.tls.certresolver=letsencrypt"
- "traefik.http.services.typebot.loadbalancer.server.port=3000"
networks:
- proxy
- interne
typebot-viewer:
image: baptistearno/typebot-viewer:TA_VERSION_ICI
restart: unless-stopped
environment:
DATABASE_URL: postgresql://n8n:${POSTGRES_PASSWORD}@postgres:5432/typebot
ENCRYPTION_SECRET: ${TYPEBOT_ENCRYPTION_SECRET}
NEXT_PUBLIC_VIEWER_URL: https://bot.${DOMAIN}
S3_ACCESS_KEY: ${MINIO_ROOT_USER}
S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
S3_BUCKET: typebot
S3_ENDPOINT: s3.${DOMAIN}
labels:
- "traefik.enable=true"
- "traefik.http.routers.bot.rule=Host(`bot.${DOMAIN}`)"
- "traefik.http.routers.bot.entrypoints=websecure"
- "traefik.http.routers.bot.tls.certresolver=letsencrypt"
- "traefik.http.services.bot.loadbalancer.server.port=3000"
networks:
- proxy
- interne
La base typebot référencée dans DATABASE_URL n’existe pas encore, une commande la crée dans l’instance PostgreSQL déjà en place :
docker compose exec postgres psql -U n8n -c "CREATE DATABASE typebot;"
Reste le point que la documentation Typebot signale et que tout le monde découvre trop tard : il faut configurer au moins un fournisseur d’authentification pour se connecter au builder, par exemple les variables SMTP pour recevoir un lien magique par email, ou un OAuth Google ou GitHub. Sans fournisseur, l’écran de connexion est une porte peinte sur un mur. Ajoute tes variables SMTP au .env et au service builder avant le premier lancement, le détail des noms est dans la page de configuration de la documentation.
Le formulaire de devis construit dans l’article sur la signature électronique de bout en bout commençait précisément ici : un Typebot qui collecte, un n8n qui orchestre. Il manque le troisième acteur de ce pipeline.
DocuSeal, la signature qui reste chez toi
Le document à signer ne quitte plus la machine : DocuSeal est le service le plus simple de la stack, un seul conteneur, et le dépôt officiel fournit son compose de référence. Par défaut il écrit dans une base SQLite locale, on le branche plutôt sur le PostgreSQL partagé, pour que la sauvegarde des bases couvre aussi les signatures :
docker compose exec postgres psql -U n8n -c "CREATE DATABASE docuseal;"
docuseal:
image: docuseal/docuseal:TA_VERSION_ICI
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://n8n:${POSTGRES_PASSWORD}@postgres:5432/docuseal
# Les emails d'invitation a signer partent par ton SMTP
SMTP_ADDRESS: ${SMTP_ADDRESS}
SMTP_PORT: 587
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_DOMAIN: ${DOMAIN}
volumes:
- docuseal_data:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.docuseal.rule=Host(`sign.${DOMAIN}`)"
- "traefik.http.routers.docuseal.entrypoints=websecure"
- "traefik.http.routers.docuseal.tls.certresolver=letsencrypt"
- "traefik.http.services.docuseal.loadbalancer.server.port=3000"
networks:
- proxy
- interne
Le volume docuseal_data reste nécessaire malgré la base externe : les PDF et les pièces y vivent. Sans SMTP, DocuSeal fonctionne mais n’envoie aucune invitation, les signataires devront recevoir leur lien par un autre canal, ton n8n par exemple, qui appelle l’API DocuSeal et distribue les liens lui-même. C’est d’ailleurs le montage du pipeline complet de signature, webhook de retour compris, décrit dans l’article dédié.
À ce stade, la stack de production est complète. Ce qui suit ne fait pas tourner tes automatisations : ça t’évite de les perdre.
Portainer et Uptime Kuma, voir et être prévenu
Une stack qu’on ne voit pas est une stack qu’on découvre en panne. Deux outils couvrent les deux sens du mot « voir » : Portainer montre l’état des conteneurs et permet d’agir dessus, Uptime Kuma surveille les services de l’extérieur et te prévient quand l’un ne répond plus.
Portainer d’abord, avec une décision assumée : il ne sera pas exposé sur Internet. Ce conteneur monte le socket Docker en écriture, c’est-à-dire le pouvoir de créer, modifier et supprimer n’importe quel conteneur de la machine. Une interface pareille derrière un simple mot de passe public, c’est une clé de la maison sous le paillasson.
portainer:
image: portainer/portainer-ce:lts
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
ports:
# Joignable uniquement depuis la machine elle-meme
- "127.0.0.1:9443:9443"
networks:
- interne
Le 127.0.0.1: devant le port fait tout le travail : l’interface n’écoute que sur la boucle locale du VPS. Pour y accéder depuis ton poste, un tunnel SSH projette ce port local chez toi :
ssh -L 9443:localhost:9443 deploy@IP_DU_VPS
# puis dans ton navigateur : https://localhost:9443
Uptime Kuma, lui, a vocation à être accessible, au moins pour toi, et sa page de statut peut même être publique. Le wiki d’installation demande un volume local et signale que l’application repose sur WebSocket : derrière Nginx il faudrait ajouter des en-têtes, Traefik les transmet nativement, rien à configurer.
kuma:
image: louislam/uptime-kuma:1
restart: unless-stopped
volumes:
- kuma_data:/app/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.kuma.rule=Host(`status.${DOMAIN}`)"
- "traefik.http.routers.kuma.entrypoints=websecure"
- "traefik.http.routers.kuma.tls.certresolver=letsencrypt"
- "traefik.http.services.kuma.loadbalancer.server.port=3001"
networks:
- proxy
Au redémarrage, status.tondomaine sert cette page, et c’est le premier écran qui te dira quelque chose de ta stack sans que tu ouvres un terminal.

Les frises vertes de la colonne de gauche portent l’essentiel : chaque barre est un contrôle, et un trou dans la frise raconte une panne passée que personne ne t’avait signalée sur le moment.
Crée un moniteur HTTPS par sous-domaine, plus un moniteur sur chaque webhook critique de ta production, celui qui reçoit les paiements par exemple. Branche les notifications vers l’email ou le canal que tu lis vraiment. Un moniteur dont l’alerte part dans un canal que personne ne consulte est une veilleuse dans une pièce vide.
Le compose est maintenant complet, et il se termine par la déclaration que tous les services référencent depuis le début, les réseaux et les volumes nommés :
networks:
# Tout ce que Traefik route
proxy:
# Tout ce qui parle a la base ou au stockage, invisible de l'exterieur
interne:
volumes:
postgres_data:
n8n_data:
minio_data:
docuseal_data:
portainer_data:
kuma_data:
diun_data:
Le fichier entier figure dans les blocs de code de cette page, tu peux le reconstituer à la main sans rien télécharger.
Une archive te fait gagner la recopie : le docker-compose.yml complet avec ses versions épinglées et ses deux réseaux séparés, plus le modèle de variables d’environnement. Elle est dans l’espace ressources de la communauté, l’accès est gratuit : récupérer l’archive de la stack.
Le compose contient volontairement des balises REPLACE_WITH_... à remplacer par des versions explicites avant le premier lancement. Il faut aussi choisir le fournisseur d’authentification du builder Typebot qui correspond à cette version. Lance d’abord PostgreSQL, crée les bases typebot et docuseal avec les deux commandes précédentes, puis démarre la stack entière.
Ces lignes ferment le fichier. La séparation en deux réseaux n’est pas décorative : un conteneur qui n’est que sur interne ne peut être joint ni depuis Internet ni via Traefik, quoi qu’il arrive à sa configuration. C’est une seconde ceinture après exposedbydefault=false, et les ceintures ne se cumulent jamais en trop sur un serveur public.
Les mises à jour : épingler, être notifié, ne jamais suivre latest
La supervision te dit quand ça casse, la politique de mise à jour détermine combien de fois ça cassera. Et ici, l’actualité impose un détour : l’outil que la moitié des tutoriels recommande encore, Watchtower, qui tirait automatiquement les nouvelles images et redémarrait les conteneurs, a été archivé en décembre 2025. Plus de correctifs, y compris de sécurité. Si tu l’utilises, son remplacement fait partie de ta prochaine maintenance.
Je n’ai jamais aimé l’auto-pull aveugle sur une stack de production, et l’archivage de Watchtower clôt le débat à ma place. Le scénario qu’il permettait est exactement celui qu’on ne veut pas : une image majeure sort pendant la nuit, tes conteneurs redémarrent dessus sans sauvegarde préalable, et tu découvres au réveil une migration de base à moitié passée. Les utilisateurs de n8n qui ont vécu le passage à la v2 voient très bien de quoi je parle, c’était le sujet de l’article sur la migration Postgres. Laisser un robot mettre à jour ta prod pendant que tu dors, très peu pour moi.
La stratégie saine tient en trois règles. Un, chaque image du compose porte une version explicite, c’est le TA_VERSION_ICI répété depuis le début, et jamais latest. Deux, un outil surveille les registres et te notifie quand une nouvelle version existe, sans rien toucher. Trois, c’est toi qui mets à jour, quand tu l’as décidé, après une sauvegarde.
L’outil de la règle deux s’appelle Diun, il ne sait rien faire d’autre que notifier, et c’est sa qualité première :
diun:
image: crazymax/diun:TA_VERSION_ICI
restart: unless-stopped
volumes:
- diun_data:/data
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
TZ: Europe/Paris
# Verifie les registres toutes les six heures
DIUN_WATCH_SCHEDULE: "0 */6 * * *"
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
# Notification par email, d'autres canaux existent
DIUN_NOTIF_MAIL_HOST: ${SMTP_ADDRESS}
DIUN_NOTIF_MAIL_PORT: 587
DIUN_NOTIF_MAIL_USERNAME: ${SMTP_USERNAME}
DIUN_NOTIF_MAIL_PASSWORD: ${SMTP_PASSWORD}
DIUN_NOTIF_MAIL_FROM: diun@${DOMAIN}
DIUN_NOTIF_MAIL_TO: ${ACME_EMAIL}
networks:
- interne
Et la règle trois devient une procédure en cinq gestes, toujours les mêmes :
cd ~/stack
./backup.sh # 1. sauvegarde, le script arrive juste apres
sed -i 's/n8n:1.x.y/n8n:1.x.z/' docker-compose.yml # 2. changer la version epinglee
docker compose pull n8n # 3. tirer la nouvelle image
docker compose up -d n8n # 4. redemarrer ce service seul
docker compose logs -f n8n # 5. regarder les logs jusqu'a etre rassure
Service par service, jamais toute la stack d’un coup, et en commençant par lire les notes de version : c’est là que se cachent les changements de variables et les migrations de schéma. Épictète rangeait les événements en deux catégories, ce qui dépend de nous et ce qui n’en dépend pas : la sortie d’une version majeure n’en dépend pas, le moment où elle entre chez toi, si.
La stratégie de sauvegarde qui te sauvera vraiment
La procédure de mise à jour commence par ./backup.sh, il est temps de l’écrire. Une sauvegarde de cette stack couvre quatre choses, et l’oubli d’une seule rend les trois autres décoratives : les bases PostgreSQL, les volumes des applications, le fichier des certificats, et le .env, parce qu’un dump de base n8n sans N8N_ENCRYPTION_KEY est un coffre-fort récupéré sans sa combinaison.
#!/usr/bin/env bash
# /home/deploy/stack/backup.sh
set -euo pipefail
STAMP=$(date +%F)
DEST=/home/deploy/backups/$STAMP
mkdir -p "$DEST"
cd /home/deploy/stack
# 1. Les trois bases, un dump chacune
for base in n8n typebot docuseal; do
docker compose exec -T postgres pg_dump -U n8n -d "$base" \
| gzip > "$DEST/$base.sql.gz"
done
# 2. Les volumes applicatifs, a froid pour les fichiers en cours d'ecriture
docker compose stop n8n docuseal
for vol in n8n_data docuseal_data minio_data kuma_data; do
docker run --rm -v stack_${vol}:/source:ro -v "$DEST":/dest alpine \
tar czf "/dest/${vol}.tar.gz" -C /source .
done
docker compose start n8n docuseal
# 3. Les certificats et le coffre
cp letsencrypt/acme.json "$DEST/"
cp .env "$DEST/env.backup"
# 4. Rotation locale : sept jours
find /home/deploy/backups -maxdepth 1 -mtime +7 -exec rm -rf {} +
Le docker compose stop avant l’archivage des volumes coûte une ou deux minutes d’interruption par nuit. C’est le prix d’archives cohérentes : un volume copié pendant qu’une application y écrit peut produire une archive qui se restaure mal, précisément le jour où tu en auras besoin. Programme le tout à une heure creuse :
crontab -e
# 0 3 * * * /home/deploy/stack/backup.sh >> /home/deploy/backups/backup.log 2>&1
Il reste le point que ce script ne règle pas : tout est encore sur le VPS. Un disque qui meurt ou un compte compromis emporte la stack et ses sauvegardes ensemble. La copie doit sortir de la machine, rclone fait ça vers n’importe quel stockage distant, y compris un second MinIO chez un autre hébergeur. Le flux complet :
Et l’étape que presque tout le monde saute : restaurer, une vraie fois, sur un VPS jetable loué une heure. La procédure mérite d’être écrite noir sur blanc, parce que c’est elle que tu suivras un jour de stress :
# Sur la machine de test : Docker installe, dossier stack/ recopie
# (docker-compose.yml, .env restaure, letsencrypt/ vide)
cd ~/stack
docker compose up -d postgres
for base in n8n typebot docuseal; do
docker compose exec -T postgres psql -U n8n -c "CREATE DATABASE $base;" || true
gunzip -c /chemin/sauvegarde/$base.sql.gz \
| docker compose exec -T postgres psql -U n8n -d "$base"
done
# Les volumes : recreer puis reinjecter les archives
docker run --rm -v stack_n8n_data:/dest -v /chemin/sauvegarde:/src alpine \
tar xzf /src/n8n_data.tar.gz -C /dest
# repeter pour docuseal_data, minio_data, kuma_data
docker compose up -d
Le verdict du test tient en une vérification : ouvrir n8n sur la machine de test et constater qu’une credential existante se déchiffre. Si elle se déchiffre, ta clé, ta base et tes volumes se sont retrouvés, et la chaîne complète fonctionne. Une sauvegarde jamais restaurée est une hypothèse, et le jour d’un incident est le pire moment pour tester une hypothèse.
Ce qui va te bloquer
La sauvegarde testée, il reste les frictions du montage lui-même. Sept obstacles reviennent sur cette stack, avec leur symptôme exact. On va droit au message d’erreur et à la commande qui répare, sans détour touristique.
Les permissions d’acme.json. Traefik démarre puis refuse de gérer les certificats, avec ce message dans docker compose logs traefik :
unable to get ACME account: permissions 644 for acme.json are too open, please use 600
Le chmod 600 de la section Traefik règle le cas. Si le fichier a été recréé par un up malheureux, refais-le.
Le port 80 déjà occupé. Sur un VPS où un Apache ou un Nginx traînait déjà :
Error response from daemon: failed to bind host port 0.0.0.0:80: address already in use
Identifie l’occupant avec sudo ss -tlnp | grep :80, désactive-le, relance. Ne déplace pas Traefik sur un autre port : Let’s Encrypt attend le 80 pour ses redirections.
Les webhooks en localhost. n8n fonctionne, mais chaque webhook affiche une URL en localhost:5678 qu’aucun service externe ne peut appeler. C’est la signature d’un WEBHOOK_URL absent ou faux. Corrige la variable, recrée le conteneur, et vérifie que l’URL affichée commence par ton domaine.
Le cookie sécurisé refusé. Tu ouvres n8n par l’IP du serveur, avant que le DNS ne pointe, et la page t’arrête sur un message commençant par « Your n8n server is configured to use a secure cookie ». Rien à désactiver : ouvre l’interface par son sous-domaine HTTPS, celui pour lequel tout est configuré. La variable qui contourne ce contrôle existe, et c’est précisément pour ça qu’il ne faut pas s’en servir.
Le builder Typebot qui refuse tout le monde. L’écran de connexion s’affiche, l’email ne part jamais, aucun compte ne se crée. Ce n’est pas un bug : aucun fournisseur d’authentification n’est configuré. Ajoute les variables SMTP au builder, ou un OAuth, puis recrée le conteneur.
Le port que UFW croit fermé. Tu ajoutes un service avec ports: "8080:8080" pour tester, UFW ne le mentionne nulle part, et pourtant il répond depuis Internet. C’est le comportement documenté de Docker, qui écrit ses règles sous UFW. Vérifie toujours de l’extérieur, avec un curl depuis une autre machine, et tiens la règle de cette architecture : aucun ports: public en dehors de Traefik, la boucle locale 127.0.0.1: pour le reste.
Les tentatives Let’s Encrypt épuisées. Un DNS pas encore propagé, quelques redémarrages de Traefik pour corriger la configuration, et chaque cycle a redemandé un certificat qui a échoué. Let’s Encrypt limite les échecs de validation répétés sur un même nom, et te voilà à attendre que la fenêtre se rouvre, avec un avertissement de certificat dans le navigateur en attendant. Si tu sais que tu vas tâtonner, fais tes essais contre le serveur de test de Let’s Encrypt en ajoutant temporairement au bloc command de Traefik la ligne du caserver de staging, documentée chez Traefik, puis retire-la une fois la configuration stabilisée : les certificats de staging ne sont pas reconnus par les navigateurs, mais les erreurs de validation n’y comptent pas contre toi.
Les limites
Ces obstacles franchis, la stack tourne. Elle a pourtant un périmètre, autant le connaître avant de s’y installer.
Un VPS unique est un point de défaillance unique. La sauvegarde te protège de la perte de données, pas de l’interruption : si la machine tombe un lundi matin, la stack est indisponible le temps de restaurer ailleurs, une heure dans le meilleur des cas. La haute disponibilité, plusieurs machines, un orchestrateur, une base répliquée, est un autre projet, avec un autre budget de complexité.
La verticalité a un plafond. Quand n8n exécute des centaines de workflows simultanés, le mode queue avec des workers dédiés devient le sujet, et il mérite sa propre machine ou son propre article. Cette stack vise l’atelier d’un indépendant ou d’une petite équipe, pas une plateforme mutualisée.
Enfin, le coût réel de l’auto-hébergement se mesure en attention, bien plus qu’en location de serveur. Compte une petite heure par mois de maintenance honnête : lire les notifications Diun, passer les mises à jour, jeter un œil aux sauvegardes. Si cette heure n’existe pas dans ton mois, rester sur du SaaS est un choix parfaitement rationnel : le même arbitrage entre posséder et louer ses outils qui départage déjà vibe coding et no-code.
Matrice de contrôle avant de dire que c’est en prod
Le périmètre étant posé, reste à prouver que tout ce qui est dedans tient vraiment. Chaque ligne se vérifie en quelques minutes, et l’ensemble fait la différence entre « ça tourne » et « c’est en production ».
| Contrôle | Résultat observable attendu |
|---|---|
| Ouvrir chaque sous-domaine en HTTP | redirection vers HTTPS, cadenas valide |
| Appeler un webhook n8n depuis l’extérieur | exécution visible dans n8n |
| Redémarrer le VPS entier | tous les conteneurs reviennent seuls, Kuma repasse au vert |
| Scanner les ports depuis une autre machine | seuls 22, 80 et 443 répondent |
| Couper un service à la main | notification Kuma reçue dans le canal choisi |
| Attendre un cycle Diun | notification reçue pour une image en retard |
| Restaurer la sauvegarde sur un VPS jetable | n8n s’ouvre et une credential se déchiffre |
Chercher latest dans le compose |
aucune occurrence |
La dernière ligne prend dix secondes, grep latest docker-compose.yml, et elle attrape les raccourcis pris un soir de flemme. La ligne des ports mérite un vrai scan externe, pas une lecture de la configuration : c’est le seul test qui voie ce que voit un attaquant.
Cette machine est maintenant la maison de toutes les briques posées au fil du guide : le pipeline de signature y tourne au complet, et le serveur MCP qui expose tes outils internes s’ajoute au compose en une dizaine de lignes, un service de plus derrière Traefik, une étiquette, un sous-domaine. La stack sert de socle : chaque brique suivante y devient moins chère à poser que la précédente.
Et si ton montage diffère, un autre reverse proxy, un autre hébergeur, une brique en plus, viens le décrire sur le hub lesnocodeurs : les architectures des uns économisent les nuits blanches des autres.


