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-roomest 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 versionpour 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
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 | shVérifiez que Docker Compose Plugin répond avant de continuer :
docker compose versionConstruire 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.productiondu dépôt avant de construire. Remplacezcollab.yourdomain.compar le sous-domaine alloué àexcalidraw-room:git clone https://github.com/excalidraw/excalidraw.gitcd excalidrawsed -i 's|^VITE_APP_WS_SERVER_URL=.*|VITE_APP_WS_SERVER_URL=https://collab.yourdomain.com|' .env.productiondocker build -t my-excalidraw:latest .Pourquoi pas
--build-arg? Le Dockerfile amont ne déclare aucunARGpour les variablesVITE_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.dockerignoreamont n'admet d'ailleurs que.env.developmentet.env.production: un.env.production.localserait 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 afficherpropre. L'image officielleexcalidraw/excalidrawconvient pour des essais locaux, mais elle vise les services publics d'Excalidraw.É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-stackContenu 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 -dConfigurer 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
UpgradeetConnection, 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.Tester la collaboration en temps réel
Ouvrez
https://draw.yourdomain.comdans 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 status200sur la requête WebSocket indique que nginx n'a pas transmis les en-têtesUpgrade.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_URLet 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
.excalidrawou 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
| Variable | Fonction | Ce 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-argsans effet,.env.production.localignoré par le.dockerignore). Remède : éditer.env.production, reconstruire, rejouer la vérificationgrepde 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émahttpau lieu dehttps, port, slash final). Remède : la valeur doit être l'origine exacte de l'interface, par exemplehttps://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. SiVITE_APP_FIREBASE_CONFIGest 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_CONFIGn'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_URLetVITE_APP_BACKEND_V2_GET_URLn'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 surlocalhost), 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ère | Excalidraw (self-hosted) | Miro | FigJam |
|---|---|---|---|
| Coût par utilisateur | Aucun (VPS seul) | Payant par siège | Payant par siège |
| Données hébergées chez | Votre infrastructure (temps réel) ; Firebase d'Excalidraw par défaut pour la scène enregistrée | Miro Inc. | Adobe / Figma |
| Membres simultanés | RAM seule | Limité par plan | Limité par plan |
| Licence logicielle | MIT (open source) | Propriétaire | Propriétaire |
| Auto-hébergeable | Partiellement : deux conteneurs, plus Firebase et partage par lien à traiter | Non | Non |
| Contrôle d'accès | IP / SSO / Basic Auth | SSO (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.