
最近在帮团队搭内部知识库前后折腾了一周多最后用 Docker 把觅思文档这套私有化文档管理系统跑了起来。整个过程踩了不少坑包括数据库连接超时、容器频繁重启、时区显示不对、上传大文件失败等等所以想把完整的部署过程、编排方案和排查经验整理出来给准备自建文档平台的朋友一个可直接抄作业的参考。这篇内容面向两类人一类是运维拿到一台空服务器不知道怎么编排依赖另一类是开发公司没有现成的文档系统想快速搭一套内部可用的平台。只要你懂最基础的 Linux 命令能登录服务器敲几行命令跟着下面的操作走基本可以实现从零到一搭好一套私有化文档管理系统。如果你已经装了 Docker可以直接跳到编排文件部分重点看 3、4、5 三节。1. 项目背景与部署方案选型1.1 私有化文档系统到底解决了什么很多团队早期都用“某个在线文档工具 聊天群发链接”的方式管文档一开始觉得够用等文档数量上百之后问题就很明显。文档散落在个人账号里人一离职内容就跟着没了权限只能做到“能看”和“能编辑”做不到按团队、按目录细分更关键的是核心资料全部放在别人服务器上数据完全不掌握在自己手里。私有化部署解决的正是这三个痛点数据落库在自有机房或云服务器、权限体系可细分、文档与账号体系可以跟企业内网打通。投入的成本无非是一台低配服务器和每天几分钟的维护时间但换来的是对数据的绝对掌控。1.2 为什么选觅思文档而不是自研或直接用 SaaS选型时我对比过好几类方案。Confluence 功能全但重对服务器要求高授权费用也不便宜开源的 ShowDoc 做接口文档很强但团队知识库、版本管理这块偏弱Notion、语雀这类 SaaS 产品体验好却绕不开数据外驻的问题。最后选觅思文档主要是看中三点。第一技术栈是 Spring Boot Vue 这套国内团队非常常见的组合有问题社区反馈及时二次开发门槛相对低第二功能覆盖面符合中小团队诉求既有空间和目录式的文档管理也支持在线编辑、历史版本和成员权限第三官方提供了 Docker 镜像等于把部署中最容易出问题的依赖环境全部封装好了不需要手动去装 JDK、Node、Maven这一点对运维来说非常友好。1.3 为什么用 Docker Compose 而不是一条条 docker run如果只跑一个应用容器docker run 确实够用。但觅思文档依赖数据库和缓存最少涉及三个容器应用本身、MySQL、Redis。三个容器用命令一条条启动先不说参数多容易漏单是启动顺序就得人工控制。数据库还没就绪应用先起来大概率报连接超时。Docker Compose 的价值在于把“容器如何编排、依赖关系是什么、数据存在哪、网络怎么连”全部声明在一个 yml 文件里。启动一条命令停止一条命令换服务器时复制目录过去再启动一遍就能恢复。这个思路解决了跨环境迁移的大问题所以下面的部署方案统一基于 Docker Compose。2. 部署环境准备从 Docker 安装到镜像获取2.1 服务器配置建议与环境检查先说硬件。觅思文档这套系统不算重但要跑文档编辑、全文检索这些场景服务器也不能太寒酸。结合我自己压过一轮的使用情况给一个参考线2 核 4G 内存起步磁盘 40G 以上系统推荐 Ubuntu 20.04/22.04 或 CentOS 7.9。如果在云上买服务器带宽按团队人数估10 个人以内 5M 足够人再多建议走内网或者对象存储做附件分发。登录服务器后先检查系统版本和内核cat /etc/os-release uname -a如果系统太老比如 CentOS 6内核版本低Docker 新版跑不起来建议直接重装系统不要在旧系统上折腾兼容性问题。还要确认防火墙端口放行了 8080应用端口、3306数据库端口仅内网需要、6379Redis 端口仅内网需要。如果用了云安全组规则跟着一起调整。2.2 安装 Docker 与 Compose不同发行版安装 Docker 的方式略有区别。Ubuntu 环境我用官方仓库装是最省事的sudo apt update sudo apt install -y apt-transport-https ca-certificates curl software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [archamd64 signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install -y docker-ce docker-compose-pluginCentOS 环境则简单很多直接执行sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin装完启动服务并设为开机自启sudo systemctl enable --now docker docker version docker compose version我这里的建议是直接使用 Docker 官方源安装 Docker Engine 和 Compose 插件。系统自带的 docker-compose 命令是老版本的 Python 实现有些新特性的支持不完整后面执行docker compose时最好使用插件版本注意命令中间没有横杠。注意不要用系统自带的老版本 docker-compose有横杠的那个它不兼容 compose spec 的很多写法。优先使用 docker compose空格子命令。2.3 镜像版本选择与导入方式镜像版本这块我吃过一次亏。刚开始图省事直接拉 latest 标签系统跑起来之后隔几天发现社区发了新版本想升级时因为 latest 是浮动的镜像缓存不一致回滚都不知道回哪个版本。所以生产环境部署强烈建议锁定固定版本号比如minedoc/minedoc:2.4.0升级时自己控制时机。拉取镜像docker pull minedoc/minedoc:2.4.0 docker pull mysql:8.0.36 docker pull redis:7.2.4拉完确认镜像存在docker images如果你的服务器没有外网权限或者网络下载镜像一直失败可以在有网的机器上执行docker pull、docker save导出为 tar 包再传到目标服务器执行docker load导入。注意版本号要一致否则编排文件里写的版本拉不到本地镜像。3. 编排文件逐段拆解与参数说明3.1 数据层MySQL 与 Redis 容器文档系统的核心是数据所以数据库容器是编排里第一个要设计的组件。我用的是 MySQL 8.0环境变量里指定了 root 密码、初始数据库名和业务账号密码。这样设计的目的不是简单“把 MySQL 跑起来”而是让 MySQL 容器在首次初始化时自动创建好数据库和账号应用容器后续只用业务账号连接避免所有服务都拿 root 权限。Redis 在这里扮演缓存和会话存储的角色。requirepass参数设置访问密码appendonly yes开启持久化。别小看 Redis 的密码如果端口不小心暴露到了公网没有密码的 Redis 等于裸奔已经被扫描器打穿多少次了。数据卷方面MySQL 数据目录挂到宿主机的./data/mysqlRedis 数据挂到./data/redis。这一步是必须的因为容器本身是无状态的一旦容器删掉没有挂载的数据也会跟着消失。挂载到宿主机之后删容器、升级版本、迁移服务器数据都还在。3.2 应用层App 容器与环境变量应用容器是整套系统的工作核心。环境变量里最关键的是数据库连接和 Redis 连接。注意连接地址不是localhost而是服务名mysql和redis。这是因为 Compose 会默认给所有服务创建一个内部网络服务之间通过服务名互相访问。用服务名的好处是即使后面端口映射改了容器内部通信地址不需要变。数据库连接串里必须带useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai这几个参数。第一个和第二个保证中文不乱码第三个避免 MySQL 8 默认走 SSL 导致连接报错第四个是数据库时区。这三个参数我一次写全后面省了很多排查时间。JVM 参数这里给了-Xms512m -Xmx1024m。4G 内存的服务器要给 MySQL 和 Redis 留足内存应用堆上限 1G 是比较稳的配置。如果你团队文档特别多、并发上来了可以调到 2G但主机内存最好加到 8G。3.3 数据卷与重启策略应用容器的数据卷主要挂两个目录上传附件目录和日志目录。附件不挂载的话容器重建一次用户传的图片就没了这个坑是很多新手反复踩的。日志挂载出来是为了后续排查问题时直接在宿主机上用tail -f就能看不用再docker exec进容器。重启策略统一设成unless-stopped。它的含义是只要不是人工执行docker compose stop显式停止容器异常退出或服务器重启后都会自动拉起。这个策略配合健康检查能保证 MySQL 起来之前应用容器不盲目启动。关于健康检查我把它加在了 MySQL 服务上用mysqladmin ping探测数据库是否真正就绪App 服务再用depends_on的condition: service_healthy来约束启动时机。这样做把“应用启动时数据库还没就绪”这类偶发故障直接消灭在编排层面。4. 实操全过程启动、初始化与正式访问4.1 准备初始化脚本与 .env 文件为了让docker-compose.yml里的敏感配置不硬编码我先在部署目录下创建一个.env文件里面放数据库密码、Redis 密码等变量。这个文件权限要收紧mkdir -p /opt/minedoc cd /opt/minedoc touch .env chmod 600 .env.env内容形如MYSQL_ROOT_PASSWORD你的root密码 MYSQL_PASSWORD你的业务密码 REDIS_PASSWORD你的redis密码新建sql目录把觅思文档官方的初始化 SQL 脚本放进去mkdir -p sql # 将 init.sql 放入 sql/ 目录MySQL 容器首次启动时会自动执行/docker-entrypoint-initdb.d/目录下的.sql文件从而初始化数据库表结构。注意这个机制只在数据目录为空时触发。如果你之前挂载过旧数据重新启动时不会重复执行这符合预期但也要求初始化脚本必须一次性正确。4.2 编写并启动 Compose 服务下面是我实际使用的docker-compose.yml完整内容你可以直接复制调整services: mysql: image: mysql:8.0.36 container_name: minedoc-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: minedoc MYSQL_USER: minedoc MYSQL_PASSWORD: ${MYSQL_PASSWORD} TZ: Asia/Shanghai command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci volumes: - ./data/mysql:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports: - 3306:3306 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 10s timeout: 5s retries: 10 redis: image: redis:7.2.4 container_name: minedoc-redis restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes volumes: - ./data/redis:/data ports: - 6379:6379 app: image: minedoc/minedoc:2.4.0 container_name: minedoc-app restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_started environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/minedoc?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: minedoc SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PORT: 6379 SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD} JAVA_OPTS: -Xms512m -Xmx1024m TZ: Asia/Shanghai volumes: - ./data/upload:/app/upload - ./data/logs:/app/logs ports: - 8080:8080写好后启动docker compose up -d没有报错的话查看所有容器状态docker compose ps三个服务的状态都应该是Up并且minedoc-mysql显示healthy。4.3 完成系统初始化和功能验证容器全部起来后浏览器访问http://服务器IP:8080。第一次访问会进入初始化页面按向导操作就行。需要设置管理员账号和密码。这里有个建议管理员密码不要用团队公用的弱口令至少 12 位混大小写和数字因为管理员能看全站文档。初始化完成后进入后台先做两件事。第一件在系统设置里把站点名称和默认语言改成自己团队的名称这个会显示在登录页和文档标题上第二件配置 SMTP 邮件服务这个不配也能用但用户找回密码和邮件通知功能会失效所以有条件尽量配。然后建一个测试空间传一个测试文档写上几个字保存刷新页面看看内容是否还在。再创建一个普通测试账号验证权限隔离是否生效。这一步确认没问题基本可以认定部署成功。4.4 反向代理与 HTTPS 接入可选如果只是内网直接用 IP 访问到上面一步就够了。但要做正式对外服务建议在前面加一层 Nginx把 8080 端口收敛到 443 并启 HTTPS。用 Nginx 代理时有几个关键点server { listen 443 ssl; server_name docs.example.com; ssl_certificate /etc/nginx/ssl/docs.example.com.pem; ssl_certificate_key /etc/nginx/ssl/docs.example.com.key; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:8080; 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; } }两个容易忽略的地方client_max_body_size要调到和附件上传大小匹配否则传大文件直接 413proxy_set_header的几个请求头必须带尤其是X-Forwarded-Proto否则系统通过request.getScheme()判断协议时会拿到 HTTP 而不是 HTTPS可能导致回调地址不正确。5. 经验向备份恢复方案设计5.1 定期备份数据库和附件部署完只是一半另一半是备份。容器部署最怕的是“以为数据在其实容器删了就没了”。虽然我们已经挂了数据卷但宿主机磁盘本身也可能损坏所以备份一定要做。数据库备份我用mysqldump在宿主机上执行docker exec minedoc-mysql sh -c exec mysqldump -uroot -p$MYSQL_ROOT_PASSWORD minedoc /opt/backup/minedoc_$(date %F).sql附件备份比较简单直接压缩挂载出来的 upload 目录tar czf /opt/backup/upload_$(date %F).tar.gz -C /opt/minedoc/data upload建议把备份和数据目录分磁盘存放避免数据盘故障时备份一起丢失。云服务器的话最理想的是备份到对象存储本地只留最近三天的备份。5.2 恢复演练步骤备份做得再好没演练过等于零。我建议至少做一次完整的恢复演练步骤是这样的先停止应用容器防止写库docker compose stop app恢复数据库docker exec -i minedoc-mysql sh -c exec mysql -uroot -p$MYSQL_ROOT_PASSWORD minedoc /opt/backup/minedoc_2025-01-01.sql解压附件tar xzf /opt/backup/upload_2025-01-01.tar.gz -C /opt/minedoc/data重启应用docker compose start app这里要提醒一个细节恢复数据库时先确认目标数据库名是否还是minedoc。如果你在.env里改过数据库实例名命令行里的数据库名要对应改否则恢复完应用找不到表报错会很奇怪。6. 常见问题排查与避坑技巧6.1 启动阶段的典型故障我部署过程中遇到最多的一类问题是“容器起来了但应用页面打不开”或“页面开了但一直报数据库错误”。首先看容器状态docker compose ps如果app容器显示 Restarting多半是启动时依赖的服务还没就绪或者是连接参数有问题。直接看日志docker logs -f minedoc-app --tail 200日志里常见的Communications link failure或Access denied前者表示 MySQL 没准备好或地址错误后者表示账号密码不对对照环境变量检查即可。还有一个很隐蔽的问题我把宿主机 3306 端口映射出去了但云安全组没有限制这个端口的来源 IP导致公网可以直连数据库。即使设置了密码也容易被暴力破解。建议数据库和 Redis 服务不要映射端口到公网或在安全组里只允许内网 IP 访问。6.2 使用阶段的高频问题运行一两天后发现附件上传失败而且小文件正常、大文件失败基本是上传大小限制问题。需要同时检查两处应用自身的spring.servlet.multipart.max-file-size默认通常是 10MB 左右和 Nginx 的client_max_body_size。只改一处效果有限。时区问题也很常见。系统时间显示差 8 小时原因通常是 MySQL 和应用容器的时区没设置成Asia/Shanghai。注意 MySQL 有两个层面容器 OS 时区由环境变量TZ控制JDBC 连接串里还要加serverTimezoneAsia/Shanghai两层都要对。内存相关的问题在低配服务器上比较典型。2G 内存的机器同时跑 MySQL、Redis、Java 应用非常容易 OOM。日志里出现Native memory allocation (mmap) failed或者容器直接被杀就是内存爆了。解决方案是调小JAVA_OPTS里的堆内存或者给系统加 swap但长期还是建议升级配置。6.3 常见问题速查表现象大概率原因排查方法容器一直重启依赖服务未就绪 / 内存不足docker logs查看具体报错页面提示数据库连接失败连接参数错误 / MySQL 未健康核对环境变量与健康检查状态中文乱码初始化字符集不匹配MySQL 初始化用 utf8mb4JDBC 加 characterEncodingutf8附件上传 413Nginx 限制调整 client_max_body_size附件上传后台报错应用上传限制调整 spring.servlet.multipart 参数时间差 8 小时时区未统一容器 TZ 和 JDBC serverTimezone 都设为 Asia/Shanghai升级容器后附件消失upload 目录未挂载检查数据卷是否指向宿主机的持久化目录备份文件占满磁盘备份策略未清理用 cron 定期清理老备份网页登录后立即掉线Redis 缓存故障 / 密码错误检查 Redis 连接与 requirepass 配置这个表建议截图存一份。等真正出问题的时候对照排查会省很多时间。最后再分享一个小技巧部署完成后把.env文件和docker-compose.yml提交到团队的 Git 仓库里但密码字段占位符保留真实密码只存在服务器上。换人或换机器时克隆仓库、填好.env、执行docker compose up -d一套环境就起来了。我按这个方式把之前一台部署环境迁移到新服务器整个过程没超过二十分钟。