Pourquoi self-héberger Windmill sur un VPS
Windmill s'exécute en continu sur votre serveur et expose chacun de vos scripts comme un endpoint REST authentifié par jeton, une tâche planifiée ou une interface auto-générée. Contrairement à une fonction serverless, il n'y a pas de démarrage à froid ni de limite d'exécution imposée par un tiers. Vos scripts Python, TypeScript ou Bash accèdent directement aux bases de données et aux services internes de votre VPS, sans traverser une passerelle cloud. Le backend est écrit en Rust : des serveurs d'API sans état et des workers qui dépilent les jobs d'une file stockée dans PostgreSQL, ce qui permet d'ajouter des workers sans toucher au reste.
Le choix se fait surtout sur la façon de travailler. Si votre besoin est de relier des SaaS entre eux sur un canevas visuel, n8n reste plus direct ; pour des processus métier très longs à garantie de reprise, Temporal est taillé pour ça. Windmill vise les équipes qui écrivent déjà du code et veulent le rendre exécutable, planifiable et utilisable par des non-développeurs en quelques minutes. Côté licence, le cœur est en AGPLv3 et l'image Community Edition est gratuite, avec des quotas et des fonctions réservées à l'édition Enterprise ; la revendre ou la proposer comme service managé à vos clients exige une licence commerciale.
Bénéfices concrets du self-hosting
- Aucune facturation à l'exécution : des milliers de jobs par jour sans facture d'infonuagique.
- Accès direct aux bases et services internes du VPS, sans tunnel.
- Secrets et clés API stockés chiffrés dans Windmill et jamais sortis de votre serveur.
- Formulaires d'entrée et endpoints REST générés automatiquement pour chaque script, à partir de la signature de sa fonction
main. - Workers additionnels ajoutables à chaud pour absorber les pics de charge.
- Plusieurs workspaces isolés sur une même instance : un par équipe, par projet ou par environnement.
- Versionnement : chaque déploiement d'un script est conservé, et Git Sync pousse le workspace vers un dépôt Git (inclus en Community Edition pour les workspaces de 2 utilisateurs au plus).
Prérequis techniques
Windmill nécessite au minimum 2 vCPU et 4 Go de RAM. La stack officielle démarre PostgreSQL 18, le serveur, trois workers généralistes plafonnés à 2 Go de mémoire chacun, un worker natif pour les jobs légers, le service windmill_extra (autocomplétion LSP et débogueur de l'éditeur) et un Caddy en frontal : sur 4 Go, réduisez donc le nombre de workers. Comptez 20 Go de disque pour la base, les logs et le cache de dépendances des workers, qui grossit avec chaque paquet pip ou npm utilisé. Ubuntu 22.04/24.04 ou Debian 12 avec Docker Engine et le plugin Docker Compose v2 suffisent, plus un nom de domaine pointé sur le VPS pour le certificat TLS. Aucun GPU n'est requis. Si vous prévoyez d'exécuter des workers Python lourds (ML, pandas sur de grands jeux de données), optez pour 4 vCPU et 8 Go de RAM au moins.
Dimensionner le VPS selon l'usage
- Règle de l'éditeur : environ 1 worker par vCPU et 1 à 2 Go de RAM par worker. Un job qui dépasse la limite mémoire de son worker (2048 Mo par défaut) est tué avec le code de sortie 137.
- Usage solo ou essai : 2 vCPU, 4 Go de RAM, 20 Go de disque, avec un seul worker généraliste en plus du worker natif. Suffisant pour des scripts planifiés et quelques webhooks.
- Petite équipe : 4 vCPU, 8 Go de RAM, 40 Go de disque. Les trois workers du fichier officiel et PostgreSQL tiennent sur le même hôte sans saturer la mémoire.
- Plusieurs équipes ou jobs Python lourds : 8 vCPU, 16 Go de RAM, 80 Go de disque pour six à huit workers. À ce stade, une base PostgreSQL séparée simplifie les sauvegardes et la montée de version.
Déployer Windmill avec Docker Compose
Récupérer les fichiers officiels
Connectez-vous en SSH à votre VPS et téléchargez les trois fichiers de référence publiés dans le dépôt Windmill : le
docker-compose.yml, leCaddyfiledu reverse proxy et le.env:mkdir -p /opt/windmill && cd /opt/windmill curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/.env -o .envInutile de cloner tout le dépôt. Ces fichiers suivent la branche
main: relisez-les avant chaque mise à jour, car la stack évolue (passage à PostgreSQL 18, servicewindmill_extra…).Renseigner le fichier .env
Le
.envne porte que deux variables. Remplacez le mot de passe par défaut de PostgreSQL et épinglez l'image sur une version publiée plutôt que surmain(1.864.1 au 06/11/2026, à vérifier sur la page des releases GitHub) :DATABASE_URL=postgres://postgres:MOT_DE_PASSE_FORT@db/windmill?sslmode=disable WM_IMAGE=ghcr.io/windmill-labs/windmill:1.864.1Reportez le même mot de passe dans
POSTGRES_PASSWORD, sous le servicedbdudocker-compose.yml. L'hôtedbest le nom du service PostgreSQL dans la stack : ne le remplacez pas parlocalhost. Pour utiliser une base existante, pointezDATABASE_URLvers elle et passez le servicedbàreplicas: 0.Démarrer la stack
Lancez l'ensemble et suivez le démarrage du serveur :
docker compose up -d docker compose ps docker compose logs -f windmill_serverdocker compose psdoit listerdb,windmill_server, trois réplicas dewindmill_worker,windmill_worker_native,windmill_extraetcaddy; l'indexeur de recherche plein texte (windmill_indexer) reste à zéro réplica, c'est une fonction Enterprise. Le serveur attend que PostgreSQL réponde à son healthcheck, puis applique lui-même les migrations de schéma. Il écoute sur le port 8000 du réseau Docker, sans être publié sur l'hôte : c'est Caddy qui reçoit le trafic.Configurer le reverse proxy et le TLS
Le plus simple est de laisser le Caddy fourni obtenir le certificat Let's Encrypt. Dans le service
caddydudocker-compose.yml, décommentez le port 443 et remplacezBASE_URL=":80"par votre domaine, puis relancezdocker compose up -d:caddy: ports: - 80:80 - 25:25 - 443:443 environment: - BASE_URL=windmill.example.comGardez le port 80 ouvert pour la validation. Si un nginx sert déjà d'autres sites sur le VPS, supprimez le service
caddy, publiez127.0.0.1:8000:8000surwindmill_serveret127.0.0.1:3000:3000surwindmill_extra, puis reproduisez le routage duCaddyfile: les chemins/ws/,/ws_mp/et/ws_debug/vont verswindmill_extra, tout le reste vers le serveur.location ~ ^/ws(_mp|_debug)?/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }Les en-têtes
UpgradeetConnectionsont indispensables : l'éditeur passe par des WebSockets pour l'autocomplétion et le débogage. Le certificat s'obtient ensuite aveccertbot --nginx -d windmill.example.com.Se connecter et sécuriser
Ouvrez
https://windmill.example.comet connectez-vous avec le compte superadmin par défaut,[email protected]/changeme. L'assistant de premier lancement propose de le remplacer par votre propre compte : faites-le, car si vous sautez l'étape, les identifiants par défaut restent actifs, et c'est la faille la plus courante sur les instances Windmill exposées publiquement. Dans les Instance settings, fixez ensuite la Base URL sur l'adresse publique enhttps://: Windmill s'en sert pour construire les URL des webhooks et les liens de retour des flows. Créez enfin votre premier workspace avec un identifiant court (prod,equipe-data).Ajuster le nombre de workers
Le fichier officiel démarre trois réplicas de
windmill_worker, chacun plafonné à 2048 Mo. Sur un VPS de 4 Go, descendez à un ou deux ; sur une machine plus grosse, montez à un worker par vCPU :docker compose up -d --scale windmill_worker=2 docker compose ps windmill_workerPour rendre le réglage permanent, modifiez
deploy.replicasdu service dans ledocker-compose.yml. Le serveur n'a pas besoin de redémarrer : les workers se déclarent eux-mêmes et apparaissent dans la page Workers de l'interface, avec leur groupe (defaultounative).Mettre à jour Windmill
Changez le tag dans
WM_IMAGEaprès avoir lu les notes de version, puis :docker compose pull docker compose up -d docker image pruneLe
pulltélécharge les nouvelles images pendant que les anciens conteneurs tournent encore ; au redémarrage, le serveur migre la base tout seul et les workers terminent leurs jobs en cours dans le délai d'arrêt de Docker (30 secondes). Faites une sauvegarde de la base juste avant, et ne lancezdocker image prune -aque si le VPS n'héberge rien d'autre : la commande supprime aussi les images des autres projets.
Configuration après l'installation
Une fois l'instance en ligne, six réglages décident de la façon dont vos scripts tourneront en production. Ils se font tous depuis l'interface, workspace par workspace.
Les réglages essentiels
- Workspaces : un workspace isole scripts, flows, secrets et membres. Créez-en un par équipe ou par environnement plutôt que de tout mélanger, et réservez le rôle superadmin à l'administration de l'instance.
- Variables et secrets : toutes les variables sont chiffrées avec une clé symétrique propre au workspace ; celles marquées secrètes ne s'affichent plus dans l'interface après saisie. C'est l'endroit des jetons d'API et des mots de passe, jamais le code du script.
- Ressources : des objets JSON typés (connexion PostgreSQL, compte SMTP, identifiants OAuth). Un paramètre typé
postgresqldans un script ne propose alors que les ressources de ce type dans le formulaire généré. - Planifications : chaque script ou flow accepte un ou plusieurs schedules au format cron à cinq champs, avec un champ secondes facultatif en tête. Le gestionnaire d'erreur de schedule est une fonction Enterprise ; en Community Edition, surveillez les exécutions en échec depuis la liste des runs.
- Webhooks : un script s'appelle en asynchrone par
/api/w/<workspace>/jobs/run/p/<chemin>(réponse : l'identifiant du job) ou en synchrone par/api/w/<workspace>/jobs/run_wait_result/p/<chemin>(réponse : le résultat). Authentifiez avec l'en-têteAuthorization: Bearer <jeton>plutôt qu'avec?token=, qui finit dans les journaux d'accès. - Git Sync : dans les paramètres du workspace, reliez un dépôt pour y pousser scripts, flows et ressources à chaque déploiement. Gratuit en Community Edition jusqu'à 2 utilisateurs par workspace ; le déploiement automatique depuis Git, lui, est réservé à l'édition Enterprise.
Exemple : un script Python de bout en bout
Ce script interroge une API, garde les éléments actifs et les insère dans PostgreSQL via une ressource Windmill. Créez un script Python, collez le code et lancez-le depuis l'éditeur :
# requirements:
# httpx
# psycopg2-binary
from typing import TypedDict
import httpx
import psycopg2
class postgresql(TypedDict):
host: str
port: int
user: str
dbname: str
sslmode: str
password: str
def main(api_url: str, db: postgresql) -> dict:
resp = httpx.get(api_url, timeout=10)
resp.raise_for_status()
rows = [r for r in resp.json() if r.get("active")]
conn = psycopg2.connect(
host=db["host"], port=db["port"], user=db["user"],
password=db["password"], dbname=db["dbname"], sslmode=db["sslmode"],
)
with conn, conn.cursor() as cur:
cur.executemany(
"INSERT INTO items (id, name) VALUES (%(id)s, %(name)s) ON CONFLICT DO NOTHING",
rows,
)
conn.close()
return {"candidates": len(rows), "source": api_url}Les commentaires # requirements: remplacent la déduction automatique des dépendances à partir des imports : utile ici, car l'import psycopg2 se publie sous le paquet psycopg2-binary. Windmill résout ces paquets, écrit un lockfile et les met en cache sur le worker, si bien qu'une même version du script tourne toujours avec les mêmes dépendances. La classe postgresql, nommée comme le type de ressource, fait apparaître un sélecteur de ressources dans le formulaire. Une fois déployé sous le chemin f/etl/import_items, le script s'appelle depuis n'importe quel outil :
curl -X POST "https://windmill.example.com/api/w/prod/jobs/run_wait_result/p/f/etl/import_items" \
-H "Authorization: Bearer $WM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"api_url": "https://api.example.com/items"}'Durcissement de l'instance
Ne publiez jamais les ports 8000 et 3000 sur l'interface publique : seul le reverse proxy doit les atteindre, en local. Sauvegardez la base avec docker compose exec -T db pg_dumpall -U postgres | gzip > /backups/windmill-$(date +%F).sql.gz : le dump doit couvrir tout le cluster, pas la seule base windmill, car Windmill crée des bases annexes et attache ses politiques de sécurité à des rôles de niveau cluster. Ce dump contient vos secrets chiffrés : stockez-le hors du VPS et protégez-le comme un secret. Ne montez le socket Docker dans les workers que si vous faites confiance à tous les utilisateurs de l'instance, puisqu'il donne aux scripts la main sur l'hôte. Enfin, laissez REQUIRE_SIGNED_DEBUG_REQUESTS=true sur windmill_extra : le désactiver sur une instance joignable depuis Internet expose un débogueur capable d'exécuter du code sans authentification.
Dépannage : les erreurs d'un premier déploiement
La plupart des incidents viennent de la base de données, de la mémoire des workers ou du reverse proxy. Les symptômes et leurs corrections :
Symptômes et corrections
- Le conteneur
dbs'arrête aussitôt après une mise à jour : le volume a été créé par PostgreSQL 16 et l'image est passée en 18, qui refuse de le lire. Suivez la procédure de migration de la documentation (dump complet puis restauration), ou épinglezpostgres:16avec son ancien point de montage : la version 16 reste maintenue jusqu'en novembre 2028. - Job interrompu avec le code 137 : il a dépassé la limite mémoire de son worker. Augmentez
resources.limits.memorydu servicewindmill_worker, ou réduisez la taille des données chargées en mémoire par le script. - Serveur ou workers incapables de joindre la base :
DATABASE_URLpointe verslocalhostau lieu du servicedb, ou le mot de passe diffère dePOSTGRES_PASSWORD. Corrigez le.env, puisdocker compose up -dpour recréer les conteneurs. ModuleNotFoundErrorou installation de dépendances en échec : le worker télécharge les paquets à la première exécution et doit joindrepypi.orgetfiles.pythonhosted.orgen HTTPS sortant. Derrière un proxy d'entreprise, les variablesPY_TRUSTED_HOSTetPY_INDEX_CERTrèglent la confiance ; un index privé se déclare dans les Instance settings (édition Enterprise).- Éditeur sans autocomplétion, débogueur muet : le reverse proxy ne route pas
/ws/,/ws_mp/et/ws_debug/verswindmill_extra, ou il supprime les en-têtesUpgradeetConnection. Reprenez le routage duCaddyfileofficiel.
Windmill, n8n ou Temporal
Faites défiler le tableau
| Critère | Windmill | n8n | Temporal |
|---|---|---|---|
| Écriture des workflows | Scripts Python, TypeScript, Go, Bash, SQL… assemblés en flows | Canevas de nœuds ; nœud Code en JavaScript ou Python | Code applicatif via SDK (Go, Java, Python, TypeScript, .NET, PHP, Ruby, Rust) |
| Licence | AGPLv3 pour le cœur ; Community Edition gratuite avec quotas, fonctions Enterprise sous licence | Sustainable Use License (fair-code), fichiers `.ee` sous licence commerciale | MIT pour le serveur |
| Interface | Éditeur web, formulaires et apps internes générés | Éditeur visuel de nœuds | Interface web d'observation ; le code s'écrit hors de l'outil |
| Pour qui | Équipes qui écrivent du code et veulent l'exposer vite | Intégrations visuelles entre SaaS | Processus longs à forte exigence de durabilité |
L'installer en un clic depuis le Marketplace ServOrbit. Si vous préférez éviter la configuration manuelle, le Marketplace ServOrbit provisionne Windmill automatiquement — serveur, deux workers, PostgreSQL 16, reverse proxy nginx et certificat Let's Encrypt — en quelques minutes. La section « Premier accès » de votre espace client indique l'URL et les identifiants générés. La fiche Windmill, accessible depuis le bouton en fin d'article, permet de démarrer.