WikiDocs 2.0 Docker Compose 部署指南

目标:在一台 Linux 服务器上用 Docker Compose 从 0 到 1 部署 WikiDocs(Wiki|Docs)2.0.2 无数据库 Markdown Wiki,覆盖镜像选型、数据目录权限、服务编排、初始化向导、public/private 模式切换、升级与备份,并沉淀部署全程踩到的 5 个真实故障的定位与修复过程。已在 Ubuntu 26.04.1 LTS + Docker 29

WikiDocsWikiDocs知识库自托管Docker Compose2026/10/30

WikiDocs 2.0 Docker Compose 部署指南

目标:在一台 Linux 服务器上用 Docker Compose 从 0 到 1 部署 WikiDocs(Wiki|Docs)2.0.2 无数据库 Markdown Wiki,覆盖镜像选型、数据目录权限、服务编排、初始化向导、public/private 模式切换、升级与备份,并沉淀部署全程踩到的 5 个真实故障的定位与修复过程。已在 Ubuntu 26.04.1 LTS + Docker 29.9.2 + Compose v5.6.1 环境实测通过。

一、环境信息

本文所有命令与回显均在以下真实环境逐条实测,读者在相近环境可按原文复现。

  • 操作系统:Ubuntu 26.04.1 LTS(内核 7.0.0-38-generic)
  • Docker:29.9.2(引擎)+ Docker Compose v5.6.1(插件)
  • 应用版本:WikiDocs 2.0.2(官方镜像 zavy86/wikidocs:2,镜像体积 411MB)
  • 后端运行时:Node.js v22.23.4(应用自报,见初始化后 /api/information 回显)
  • 服务端口:宿主机 8132 → 容器内 3210(3210 为 2.0 系列官方默认监听端口)
  • 数据目录:/opt/wikidocs/datasets(挂载到容器内 /var/lib/wikidocs/datasets)
  • 部署目录:/opt/wikidocs(存放 docker-compose.yml)

💡 说明:WikiDocs 2.0 由 NestJS 后端 + Angular Web 客户端构成,单个 Docker 镜像同时提供 Web 界面与 REST API,无外部数据库依赖,数据集就是一个目录。选型背景与版本线说明见 《WikiDocs 介绍与选型指南》。

二、镜像选型说明

官方在 Docker Hub 发布 zavy86/wikidocs,README 明确「Running Wiki|Docs in a container is the recommended way to host it yourself」(容器自托管是官方推荐方式)。标签选择策略如下:

标签内容适用场景
22.0 系列最新构建(本文实测时对应 2.0.2)常规部署,自动获得 2.x 修订
2.0.2锁定到具体版本生产环境版本锁定
1.x 系列 / legacy旧 PHP 实现存量 1.x 迁移过渡,不建议新部署

本文采用 zavy86/wikidocs:2 并实测其元数据:

docker image inspect zavy86/wikidocs:2 --format '{{json .Config.Env}}'
["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NODE_VERSION=22.23.4","YARN_VERSION=1.22.22","NODE_ENV=production","MODE=public","PORT=3210","FRONTEND=/app/frontend","SECRET=wikidocs-insecure-secret-to-be-changed","DATASETS=/var/lib/wikidocs/datasets"]
docker image ls zavy86/wikidocs --format '{{.Repository}}:{{.Tag}} {{.Size}}'
zavy86/wikidocs:2   411MB

镜像内置 5 个可覆盖的环境变量(MODE/SECRET/PORT/DATASETS/FRONTEND),这正是第四章环境变量表的实测依据;SECRET 默认值 wikidocs-insecure-secret-to-be-changed 明确提示必须替换。

三、前置条件

  1. 服务器已安装 Docker 引擎与 Compose 插件(验证命令与回显):
docker compose version
Docker Compose version v5.6.1
  1. 规划并确认宿主机端口未被占用。WikiDocs 容器内固定监听 3210,对外映射端口按需选择,本文用 8132。先查占用再定端口:
ss -ltn | grep -E ':(8132|3210)\b' || echo '端口空闲'
端口空闲
  1. 创建数据目录并预置属主。这是本组件部署的第一个关键点:容器内进程以 node 用户(uid=1000)运行,宿主数据目录若属 root,容器启动会因数据目录不可写而无限重启(故障实录见第七章 7.1):
mkdir -p /opt/wikidocs/datasets && chown -R 1000:1000 /opt/wikidocs/datasets
ls -ld /opt/wikidocs/datasets
drwxr-xr-x 2 1000 1000 4096 Oct 30 14:45 /opt/wikidocs/datasets
  1. 拉取镜像(网络不佳时可配置镜像加速或代理后再拉):
docker pull zavy86/wikidocs:2
2: Pulling from zavy86/wikidocs
d4d21753bd21: Pull complete
bf077dfaf22d: Pull complete
Digest: sha256:a40eb007b1841aed43b8892341965edbd14f589f2b0c4a4fb65ccf6cd6497f0a
Status: Downloaded newer image for zavy86/wikidocs:2
docker.io/zavy86/wikidocs:2

四、部署步骤

4.1 编写 docker-compose.yml

在 /opt/wikidocs 下创建编排文件。name: 显式指定 Compose 项目名,避免与服务器上其他 Compose 栈同名互相干扰:

name: wikidocs-2.0.2-stack

services:
  wikidocs:
    image: zavy86/wikidocs:2
    container_name: wikidocs-2.0.2-web
    environment:
      MODE: private
      SECRET: q1w2e3r4-instance-secret-key
      PORT: "3210"
    volumes:
      - /opt/wikidocs/datasets:/var/lib/wikidocs/datasets
    ports:
      - "8132:3210"
    restart: unless-stopped

逐项说明:

  • MODE: private:私有模式,所有页面需登录访问;对外公开的知识库改为 public(游客免登录只读),详见 4.5;
  • SECRET:登录令牌(JWT)的签名密钥,必须替换为随机长串,绝不能沿用镜像默认值 wikidocs-insecure-secret-to-be-changed;
  • PORT:容器内监听端口,保持官方默认 3210 即可,对外暴露靠 ports 映射;
  • volumes:把宿主数据目录挂载到容器内数据集位置,所有文档、账号、设置都落在这个目录;
  • restart: unless-stopped:服务器重启后自动拉起。

⚠️ 注意:项目名 wikidocs-2.0.2-stack 中的点号会被 Compose 规范化剥掉,实际生成的网络名为 wikidocs-202-stack_default,docker compose down 等命令在部署目录内执行即可自动对上,无需手工处理。

4.2 启动服务

cd /opt/wikidocs && docker compose up -d
 Container wikidocs-2.0.2-web  Created
 Container wikidocs-2.0.2-web  Starting
 Container wikidocs-2.0.2-web  Started
docker compose ps
NAME                 IMAGE               COMMAND                  SERVICE    CREATED          STATUS          PORTS
wikidocs-2.0.2-web   zavy86/wikidocs:2   "docker-entrypoint.s…"   wikidocs   13 minutes ago   Up 13 minutes   0.0.0.0:8132->3210/tcp, [::]:8132->3210/tcp

PORTS 列显示宿主 8132 已正确映射到容器 3210。

4.3 观察启动日志

docker compose logs --tail 8 wikidocs

输出节选(实测首尾):

wikidocs-2.0.2-web  [Nest] 1  - 10/30/2026, 6:45:28 AM   LOG [RouterExplorer] Mapped {/api/document, GET} route
wikidocs-2.0.2-web  [Nest] 1  - 10/30/2026, 6:45:28 AM   LOG [RouterExplorer] Mapped {/api/attachment, POST} route
wikidocs-2.0.2-web  [Nest] 1  - 10/30/2026, 6:45:28 AM   LOG [RouterExplorer] Mapped {/api/trash, DELETE} route
wikidocs-2.0.2-web  [Nest] 1  - 10/30/2026, 6:45:28 AM   LOG [NestApplication] Nest application successfully started
wikidocs-2.0.2-web  [Nest] 1  - 10/30/2026, 6:45:28 AM   LOG [Bootstrap] Wiki|Docs is listening on http://[::1]:3210

看到 Nest application successfully started 与 listening on 即启动完成。日志里 http://[::1]:3210 只是日志展示格式,服务实际监听 :::3210(全部接口,IPv4/IPv6 双栈),不要据此误判为仅环回可用——端口映射后的宿主访问是正常的(实测见 4.4)。

⚠️ 注意:若日志反复刷 FATAL [Bootstrap] DATASETS directory </var/lib/wikidocs/datasets> is not writable 并无限重启,是数据目录属主问题,处理方法见第七章 7.1。

4.4 HTTP 探活验证

curl -s -o /dev/null -w 'GET / -> %{http_code}\n' http://127.0.0.1:8132/
curl -s -o /dev/null -w 'GET /api/ -> %{http_code}\n' http://127.0.0.1:8132/api/
curl -s -o /dev/null -w 'GET /api/health -> %{http_code}\n' http://127.0.0.1:8132/api/health
GET / -> 200
GET /api/ -> 200
GET /api/health -> 204

三个探针全部通过:/ 是 Web 客户端,/api/ 是 Swagger 文档页,/api/health 是健康检查端点(无内容、204 即健康)。

4.5 首次初始化(建管理员)

首次访问 http://<服务器IP>:8132/ 时 Web 客户端会引导完成站点初始化;也可以直接调初始化接口(与向导等效),管理员账号按演示规范使用 admin@lab.local / q1w2e3r4:

curl -s -X POST http://127.0.0.1:8132/api/initialize \
  -H 'Content-Type: application/json' \
  -d '{"title":"Lab Wiki","account":"admin@lab.local","firstname":"Admin","lastname":"User","password":"q1w2e3r4"}'

返回 201(无响应体)。初始化成功后数据目录会落盘四个文件:

ls -la /opt/wikidocs/datasets/
-rw-r--r--  1 1000 1000  240 Oct 30 14:50 settings.json
-rw-r--r--  1 1000 1000  208 Oct 30 14:50 accounts.json
-rw-r--r--  1 1000 1000    3 Oct 30 14:50 pinned.json
drwxr-xr-x  4 1000 1000 4096 Oct 30 14:50 documents
drwxr-xr-x  2 1000 1000 4096 Oct 30 14:50 trash

初始化是否完成可用公开端点复核(initialized 字段):

curl -s http://127.0.0.1:8132/api/information
{"mode":"private","initialized":true,"service":"wikidocs-backend","version":"2.0.2","host":"10.89.58.2","platform":"linux","engine":"v22.23.4","pid":1,"uptime":157,"memory":38514688}

⚠️ 注意:初始化接口只能成功调用一次,重复调用返回 400(实例已初始化);入参邮箱格式不合法时同样返回 400,实测报错回显为 {"message":["account must be an email"],"error":"Bad Request","statusCode":400}。私钥、管理员邮箱与密码务必在初始化时确定,后续修改走应用内设置页。

4.6 登录验证

用初始化的管理员账号换取访问令牌,验证账号体系已生效:

curl -s -X POST http://127.0.0.1:8132/api/authenticate \
  -H 'Content-Type: application/json' \
  -d '{"account":"admin@lab.local","password":"q1w2e3r4"}'

输出节选(实测首尾):

{"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkdXJhdGlvbiI6OTAwLCJnZW5lcmF0aW9uIjoiMjAyNi0xMC0zMFQwNjo1MTowOC40MTZaIiwiZXhwaXJhdGlvbiI6IjIwMjYtMTAtMzBUMDc6MDY6MDguNDE2WiIsImFjY291bnQiOiJhZG1pbkBsYWIubG9jYWwiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImF1dGhvcml6YXRpb25zIjpbInJlYWQiLCJ3cml0ZSIsImRlbGV0ZSIsInN5bmMiLCJtYW5hZ2UiXSwiaWF0IjoxNzkzMzQzMDY4fQ.rDzLBd…"}

令牌有效期 900 秒(15 分钟),携带 read/write/delete/sync/manage 全量授权。浏览器访问 http://<服务器IP>:8132/ 用同一账号登录即可开始写作。

4.7 模式切换(private ↔ public)

私有模式下游客访问接口被拒(实测回显):

curl -s http://127.0.0.1:8132/api/guest
{"message":"Guest access is enabled only in public mode","error":"Unauthorized","statusCode":401}

要开放公开只读浏览,修改 docker-compose.yml 中 MODE: public 后重建服务:

cd /opt/wikidocs && docker compose up -d
 Container wikidocs-2.0.2-web  Recreate
 Container wikidocs-2.0.2-web  Recreated
 Container wikidocs-2.0.2-web  Started

复核模式与游客令牌(public 模式下游客免登录领取只读令牌,有效期 86400 秒):

curl -s http://127.0.0.1:8132/api/information | head -c 120; echo
curl -s http://127.0.0.1:8132/api/guest | head -c 120; echo
{"mode":"public","initialized":true,"service":"wikidocs-backend","version":"2.0.2"
{"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkdXJhdGlvbiI6ODY0MDAsImdlbmVyYXRpb24iOiIyMDI2LTEwLTMwVDA2OjU1OjMyLjMxMloiLCJhY2NvdW50IjoiZ3Vlc3Qi…

⚠️ 注意:MODE=public docker compose up -d 这种「命令行塞环境变量」的方式不会覆盖 compose 文件里的 environment: 值(实测 /api/information 仍返回 private),必须直接修改 docker-compose.yml 后再 up -d。

五、数据持久化说明

WikiDocs 是无数据库文件型应用,/opt/wikidocs/datasets 就是全部状态的存放处,结构如下(初始化 + 建页后的实测布局):

datasets/
├── settings.json        # 站点设置(标题/语言/主题色等 9 字段)
├── accounts.json        # 账号表(密码为 sha256 摘要)
├── pinned.json          # 置顶文档列表
├── documents/           # 文档树:每页一个目录,content.md 为正文
│   └── guides/quick-start/
│       ├── content.md
│       ├── note.txt     # 附件与正文同目录
│       └── _versions/1793343178711.md   # 版本快照(毫秒时间戳命名)
└── trash/               # 回收站(删除页带毫秒时间戳前缀存放)

备份与恢复因此非常直接——备份即打包目录,恢复即解包还原:

tar czf /opt/backup/wikidocs-datasets-$(date +%F).tar.gz -C /opt/wikidocs datasets

升级应用版本不影响该目录(2.0.2 官方迁移说明明确数据文件完全兼容),建议升级前照上例做一次快照。设置项与账号文件的字段级说明见 《WikiDocs 2.0 配置参数详解》。

六、环境变量表

镜像可识别的全部环境变量(依据镜像元数据与官方 Compose 样例,默认值实查):

变量默认值取值与作用
MODEpubliclocal(桌面端本地形态)/ private(登录访问)/ public(游客免登录只读)三态;决定游客令牌端点是否可用
SECRETwikidocs-insecure-secret-to-be-changed登录令牌签名密钥,生产必须改为随机长串;更换后所有已发令牌失效
PORT3210容器内监听端口;改端口需同步调整 ports 映射右值与容器内探活地址
DATASETS/var/lib/wikidocs/datasets数据集根目录;仅当自定义卷挂载点时同步调整
FRONTEND/app/frontend前端静态资源目录,镜像内已固定,无需修改

模式语义补充(实测结论):public 模式下 /api/guest 可匿名领取 24 小时只读令牌;private 模式下同一端点返回 401(回显见 4.7)。local 供桌面应用本地形态使用,服务器部署不使用该值。

七、常见问题(FAQ)

以下 5 个问题全部是本次部署实测踩到并解决的真实故障,现象、定位、修复均给出原始回显。

7.1 启动即崩溃重启:DATASETS directory is not writable

现象:容器反复重启,日志连续刷同一行 FATAL:

[Nest] 1  - 10/30/2026, 6:45:01 AM   FATAL [Bootstrap] DATASETS directory </var/lib/wikidocs/datasets> is not writable

定位:容器进程以 node(uid=1000)运行,而宿主数据目录由 root 创建(drwxr-xr-x root root),容器内无写权限:

docker inspect wikidocs-2.0.2-web --format 'User: [{{.Config.User}}]'
User: [node]
docker run --rm --entrypoint sh zavy86/wikidocs:2 -c 'id'
uid=1000(node) gid=1000(node) groups=1000(node),1000(node)

修复:宿主上把数据目录属主改为 1000:1000 后重启服务:

chown -R 1000:1000 /opt/wikidocs/datasets
cd /opt/wikidocs && docker compose restart

复核容器转为稳定运行(docker compose ps 状态 Up,日志出现 Nest application successfully started)。

7.2 端口映射后 curl 得到空回复

现象:服务日志正常,但宿主机 curl http://127.0.0.1:8132/ 返回码 000(Empty reply from server)。

定位:用 ss -ltnp 查该端口的监听进程,发现 8130 已被服务器上另一个常驻进程占用(本例为 IDE 组件):

LISTEN 0  511  127.0.0.1:8130  0.0.0.0:*  users:(("trae-cn",pid=49440,fd=70))

端口被抢占导致请求打到了别的进程上。

修复:换一个 ss -ltn 确认空闲的端口(本文最终用 8132),改 docker-compose.yml 的 ports 后 docker compose up -d 重建。教训:端口预检要在启动前后各做一次——启动前查规划端口是否空闲,启动后用 docker compose ps 核对映射确实落在预期端口。

7.3 命令行注入的 MODE 环境变量不生效

现象:执行 MODE=public docker compose up -d 后 /api/information 仍返回 "mode":"private"。

定位与修复:Compose 文件 environment: 段的值优先于命令行 Shell 变量;Shell 变量只能填充 docker-compose.yml 里的 ${VAR} 占位符。直接修改文件中的 MODE: 值再 up -d 重建即可(实测切换过程回显见 4.7)。

7.4 日志显示监听 [::1] 是否只能本机访问

现象:日志出现 Wiki|Docs is listening on http://[::1]:3210,担心仅绑定环回。

结论:这是 Nest 日志的展示格式。进入容器内核实实际监听地址:

docker exec wikidocs-2.0.2-web netstat -ltn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 :::3210                 :::*                    LISTEN

:::3210 表示监听全部接口(IPv4/IPv6 双栈),宿主经端口映射访问正常(4.4 实测 200/204)。

7.5 启动日志出现 Unable to refresh latest release: fetch failed

现象:启动后偶见:

[Nest] 1  - 10/30/2026, 6:45:33   ERROR [ReleaseService] Unable to refresh latest release: fetch failed

结论:应用会到 GitHub 检查最新版本号,服务器无外网(或出网受限)时该检查失败并打告警日志,不影响 Wiki 功能;网络恢复后 GET /api/release 会正常返回当前与最新版本(实测 {"current":"2.0.2","latest":"2.0.2"})。离线环境可忽略该告警。

八、参考链接