Por qué autoalojar Excalidraw en lugar de usar la versión pública
La versión pública en excalidraw.com funciona bien para bocetos rápidos en solitario. Enruta las sesiones de colaboración en tiempo real a través de un servidor gestionado por Excalidraw s.r.o. Es aceptable para una lluvia de ideas puntual. Lo es mucho menos cuando las pizarras contienen mockups de flujos de clientes, diagramas de arquitectura de sistemas internos o wireframes de proyectos bajo NDA.
Autoalojar Excalidraw desplaza el perímetro de control a tu propia infraestructura. El tiempo real pasa por tu servidor WebSocket, en tu dominio; lo que una instalación estándar sigue dejando en un tercero (la escena guardada, los enlaces para compartir) se detalla más abajo. Tú decides quién se conecta — mediante allowlist de IP, SSO OIDC o HTTP Basic Auth delante de nginx. Y como los dos componentes están publicados bajo licencia MIT, no se aplica ninguna restricción comercial independientemente del número de clientes o colaboradores que trabajen en la instancia.
Beneficios concretos
- Cero suscripción de software — los dos componentes (interfaz y servidor de colaboración) son MIT. Pagas el VPS, nada más.
- Confidencialidad de los mockups — el dibujo en directo permanece en tu infraestructura. La escena guardada y las imágenes de una sala hay que tratarlas aparte (Firebase por defecto): consulta « Lo que funciona de verdad al autoalojar ».
- Acceso bajo tu control — allowlist de IP, SSO OIDC o HTTP Basic Auth delante de nginx según el nivel de aislamiento requerido.
- Sesiones y miembros ilimitados — sin límite por plan; el único límite es la RAM del servidor, y
excalidraw-roomes un servicio Node.js ligero. - Persistencia opcional — monta un volumen Docker o conecta un bucket S3 compatible para archivar pizarras exportadas como JSON.
- Integración en un stack autoalojado — una instancia de Excalidraw junto a Penpot, Mattermost o Docmost cierra el perímetro SaaS de una agencia completa.
Arquitectura: dos contenedores, roles distintos
Excalidraw separa lo que la mayoría considera una sola herramienta en dos componentes independientes:
- excalidraw/excalidraw — la interfaz React compilada, servida como sitio estático por nginx dentro del contenedor. No ejecuta ningún servidor de aplicación. Las direcciones de sus servicios (sala, Firebase, compartir) son variables VITE_APP_* leídas en la compilación y grabadas en el bundle JavaScript: cambiarlas en un contenedor ya en ejecución no tiene ningún efecto.
- excalidraw/excalidraw-room — un servidor Node.js Socket.IO que retransmite en memoria los eventos de dibujo entre los participantes de una misma sesión. Es sin estado: no persiste nada y olvida todo al reiniciar. La escena guardada de una sala no pasa por él (ver más abajo). Su imagen de Docker Hub no se republica desde diciembre de 2023 y su Dockerfile de origen parte de Node 12: mantenlo detrás de nginx, en la red de Docker, sin puerto publicado.
Consecuencia práctica: la imagen Docker oficial se construyó con las direcciones de excalidraw.com. Para apuntar a tu propio excalidraw-room, debes construir tu propia imagen tras modificar VITE_APP_WS_SERVER_URL en .env.production.
Requisitos previos
El VPS Start de ServOrbit (2 vCPU, 4 GB RAM) cubre el caso estándar: la interfaz estática tiene carga casi nula, y excalidraw-room consume menos de 256 MB en condiciones normales. Con diez colaboradores simultáneos te quedarás muy por debajo de la mitad de la RAM disponible.
Lo que necesitas antes de empezar:
Lista de comprobación de requisitos
- Un VPS con al menos 1 vCPU y 512 MB de RAM (1 GB recomendado para un equipo).
- Docker Engine y Docker Compose Plugin instalados (
docker compose versionpara verificar). - Un nombre de dominio apuntando a la IP del VPS — sin TLS los WebSockets quedan bloqueados en los navegadores modernos.
- Acceso SSH root o un usuario con derechos
sudo. - Un certificado TLS válido — ServOrbit configura Let's Encrypt automáticamente.
Desplegar Excalidraw en 5 pasos
Crear el VPS e instalar Docker
Aprovisiona un VPS Ubuntu 24.04 desde tu panel de ServOrbit. Una vez conectado por SSH, si Docker no está instalado aún, ejecuta:
curl -fsSL https://get.docker.com | shVerifica que Docker Compose Plugin responde antes de continuar:
docker compose versionConstruir la imagen con tu URL de colaboración
Clona el repositorio oficial y fija la dirección de la sala en el archivo
.env.productiondel repositorio antes de construir. Reemplazacollab.yourdomain.comcon el subdominio que asignes aexcalidraw-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 .¿Por qué no
--build-arg? El Dockerfile de origen no declara ningúnARGpara las variablesVITE_APP_*: un--build-arg VITE_APP_WS_SERVER_URL=…nunca llega al comando de build, la imagen se construye con normalidad y sigue apuntando al servidor de Excalidraw. Además, el.dockerignorede origen solo admite.env.developmenty.env.production, así que un.env.production.localse ignoraría. La dirección queda grabada en el bundle, por lo que cambiar de dominio obliga a reconstruir.Comprueba el resultado:
docker run --rm --entrypoint sh my-excalidraw:latest -c "grep -rl oss-collab.excalidraw.com /usr/share/nginx/html || echo limpio"debe mostrarlimpio. La imagen oficialexcalidraw/excalidrawsirve para pruebas locales, pero apunta a los servicios públicos de Excalidraw.Escribir el archivo docker-compose.yml
Crea un directorio de trabajo y escribe el siguiente archivo. Ambos servicios escuchan internamente; nginx (siguiente paso) es el único punto de entrada público:
mkdir ~/excalidraw-stack && cd ~/excalidraw-stackContenido del
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:Arranca el stack:
docker compose up -dConfigurar nginx con las cabeceras WebSocket
Este es el paso donde fallan en silencio la mayoría de los despliegues: sin las cabeceras
UpgradeyConnection, el navegador abre una conexión HTTP normal en lugar de un WebSocket, y la colaboración parece no funcionar sin ningún mensaje de error visible.Bloque nginx para el frontend de Excalidraw (
draw.yourdomain.com):server { listen 443 ssl; server_name draw.yourdomain.com; location / { proxy_pass http://excalidraw:80; proxy_set_header Host $host; } }Bloque nginx para el servidor de sala (
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; } }Recarga nginx (
nginx -s reload) tras colocar los bloques.Probar la colaboración en tiempo real
Abre
https://draw.yourdomain.comen dos pestañas o dos navegadores distintos. En la barra de herramientas, haz clic en Live collaboration y crea una sesión. Copia el enlace de sesión en la segunda pestaña.Dibuja algo en la primera pestaña: el trazo debe aparecer en la segunda en tiempo real. Si no aparece, revisa primero las cabeceras WebSocket en nginx (las herramientas de red del navegador muestran si la conexión ascendió a
101 Switching Protocols).Abre después la pestaña de red del navegador: la conexión en tiempo real debe apuntar a
collab.yourdomain.com, nunca a oss-collab.excalidraw.com. Lo que recibe Firebase se trata en la sección siguiente.
Lo que funciona de verdad al autoalojar
Seamos precisos: « dos contenedores y listo » es el atajo que más repite este tipo de guía. El hilo paraguas del repositorio de origen (excalidraw/excalidraw, issue n.º 1772, abierto desde junio de 2020 y aún activo en octubre de 2026) resume tres muros, que hemos contrastado con el código y los archivos .env del repositorio.
Muro n.º 1 — las direcciones se fijan al construir. Las direcciones del servidor de colaboración, del servicio para compartir y del proyecto Firebase son variables VITE_APP_* que Vite lee durante el build. La imagen oficial excalidraw/excalidraw se construyó con los valores de .env.production: por tanto sigue hablando con los servicios de excalidraw.com. Una variable pasada a docker run no cambia nada, y el Dockerfile de origen no declara ningún ARG para trasladar una al build.
Muro n.º 2 — la colaboración es medio local. El tiempo real (quién dibuja qué, en directo) pasa por excalidraw-room, que alojas tú. Pero la escena guardada y las imágenes pegadas en una sala van a Firebase: el código llama a saveToFirebase y saveFilesToFirebase, con la configuración de VITE_APP_FIREBASE_CONFIG. Mientras esa variable conserve el valor de origen, esos datos (cifrados en el navegador con la clave de la sala) se almacenan en el proyecto Firebase de Excalidraw, no en el tuyo. Eso concilia los testimonios contradictorios del hilo: uno comprueba que con VITE_APP_FIREBASE_CONFIG='{}' la colaboración falla, otro que Jitsi Meet retiró la parte de Firebase y el tiempo real funciona solo con la sala. El directo prescinde de Firebase; guardar y retomar, no.
Muro n.º 3 — los enlaces para compartir exigen un servidor de almacenamiento que el proyecto no publica. Un enlace #json= se crea con una petición a VITE_APP_BACKEND_V2_POST_URL y se relee mediante VITE_APP_BACKEND_V2_GET_URL, por defecto json.excalidraw.com. El hilo menciona reescrituras de la comunidad (una en Next.js sobre S3, otra sobre almacenamiento compatible con S3), pero no señala ninguna como oficial.
Tres montajes posibles, y lo que cada uno deja en un tercero
- Montaje A — solo la sala (el de esta guía). Cambias
VITE_APP_WS_SERVER_URLy nada más. El dibujo en directo transita por tu servidor; la escena guardada y las imágenes de las salas siguen escribiéndose en el Firebase de Excalidraw. Aceptable para pizarras poco sensibles, no para presentarlo como « nada en un tercero ». - Montaje B — sala + tu propio proyecto Firebase. Creas un proyecto Firebase propio y pones su configuración JSON en
VITE_APP_FIREBASE_CONFIG. El código solo lee esa variable; no lo hemos desplegado de punta a punta, y Firebase sigue siendo un servicio de Google, no autoalojamiento en sentido estricto. - Montaje C — instancia en solitario. Sirves la interfaz sola, sin sala ni enlace: cada uno dibuja, exporta a
.excalidrawo a imagen, y archiva los archivos en tu infraestructura. Nada sale hacia un tercero por colaboración, porque no la hay. - Lo que no existe en el lado oficial. Ningún servidor de almacenamiento abierto para los enlaces
#json=, ni una opción que traslade todo el perímetro a tu dominio con una sola variable en ejecución. Si tu requisito es « ningún dato en un tercero » con colaboración y enlaces, la respuesta honesta es un trabajo de adaptación o de fork, no una instalación.
Variable → función → qué se rompe sin ella
Desplace la tabla
| Variable | Función | Qué se rompe sin ella |
|---|---|---|
| `VITE_APP_WS_SERVER_URL` (compilación) | Dirección del servidor de tiempo real Socket.IO (`excalidraw-room`). | Valor de origen conservado: tus sesiones pasan por oss-collab.excalidraw.com. Dirección inalcanzable: la colaboración no se establece. |
| `VITE_APP_FIREBASE_CONFIG` (compilación) | Configuración JSON del proyecto Firebase: escena guardada de una sala e imágenes pegadas. | Valor de origen: esos datos quedan en Excalidraw. Vacía o inválida (`{}` incluida): guardar y retomar una sala falla y, según un testimonio del hilo, la colaboración también. Un JSON ilegible registra `Error JSON parsing firebase config`. |
| `VITE_APP_BACKEND_V2_POST_URL` (compilación) | Creación de un enlace `#json=` (envío de la escena cifrada). | Valor de origen: el enlace se crea en json.excalidraw.com. Dirección sin servidor compatible: la creación del enlace falla. |
| `VITE_APP_BACKEND_V2_GET_URL` (compilación) | Relectura de un enlace `#json=` al abrirlo. | Valor de origen: el enlace se lee en Excalidraw. Dirección sin servidor compatible: la apertura del enlace falla. |
| `VITE_APP_DISABLE_SENTRY` (compilación) | Desactiva el envío de informes de errores a Sentry. | No se rompe nada; sin ella, la aplicación puede enviar informes de errores a un tercero. El script `build:app:docker` de origen ya la fija en `true`. |
| `VITE_APP_DISABLE_PREVENT_UNLOAD` (compilación) | Suprime el aviso antes de salir de una sala cuyo guardado no ha terminado. | No se rompe nada, pero cerrar la pestaña ya no avisa. Resérvala para depurar. |
| `CORS_ORIGIN` (ejecución, `excalidraw-room`) | Origen autorizado a abrir una conexión con el servidor de sala. | Ausente: el valor por defecto es `*`, cualquier página web puede conectarse. Fija `https://draw.yourdomain.com`. |
| `PORT` (ejecución, `excalidraw-room`) | Puerto de escucha del servidor de sala. | Ausente: escucha en el puerto 80 fuera del modo desarrollo, que es lo que suponen los bloques nginx de esta guía. Si lo cambias, `proxy_pass` debe seguirlo. |
Comprobar quién recibe qué
La única forma de saber qué sale de tu instancia es mirarlo. Abre la pestaña de red del navegador, inicia una sesión de colaboración, dibuja, pega una imagen y crea un enlace para compartir. Se leen tres familias de destinos: tu subdominio de sala (el tiempo real, esperado); los dominios googleapis.com de Firebase (escena e imágenes guardadas, esperado con la configuración de origen, a eliminar si pensabas lo contrario); json.excalidraw.com (enlaces para compartir). Esta inspección es la prueba de aceptación del montaje elegido: sin ella, « en mi servidor » sigue siendo una hipótesis.
Recuerda por último que el servidor de sala no autentica a nadie: CORS_ORIGIN limita qué páginas web pueden conectarse, no qué personas. La identidad de los participantes se resuelve delante de la interfaz (Basic Auth, SSO), como se describe más abajo.
Solución de problemas: la colaboración no arranca, o no recuerda nada
Los síntomas siguientes se deducen del código y del hilo de origen; no se reproduce ningún mensaje salvo los leídos en el repositorio.
Síntomas, causas, remedios
- La sesión va a oss-collab.excalidraw.com (pestaña de red) aunque construiste tu imagen. Causa: la dirección no se tuvo en cuenta al construir (
--build-argsin efecto,.env.production.localignorado por el.dockerignore). Remedio: editar.env.production, reconstruir y repetir la comprobacióngrepdel paso 2. Un cambio de dominio sigue la misma regla. - El navegador ve rechazada la conexión con la sala. Causa frecuente tras fijar
CORS_ORIGIN: el origen no coincide exactamente con el de la página (esquemahttpen lugar dehttps, puerto, barra final). Remedio: el valor debe ser el origen exacto de la interfaz, por ejemplohttps://draw.yourdomain.com. - Los trazos aparecen en directo, pero un participante que se une más tarde, o tras un reinicio de la sala, no recupera el contenido ni las imágenes. Causa: la escena y las imágenes se guardan a través de Firebase, no por
excalidraw-room, que no retiene nada. SiVITE_APP_FIREBASE_CONFIGestá vacía o inalcanzable, nada se relee. Remedio: elegir el montaje A o B de arriba. - Aviso
Error JSON parsing firebase config. Supplied value:en la consola del navegador. Causa:VITE_APP_FIREBASE_CONFIGno es un JSON válido, casi siempre por comillas mal protegidas o un salto de línea en el archivo. Remedio: JSON en una sola línea, entre comillas simples en.env.production, como en el archivo de origen. - Compartir por enlace falla, o el enlace generado apunta a json.excalidraw.com. Causa:
VITE_APP_BACKEND_V2_POST_URLyVITE_APP_BACKEND_V2_GET_URLno se cambiaron, o apuntan a un servidor que no implementa la API esperada. Remedio: conservar el valor por defecto con conocimiento de causa, o intercambiar archivos.excalidraw. - Error de cifrado o de generación de clave en una red local con
http://. Causa: la API de cifrado del navegador (window.crypto) solo se expone en un contexto seguro; el hilo de origen señala esta trampa para el autoalojamiento en red local. Remedio: servir la interfaz en HTTPS (o enlocalhost), como exige el requisito TLS de arriba.
Persistencia de pizarras: lo que se guarda de verdad
Por defecto, excalidraw-room es completamente sin estado: si el servidor se reinicia, el relé arranca de cero. La escena guardada de una sala vive en otro sitio (Firebase, ver más arriba), y cada participante conserva además su dibujo en el localStorage de su navegador.
Opción 1 — Exportación manual de archivos .excalidraw. El menú de exportación de la interfaz produce un archivo .excalidraw (o una imagen) que se puede reimportar en cualquier momento. Excalidraw no expone ninguna API de exportación que llamar desde un cron: es un gesto hecho en el navegador, cuyo archivo depositas luego en un volumen o un repositorio git.
Opción 2 — Bucket S3 compatible para archivar. ServOrbit ofrece almacenamiento de objetos compatible con S3. Copia allí los archivos exportados con la herramienta que prefieras (rclone, aws s3 cp); la restauración consiste en reimportar el último archivo. El propio Excalidraw no escribe en S3.
Excalidraw autoalojado vs alternativas SaaS
Desplace la tabla
| Criterio | Excalidraw (autoalojado) | Miro | FigJam |
|---|---|---|---|
| Coste por usuario | Ninguno (solo VPS) | De pago por puesto | De pago por puesto |
| Datos alojados en | Tu infraestructura (tiempo real); Firebase de Excalidraw por defecto para la escena guardada | Miro Inc. | Adobe / Figma |
| Miembros simultáneos | Solo RAM | Limitado por plan | Limitado por plan |
| Licencia de software | MIT (open source) | Propietaria | Propietaria |
| Autoalojable | Parcialmente: dos contenedores, más Firebase y enlaces para compartir por resolver | No | No |
| Control de acceso | IP / SSO / Basic Auth | SSO (planes Pro) | SSO (planes Edu+) |
Más allá: SSO y listas de IPs permitidas
Para agencias con requisitos estrictos de confidencialidad, exponer Excalidraw en un subdominio público no siempre es aceptable. Dos configuraciones habituales refuerzan el acceso sin modificar Excalidraw.
HTTP Basic Auth a través de nginx. Añade una directiva auth_basic delante del bloque location / del frontend. Cualquier persona que intente abrir la interfaz deberá autenticarse antes de que cargue la aplicación React. Sencillo de implementar y suficiente para proyectos internos.
Allowlist de IP. Si los colaboradores se conectan desde IPs fijas o una VPN corporativa, un bloque allow/deny en nginx basta para cerrar la instancia al resto de Internet:
allow 203.0.113.0/24;
allow 198.51.100.42;
deny all;SSO OIDC a través de un proxy reverso autenticador. Para equipos que ya disponen de un proveedor de identidad (Keycloak, Authelia, Authentik), un proxy como Oauth2-Proxy o Authelia delante de nginx delega la autenticación al SSO existente. Excalidraw no necesita saber que existe una capa de autenticación adicional.
Integración en un stack autoalojado
Excalidraw se integra de forma natural en un conjunto de herramientas autoalojadas. Si ya has desplegado Mattermost para la comunicación del equipo, puedes pegar un enlace de sesión de Excalidraw en un canal e invitar a los colaboradores a unirse a la pizarra directamente. Si Penpot cubre el diseño de alta fidelidad, Excalidraw gestiona el trabajo previo — wireframes rápidos, diagramas de flujo y sesiones de lluvia de ideas — antes de que los diseños entren en Penpot.
Esta complementariedad es el argumento real frente a los SaaS: cada herramienta permanece dentro de su área de competencia, sin una suscripción de plataforma que factura por funcionalidades que no necesitas.