Tutorial

Alojar Excalidraw en un VPS: pizarra colaborativa sin suscripción

Autoalojamiento15 min de lectura5 pasos

Miro, FigJam, Lucidspark: las pizarras SaaS cobran por puesto, almacenan tus mockups en sus servidores y limitan los miembros según el plan. Excalidraw es la alternativa open source (MIT): una pizarra colaborativa en tiempo real de estilo dibujado a mano, cuyos dos componentes — la interfaz React y el servidor WebSocket de sala — se autoalojan en un VPS en dos contenedores Docker. Esta guía muestra cómo desplegarlos, configurar nginx para WebSockets y dice sin rodeos lo que una instalación estándar sigue dejando en servicios de terceros (Firebase, enlaces para compartir).

Contenido· Por qué autoalojar Excalidraw en lugar de usar la versión pública1/16
  1. 01Por qué autoalojar Excalidraw en lugar de usar la versión pública
  2. 02Beneficios concretos
  3. 03Arquitectura: dos contenedores, roles distintos
  4. 04Requisitos previos
  5. 05Lista de comprobación de requisitos
  6. 06Desplegar Excalidraw en 5 pasos
  7. 07Lo que funciona de verdad al autoalojar
  8. 08Tres montajes posibles, y lo que cada uno deja en un tercero
  9. 09Variable → función → qué se rompe sin ella
  10. 10Comprobar quién recibe qué
  11. 11Solución de problemas: la colaboración no arranca, o no recuerda nada
  12. 12Síntomas, causas, remedios
  13. 13Persistencia de pizarras: lo que se guarda de verdad
  14. 14Excalidraw autoalojado vs alternativas SaaS
  15. 15Más allá: SSO y listas de IPs permitidas
  16. 16Integración en un stack autoalojado

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-room es 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 version para 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

  1. 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 | sh

    Verifica que Docker Compose Plugin responde antes de continuar:

    docker compose version
  2. Construir la imagen con tu URL de colaboración

    Clona el repositorio oficial y fija la dirección de la sala en el archivo .env.production del repositorio antes de construir. Reemplaza collab.yourdomain.com con el subdominio que asignes a 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 .

    ¿Por qué no --build-arg? El Dockerfile de origen no declara ningún ARG para las variables VITE_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 .dockerignore de origen solo admite .env.development y .env.production, así que un .env.production.local se 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 mostrar limpio. La imagen oficial excalidraw/excalidraw sirve para pruebas locales, pero apunta a los servicios públicos de Excalidraw.

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

    Contenido 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 -d
  4. Configurar nginx con las cabeceras WebSocket

    Este es el paso donde fallan en silencio la mayoría de los despliegues: sin las cabeceras Upgrade y Connection, 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.

  5. Probar la colaboración en tiempo real

    Abre https://draw.yourdomain.com en 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_URL y 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 .excalidraw o 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

VariableFunciónQué 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-arg sin efecto, .env.production.local ignorado por el .dockerignore). Remedio: editar .env.production, reconstruir y repetir la comprobación grep del 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 (esquema http en lugar de https, puerto, barra final). Remedio: el valor debe ser el origen exacto de la interfaz, por ejemplo https://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. Si VITE_APP_FIREBASE_CONFIG está 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_CONFIG no 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_URL y VITE_APP_BACKEND_V2_GET_URL no 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 en localhost), 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

CriterioExcalidraw (autoalojado)MiroFigJam
Coste por usuarioNinguno (solo VPS)De pago por puestoDe pago por puesto
Datos alojados enTu infraestructura (tiempo real); Firebase de Excalidraw por defecto para la escena guardadaMiro Inc.Adobe / Figma
Miembros simultáneosSolo RAMLimitado por planLimitado por plan
Licencia de softwareMIT (open source)PropietariaPropietaria
AutoalojableParcialmente: dos contenedores, más Firebase y enlaces para compartir por resolverNoNo
Control de accesoIP / SSO / Basic AuthSSO (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.

Una plataforma para tu VPS, dominio y copias de seguridad

ServOrbit centraliza VPS, dominio y copias de seguridad en un único espacio de agencia: despliega Excalidraw para tu equipo y gestiona la infraestructura de tus clientes desde el mismo panel.

¿Necesita ayuda?

Consulte nuestro centro de ayuda y nuestra FAQ, o contacte con nuestro equipo: llamada, WhatsApp o correo electrónico. Soporte en francés, inglés y árabe.

Escribir por WhatsAppse abre en una pestaña nueva