Skip to content

Docker 部署 OtterWiki:带 Git 版本历史的私有 Markdown Wiki ​

An Otter Wiki(下称 OtterWiki)是 Python + Flask 写的轻量 Wiki,页面用 Markdown 编写,内容存放在一个 Git 仓库里,每次保存都产生提交记录,因此页面历史、回溯和整站导出都是 Git 语义。单容器即可运行,不需要外部数据库或缓存,进程内由 supervisord 拉起 nginx 和 uWSGI。

官方推荐的运行方式是 docker compose,主镜像按大版本发布 redimp/otterwiki:2;2026-09-23 该 tag 对应 2.24.1,镜像提供 amd64、arm64、armv7、armv6 四种平台。下面按 linux/arm64 + Docker 29.7.2 环境给出部署、验收、备份恢复与权限收紧的完整步骤。

部署结论 ​

项结论
GitHub 地址redimp/otterwiki
官方镜像redimp/otterwiki,稳定版按大版本打 tag,本文使用 redimp/otterwiki:2
项目版本2.24.1
部署日期2026-09-23
主部署方法image-compose:官方文档给出的单服务 Compose,绑定目录持久化 /app-data
备选方法image-run:官方 docker run 一行式启动,参数与主路径完全一致
维护范围maintained:长期运行,需要备份、升级与回滚
对外端口容器内 nginx 同时监听 80 和 8080;宿主机端口自定义,示例统一使用 127.0.0.1:8080:80
持久化位置./app-data:settings.cfg、db.sqlite、repository/(Git 仓库)三项缺一不可
依赖服务无。数据库是镜像内置 SQLite,页面存储是镜像内置 Git
官方支持平台amd64、arm64、armv7、armv6;树莓派 1 A 级设备可运行,文档给出的内存参考约 100 MiB

选 image-compose 的理由:官方 Installation 把 docker compose 写成推荐方式,Wiki 属于长期演进的有状态服务,读者需要一份能重复 up、pull、down 的文件;单容器无依赖服务,因此不需要为拓扑额外拆分。仓库根目录的 docker-compose.yml 使用 build: .,注释明确写着「for running the latest development version」,属于开发用途,正式部署应使用发布镜像。

架构说明 ​

Mermaid 流程图
查看源码
flowchart LR
  browser[浏览器] --> hostport["宿主端口 127.0.0.1:8080"]
  hostport --> nginx["nginx 容器内监听 80 与 8080"]
  nginx -->|unix socket /tmp/uwsgi.sock| uwsgi["uWSGI + Flask 应用"]
  uwsgi --> cfg["settings.cfg:SECRET_KEY、路径配置"]
  uwsgi --> db["db.sqlite:账号、会话、界面设置"]
  uwsgi --> repo["repository/:Git 页面与全部历史"]

三层职责决定了运维边界。nginx 与 supervisord 以 root 启动,uWSGI、nginx worker 和 Git 子进程实际以 uid 33(www-data)运行;页面正文只落在 repository/,账号、会话和管理界面里保存的偏好落在 db.sqlite,SECRET_KEY 与两个路径写在 settings.cfg。三处配置的生效次序由官方文档给定为 Settings Interface > Environment Variables > settings.cfg,界面里保存过的设置优先级最高。

前置条件 ​

  • Docker Engine 与 Docker Compose 插件可用(docker compose version)。
  • 一个专用域名或 IP:端口 入口。OtterWiki 不能部署在子路径下,domain.tld/wiki 这类映射不成立。
  • 浏览器需启用 JavaScript,Wiki 界面依赖脚本;Wiki 服务端本身不需要出网。
  • 磁盘按内容估算,且只增不减:Git 后端不会因删除页面或附件释放空间。
  • 宿主端口未占用。官方示例使用 8080,与其他服务冲突时改冒号左侧即可。
  • 内网或 NAS 环境若无法直连 Docker Hub,先按 Docker 镜像源配置教程 处理加速器。常用加速器 docker.m.daocloud.io 对该镜像返回“不在白名单”(2026-09-23 核对),当时改用第三方 pull-through 缓存才取到 redimp/otterwiki:2;更换镜像前缀即更换分发信任路径,落地前应核对镜像摘要与启动日志里的版本横幅。

Docker Compose 部署 ​

在宿主机新建一个项目目录,把下面的内容保存为该目录下的 compose.yaml。app-data 交给容器首次启动时自行初始化,不要预先写入内容。

yaml
services:
  otterwiki:
    image: redimp/otterwiki:2
    restart: unless-stopped
    ports:
      - 127.0.0.1:8080:80
    volumes:
      - ./app-data:/app-data

冒号左侧换成未被占用的端口。首次启动的实例默认任何人可读、可编辑、可注册并自动通过审批,因此先只绑定 127.0.0.1,按后文收紧权限并注册管理员之后,再放开到局域网或反向代理。

先做静态校验,再启动:

bash
docker compose config
docker compose up -d

docker compose config 只证明 YAML 可解析。首次启动时入口脚本会创建 app-data/settings.cfg(含随机生成的 SECRET_KEY)、app-data/db.sqlite,并在 app-data/repository 执行 git init -b main,随后自动写入 Home 页面。

备选:单条 docker run ​

官方 Installation 同时给出一条命令版本,参数与上面的 Compose 等价,适合临时体验或只需要一个容器的环境:

bash
docker run --name otterwiki \
  -p 127.0.0.1:8080:80 \
  -v $PWD/app-data:/app-data \
  redimp/otterwiki:2

长期运行建议保留 Compose 文件:升级、重建和环境变量维护都靠这份文件复现,docker run 的参数一旦离开 shell 历史就无法核对。

验证部署结果 ​

四类检查依次覆盖容器状态、应用响应、日志与健康、持久化,命令与实测结果成对给出。示例统一使用宿主端口 8080;本次实测环境的 8080 已被其他服务占用,实际映射为 127.0.0.1:18080 -> 80,除端口号外结果一致。

bash
docker compose ps

预期看到 STATUS 为 Up … (healthy)。镜像自带 HEALTHCHECK,命令是容器内 curl -A "docker-healthcheck" -f http://localhost:8080/-/healthz,间隔 5 分钟、启动宽限 30 秒;本次在启动 12 秒后检查,状态已为 healthy。

bash
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
curl -sS http://127.0.0.1:8080/-/healthz

预期分别是 200 和 200 ok。/-/healthz 是无认证探针,反向代理或监控可以直接使用。

bash
docker compose logs --tail 20 otterwiki

预期看到版本横幅 *** Starting An Otter Wiki 2.24.1 …、server: Created initial page /home,以及 nginx 访问日志里带 docker-healthcheck 的 GET /-/healthz 200 记录。横幅里的版本号是确认浮动 tag 实际拉到哪个版本的最快方法。

bash
ls app-data
git -C app-data/repository log --oneline

预期看到 db.sqlite、settings.cfg、repository,Git 历史里有一条 Initial commit。

持久化检查用重建容器完成,不触碰数据目录:

bash
docker compose down
docker compose up -d

再次访问首页与已有页面,内容与 Git 历史应完全保留。本次还额外验证了「写入—重建—读取」闭环:登录后台保存了一个测试页面,repository 出现对应提交,docker compose down 后重新 up -d,页面与该提交仍可读取。

首次初始化 ​

第一个注册的账号即管理员,可进入 /-/settings,官方文档明确该账号免邮箱确认、免审批。这带来一个次序问题:默认配置下任何人可读可写、任何人可注册,先暴露再注册等于把管理员位置留给抢先到达的请求。两种稳妥做法:

  1. 保持上面的 127.0.0.1 端口绑定,在浏览器里注册管理员,再放开访问范围;
  2. 不开注册界面,直接用镜像内的命令行建号(本次实测可用,otterwiki 需与 Compose 里的服务名一致):
bash
docker compose exec -u www-data otterwiki flask user create you@example.com "Admin" -p admin
docker compose exec -u www-data otterwiki flask user password you@example.com --generate

第二条命令会打印一个随机口令,登录后可在界面里改密。flask user list 按行列出 Admin、Approved、Confirmed、Permissions、Password 五项状态,用于确认账号能否直接登录。

随后在管理界面确认以下几项,这几项决定后续所有运维行为:站点名与 SERVER_NAME(用于生成外部链接)、读写权限、是否允许注册、邮箱与附件上传上限。官方 Configuration 的 ADMIN_USER_EMAIL 属于手工创建 settings.cfg 时才生效的高级项,通过 Compose 环境变量收紧权限时应以文档列出的权限变量为准。

收紧默认权限 ​

OtterWiki 的读取、写入、附件写入默认值都是 ANONYMOUS,注册默认开放且自动通过审批。对内网 Wiki 而言,匿名写意味着任何能访问端口的人都能改页面并留下 Git 提交。把权限写进 Compose,让配置随文件复现:

yaml
services:
  otterwiki:
    image: redimp/otterwiki:2
    restart: unless-stopped
    ports:
      - 127.0.0.1:8080:80
    volumes:
      - ./app-data:/app-data
    environment:
      SITE_NAME: "Team Wiki"
      SERVER_NAME: "wiki.domain.tld"
      READ_ACCESS: REGISTERED
      WRITE_ACCESS: REGISTERED
      ATTACHMENT_ACCESS: REGISTERED
      DISABLE_REGISTRATION: True
      AUTO_APPROVAL: False
      NOTIFY_ADMINS_ON_REGISTER: True

docker compose up -d 会重建容器使变更生效。验证方式是匿名探针:

bash
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' http://127.0.0.1:8080/Home
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/Home/edit

收紧前两条分别是 200(匿名可读)和 200(匿名可进编辑表单);设置成 REGISTERED 后实测为 302 跳转到 /-/login?next=/Home 与 403。

两个限制必须知道。第一,界面里保存的同名设置写进 db.sqlite,会把 Compose 中的对应变量压掉,改过一次之后环境变量不再说话;Repository Management 相关的设置遵循同一规则。第二,如果希望随机访客能看页面但不能编辑,只收紧 WRITE_ACCESS 和 ATTACHMENT_ACCESS,保留 READ_ACCESS: ANONYMOUS。

日常运维 ​

数据与恢复边界 ​

路径内容丢失后果
app-data/repository/全部页面与附件的 Git 历史内容全丢,且历史无法重建
app-data/db.sqlite用户、密码哈希、会话、管理界面保存的偏好账号与界面设置全丢,页面仍在
app-data/settings.cfg随机 SECRET_KEY、仓库与数据库路径会话全部失效;若启用了由 SECRET_KEY 派生的拉取回调地址,该地址随之变化

恢复单元是整个 app-data 目录,只恢复 repository 会得到一个没有账号和站点设置的 Wiki。停写方式就是 docker compose down,SQLite 与 Git 同时静止,随后整目录复制即为一套可用备份。通用归档与恢复方法见 Docker 镜像数据备份和恢复,此处只列 OtterWiki 特有的部分。

OtterWiki 另外提供两种把内容导出到实例之外的服务级机制,官方把这一组功能标注为 experimental:

  • GIT_REMOTE_PUSH_ENABLED + GIT_REMOTE_PUSH_URL + SSH 私钥:每次页面变更自动推送到远端仓库,等于一份异地实时镜像;
  • GIT_WEB_SERVER:在 HTTP 上开放 Git 端点,具备读取权限的用户可以随时 git clone 整站,具备上传权限的用户可以推送,属于按需导出的副本通道。

用环境变量配置远端时,避免在 Compose 文件里内联私钥,也避免把私钥提交进版本库;管理界面里保存过的远端配置会覆盖环境变量。恢复后的验收标准是同一套:管理员能登录、任一页面能打开、该页面的 History 能列出多次提交。

升级与回滚 ​

官方 Upgrade 页给出的 Compose 升级命令是:

bash
docker compose pull && docker compose up -d

次版本升级(2.x 到 2.y)按官方说明不需要迁移步骤。升级前后各做一次验收:docker compose ps 回到 healthy、日志横幅里的新版本号符合预期、匿名与登录探针行为未变、/-/healthz 返回 200。

redimp/otterwiki:2 是随 2.x 发布的浮动 tag。需要精确复现时,先记录当前镜像摘要,再把 Compose 的 image: 改为带摘要的写法:

bash
docker image inspect redimp/otterwiki:2 --format '{{index .RepoDigests 0}}'

经加速器或缓存代理拉取时,该命令输出的仓库前缀会保留代理域名,写回 Compose 前应确认摘要对应的仍是官方镜像。

yaml
image: redimp/otterwiki:2@sha256:<上一步输出的摘要>

回滚依据同样来自官方文档:管理界面的偏好存放在 /app-data/db.sqlite 而非镜像内,换回旧 tag 重建容器即可恢复旧行为且不丢配置。跨大版本回退前必须先完整备份 app-data,官方只声明 2.x 次版本间无需迁移,未对回退到更早大版本给出兼容承诺。

日常版本变更依据 CHANGELOG 判断,近期版本以 HTML 清洗与路径校验类安全修复为主,暴露在网络上的实例应把这类版本视为及时更新项。

日志与健康 ​

bash
docker compose logs -f otterwiki
docker inspect --format '{{.State.Health.Status}} {{.State.Health.FailingStreak}}' $(docker compose ps -q otterwiki)

nginx 的访问与错误日志已在镜像内软链到标准输出,因此 docker compose logs 同时包含 nginx 访问记录、uWSGI 启动横幅和应用日志。挂在反向代理后面时,容器日志里的来源 IP 只有代理地址,需要设置 REAL_IP_FROM 为代理所在的地址或网段,nginx 才会采信 X-Real-IP。

服务生命周期(重复启动、停用、卸载)的通用步骤见 Docker Compose 服务更新、回滚与卸载。卸载边界需要明确:docker compose down 不删除 ./app-data,因为 ./app-data 是绑定目录而不是命名卷;确认备份可用之前不要删除该目录,Git 历史和账号数据只存在于此。

生产环境安全加固 ​

通用基线见 Docker 生产环境安全,公网入口与证书见 Docker 服务反向代理与 HTTPS。这里只保留 OtterWiki 特有的判断:

  • 镜像内外都不提供 HTTPS。生产访问必须经反向代理终结 TLS,官方给出 nginx、apache、caddy 三段示例,其中 nginx 示例把 client_max_body_size 设为 64M 以容纳附件。
  • 默认暴露面是「任何人可读可写、可注册」。上线前至少完成 WRITE_ACCESS、ATTACHMENT_ACCESS 与 DISABLE_REGISTRATION 三项收紧,用上一条的匿名探针复验。
  • 完整镜像内 nginx 主进程与 supervisord 以 root 运行,应用进程为 uid 33。需要去掉 root 组件时官方提供 2-slim 变体:基于 alpine、无 nginx、uWSGI 以 uid 33 监听 8080,此时端口映射必须改成容器侧 8080;2-slim 与完整镜像除端口外可互换,官方站点 otterwiki.com 本身就跑在 2-slim 上。
  • 附件与表单大小有两个独立上限:代理侧 client_max_body_size,应用侧 MAX_FORM_MEMORY_SIZE(默认 1000000 字节)。容器内 nginx 的 NGINX_MAX_UPLOAD 默认不限制请求体。
  • 把 app-data 所在磁盘纳入备份体系时,注意 Git 历史只增不减,删除页面不会释放空间。
  • 放入 /app-data/plugins/<name>/ 的插件目录会在每次容器启动时被 pip install -U . 安装。该机制等于在启动路径上执行任意 Python 打包代码,插件目录的写入权限应按凭据级别管理。
  • 若使用 GIT_REMOTE_PULL_URL_SECURE: true,回调地址由 SECRET_KEY 派生,备份中必须包含 settings.cfg,否则恢复后回调地址失效。

常见问题 ​

页面能打开,但任何人都能编辑,注册按钮也在。 这是默认值而非配置错误:READ_ACCESS、WRITE_ACCESS、ATTACHMENT_ACCESS 均为 ANONYMOUS,DISABLE_REGISTRATION 为 False。按「收紧默认权限」一节处理,并记得管理界面里保存过的同名设置优先于环境变量。

容器反复重启,日志只有 Fatal: PGID must not be 0 (root). 入口脚本拒绝 PGID: 0。把 PUID/PGID 设为宿主机普通用户的非零数字(例如 1000:1000),或删掉这两个变量回到镜像默认的 33:33。本次实测在删除该变量并 up -d --force-recreate 后恢复 healthy。-slim 变体改用 user: 1000:1000 而不是 PUID/PGID。

挂载目录权限被拒。 Linux 上确认 app-data 对 uid 33(或你设置的 PUID)可写;官方提示可以先用完整镜像初始化目录,再切到 -slim。使用 podman 且开启 SELinux 时,绑定目录要写成 ./app-data:/app-data:Z。

访问页面正常,保存或登录报表单错误。 CSRF 令牌默认有效期 86400 秒,标签页挂机超时后提交会失败。提高 WTF_CSRF_TIME_LIMIT,不要用 WTF_CSRF_ENABLED=False 绕过。

上传附件返回 413。 应用侧调大 MAX_FORM_MEMORY_SIZE,代理侧同步 client_max_body_size,两侧不一致时以先拒绝的一方为准。

换到 -slim 后服务无响应。 2-slim 只在容器内监听 8080,完整镜像的 8080:80 映射对 2-slim 无效,需改成 8080:8080。群晖上使用 -slim 曾出现「Listen queue size is greater than the system max net.core.somaxconn」,官方给出的处理是在服务里加 sysctls: ["net.core.somaxconn=1024"]。

想把 Wiki 放在 domain.tld/wiki 下。 不支持,官方要求专用域名或独立入口。

管理员密码忘了,或者最后一个管理员被删了。 用镜像内命令行重建,不依赖浏览器和邮件服务:

bash
docker compose exec -u www-data otterwiki flask user password you@example.com --generate
docker compose exec -u www-data otterwiki flask user create you@example.com "Admin" -p admin

拉不到 redimp/otterwiki:2。 先区分是网络不可达还是加速器不代理该命名空间;加速器配置见 Docker 镜像源配置教程,离线环境可用 docker save / docker load 转运镜像。更换镜像前缀即改变分发信任路径,落地前应核对镜像摘要与版本横幅。

总结 ​

OtterWiki 的 Docker 部署本身只有一个文件、一条 up 命令:官方 redimp/otterwiki:2 镜像加 ./app-data:/app-data 绑定目录即可稳定运行,容器内由 nginx、uWSGI 和 Git 完成全部工作,实测启动 12 秒后健康检查即通过。真正需要花费判断力的是三件事——上线前收紧匿名读写与开放注册的默认值,把 settings.cfg、db.sqlite、repository/ 当作一个不可拆分的备份单元,以及在接受浮动 tag 与可复现摘要之间做出选择。把这三点写进 Compose 文件和备份任务,这套 Wiki 就可以长期跑在一台 NAS 或一台小型服务器上。

参考资料 ​

全部公开文章由同一个站点构建和发布。