MAATRIX / Блог / Excalidraw на Ubuntu 24.04: пошаговая установка

Excalidraw на Ubuntu 24.04: пошаговая установка

MAATRIX

Excalidraw — удобная доска для набросков и схем от руки, но публичная версия на excalidraw.com не подходит, если рисуете архитектуру инфраструктуры, приватные диаграммы или работаете с NDA-проектом. Ниже — рабочий способ поднять свой экземпляр на Ubuntu 24.04: со своим доменом, TLS и настоящим self-hosted сервером для совместного редактирования в реальном времени, а не проксированием через инфраструктуру самого Excalidraw.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Как устроен self-hosted Excalidraw

Excalidraw — это не одно приложение, а два независимых компонента:

  • excalidraw — статический фронтенд (React-приложение, собранное Vite и отданное через nginx). Сам по себе редактор полностью рабочий: доска, фигуры, экспорт — всё это работает офлайн, без единого запроса на сервер.
  • excalidraw-room — небольшой Node.js-сервер на socket.io, который занимается только ретрансляцией событий совместного редактирования между участниками одной комнаты (room). Сервер не хранит содержимое доски — данные шифруются на стороне клиента AES-ключом, который зашит во фрагмент URL (#room=...,key=...), а room-сервер видит только зашифрованные блобы и разгоняет их между подключёнными сокетами.

Важный нюанс, который стоит понять сразу: официальный публичный Docker-образ excalidraw/excalidraw собран с переменной VITE_APP_WS_SERVER_URL, указывающей на oss-collab.excalidraw.com — то есть совместное редактирование в нём технически работает, но через инфраструктуру самого проекта, а не через ваш сервер. Чтобы получить действительно изолированную установку, фронтенд придётся пересобрать самим, указав адрес своего room-сервера. Ниже показано, как это сделать.

Также у Excalidraw нет открытого self-hosted бэкенда для кнопки «Share» (создание публичной ссылки на снапшот доски) — она всё равно уходит на json.excalidraw.com. Это ограничение, а не баг конфигурации, и в разделе про сохранение данных я объясню, как с этим жить.

Подготовка сервера

Понадобится Ubuntu 24.04 с 1–2 vCPU и 1–2 ГБ RAM — сервис лёгкий: статика плюс тонкий websocket-релей, без базы данных. Точную нагрузку под вашу команду проверяйте по факту, это ориентир, а не гарантия.

Обновите систему и поставьте Docker с плагином Compose:

apt update && apt upgrade -y
apt install -y ca-certificates curl git ufw

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Базовый firewall (подробный разбор параметров — в статье про настройку ufw на Ubuntu 24.04):

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Держите в уме особенность Docker: он сам прописывает правила в iptables при публикации портов и по умолчанию обходит ufw. В нашей схеме наружу и так должны смотреть только 80/443 — то, что мы и открыли, — но если позже добавите другие сервисы с проброшенными портами, для нормальной работы ufw с Docker понадобится отдельная настройка (например, утилита ufw-docker), иначе правила ufw для этих портов будут просто не работать.

Заведите домен (например, draw.example.com) и направьте A-запись на IP сервера — Caddy получит TLS-сертификат автоматически, об этом ниже.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Сборка образов: фронтенд и room-сервер

Создайте рабочую директорию и склонируйте оба репозитория:

mkdir -p /opt/excalidraw && cd /opt/excalidraw
git clone https://github.com/excalidraw/excalidraw.git app
git clone https://github.com/excalidraw/excalidraw-room.git room

Готовый Dockerfile фронтенда не пробрасывает переменные окружения в сборку как build-arg — их нужно добавить самим. Откройте app/Dockerfile и сразу после строки FROM node:... AS builder (и до RUN yarn build) добавьте:

ARG VITE_APP_WS_SERVER_URL
ENV VITE_APP_WS_SERVER_URL=$VITE_APP_WS_SERVER_URL

Это заставит Vite подставить адрес вашего room-сервера вместо oss-collab.excalidraw.com из .env.production при сборке статики. Дальше собираем образ, передавая свой домен:

cd /opt/excalidraw/app
docker build \
  --build-arg VITE_APP_WS_SERVER_URL=https://draw.example.com \
  -t excalidraw-self:latest .

С room-сервером проще: CORS_ORIGIN и PORT он читает из окружения уже во время работы (это обычный Node/Express-процесс, а не собранная статика), поэтому Dockerfile трогать не нужно — значения передадим прямо в docker-compose. Единственное, на что стоит обратить внимание: репозиторий excalidraw-room на момент подготовки статьи собирался на образе node:12-alpine — версия давно вне поддержки апстримом. Замена первой строки FROM node:12-alpine на что-то актуальнее (например, node:20-alpine) на процесс сборки не влияет, но снижает риски со стороны устаревшего рантайма.

docker-compose.yml для продакшена

Соберите стек из трёх сервисов: фронтенд, room-сервер и Caddy как единую точку входа с TLS. Файл /opt/excalidraw/docker-compose.yml:

services:
  excalidraw:
    build:
      context: ./app
      args:
        VITE_APP_WS_SERVER_URL: "https://draw.example.com"
    image: excalidraw-self:latest
    container_name: excalidraw
    restart: unless-stopped
    expose:
      - "80"

  excalidraw-room:
    build:
      context: ./room
    image: excalidraw-room-self:latest
    container_name: excalidraw-room
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - PORT=80
      - CORS_ORIGIN=https://draw.example.com
    expose:
      - "80"

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - excalidraw
      - excalidraw-room

volumes:
  caddy_data:
  caddy_config:

Обратите внимание: у excalidraw и excalidraw-room намеренно нет секции ports — только expose, то есть наружу за пределы Docker-сети контейнеры не торчат вообще, весь трафик идёт через Caddy. Это общий принцип продакшен-конфигураций, подробнее — в статье про Docker Compose для продакшена.

Caddy: домен, TLS и маршрутизация websocket

Файл /opt/excalidraw/Caddyfile:

draw.example.com {
    @ws path /socket.io/*
    reverse_proxy @ws excalidraw-room:80

    reverse_proxy excalidraw:80
}

Здесь важно не использовать handle_path (он обрезает совпавший префикс перед проксированием) — socket.io-клиент по умолчанию стучится именно на путь /socket.io/..., и room-сервер ждёт запросы с этим префиксом. Обычный reverse_proxy с матчером по пути передаёт запрос как есть, ничего не обрезая.

Отдельно настраивать заголовки Upgrade/Connection для протокола websocket, как это требуется в nginx, не нужно — Caddy 2 определяет апгрейд соединения автоматически. TLS-сертификат от Let's Encrypt Caddy тоже получит и обновит сам, если домен указывает на сервер и открыт порт 80/443 — сама логика ACME подробно разобрана в статье про Caddy с автоматическим SSL.

Запускаем стек:

cd /opt/excalidraw
docker compose up -d --build
docker compose logs -f caddy

В логах Caddy должна появиться строка о выпуске сертификата для draw.example.com. Если её нет — проверьте, что DNS уже разрешается на нужный IP и порты 80/443 действительно доступны снаружи.

Проверка совместной работы и что происходит с данными

Откройте https://draw.example.com — редактор загрузится, доска будет пустой. Нарисуйте что-нибудь и нажмите иконку «Live collaboration» (человечки в правом верхнем углу) — сгенерируется ссылка вида https://draw.example.com/#room=xxxxx,yyyyy. Откройте её во втором браузере (или дайте коллеге) — курсор второго участника и его правки должны появляться в реальном времени. Это и есть проверка, что фронтенд действительно достучался до вашего excalidraw-room, а не до oss-collab.excalidraw.com.

Теперь про сохранение — это единственное место, где self-hosted Excalidraw не такой удобный, как хотелось бы:

  • Содержимое доски по умолчанию хранится только в localStorage браузера, локально на устройстве. Ни фронтенд, ни room-сервер это не сохраняют — room-сервер вообще ничего не пишет на диск, он только ретранслирует зашифрованные события, пока в комнате есть хотя бы два подключённых клиента.
  • Закрыли последнюю вкладку с открытой комнатой — совместная сессия исчезла. Открыть ту же ссылку с чистого браузера означает начать с пустой доски: старое содержимое привязано к localStorage конкретного устройства, а не к ссылке комнаты.
  • Кнопка «Share» в меню экспорта создаёт публичную ссылку через json.excalidraw.com — то есть данные всё-таки уходят на инфраструктуру Excalidraw. Открытого self-hosted бэкенда для этой функции проект не поставляет. Если такой канал утечки данных недопустим — просто не используйте эту кнопку, а вместо неё договоритесь с командой сохранять файлы вручную.
  • Рабочий способ сохранять и передавать доски между людьми — экспорт в файл .excalidraw (меню → «Save to» → «Disk») и импорт обратно тем же меню. Файл — обычный JSON, его можно класть в git-репозиторий или в общую папку и открывать заново на любом устройстве.

Если для команды принципиально держать историю досок централизованно, а не разбрасывать .excalidraw-файлы по локальным машинам — заведите простое соглашение: экспортированные файлы складывать в общий каталог на сервере (например, синхронизируемый через тот же Nextcloud) и делать по ним обычный бэкап каталога. Полноценной альтернативой с хранением документов на сервере из коробки будет CryptPad — там есть встроенный whiteboard-модуль и постоянное серверное хранение, но это уже другой инструмент с другими компромиссами (сложнее в поддержке, требует MongoDB).

Обновление и обслуживание

Обновление сводится к пересборке образов из свежего кода апстрима:

cd /opt/excalidraw/app && git pull
cd /opt/excalidraw/room && git pull
cd /opt/excalidraw
docker compose build --no-cache
docker compose up -d

Не забывайте после каждого git pull в app заново применять патч Dockerfile из раздела выше (добавление ARG/ENV для VITE_APP_WS_SERVER_URL) — апстрим может переписать файл, и патч слетит. Проще всего держать эти две строки в отдельном .patch-файле и применять git apply перед сборкой.

Для мониторинга состояния контейнеров:

docker compose ps
docker compose logs -f excalidraw-room

Если room-сервер не отвечает — участники комнаты просто не увидят курсоры друг друга, при этом сам редактор продолжит работать локально: деградация мягкая, доска не ломается.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужна ли регистрация или логин для пользователей?

Нет, Excalidraw в такой конфигурации полностью анонимный — доступ к доске определяется владением ссылкой с ключом комнаты.

Видит ли room-сервер содержимое доски?

Нет, данные шифруются на клиенте AES-ключом из фрагмента URL, сервер получает только зашифрованные блобы и не может их прочитать без ключа.

Можно ли ограничить доступ снаружи, кроме самой ссылки на комнату?

Да, добавьте Basic Auth на уровне Caddy (директива basic_auth) как дополнительный слой перед редактором — тогда даже знание ссылки на комнату не даст войти без пароля.

Что будет, если участников в комнате станет больше десяти?

Официальных ограничений на число участников нет, но чем больше активных курсоров и правок одновременно — тем выше нагрузка на websocket-соединение каждого клиента; для рабочих досок с командой это, как правило, не проблема.

Можно ли обойтись без своего room-сервера и просто использовать публичный образ?

Можно, если устраивает, что трафик совместного редактирования идёт через инфраструктуру Excalidraw, а не через ваш сервер — тогда шаги с патчем Dockerfile и сборкой excalidraw-room можно пропустить и развернуть только фронтенд.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →