Tutoriel

Self-hosting

Héberger Excalidraw sur VPS : tableau blanc sans abonnement

Miro, FigJam, Lucidspark : les tableaux blancs SaaS facturent au siège, stockent vos maquettes sur leurs serveurs et plafonnent les membres par plan. Excalidraw est l'alternative open source (MIT) : un tableau blanc dessiné à la main, collaboratif en temps réel, dont les deux composants — l'interface React et le serveur de salle WebSocket — s'auto-hébergent sur un VPS en deux conteneurs Docker. Ce guide montre comment les déployer, câbler nginx pour les WebSockets, et dit sans détour ce qui reste, dans une installation standard, chez des services tiers (Firebase, partage par lien).

15 min de lecture5 étapes
Sommaire· Pourquoi auto-héberger Excalidraw plutôt que d'utiliser la version en ligne1/16
  1. 01Pourquoi auto-héberger Excalidraw plutôt que d'utiliser la version en ligne
  2. 02Ce que vous gagnez concrètement
  3. 03Architecture : deux conteneurs, rôles distincts
  4. 04Prérequis
  5. 05Checklist prérequis
  6. 06Déployer Excalidraw en 5 étapes
  7. 07Ce qui marche vraiment en auto-hébergement
  8. 08Trois montages possibles, et ce que chacun laisse chez un tiers
  9. 09Variable → fonction → ce qui casse sans elle
  10. 10Vérifier qui reçoit quoi
  11. 11Dépannage : la collaboration ne démarre pas, ou ne se souvient de rien
  12. 12Symptômes, causes, remèdes
  13. 13Persistance des tableaux : ce qui est réellement enregistré
  14. 14Excalidraw auto-hébergé vs alternatives SaaS
  15. 15Aller plus loin : SSO et allowlist d'IP
  16. 16Intégration dans un parc self-hosted

Pourquoi auto-héberger Excalidraw plutôt que d'utiliser la version en ligne

La version publique sur excalidraw.com fonctionne bien pour un dessin rapide en solo. Elle envoie les sessions de collaboration en temps réel vers un serveur géré par Excalidraw s.r.o. C'est acceptable pour un brainstorming ponctuel. Ce l'est beaucoup moins quand les tableaux contiennent des maquettes de flux clients, des schémas d'architecture de systèmes internes ou des wireframes de projets sous NDA.

Auto-héberger Excalidraw déplace le périmètre de contrôle sur votre propre infrastructure. Le temps réel passe par votre serveur WebSocket, sur votre domaine ; ce qui reste chez un tiers dans une installation standard (scène enregistrée, liens de partage) est détaillé plus bas. Vous décidez qui se connecte — par allowlist d'IP, par SSO OIDC ou par HTTP Basic Auth devant nginx. Et comme les deux composants sont publiés sous licence MIT, aucune restriction commerciale ne s'applique quel que soit le nombre de clients ou de collaborateurs que vous faites travailler sur l'instance.

Ce que vous gagnez concrètement

  • Zéro abonnement logiciel — les deux composants (interface et serveur de collaboration) sont MIT. Vous payez le VPS, rien d'autre.
  • Confidentialité des maquettes — le dessin en direct reste sur votre infrastructure. La scène enregistrée et les images d'une salle sont à traiter (Firebase par défaut) : voir « Ce qui marche vraiment en auto-hébergement ».
  • Accès sous votre contrôle — allowlist d'IP, SSO OIDC ou HTTP Basic Auth devant nginx selon le niveau d'isolation requis.
  • Sessions et membres sans quota imposés — pas de plafond par plan ; la seule limite est la RAM du serveur, et excalidraw-room est un service Node.js léger.
  • Persistance optionnelle — montez un volume Docker ou branchez un bucket S3 compatible pour archiver les tableaux exportés en JSON.
  • Intégration dans un parc self-hosted — une instance Excalidraw aux côtés de Penpot, Mattermost ou Docmost ferme le périmètre SaaS de toute une agence.

Architecture : deux conteneurs, rôles distincts

Excalidraw sépare ce que la plupart des gens considèrent comme un seul outil en deux composants indépendants :

- excalidraw/excalidraw — l'interface React compilée, servie comme un site statique par nginx à l'intérieur du conteneur. Elle ne fait tourner aucun serveur applicatif. Ses adresses de services (salle, Firebase, partage) sont des variables VITE_APP_* lues à la compilation et gravées dans le bundle JavaScript : les changer sur un conteneur déjà lancé n'a aucun effet.

- excalidraw/excalidraw-room — un serveur Node.js Socket.IO qui relaie en mémoire les événements de dessin entre les participants d'une même session. Il est stateless : il ne persiste rien et oublie tout au redémarrage. La scène enregistrée d'une salle ne passe pas par lui (voir plus bas). Son image Docker Hub n'a pas été republiée depuis décembre 2023 et son Dockerfile amont part de Node 12 : gardez-le derrière nginx, sur le réseau Docker, sans port publié.

Conséquence pratique : l'image Docker officielle a été construite avec les adresses d'excalidraw.com. Pour viser votre propre excalidraw-room, vous devez construire votre propre image après avoir modifié VITE_APP_WS_SERVER_URL dans .env.production.

Prérequis

Le VPS Solo de ServOrbit (2 vCPU, 4 GB RAM) couvre le cas nominal : l'interface statique est quasi sans charge, et excalidraw-room consomme moins de 256 Mo en régime courant. Avec une dizaine de collaborateurs simultanés, vous resterez largement sous la moitié de la RAM disponible.

Ce qu'il vous faut avant de commencer :

Checklist prérequis

  • Un VPS avec au moins 1 vCPU et 512 Mo de RAM (1 Go recommandé pour une équipe).
  • Docker Engine et Docker Compose Plugin installés (docker compose version pour vérifier).
  • Un nom de domaine pointé vers l'IP du VPS — sans TLS les WebSockets sont bloqués par les navigateurs modernes.
  • Accès SSH root ou un utilisateur avec droits sudo.
  • Un certificat TLS valide — ServOrbit configure Let's Encrypt automatiquement.

Déployer Excalidraw en 5 étapes

  1. Créer le VPS et installer Docker

    Provisionnez un VPS Ubuntu 24.04 depuis l'espace ServOrbit. Une fois connecté en SSH, si Docker n'est pas encore présent, installez-le en une passe :

    curl -fsSL https://get.docker.com | sh

    Vérifiez que Docker Compose Plugin répond avant de continuer :

    docker compose version
  2. Construire l'image avec votre URL de collaboration

    Clonez le dépôt officiel, puis réglez l'adresse de la salle dans le fichier .env.production du dépôt avant de construire. Remplacez collab.yourdomain.com par le sous-domaine alloué à excalidraw-room :

    git clone https://github.com/excalidraw/excalidraw.git
    cd excalidraw
    sed -i 's|^VITE_APP_WS_SERVER_URL=.*|VITE_APP_WS_SERVER_URL=https://collab.yourdomain.com|' .env.production
    docker build -t my-excalidraw:latest .

    Pourquoi pas --build-arg ? Le Dockerfile amont ne déclare aucun ARG pour les variables VITE_APP_* : un --build-arg VITE_APP_WS_SERVER_URL=… n'atteint jamais la commande de build, l'image se construit normalement et continue de viser le serveur d'Excalidraw. Le .dockerignore amont n'admet d'ailleurs que .env.development et .env.production : un .env.production.local serait ignoré. L'adresse est gravée dans le bundle, donc changer de domaine impose de reconstruire.

    Vérifiez le résultat : docker run --rm --entrypoint sh my-excalidraw:latest -c "grep -rl oss-collab.excalidraw.com /usr/share/nginx/html || echo propre" doit afficher propre. L'image officielle excalidraw/excalidraw convient pour des essais locaux, mais elle vise les services publics d'Excalidraw.

  3. Écrire le fichier docker-compose.yml

    Créez un répertoire de travail et posez le fichier suivant. Les deux services écoutent en interne ; nginx (à l'étape suivante) est le seul point d'entrée public :

    mkdir ~/excalidraw-stack && cd ~/excalidraw-stack

    Contenu du docker-compose.yml :

    services:
      excalidraw:
        image: my-excalidraw:latest
        restart: unless-stopped
        networks:
          - excalidraw
    
      excalidraw-room:
        image: excalidraw/excalidraw-room:latest
        environment:
          - CORS_ORIGIN=https://draw.yourdomain.com
        restart: unless-stopped
        networks:
          - excalidraw
    
    networks:
      excalidraw:

    Lancez la stack :

    docker compose up -d
  4. Configurer nginx avec les en-têtes WebSocket

    C'est l'étape où la plupart des déploiements échouent en silence : sans les en-têtes Upgrade et Connection, le navigateur ouvre une connexion HTTP normale au lieu d'un WebSocket, et la collaboration semble ne pas démarrer sans aucun message d'erreur visible.

    Bloc nginx pour le frontend Excalidraw (draw.yourdomain.com) :

    server {
        listen 443 ssl;
        server_name draw.yourdomain.com;
    
        location / {
            proxy_pass http://excalidraw:80;
            proxy_set_header Host $host;
        }
    }

    Bloc nginx pour le serveur de salle (collab.yourdomain.com) :

    server {
        listen 443 ssl;
        server_name collab.yourdomain.com;
    
        location / {
            proxy_pass http://excalidraw-room:80;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_read_timeout 86400s;
        }
    }

    Rechargez nginx (nginx -s reload) après avoir posé les blocs.

  5. Tester la collaboration en temps réel

    Ouvrez https://draw.yourdomain.com dans deux onglets ou deux navigateurs différents. Dans la barre d'outils, cliquez sur Live collaboration et créez une session. Copiez le lien de session dans le second onglet.

    Dessinez quelque chose dans le premier onglet : le trait doit apparaître dans le second en temps réel. S'il n'apparaît pas, vérifiez d'abord les en-têtes WebSocket dans nginx (les devtools réseau du navigateur montrent si la connexion est passée en 101 Switching Protocols). Un status 200 sur la requête WebSocket indique que nginx n'a pas transmis les en-têtes Upgrade.

    Ouvrez ensuite l'onglet réseau du navigateur : la connexion temps réel doit viser collab.yourdomain.com, jamais oss-collab.excalidraw.com. Ce que Firebase reçoit est traité dans la section suivante.

Ce qui marche vraiment en auto-hébergement

Soyons précis : « deux conteneurs et c'est fini » est le raccourci que ce genre de guide répète le plus souvent. Le fil parapluie du dépôt amont (excalidraw/excalidraw, ticket n° 1772, ouvert depuis juin 2020 et encore actif en octobre 2026) résume trois murs, que nous avons recoupés avec le code et les fichiers .env du dépôt.

Premier mur — les adresses sont gravées à la construction. Les adresses du serveur de collaboration, du service de partage et du projet Firebase sont des variables VITE_APP_* lues par Vite au moment du build. L'image officielle excalidraw/excalidraw a été construite avec les valeurs de .env.production : elle parle donc toujours aux services d'excalidraw.com. Une variable passée à docker run n'y change rien, et le Dockerfile amont ne déclare aucun ARG pour en relayer une au build.

Deuxième mur — la collaboration est à moitié locale. Le temps réel (qui dessine quoi, en direct) passe par excalidraw-room, que vous hébergez. Mais la scène enregistrée et les images collées dans une salle partent vers Firebase : le code appelle saveToFirebase et saveFilesToFirebase, avec la configuration de VITE_APP_FIREBASE_CONFIG. Tant que cette variable garde la valeur amont, ces données (chiffrées côté navigateur avec la clé de la salle) sont stockées dans le projet Firebase d'Excalidraw, pas chez vous. C'est ce qui réconcilie les retours contradictoires du fil : l'un constate qu'avec VITE_APP_FIREBASE_CONFIG='{}' la collaboration échoue, un autre que Jitsi Meet a retiré la partie Firebase et que le temps réel marche avec la seule salle. Le direct se passe de Firebase, l'enregistrement et la reprise non.

Troisième mur — les liens de partage exigent un serveur de stockage que le projet ne publie pas. Un lien #json= est créé par une requête vers VITE_APP_BACKEND_V2_POST_URL et relu via VITE_APP_BACKEND_V2_GET_URL, par défaut json.excalidraw.com. Le fil mentionne des réécritures communautaires (une en Next.js sur S3, une autre sur un stockage compatible S3) mais n'en désigne aucune comme officielle.

Trois montages possibles, et ce que chacun laisse chez un tiers

  • Montage A — salle seule (celui de ce guide). Vous changez VITE_APP_WS_SERVER_URL et rien d'autre. Le dessin en direct transite par votre serveur ; la scène enregistrée et les images des salles restent écrites dans le Firebase d'Excalidraw. Acceptable pour des tableaux peu sensibles, à ne pas présenter comme « rien chez un tiers ».
  • Montage B — salle + votre propre projet Firebase. Vous créez un projet Firebase à vous et placez sa configuration JSON dans VITE_APP_FIREBASE_CONFIG. Le code ne lit que cette variable ; nous ne l'avons pas déployé de bout en bout, et Firebase reste un service de Google, pas de l'auto-hébergement au sens strict.
  • Montage C — instance en solo. Vous servez l'interface seule, sans salle ni lien : chacun dessine, exporte en .excalidraw ou en image, et archive les fichiers chez vous. Rien ne part vers un tiers pour la collaboration, puisqu'il n'y en a pas.
  • Ce qui n'existe pas côté officiel. Aucun serveur de stockage ouvert pour les liens #json=, ni une option qui bascule tout le périmètre vers votre domaine par une seule variable à l'exécution. Si votre exigence est « aucune donnée chez un tiers » avec collaboration et partage, la réponse honnête est un travail d'adaptation ou de fork, pas une installation.

Variable → fonction → ce qui casse sans elle

Faites défiler le tableau

VariableFonctionCe qui casse sans elle
`VITE_APP_WS_SERVER_URL` (construction)Adresse du serveur temps réel Socket.IO (`excalidraw-room`).Valeur amont conservée : vos sessions passent par oss-collab.excalidraw.com. Adresse injoignable : la collaboration ne s'établit pas.
`VITE_APP_FIREBASE_CONFIG` (construction)Configuration JSON du projet Firebase : scène enregistrée d'une salle et images collées.Valeur amont : ces données restent chez Excalidraw. Vide ou invalide (`{}` compris) : l'enregistrement et la reprise d'une salle échouent, et selon un retour du fil la collaboration aussi. Un JSON illisible journalise `Error JSON parsing firebase config`.
`VITE_APP_BACKEND_V2_POST_URL` (construction)Création d'un lien de partage `#json=` (envoi de la scène chiffrée).Valeur amont : le lien est créé chez json.excalidraw.com. Adresse sans serveur compatible : la création du lien échoue.
`VITE_APP_BACKEND_V2_GET_URL` (construction)Relecture d'un lien `#json=` à son ouverture.Valeur amont : le lien est lu chez Excalidraw. Adresse sans serveur compatible : l'ouverture du lien échoue.
`VITE_APP_DISABLE_SENTRY` (construction)Désactive l'envoi de rapports d'erreurs à Sentry.Rien ne casse ; sans elle, l'application peut envoyer des rapports d'erreurs à un tiers. Le script `build:app:docker` amont la positionne déjà à `true`.
`VITE_APP_DISABLE_PREVENT_UNLOAD` (construction)Supprime l'alerte avant de quitter une salle dont l'enregistrement n'est pas terminé.Rien ne casse, mais fermer l'onglet n'avertit plus. À réserver au débogage.
`CORS_ORIGIN` (exécution, `excalidraw-room`)Origine autorisée à ouvrir une connexion au serveur de salle.Absente : la valeur par défaut est `*`, n'importe quelle page web peut s'y connecter. Posez `https://draw.yourdomain.com`.
`PORT` (exécution, `excalidraw-room`)Port d'écoute du serveur de salle.Absente : écoute sur le port 80 hors mode développement, ce que supposent les blocs nginx de ce guide. Si vous la changez, le `proxy_pass` doit suivre.

Vérifier qui reçoit quoi

Le seul moyen de savoir ce qui sort de votre instance est de regarder. Ouvrez l'onglet réseau du navigateur, démarrez une session de collaboration, dessinez, collez une image, puis créez un lien de partage. Trois familles de destinations se lisent : votre sous-domaine de salle (le temps réel, attendu) ; les domaines googleapis.com de Firebase (scène et images enregistrées, attendu avec la configuration amont, à éliminer si vous pensiez le contraire) ; json.excalidraw.com (partage par lien). Cette inspection est la recette du montage choisi : sans elle, « sur mon serveur » reste une hypothèse.

Rappelez-vous enfin que le serveur de salle n'authentifie personne : CORS_ORIGIN limite les pages web autorisées à s'y connecter, pas les personnes. L'identité des participants se règle devant l'interface (Basic Auth, SSO), comme décrit plus bas.

Dépannage : la collaboration ne démarre pas, ou ne se souvient de rien

Les symptômes ci-dessous découlent du code et du fil amont ; aucun message n'est reproduit hors de ceux lus dans le dépôt.

Symptômes, causes, remèdes

  • La session part vers oss-collab.excalidraw.com (onglet réseau) alors que vous avez construit votre image. Cause : l'adresse n'a pas été prise en compte à la construction (--build-arg sans effet, .env.production.local ignoré par le .dockerignore). Remède : éditer .env.production, reconstruire, rejouer la vérification grep de l'étape 2. Un changement de domaine suit la même règle.
  • La connexion à la salle est refusée depuis le navigateur. Cause fréquente après avoir posé CORS_ORIGIN : l'origine ne correspond pas exactement à celle de la page (schéma http au lieu de https, port, slash final). Remède : la valeur doit être l'origine exacte de l'interface, par exemple https://draw.yourdomain.com.
  • Les traits passent en direct, mais un participant qui rejoint plus tard, ou après un redémarrage de la salle, ne retrouve pas le contenu ou les images. Cause : la scène et les images sont enregistrées via Firebase, pas par excalidraw-room, qui ne retient rien. Si VITE_APP_FIREBASE_CONFIG est vide ou injoignable, rien n'est relu. Remède : choisir le montage A ou B ci-dessus.
  • Avertissement Error JSON parsing firebase config. Supplied value: dans la console du navigateur. Cause : VITE_APP_FIREBASE_CONFIG n'est pas un JSON valide, le plus souvent à cause de guillemets mal protégés ou d'un retour à la ligne dans le fichier. Remède : JSON sur une seule ligne, entre apostrophes dans .env.production, comme dans le fichier amont.
  • Le partage par lien échoue, ou le lien produit pointe vers json.excalidraw.com. Cause : VITE_APP_BACKEND_V2_POST_URL et VITE_APP_BACKEND_V2_GET_URL n'ont pas été changées, ou visent un serveur qui n'implémente pas l'API attendue. Remède : garder le défaut en connaissance de cause, ou échanger des fichiers .excalidraw.
  • Erreur de chiffrement ou de génération de clé sur un réseau local en http://. Cause : l'API de chiffrement du navigateur (window.crypto) n'est exposée qu'en contexte sécurisé ; le fil amont signale ce piège pour l'auto-hébergement en réseau local. Remède : servir l'interface en HTTPS (ou sur localhost), comme l'exige le prérequis TLS plus haut.

Persistance des tableaux : ce qui est réellement enregistré

Par défaut, excalidraw-room est entièrement stateless : si le serveur redémarre, le relais repart de zéro. La scène enregistrée d'une salle vit ailleurs (Firebase, voir plus haut), et chaque participant garde en plus son dessin dans le localStorage de son navigateur.

Option 1 — Export manuel de fichiers .excalidraw. Le menu d'export de l'interface produit un fichier .excalidraw (ou une image) qui se réimporte à tout moment. Excalidraw n'expose pas d'API d'export à appeler depuis un cron : c'est un geste fait dans le navigateur, dont vous déposez ensuite le fichier dans un volume ou un dépôt git.

Option 2 — Bucket S3 compatible pour l'archivage. ServOrbit propose un stockage objet compatible S3. Copiez-y les fichiers exportés avec l'outil de votre choix (rclone, aws s3 cp) ; la restauration consiste à réimporter le dernier fichier. Excalidraw lui-même n'écrit pas dans S3.

Excalidraw auto-hébergé vs alternatives SaaS

Faites défiler le tableau

CritèreExcalidraw (self-hosted)MiroFigJam
Coût par utilisateurAucun (VPS seul)Payant par siègePayant par siège
Données hébergées chezVotre infrastructure (temps réel) ; Firebase d'Excalidraw par défaut pour la scène enregistréeMiro Inc.Adobe / Figma
Membres simultanésRAM seuleLimité par planLimité par plan
Licence logicielleMIT (open source)PropriétairePropriétaire
Auto-hébergeablePartiellement : deux conteneurs, plus Firebase et partage par lien à traiterNonNon
Contrôle d'accèsIP / SSO / Basic AuthSSO (plans Pro)SSO (plans Edu+)

Aller plus loin : SSO et allowlist d'IP

Pour une agence dont les clients ont des exigences de confidentialité strictes, exposer Excalidraw sur un sous-domaine public n'est pas toujours acceptable. Deux configurations courantes permettent de durcir l'accès sans modifier Excalidraw lui-même.

HTTP Basic Auth via nginx. Ajoutez une directive auth_basic devant le bloc location / du frontend. Toute personne qui tente d'ouvrir l'interface devra s'authentifier avant même que l'app React se charge. Simple à mettre en place, suffisant pour des projets internes.

Allowlist d'IP. Si les collaborateurs se connectent depuis des IP fixes ou depuis un VPN d'entreprise, un bloc allow/deny dans nginx suffit à fermer l'instance au reste d'Internet :

allow 203.0.113.0/24;
allow 198.51.100.42;
deny all;

SSO OIDC via un reverse proxy authentifiant. Pour les équipes qui disposent déjà d'un fournisseur d'identité (Keycloak, Authelia, Authentik), un proxy authentifiant comme Oauth2-Proxy ou Authelia posé devant nginx délègue l'authentification au SSO existant. Excalidraw n'a pas besoin de savoir qu'une couche d'auth supplémentaire existe.

Intégration dans un parc self-hosted

Excalidraw s'intègre naturellement dans un parc d'outils auto-hébergés. Si vous avez déjà déployé Mattermost pour la communication d'équipe, vous pouvez coller un lien de session Excalidraw dans un canal et inviter vos collaborateurs à rejoindre le tableau directement. Si Penpot couvre le design haute fidélité, Excalidraw prend en charge le travail en amont — les wireframes rapides, les schémas de flux et les sessions de brainstorming — avant que les designs n'entrent dans Penpot.

Cette complémentarité est le vrai argument face aux SaaS : chaque outil reste dans son périmètre de compétences, sans forfait plateforme qui facture pour des fonctions dont vous n'avez pas besoin.

Une plateforme pour votre VPS, votre domaine et vos sauvegardes

ServOrbit.com regroupe VPS, nom de domaine et sauvegardes sur votre compte : déployez Excalidraw pour votre équipe depuis votre espace client.