白板工具如Excalidraw、tldraw、WBO等,在自托管场景下通常由前端静态资源和独立的协作服务组成。前端负责画布渲染与交互,协作服务通过WebSocket同步多人光标、图形变更和撤销操作。直接部署在宿主机上时,需要手动安装Node.js、配置静态文件目录、启动Socket.IO服务,还要处理不同成员机器上Node版本不一致导致的构建失败。把这些依赖打包进Docker镜像后,运行环境被固化,配合Compose可以一键拉起完整服务。下面从镜像构建、服务编排、持久化与代理三个环节逐一说明。

一、白板工具镜像构建:选对基础镜像,避免运行时膨胀
白板工具的前端构建产物通常是一堆HTML、CSS和JavaScript文件,运行时只需要一个静态文件服务器。如果直接用Node镜像跑生产环境,镜像体积会多出几百MB,而且暴露不必要的Node运行时。推荐采用多阶段构建:第一阶段使用Node镜像安装依赖并执行构建,第二阶段只保留Nginx或Caddy作为静态服务器。这样最终镜像只有几十MB,启动速度更快,攻击面也更小。
下面是一个针对Excalidraw或tldraw前端项目的通用Dockerfile示例:
FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
这个文件的关键点有两个:第一,先复制package*.json再执行npm ci,可以充分利用Docker层缓存,后续修改源码重新构建时不会重复安装依赖;第二,COPY --from=build只把构建好的dist目录拷贝到Nginx镜像,避免把node_modules带进运行时。如果白板工具提供了官方Docker镜像,比如Excalidraw官方仓库可以直接拉取,但定制品牌Logo或内网API地址时仍需自行构建,此时上述写法能保持生产镜像干净。
构建时还可以通过ARG注入环境相关的配置。例如有些白板工具需要在构建阶段写入协作服务地址,可以定义ARG COLLAB_URL,在RUN npm run build之前写入环境变量,避免把配置写死在代码里。这样同一份代码可以构建出面向测试环境和生产环境的不同镜像。
二、使用Compose编排白板前端、协作服务与缓存
白板工具最容易踩的坑是把前端静态页面部署成功就认为完成了自托管,结果多人协作时各自画的内容互不同步。原因是协作服务与前端是分离的,前端通过WebSocket连接协作服务广播操作。简单启动一个静态服务器只解决了展示问题,没有启动协作进程。Docker Compose可以把前端、协作服务和Redis缓存放在同一个网络里统一管理,避免漏起服务。
下面以WBO(Whiteboard Online)或类似架构为例,给出一个完整的Compose编排文件:
version: "3.9"
services:
whiteboard:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./dist:/usr/share/nginx/html:ro
depends_on:
- collab
collab:
build: ./collab-server
environment:
- PORT=3000
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
redis-data:
这个编排把前端whiteboard服务跑在Nginx 8080端口,协作服务collab通过构建目录启动,并配置了Redis作为协作状态的缓存。如果白板工具不支持Redis,可以去掉该依赖,使用本地文件存储协作数据,但文件存储需要额外挂载数据卷。否则协作服务重启后,房间状态和用户会话会丢失,表现为正在协作的成员被强制退出。
生产环境中不应把协作服务的3000端口直接暴露到公网,只需让前端Nginx反向代理该端口即可。这里把collab服务端口通过Compose内部网络互通,外部只访问前端的8080端口。启动顺序上,可以给collab和redis增加健康检查,前端使用depends_on的条件形式等待协作服务就绪,避免用户打开页面时协作连接失败。日常开发中执行docker compose up -d --build就能完成构建和启动,极大降低部署成本。
三、持久化白板数据与配置反向代理
白板工具的数据存储方式分为文件型和数据库型。WBO将白板内容保存为JSON文件,默认存放在容器的/save目录,如果不挂载数据卷,容器重建后所有白板内容都会消失。Excalidraw的协作房间是内存态,依赖外部持久化后端或定期导出。因此,无论哪种类型,都需要在运行配置中明确数据落盘位置,否则涂鸦数据会随着容器删除而永久丢失。
对于文件型白板工具,可以在Compose中增加一个数据卷,例如修改collab服务加一行volumes: - ./board-data:/save,把宿主机目录挂载进去。对于需要数据库的场景,使用Redis或PostgreSQL时同样要配置数据卷,并定期备份数据文件。这样即使升级镜像版本或迁移服务器,白板内容也能完整恢复。
反向代理是另一个高频故障点。很多团队配置Nginx时只代理了HTTP请求,没有处理WebSocket升级,导致白板页面能打开,但多人协作时连接被断开。这是因为Socket.IO或原生WebSocket需要HTTP升级为长连接。正确的Nginx配置示例如下:
server {
listen 80;
server_name board.ipipp.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /socket.io/ {
proxy_pass http://collab:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
其中Upgrade和Connection这两个头必须显式设置,否则WebSocket握手无法完成。如果白板工具使用原生WebSocket而不是Socket.IO,路径可能会是/ws或/api/collab,需要根据实际路由调整location匹配规则。配置完成后可以用浏览器的开发者工具观察网络面板,确认WebSocket连接状态为101 Switching Protocols且没有频繁重连。
生产环境中还应限制容器资源使用,例如在Compose中为每个服务设置deploy.resources.limits,避免某个协作服务内存泄漏拖垮整台机器。同时配置日志轮转,防止容器日志占满磁盘。数据卷定期备份可以使用docker run --rm -v board-data:/data -v /backup:/backup alpine tar czf /backup/board-data.tar.gz -C /data .这样的命令,把持久化数据打包到宿主机备份目录。这些细节决定了一套白板工具能否长期稳定运行,而不仅仅是可以启动。