Guide de déploiement

Installer Windmill sur un VPS : automatisation self-hosted

Déployer sur un VPS Cloud →

Tutoriel

Automatisation

Installer Windmill sur un VPS : automatisation self-hosted

Windmill est une plateforme open source qui transforme du code (Python, TypeScript, Go, Bash, SQL) en workflows, API et interfaces internes. Pensée pour les développeurs, elle combine la souplesse du code et la rapidité du no-code. Self-hébergée sur un VPS, elle devient votre moteur d'automatisation et votre app builder interne, sans dépendre d'un SaaS.

13 min de lecture7 étapes
Sommaire· Pourquoi self-héberger Windmill sur un VPS1/12
  1. 01Pourquoi self-héberger Windmill sur un VPS
  2. 02Bénéfices concrets du self-hosting
  3. 03Prérequis techniques
  4. 04Dimensionner le VPS selon l'usage
  5. 05Déployer Windmill avec Docker Compose
  6. 06Configuration après l'installation
  7. 07Les réglages essentiels
  8. 08Exemple : un script Python de bout en bout
  9. 09Durcissement de l'instance
  10. 10Dépannage : les erreurs d'un premier déploiement
  11. 11Symptômes et corrections
  12. 12Windmill, n8n ou Temporal

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

  1. 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, le Caddyfile du 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 .env

    Inutile 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, service windmill_extra…).

  2. Renseigner le fichier .env

    Le .env ne 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 sur main (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.1

    Reportez le même mot de passe dans POSTGRES_PASSWORD, sous le service db du docker-compose.yml. L'hôte db est le nom du service PostgreSQL dans la stack : ne le remplacez pas par localhost. Pour utiliser une base existante, pointez DATABASE_URL vers elle et passez le service db à replicas: 0.

  3. 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_server

    docker compose ps doit lister db, windmill_server, trois réplicas de windmill_worker, windmill_worker_native, windmill_extra et caddy ; 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.

  4. 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 caddy du docker-compose.yml, décommentez le port 443 et remplacez BASE_URL=":80" par votre domaine, puis relancez docker compose up -d :

      caddy:
        ports:
          - 80:80
          - 25:25
          - 443:443
        environment:
          - BASE_URL=windmill.example.com

    Gardez le port 80 ouvert pour la validation. Si un nginx sert déjà d'autres sites sur le VPS, supprimez le service caddy, publiez 127.0.0.1:8000:8000 sur windmill_server et 127.0.0.1:3000:3000 sur windmill_extra, puis reproduisez le routage du Caddyfile : les chemins /ws/, /ws_mp/ et /ws_debug/ vont vers windmill_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 Upgrade et Connection sont indispensables : l'éditeur passe par des WebSockets pour l'autocomplétion et le débogage. Le certificat s'obtient ensuite avec certbot --nginx -d windmill.example.com.

  5. Se connecter et sécuriser

    Ouvrez https://windmill.example.com et 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 en https:// : 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).

  6. 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_worker

    Pour rendre le réglage permanent, modifiez deploy.replicas du service dans le docker-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 (default ou native).

  7. Mettre à jour Windmill

    Changez le tag dans WM_IMAGE après avoir lu les notes de version, puis :

    docker compose pull
    docker compose up -d
    docker image prune

    Le pull té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 lancez docker image prune -a que 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é postgresql dans 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ête Authorization: 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 db s'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 épinglez postgres:16 avec 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.memory du service windmill_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_URL pointe vers localhost au lieu du service db, ou le mot de passe diffère de POSTGRES_PASSWORD. Corrigez le .env, puis docker compose up -d pour recréer les conteneurs.
  • ModuleNotFoundError ou installation de dépendances en échec : le worker télécharge les paquets à la première exécution et doit joindre pypi.org et files.pythonhosted.org en HTTPS sortant. Derrière un proxy d'entreprise, les variables PY_TRUSTED_HOST et PY_INDEX_CERT rè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/ vers windmill_extra, ou il supprime les en-têtes Upgrade et Connection. Reprenez le routage du Caddyfile officiel.

Windmill, n8n ou Temporal

Faites défiler le tableau

CritèreWindmilln8nTemporal
Écriture des workflowsScripts Python, TypeScript, Go, Bash, SQL… assemblés en flowsCanevas de nœuds ; nœud Code en JavaScript ou PythonCode applicatif via SDK (Go, Java, Python, TypeScript, .NET, PHP, Ruby, Rust)
LicenceAGPLv3 pour le cœur ; Community Edition gratuite avec quotas, fonctions Enterprise sous licenceSustainable Use License (fair-code), fichiers `.ee` sous licence commercialeMIT pour le serveur
InterfaceÉditeur web, formulaires et apps internes générésÉditeur visuel de nœudsInterface web d'observation ; le code s'écrit hors de l'outil
Pour quiÉquipes qui écrivent du code et veulent l'exposer viteIntégrations visuelles entre SaaSProcessus 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.

Déployez Windmill en un clic sur votre VPS ServOrbit

Le Marketplace ServOrbit provisionne automatiquement Windmill avec PostgreSQL 16, deux workers et un reverse proxy — vos scripts tournent en production en quelques minutes, sans ligne de configuration.