Excalidraw本地化部署实战:从Docker到生产环境的私有白板搭建
1. 从云端到本地:为什么我们需要一个“离线版”白板
最近在和一些做产品设计、技术架构的朋友聊天时,发现一个挺有意思的现象:大家手头都有一堆好用的在线协作工具,但一遇到需要深度思考、梳理复杂逻辑,或者涉及一些内部敏感信息草稿时,第一反应还是打开一个离线的、完全受自己控制的画布。Excalidraw 这个工具,相信很多做技术方案设计、产品原型草绘的朋友都不陌生。它那种手绘风格的清爽界面,加上对图形、箭头、文本的优秀支持,让它成了快速表达想法的利器。但它的官方版本毕竟是个在线服务,数据存在别人的服务器上,网络一卡顿,思路就断了;画点内部架构草图,心里总有点不踏实。
这就是“Excalidraw本地化部署”这个需求最直接的来源。说白了,就是把那个好用的、在浏览器里运行的 Excalidraw,完整地搬到我们自己的服务器或者电脑上,让它变成一个可以离线运行、数据完全私有的内部工具。这不仅仅是“装个软件”那么简单,它涉及到将一个现代的前端应用及其后端服务进行完整的自托管。从搜索热词来看,像“deepseek本地化部署”、“本地化安装部署minimax”这类需求也层出不穷,说明大家对于将优秀的AI或工具服务进行私有化部署,掌握完全的数据主权和控制权,已经成为一个普遍且强烈的诉求。
我自己在团队内部推动过几次这类工具的本地化,从最初的“图个新鲜”到后来的“离不开”,感触很深。本地化部署后的 Excalidraw,不仅仅是解决了网络和隐私焦虑,它更成为了团队知识沉淀的一个“安全屋”。我们可以把一些长期的、迭代中的技术架构图、业务流程草图持久化地放在内网服务器上,随时增删改查,不用担心服务商突然收费、改政策或者停止服务。接下来,我就把自己从零开始,在一台干净的Linux服务器上部署 Excalidraw 的完整过程、遇到的坑以及一些优化心得,详细地分享出来。整个过程不复杂,但有些细节决定了部署的顺畅程度和使用体验。
2. 部署前的核心准备:理解架构与选择战场
在动手敲命令之前,我们得先搞清楚我们要部署的到底是个什么东西。Excalidraw 本质上是一个“前后端分离”的现代 Web 应用。这意味着我们需要准备两个部分:
- 前端:用户直接在浏览器里交互的界面,就是那些画布、工具栏。这部分是纯粹的静态文件(HTML、CSS、JavaScript)。在本地化部署中,我们需要自己构建或获取这些文件。
- 后端(可选但推荐):官方提供了配套的后端服务,主要用于房间协作(多人实时编辑)和持久化存储(将绘图保存到服务器)。如果你只需要个人单机离线使用,理论上只需要前端。但如果你想拥有一个功能完整、可以分享链接、支持多人协作的内网白板,后端服务必不可少。
理解了架构,我们就要选择“战场”——也就是运行环境。最常见的选择有:
- 直接Docker部署:这是最快捷、最干净的方式。Excalidraw 官方提供了 Docker 镜像,我们只需要一条
docker run命令,就能同时启动前端和后端服务。这种方式隔离性好,依赖问题少,非常适合快速搭建和体验。 - 从源码构建与部署:这种方式更“硬核”,也更有掌控感。你需要克隆 GitHub 上的源码,安装 Node.js、Yarn 等构建工具,自己执行构建命令生成静态文件,然后配置一个 Web 服务器(如 Nginx)来提供这些文件,同时可能还需要配置后端服务。这种方式适合需要深度定制(比如修改UI、增加组件)或学习其内部机制的场景。
对于绝大多数以“用起来”为目标的团队和个人,我强烈推荐Docker 部署方案。它屏蔽了环境差异和复杂的构建流程,让我们能专注于服务本身的配置和使用。本文也将以 Docker 方案为主线进行讲解。我们的目标环境是一台安装了 Docker 和 Docker Compose 的 Linux 服务器(Ubuntu 20.04/22.04 或 CentOS 7/8 均可),拥有一个可以通过内网或公网IP访问的域名。
注意:无论选择哪种方式,请确保你的服务器资源充足。Excalidraw 本身不重,但如果你预期会有很多用户同时进行高强度的协作编辑(比如几十人同时在一个房间画图),那么需要为后端服务分配足够的内存(建议不少于1GB)和CPU资源。
3. 基于Docker的一键式部署实战
假设我们已经有一台安装了 Docker 和 Docker Compose 的服务器,并且可以通过 SSH 登录。下面我们开始一步步操作。
3.1 使用官方Docker镜像快速启动
Excalidraw 团队在 Docker Hub 上维护了官方镜像excalidraw/excalidraw。这是启动服务最快的方式。
首先,创建一个专门的工作目录,并进入:
mkdir -p /opt/excalidraw && cd /opt/excalidraw然后,直接运行以下 Docker 命令:
docker run -d \ --name excalidraw \ -p 80:80 \ -e NODE_ENV=production \ excalidraw/excalidraw:latest这条命令做了几件事:
-d:让容器在后台运行。--name excalidraw:给容器起个名字,方便管理。-p 80:80:将容器内部的 80 端口映射到宿主机的 80 端口。这意味着你通过服务器的IP地址就能访问服务。-e NODE_ENV=production:设置环境变量为生产模式。excalidraw/excalidraw:latest:指定使用的镜像。
执行后,使用docker ps命令查看容器是否正常运行。如果看到excalidraw容器状态为Up,就说明启动成功了。此时,在浏览器中访问http://你的服务器IP,应该就能看到 Excalidraw 的界面了。
但是,这种方式有一个很大的局限性:它只启动了前端服务,没有后端。这意味着你无法使用“房间”协作功能,也无法将绘图保存到服务器(只能保存为本地文件)。要获得完整功能,我们需要使用 Docker Compose 来编排前端和后端两个服务。
3.2 使用Docker Compose部署完整服务链
Docker Compose 允许我们用一个配置文件(docker-compose.yml)来定义和运行多个相关联的容器。对于 Excalidraw,我们需要两个服务:前端 (excalidraw) 和后端 (excalidraw-room)。
在工作目录下创建docker-compose.yml文件:
vim docker-compose.yml将以下内容粘贴进去。这里我使用了一个社区维护的、包含完整前后端的 Compose 配置示例,它比单纯运行前端镜像更实用:
version: '3.8' services: excalidraw: image: excalidraw/excalidraw:latest container_name: excalidraw-app restart: unless-stopped ports: - "3000:80" # 前端访问端口 environment: - NODE_ENV=production - REACT_APP_BACKEND_V1_GET_URL=http://localhost:3001/api/v1 - REACT_APP_BACKEND_V1_POST_URL=http://localhost:3001/api/v1 - REACT_APP_WS_SERVER_URL=ws://localhost:3002 - REACT_APP_FIREBASE_CONFIG='{}' # 禁用Firebase,使用自托管后端 depends_on: - excalidraw-room networks: - excalidraw-net excalidraw-room: image: excalidraw/excalidraw-room:latest container_name: excalidraw-room restart: unless-stopped ports: - "3001:80" # HTTP API 端口 - "3002:80" # WebSocket 端口 (用于实时协作) environment: - NODE_ENV=production - PORT=80 - ALLOWED_ORIGINS=http://localhost:3000 # 允许的前端地址,生产环境需替换为你的域名 volumes: - excalidraw-data:/app/data # 持久化存储绘图数据 networks: - excalidraw-net volumes: excalidraw-data: # 声明一个数据卷,用于持久化房间和绘图数据 networks: excalidraw-net: # 创建一个独立的网络,让两个容器可以互相通信关键配置解析:
- 端口映射:
- 前端映射到宿主机的
3000端口。 - 后端服务映射了两个端口:
3001用于 HTTP API(创建房间、获取绘图数据),3002用于 WebSocket(实现实时协作)。
- 前端映射到宿主机的
- 环境变量:
- 前端容器中,
REACT_APP_BACKEND_V1_*和REACT_APP_WS_SERVER_URL告诉前端去哪里找后端服务。这里配置的是通过 Docker 内部网络通信的地址(localhost),因为它们在同一个自定义网络excalidraw-net下。 REACT_APP_FIREBASE_CONFIG='{}'至关重要,它禁用了 Excalidraw 默认集成的 Firebase 后端,强制其使用我们自托管的后端。- 后端容器中,
ALLOWED_ORIGINS设置了允许跨域请求的前端地址。在开发或内网测试时,可以用http://localhost:3000。在生产环境,你必须将其改为你实际访问前端的域名或IP地址,例如http://draw.your-company.com,否则浏览器会因为跨域策略(CORS)而阻止请求。
- 前端容器中,
- 数据持久化:我们创建了一个名为
excalidraw-data的 Docker 数据卷,并挂载到后端容器的/app/data路径。这样,所有创建的房间和绘图数据都会保存在这个卷里,即使容器被删除重建,数据也不会丢失。你可以通过docker volume inspect excalidraw-data查看卷的实际存储位置。
保存文件后,在同一个目录下运行:
docker-compose up -d-d参数同样表示后台运行。Docker Compose 会自动拉取镜像(如果本地没有)并启动两个容器。
使用docker-compose ps查看服务状态,两个服务都应为Up。现在,访问http://你的服务器IP:3000,你应该能看到完整的 Excalidraw 界面。尝试创建一个“房间”(点击右上角的“合作”按钮),如果一切正常,你应该能获得一个可分享的链接,并且刷新页面后绘图不会丢失——这说明后端服务正在正常工作。
3.3 配置反向代理与HTTPS(生产环境必备)
直接通过IP和端口号访问既不安全也不专业。在生产环境,我们通常会用 Nginx 这样的反向代理服务器,将服务映射到一个友好的域名下,并配置 HTTPS 加密。
假设你有一个域名draw.yourdomain.com已经解析到了你的服务器IP。首先,安装 Nginx 和 Certbot(用于申请免费的 Let‘s Encrypt SSL 证书):
# Ubuntu/Debian sudo apt update sudo apt install nginx certbot python3-certbot-nginx -y # CentOS/RHEL sudo yum install nginx certbot python3-certbot-nginx -y然后,为 Excalidraw 创建一个 Nginx 配置文件:
sudo vim /etc/nginx/conf.d/excalidraw.conf写入以下配置。这个配置将把所有访问draw.yourdomain.com的流量,代理到我们本地的3000端口(前端),同时将/api/v1和/socket.io的请求代理到后端对应的端口。
server { listen 80; server_name draw.yourdomain.com; # 替换为你的域名 # 将HTTP请求重定向到HTTPS(配置完证书后启用) # return 301 https://$server_name$request_uri; location / { proxy_pass http://localhost:3000; # 前端服务 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 以下两行对Web应用很重要 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 代理后端API请求 location /api/v1 { proxy_pass http://localhost:3001/api/v1; # 后端API服务 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 代理WebSocket连接(用于实时协作) location /socket.io { proxy_pass http://localhost:3002/socket.io; # 后端WebSocket服务 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }保存并退出。检查 Nginx 配置语法是否正确:
sudo nginx -t如果显示syntax is ok,就可以重新加载 Nginx 使配置生效:
sudo systemctl reload nginx现在,你需要修改之前docker-compose.yml文件中的ALLOWED_ORIGINS环境变量,以及前端容器中指向后端的地址。因为现在流量都经过 Nginx,前端在浏览器中运行时,它看到的“后端”地址应该是你的域名,而不是localhost。
更新docker-compose.yml中的后端服务配置:
environment: - NODE_ENV=production - PORT=80 - ALLOWED_ORIGINS=https://draw.yourdomain.com # 改为你的HTTPS域名同时,更新前端服务的环境变量,将localhost替换为你的域名:
environment: - NODE_ENV=production - REACT_APP_BACKEND_V1_GET_URL=https://draw.yourdomain.com/api/v1 - REACT_APP_BACKEND_V1_POST_URL=https://draw.yourdomain.com/api/v1 - REACT_APP_WS_SERVER_URL=wss://draw.yourdomain.com/socket.io # 注意是wss - REACT_APP_FIREBASE_CONFIG='{}'重要提示:在修改了 Compose 文件后,需要重启服务以使配置生效:
docker-compose down docker-compose up -d最后,为你的域名申请并配置 SSL 证书。使用 Certbot 可以自动化完成:
sudo certbot --nginx -d draw.yourdomain.com按照提示操作,Certbot 会自动修改你的 Nginx 配置文件,添加 HTTPS 支持,并设置自动续期。完成后,你就可以通过https://draw.yourdomain.com安全地访问你的私有 Excalidraw 服务了。
4. 部署后的关键配置、管理与优化
服务跑起来只是第一步,要让它在生产环境稳定、好用,还需要一些额外的配置和日常管理操作。
4.1 数据备份与恢复策略
我们的绘图数据保存在 Docker 卷excalidraw-data中。定期备份这个卷是重中之重。你可以编写一个简单的备份脚本:
#!/bin/bash # backup_excalidraw.sh BACKUP_DIR="/path/to/your/backup/folder" VOLUME_NAME="excalidraw-data" # 根据 `docker volume ls` 确认卷名 TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="$BACKUP_DIR/excalidraw_backup_$TIMESTAMP.tar.gz" # 停止相关容器,确保数据一致性(可选,但推荐) docker-compose -f /opt/excalidraw/docker-compose.yml down # 创建备份 docker run --rm -v $VOLUME_NAME:/source -v $BACKUP_DIR:/backup alpine \ tar czf /backup/$(basename $BACKUP_FILE) -C /source . # 重启服务 docker-compose -f /opt/excalidraw/docker-compose.yml up -d echo "Backup completed: $BACKUP_FILE" # 可以在此添加删除旧备份的逻辑,例如保留最近30天的备份将脚本设为可执行,并添加到 crontab 中实现每日自动备份。
恢复数据时,操作类似,但顺序相反:
# 1. 停止服务 docker-compose down # 2. 创建一个临时容器,将备份文件解压到数据卷 docker run --rm -v excalidraw-data:/target -v /path/to/backup.tar.gz:/backup.tar.gz:ro alpine \ sh -c "rm -rf /target/* && tar xzf /backup.tar.gz -C /target" # 3. 重启服务 docker-compose up -d4.2 用户认证与访问控制(进阶)
默认部署的 Excalidraw 是完全开放的,任何人拿到链接都可以编辑。对于内部敏感信息,这显然不够。有几种方式可以增加访问控制:
- 网络层控制:最简单的方式是只在内网环境部署,通过防火墙规则限制外部IP访问服务器的 3000、3001、3002 端口或 Nginx 的 80/443 端口。
- Nginx 基础认证:在 Nginx 配置中增加用户名密码验证。
这种方式简单,但所有用户共享一个密码,且体验一般。location / { auth_basic "Restricted Area"; auth_basic_user_file /etc/nginx/.htpasswd; # 使用htpasswd命令创建此文件 ... # 原有的proxy_pass等配置 } - 集成第三方认证(如OAuth):这是更专业的方式。你可以在 Nginx 前再部署一个反向代理网关,例如 oauth2-proxy 或 Authelia ,将 Excalidraw 服务保护在后面,要求用户使用公司的单点登录(如 Google Workspace, GitHub Organization, 或自建的 OIDC 服务)进行认证。这需要额外的配置,但安全性和用户体验最好。
4.3 性能监控与日志查看
了解服务的运行状态很重要。
查看容器日志:
# 查看所有服务的日志 docker-compose logs # 实时跟踪日志 docker-compose logs -f # 查看特定服务(如后端)的日志 docker-compose logs excalidraw-room日志可以帮助你排查启动失败、API错误或协作连接问题。
监控资源使用:使用
docker stats命令可以实时查看容器的 CPU、内存使用情况。如果发现内存持续增长(可能的内存泄漏),或者协作时CPU占用过高,可能需要考虑升级服务器配置或调整 Docker 容器的资源限制(在docker-compose.yml中使用mem_limit,cpus等参数)。
4.4 版本更新与回滚
Excalidraw 项目仍在活跃开发中,定期更新可以获取新功能和 bug 修复。
更新:进入项目目录,拉取最新的镜像并重启服务即可。
cd /opt/excalidraw docker-compose pull # 拉取最新镜像 docker-compose down docker-compose up -d注意:更新前务必确认你的数据卷备份是最新的。虽然官方镜像更新通常很平滑,但以防万一。
回滚:如果新版本出现问题,需要回滚到旧版本。你需要知道之前稳定运行的镜像标签(Tag)。假设之前用的是
excalidraw/excalidraw:sha-abc123。- 首先,修改
docker-compose.yml文件,将image: excalidraw/excalidraw:latest改为image: excalidraw/excalidraw:sha-abc123(后端镜像同理)。 - 然后执行
docker-compose down && docker-compose up -d。 - 更稳妥的做法是,在每次更新前,给当前的
docker-compose.yml文件打一个标签备份,例如docker-compose.yml.bak.20240501,这样回滚时直接替换文件即可。
- 首先,修改
5. 常见问题排查与踩坑实录
即使按照步骤操作,也可能会遇到一些问题。下面是我在多次部署中遇到的一些典型问题及其解决方案。
5.1 容器启动失败:端口冲突
问题描述:运行docker-compose up -d后,使用docker-compose ps发现某个容器状态是Exit或不断重启。查看日志 (docker-compose logs <service-name>) 发现类似Error: listen EADDRINUSE: address already in use :::3000的错误。
根因分析:这意味着宿主机上的 3000、3001 或 3002 端口已经被其他进程占用。Docker 容器启动时无法绑定到这些端口。
解决方案:
- 检查端口占用:
sudo netstat -tulpn | grep :3000。 - 如果确认被占用,有两种选择:
- 停止占用进程:如果是不重要的服务,可以停止它。
- 修改映射端口:在
docker-compose.yml中修改ports配置,例如将- "3000:80"改为- "8080:80",然后访问时就用http://服务器IP:8080。记得同时更新 Nginx 配置和前端环境变量中关于地址的配置。
5.2 协作功能失效:WebSocket连接错误
问题描述:可以打开白板,但创建或加入房间时,浏览器控制台(F12)报错,提示 WebSocket 连接失败(如WebSocket connection to 'ws://...' failed),或者无法实时看到其他人的操作。
排查过程:
- 检查后端服务:首先确认
excalidraw-room容器是否正常运行 (docker-compose ps)。 - 检查环境变量:这是最常见的原因。确认前端容器中的
REACT_APP_WS_SERVER_URL环境变量配置正确。如果使用了 Nginx 反向代理,这个地址必须是可被浏览器访问的公开地址,并且协议要匹配(HTTP用ws://,HTTPS用wss://)。同时,后端的ALLOWED_ORIGINS必须包含前端的访问地址。 - 检查Nginx配置:确保 Nginx 配置中正确代理了
/socket.io路径,并且包含了proxy_set_header Upgrade和Connection "upgrade"这两行关键指令,这是 WebSocket 协议升级所必需的。 - 检查防火墙/安全组:如果服务部署在云服务器,确保安全组规则放行了后端 WebSocket 服务映射的端口(如3002),以及 Nginx 的 80/443 端口。
我的踩坑点:我曾将REACT_APP_WS_SERVER_URL错误地配置为ws://localhost:3002,但在生产环境,前端代码是在用户的浏览器里运行的,用户的浏览器根本无法直接访问我服务器上的localhost:3002。必须配置为服务器对外的域名或IP。
5.3 绘图无法保存/房间消失:数据卷权限问题
问题描述:创建房间并绘图后,刷新页面或重新打开链接,发现房间不存在或绘图内容丢失。
排查过程:
- 检查后端日志:
docker-compose logs excalidraw-room,看是否有关于文件读写的权限错误,例如EACCES: permission denied, open '/app/data/...'。 - 检查数据卷:
docker volume inspect excalidraw-data,查看Mountpoint,然后到宿主机对应目录检查文件是否存在,以及文件属主和权限。
解决方案:这通常是因为后端容器(以某个非root用户运行)没有权限写入挂载的数据卷目录。在宿主机上,手动修改数据卷挂载点的权限:
# 找到数据卷路径 VOLUME_PATH=$(docker volume inspect excalidraw-data --format '{{ .Mountpoint }}') # 将目录权限设置为777(最简单粗暴,但不够安全)或改为合适的用户组 sudo chmod -R 777 $VOLUME_PATH # 或者,更安全的方式是找出容器内运行进程的用户ID,并让宿主机目录归属该用户 # 1. 进入容器查看用户ID docker exec excalidraw-room id # 假设输出 uid=1000(node) gid=1000(node) # 2. 在宿主机修改目录属主 sudo chown -R 1000:1000 $VOLUME_PATH修改权限后,重启后端服务:docker-compose restart excalidraw-room。
5.4 前端加载缓慢或样式错乱:静态资源问题
问题描述:访问页面很慢,或者界面样式不正常,浏览器控制台提示某些.js或.css文件加载失败 (404)。
根因分析:这可能是由于 Docker 镜像构建问题,或者前端构建产物不完整。也可能是 Nginx 配置中,对静态资源的缓存或 MIME 类型设置不正确。
解决方案:
- 尝试清除浏览器缓存,或使用无痕模式访问。
- 检查 Nginx 配置,确保对前端服务的
proxy_pass指向正确,并且没有错误的location规则拦截了静态资源请求。 - 如果问题持续,考虑重建前端镜像。进入项目目录,执行:
这会强制拉取最新的前端镜像并重建容器。docker-compose down docker rmi excalidraw/excalidraw:latest docker-compose up -d
6. 从部署到深度使用:一些个人经验与建议
把 Excalidraw 部署起来只是开始,真正让它融入工作流才能发挥价值。这里分享几点我个人的使用心得。
关于“房间”与“项目”管理:本地化部署的后端提供了一个简单的基于房间ID的存储。但它没有文件夹或标签功能。我们的做法是,建立一个内部的 Wiki 页面或文档,用来记录重要的绘图链接,并附上简单的标题和描述。例如,一个技术架构图房间的链接,我们会把它记录在对应的项目文档里。对于需要长期维护的图,我们甚至会将最终的稳定版本导出为 PNG 或 SVG,放入版本控制系统(如 Git)中,而房间链接则用于日常的协作和修改讨论。
性能与规模考量:Excalidraw-room 后端默认使用内存存储,并通过 Socket.IO 进行实时同步。在我们的使用中,一个房间内同时有 5-10 人进行频繁绘制时,体验依然流畅。但如果人数更多,或者绘图元素极其复杂(成千上万个图形),可能会对服务器内存和网络带宽造成压力。目前还没有遇到瓶颈,但这是需要留意的。对于超大规模的使用,可能需要考虑对后端服务进行水平扩展,但这已经超出了基础部署的范畴。
备份策略的细化:除了全量备份数据卷,我们还定期(每周)将重要的、已完成的绘图手动导出为.excalidraw文件(这是一种 JSON 格式),并归档到团队网盘。数据卷备份是为了灾难恢复,而手动导出归档则是为了重要的版本留痕和知识管理。
探索自定义与集成:本地化部署的最大优势之一是你可以修改代码。虽然 Excalidraw 的代码结构比较复杂,但社区有一些有趣的修改案例,比如增加自定义图形库、修改默认字体、集成内部图标系统等。如果你有前端开发能力,可以 Fork 其源码,在构建自己的 Docker 镜像前进行定制。另一种更轻量的集成方式是,利用 Excalidraw 提供的 API 将其嵌入到其他内部系统中,比如在项目管理工具里直接打开一个画板来讨论需求。
最后,我想说的是,本地化部署这类工具,技术上的难点往往不大,真正的挑战在于让团队成员接受并习惯使用它。我们最初也经历了一个“推广期”,主动在技术评审、需求讨论中使用这个白板,并展示其便捷性(比如实时协作修改架构图)。当大家发现它确实比反复传文件、截图更新要高效时,自然就用起来了。现在,它已经成了我们团队远程协作的一个基础设施。希望这份详细的指南,能帮你顺利搭建起属于自己的、安全可靠的数字白板。
