ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零到外部访问:Docker Compose部署Wiki.js完整实战指南

从零到外部访问:Docker Compose部署Wiki.js完整实战指南 最开始其实没想过用 Wiki.js。家里有一台一直开着的服务器平时跑点小服务找知识库方案的时候试过好几款开源 wiki 软件最后在本地部署 外部访问这个组合需求下Wiki.js 成了我最顺手的选择。这两年多下来从最初的 Docker Compose 搭建到后面配置 Nginx 反向代理、接上 HTTPS、搞定家用宽带的 DDNS 和端口转发中间踩的坑不少但整套链路跑通之后确实很稳。这篇文章就把我实际落地的完整过程摊开讲重点放在外部访问这条最容易出问题的链路上。先说清楚这篇讲的是什么不是把容器拉起来截个图就完事的教程而是从选型对比、部署架构、数据库选择、反向代理与 WebSocket 透传、家用宽带接入一直到备份维护的完整实操记录。适合的对象很明确想在个人服务器、NAS 或者公司内网里部署一个真正能长期用的 wiki并且希望团队成员在外的网络环境下也能顺畅访问的人。1. 想清楚一件事你为什么需要自己部署 Wiki1.1 对比过 Confluence、BookStack、Outline 之后的结论在决定 Wiki.js 之前我把市面上主流能自托管的方案都过了一遍。这里直接给对比结果省得大家重复踩坑方案部署难度资源占用编辑体验内容存储MediaWiki中高PHP MySQL 全家桶传统老派数据库BookStack低低PHP MySQL简洁但偏年迈数据库Outline中偏高中需数据库 Redis接近 Notion数据库Confluence中高高Java 生态吃内存最成熟但最重数据库Wiki.js低低Node.js PostgreSQLMarkdown 可视化并存数据库 / Git / 本地文件当时我手里的服务器配置很普通内存也就 2G还要跑其他服务。Confluence 的 Java 堆内存一开就是 1G 起步直接劝退。BookStack 其实装起来最容易但对 Markdown 的支持一直差口气我写技术文档的习惯是纯 Markdown 本地编辑所以更想要一个Markdown 优先的方案。Outline 的编辑体验确实现代化可它依赖的组件太多部署完毕之后的维护成本明显高于 Wiki.js。Wiki.js 最终胜出的原因也很朴素Docker 镜像官方维护得很勤Node.js 单进程跑起来内存占用非常克制再加上内置的所见即所得编辑器和 Markdown 源码模式可以随时切换团队里习惯写 Markdown 的和习惯直接编辑的人都能用。1.2 Wiki.js 真正打动我的几个点界面现代化这点排第一别笑wiki 软件里丑的实在太多了。Wiki.js 基于 Vue 开发后台界面清爽响应式做得也不错手机浏览器直接访问不会让人想关掉页面。第二是内容存储的灵活性。Wiki.js 默认把页面数据存进数据库但它支持把页面同步到 Git 仓库。这一点在知识管理场景里几乎是杀手级功能所有页面变更都有版本记录可以和团队现有的 Git 工作流打通即使数据库整个挂了Git 仓库里还有全部内容的 Markdown 备份。后面我会单独讲如何配置。第三是对多用户和权限的支持比较完整。本地账号、LDAP、OAuth 都有目录树支持嵌套分级给不同团队划不同的空间权限很顺手。对于三五个人用的小团队默认的管理员 成员角色就够了。另外它还内置了多语言界面给不熟悉英文的同事部署时可以切到中文。1.3 版本选择稳定版 v2 与之后的迭代版本这里必须先提醒一句Wiki.js 的官方 Docker 镜像标签如果你直接拉latest有可能跑到非稳定版本上。目前我生产环境用的是 v2 系列镜像标签对应requarks/wiki:2。v3 从很早之前就在大版本重写架构变化很大功能还在完善阶段我不建议用来做正经知识库。等以后 v3 稳定了再迁移也不迟过程通常就是导数据库 换镜像的事。一句话总结选型逻辑如果你和我一样需要轻量、现代、Markdown 友好、能长期维护的开源 wiki而且有外部访问需求那么 Wiki.js 确实值得花一个下午完整搭起来。2. 部署前必须定的三件事访问路径、运行方式和数据目录2.1 先画出访问链路再动手很多人一上来就docker run等到要外部访问了才回头改架构结果来回折腾。我这里建议第一步先画清访问链路后面每一步都对号入座。两种典型场景仅内网访问浏览器 → 服务器内网 IP:3000 → Wiki.js 容器 → PostgreSQL 容器。最简单一条直线。需要外部访问浏览器 → 域名HTTPS→ 运营商公网 → 路由器端口转发 → 服务器反向代理 → 127.0.0.1:3000 → Wiki.js 容器 → PostgreSQL 容器。请注意外部访问场景里3000 端口永远不会直接暴露在公网中间必须有一层反向代理负责 HTTPS 和转发。这个设计不是洁癖而是安全刚需裸奔的 HTTP 端口会被扫描器盯上暴力破解登录页的情况我后面会专门讲。2.2 裸机部署和 Docker 部署怎么选Wiki.js 官方支持两种主流部署方式直接在服务器上装 Node.js 跑或者用 Docker。我的建议很直接除非你有特殊理由比如机器上实在装不了 Docker否则一律用 Docker Compose。对比项裸机部署Docker 部署环境隔离一般Node 依赖容易污染系统好所有依赖都在容器里升级回滚手动替换文件麻烦拉新镜像 重启一条命令数据库配套需要自己装 PostgreSQLCompose 里一并编排备份迁移手工找目录卷和 pg_dump 都很直观我选择 Docker 还有一个很实际的理由这台服务器上还跑着其他服务如果把 Node 环境和 PostgreSQL 都装在宿主系统里时间一长依赖就乱了。Docker Compose 把 Wiki.js 和 PostgreSQL 两个容器放在一个项目里管理清理和迁移都干净。2.3 数据库请直接选 PostgreSQLWiki.js 支持 SQLite、PostgreSQL、MySQL、MariaDB。官方文档推荐生产环境用 PostgreSQL这不是随口一说。SQLite 只建议拿来本地测试因为多个用户并发写入时锁竞争明显跑一段时间就可能遇到页面保存超时。MySQL 也能用但实际使用中我遇到过一些字符排序和全文检索的老问题犯不上。PostgreSQL 我用的版本是 15镜像标签postgres:15-alpine。Alpine 版本体积小跑在 2G 内存的小服务器上压力小。有同事问过我为什么不用更新的 16 或 17对我来说 wiki 这种负载用 15 完全够而且镜像更稳定没必要追新。3. 从零到向导落地用 Docker Compose 把 PostgreSQL 和 Wiki.js 跑起来3.1 第一步是写好 docker-compose.yml我习惯把整个项目放在/opt/wiki目录下所有配置和备份脚本都归拢到一处。下面是实际在用的 Compose 文件去掉了机器相关信息version: 3.8 services: db: image: postgres:15-alpine container_name: wiki-db environment: POSTGRES_DB: wiki POSTGRES_USER: wiki POSTGRES_PASSWORD: wiki_strong_password volumes: - db-data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U wiki] interval: 10s timeout: 5s retries: 5 restart: unless-stopped wiki: image: requarks/wiki:2 container_name: wiki-app depends_on: db: condition: service_healthy ports: - 127.0.0.1:3000:3000 environment: DB_TYPE: postgres DB_HOST: db DB_PORT: 5432 DB_NAME: wiki DB_USER: wiki DB_PASS: wiki_strong_password volumes: - wiki-data:/data restart: unless-stopped volumes: db-data: wiki-data:有几个地方值得展开说明。首先是端口映射这里写的是127.0.0.1:3000:3000意思是只把 3000 端口绑定在服务器本机回环地址上。这样同一台机器上的 Nginx 能访问到但从局域网其他机器直接通过 IP 访问是访问不到的。如果你的场景短期内不打算配反向代理只希望内网同事能先用起来可以改成0.0.0.0:3000:3000或者直接写3000:3000等反代配好之后再收回来。其次是db服务的 healthcheck。这个细节很多人会漏掉。depends_on默认只保证容器启动顺序不保证 PostgreSQL 已经准备好接受连接。不加健康检查的话Wiki.js 起来得比数据库快就会报数据库连接失败然后一直在启动失败和重启间循环。加了pg_isready健康检查之后condition: service_healthy会确保数据库真正就绪才启动 Wiki.js。启动命令很简单cd /opt/wiki docker compose up -d docker compose logs -f wiki日志里看到类似Server ready at http://localhost:3000的输出就说明应用层起来了。3.2 初始化安装向导的几个关键输入浏览器访问http://服务器IP:3000会进入安装向导。这里要填管理员邮箱、管理员密码以及最重要的一项站点 URLSite URL。站点 URL 必须认真填不要随手写http://localhost:3000。它会被用于生成页面内的所有绝对链接、邮件里的跳转地址、OAuth 回调地址。如果你打算最终通过https://wiki.example.com访问那就直接填这个最终地址。如果填错了后续虽然可以在后台管理 → 常规 → 站点 URL里修改但已生成的历史链接不会自动更新可能留一堆死链。3.3 首次登录后的基础配置清单启动完成后我建议按这个清单做一轮基础配置别急着把链接发给同事创建第一个页面 Home把使用规范写进去在管理 → 常规里检查站点 URL、语言、默认主页是否正确在管理 → 用户和组里建好团队账号权限先放开只读等大家熟悉了再给编辑权限给自己的管理员账号开启双因素认证TOTP。这一步强烈建议做后面讲暴力破解时你会感谢这个决定。3.4 本地访问验证配置完成后先不要挂反代在服务器上做一轮本地验证curl -I http://127.0.0.1:3000 docker compose logs --tail50 wiki如果 curl 返回 200日志没有报错再把防火墙放行 3000 端口给内网同事测一轮。Ubuntu 上用 ufw 的话大概是这样sudo ufw allow from 192.168.1.0/24 to any port 3000 proto tcp注意这里是按来源网段限制的而不是对全世界开放。内网验证没问题之后我们再进入最关键的环节外部访问。4. 外部访问的一整套链路反代、HTTPS 与公网接入4.1 为什么不要裸奔 3000 端口先给结论让外网直接通过http://服务器IP:3000访问是 wiki 部署里最不应该做的事情。原因有三个。第一纯 HTTP 明文传输管理员密码、页面内容全都能被中间人截获。第二3000 端口一旦对公网开放扫描器几分钟内就会盯上接着就是源源不断的登录尝试。第三你失去了统一管理的入口。以后你想给 wiki 加访问限制、加 WAF 规则、换域名都得在 Wiki.js 应用层处理远不如在一层 Nginx 上处理灵活。正确的姿势是保留反代软件监听公网端口 → 转发到本机 3000的架构。反代承担三件事HTTPS 加密、WebSocket 协议升级透传、以及把请求转发给 Wiki.js 容器。4.2 用 Nginx 接住 WebSocket配置拆解Nginx 是我最常用的反代。有一个词要先说清楚WebSocket。Wiki.js 的页面树展开、协作编辑器、实时通知都依赖 WebSocket 长连接。普通 HTTP 转发不会自动支持 WebSocket 的协议升级必须在 Nginx 配置里显式声明。下面是一个完整可用的 Nginx 站点配置server { listen 443 ssl http2; server_name wiki.example.com; ssl_certificate /etc/letsencrypt/live/wiki.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/wiki.example.com/privkey.pem; # 前端主干流量 location / { proxy_pass http://127.0.0.1:3000; 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; proxy_buffering off; proxy_read_timeout 60s; } # WebSocket 专用路径 location /socket.io/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; } }里面的关键行是proxy_set_header Upgrade $http_upgrade和proxy_set_header Connection upgrade这两行负责告诉后端这是一个 WebSocket 升级请求。proxy_buffering off是有一次排障后加上的。Wiki.js 页面更新会有服务端推送事件如果 Nginx 开了缓冲实时刷新会有明显延迟。不冲突的情况下建议直接关掉。证书我用的 Lets Encrypt签发和续期都是 certbot 自动完成sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d wiki.example.com4.3 想省事就上 Caddy反向代理里的省心之选如果你不想维护 Nginx 那一大段配置可以考虑 Caddy。它最大的特点是内置自动 HTTPS域名解析生效后配置文件一写证书申请、续期、HTTP 到 HTTPS 跳转全部自动完成WebSocket 也会自动透传。Caddyfile 的核心内容只有一行wiki.example.com { reverse_proxy 127.0.0.1:3000 }把 Caddy 跑起来之后访问https://wiki.example.com就直接通了。Caddy 适合不想折腾、也不担心性能上限的场景。我两台服务器上一台用 Nginx 一台用 Caddy实际体验都很稳定。4.4 家用宽带的最后一段DDNS 与端口转发到这里服务器内部链路已经通了剩下的问题是外网怎么进到你家这台服务器上。这需要先确认一个前提你的宽带有没有公网 IPv4 地址。最简单的判断方法在服务器上执行curl ifconfig.me拿到公网 IP再去路由器后台看 WAN 口 IP两者一致说明你有公网 IPv4。如果不一致说明运营商给你的是大内网地址标准的端口转发方案走不通只能退而求其次考虑 IPv6 直连或者隧道方案——这部分展开太多先按下不表。有公网 IPv4 的话下一步是解决动态 IP 问题。家用宽带的公网 IP 会定期变化所以你需要一个域名并配置 DDNS 让它始终指向最新的 IP。我用的是阿里云域名 云解析在路由器里直接填入 DNSPod 或阿里的 DDNS 凭据路由器每检测到 IP 变化就会自动更新解析记录。家里路由器没有内置 DDNS 功能的找一台常开的机器跑一个更新脚本也可以。还有一个非常现实的坑运营商经常会封禁 80 和 443 端口特别是没有备案的域名直接走 80 端口基本是访问不通的。这种情况下的做法是反代监听一个高位端口比如11443路由器把公网的11443端口转发到服务器内网 IP 的443端口。举个例子路由器端口转发配置大致是这样外部端口11443TCP内部 IP192.168.1.8你的服务器内网地址内部端口443服务器上 Nginx 监听的 HTTPS 端口网站访问地址就是https://wiki.example.com:11443。注意填 Wiki.js 后台的站点 URL 时也要带上这个端口号否则生成出来的链接会指向 443打不开。4.5 外部访问自测清单配置完了别急着宣布完工按这个清单自测一遍在手机流量网络不是家里 WiFi下访问https://wiki.example.com:11443确认能打开登录页编辑一个页面确认编辑器能正常加载、保存无异常开两个浏览器窗口同时编辑同一页面确认实时同步正常——这一步能验证 WebSocket 通没通查看反代日志确认没有大量 502、504检查 HTTPS 证书状态确认没有过期警告。5. 踩坑实录从部署到现在遇到的真问题5.1 容器端口映射搞混反代一直 502第一次配 Nginx 反代时我遇到的现象是内网直连3000端口一切正常但通过https://wiki.example.com:11443访问一直 502。排查链路是这样的先看 Nginx 错误日志提示connect() failed (111: Connection refused) while connecting to upstream说明 Nginx 连不上要转发的目标。然后在服务器上测试curl -I http://127.0.0.1:3000结果也是拒绝。但怪的是局域网 IP 访问 3000 是通的。最后查出来Compose 文件里端口写的是3000:3000等价于0.0.0.0:3000:3000正常情况下应该能访问啊。细看才发现这台服务器上之前装过一次 Wiki.js 的测试容器旧的容器还占着 3000 端口新的配置根本没生效。docker ps一看两个同名容器打架旧的把新项目顶掉了。这件事的教训有两点第一部署新服务前先把旧容器清干净docker ps -a确认没有残留第二反代排查永远先分层测试从最内层容器日志开始往外一层层查Nginx 报 502 时先确认内层到底通不通而不是急着改反代配置。5.2 编辑器加载不全WebSocket 被卡住了这个问题出现在给同事开放外部访问之后。外部网络访问时页面框架能打开文章也能看到但新建页面时编辑器一直转圈日志里反复出现 WebSocket 连接失败的报错。原因很明确Nginx 配置里漏了 WebSocket 升级相关的 header。我当时的配置只保留了常见的几个proxy_set_header没有加Upgrade和Connection upgrade。WebSocket 握手请求到达 Nginx 后没有被正确升级Wiki.js 前端的实时连接全部失败而页面静态内容是通过普通 HTTP 加载的所以看起来部分正常。修复方式就是把第 4.2 节那段配置补全。这里有个可以提前判断问题的技巧浏览器开发者工具里刷新页面搜索socket或ws请求如果看到 400、403 或者连接反复断开基本就是反代没有正确透传 WebSocket。5.3 PostgreSQL 起不来权限和顺序问题还有一个高频问题就是 PostgreSQL 容器反复重启。一个典型现象是docker compose up -d之后wiki容器日志先是报数据库连接失败接着db容器也跟着异常退出。这类问题一般有两个根因。第一个根因是健康检查缺失导致 Wiki.js 在 PostgreSQL 还没就绪时就去连库。这个用前面写的 healthcheck 方案能解决。第二个根因是权限常见于把 PostgreSQL 数据目录挂载到了宿主机的某个目录。宿主机目录的所有者和容器里的postgres用户 UID默认 999不一致数据库就启动不了。解决方法很简单用命名卷db-data不要手动挂载宿主目录。虽然看起来文件都在宿主里更直观但权限问题会让你怀疑人生。真要备份用pg_dump导数据别直接去拷数据库文件。5.4 一次升级差点翻车备份永远在最前面Wiki.js 的升级本身很简单docker compose pull docker compose up -d就完事了。我翻过一次车是在一个任务中升级时新版启动后页面全部 500查日志是数据库结构不兼容。幸好当时只是测试环境生产环境我升级前一定会先做数据库备份那次之后我把升级 备份 升级 验证写成了固定流程。具体流程我放在后面的维护章节这里只说一句话数据库备份永远在升级命令之前哪怕是 patch 版本。5.5 公网暴露后的暴力破解我看到的真实攻击把服务暴露到公网之后最直观的感受就是扫描和登录尝试根本不带停的。Nginx 日志里隔几分钟就有来自各个 IP 的 POST/login请求字典用户名轮番试。WiKi.js 自带登录限流吗有一定的基础防护但对高强度爆破还是不够。我做了三件事效果立竿见影所有人强制使用双因素认证TOTP就算密码被撞库也进不来Nginx 针对登录接口做限频比如限制每分钟同一 IP 最多 5 次登录尝试只开放需要的端口3000 端口永远不暴露公网上只留反代的 HTTPS 高位端口。Nginx 限频配置大概长这样limit_req_zone $binary_remote_addr zonewiki_login:10m rate5r/m; server { location /login { limit_req zonewiki_login burst10 nodelay; proxy_pass http://127.0.0.1:3000; } }实际效果是暴力尝试基本被挡在 Nginx 层Wiki.js 应用层几乎看不到压力。6. 日常维护备份、升级与长期稳定运行6.1 数据备份PostgreSQL 定时导出Wiki.js 的所有页面内容、账号、权限都在 PostgreSQL 里所以备份的核心就是数据库。我用一个 shell 脚本配合 crontab 每天凌晨导出一份 SQL 文件并保留最近 30 天。#!/bin/bash BACKUP_DIR/opt/wiki/backups DATE$(date %Y%m%d_%H%M%S) cd /opt/wiki docker compose exec -T db pg_dump -U wiki wiki | gzip $BACKUP_DIR/wiki_$DATE.sql.gz find $BACKUP_DIR -name *.sql.gz -mtime 30 -delete脚本思路很简单pg_dump -U wiki wiki导出全库gzip 压缩按日期命名删除 30 天前的旧文件。这里用的是exec -T避免在 cron 环境里因为没有 TTY 而报错。恢复的流程也要会虽然希望永远用不上gunzip -c wiki_20250101.sql.gz | docker compose exec -T db psql -U wiki -d wiki恢复之前最好先停掉 Wiki.js 容器避免它在恢复过程中写入数据造成冲突。6.2 内容层冗余把页面同步到 Git 仓库前面说过 Wiki.js 支持 Git 存储这是我最喜欢的功能之一。在管理 → 存储 → Git里配置一个仓库地址和访问凭据Wiki.js 会在后台把页面以 Markdown 文件的形式推送到 Git 仓库。之后每次页面编辑、创建、删除都会自动生成对应提交。这意味着你的知识库有两条独立的备份链路数据库一份Git 仓库一份。日常内容恢复、历史版本对比、导出给其他工具用都非常方便。我甚至习惯在本地 clone 仓库离线时也能翻文档。配置 Git 同步时需要注意仓库的目录结构。Wiki.js 默认按页面路径创建目录层级比较深记得在仓库里指定一个专用目录而不是直接推到根目录否则和代码文件混在一起很乱。6.3 升级流程我固定下来的三步走现在我的升级流程几乎是肌肉记忆了分享给同样在维护这个项目的你先执行备份脚本确认备份文件生成且非空docker compose pull拉最新镜像docker compose up -d重建容器观察日志并实际打开页面做一轮功能验证登录、编辑、保存、查看历史版本。如果是跨大版本升级升级前我还要做一件事查官方 changelog。有一次 v2 系列内部小版本升级就引入了配置项的变更不看 changelog 的话很难定位问题。一旦新版有问题回滚也很简单docker compose里把镜像标签改回原来的版本重新up -d再用备份的 SQL 恢复数据库。所以版本号一定要固化写死不要用latest不然回滚时连原来用的到底是哪个版本都说不清。6.4 长期稳定运行我再加的三道保险备份和升级之外还有几个细节能让这套系统多安稳几年。第一监控。我挂了一个 Uptime Kuma 定时检查 wiki 的页面响应和外部访问端口有问题会推消息到手机。家里宽带偶尔断个十分钟不至于等同事反馈才知道服务挂了。第二磁盘空间。PostgreSQL 数据文件、Git 仓库、备份文件都会增长。我给自己定的是每个月看一眼df -h顺便检查日志文件有没有被 Nginx 撑爆。第三定期做恢复演练。备份文件存在不等于能恢复成功。每隔几个月我会在一台临时机器上把最新的备份 SQL 恢复到空容器里确认数据完整、能正常登录。这个习惯帮我发现过一次 cron 脚本因为路径写错、备份一直没成功的隐患。说实话Wiki.js 本身是个省心的项目配置好之后几乎不用管。真正让我花费心思的从来不是软件本身而是外面这一圈网络链路、反代、证书、备份。把这圈理清楚了一个本地部署的开源 wiki 才能真正变成团队日常离不开的知识库。最后再分享一个我个人的实战体会所有配置改动小到 Nginx 的一条 header大到 Compose 编排结构调整我都会先在文本文件里留下记录并且顺手把最终的配置同步到文档页里。wiki 里最该记录的文档就是你部署和维护这个 wiki 本身的完整过程。哪天服务器要迁移或重装这份文档能让你少走一大半弯路。
返回列表