看到这个标题的人,多半正在查项目自托管部署的路线,要么想快速搭一套内部知识管理系统,要么被手工安装那套零散依赖折腾过。我前一阶段在自己的服务器上把 WeKnora 整套跑起来了,前前后后改了三版 docker-compose,中间踩掉的坑不少,所以把最终能直接照着做的流程整理成这篇博文。WeKnora 是一个以语义化资源管理为核心的 Web 应用套件,后端提供 REST API、前端提供管理界面,数据统一落到数据库里。只要你的机器上装了 Docker 和 Docker Compose,即使你之前没读过它的源码,也能按后面的步骤完成部署,并且把升级、备份、日志轮换这些后续问题一并安排好。
这套东西非常适合三类人去用:一是档案、文献项目组,要把结构化的资源信息和媒体文件管理起来;二是团队内部要做知识库,但不想被商业化 SaaS 平台绑定;三是刚接触容器编排的开发者,拿它当一个练手项目,顺便体会一下多容器协作到底是怎么回事。下面我从架构讲起,然后给一份我已经验证过可用的 compose 文件,再带你完整走一遍初始化过程。
1. 部署前的准备与整体架构
1.1 理解 WeKnora 的三个核心部件
很多人第一次看到“WeKnora”这个名称,会以为它是个单一应用,实际上它是典型的“后端 + 前端 + 数据库”三层结构。后端叫weknora-server,所有 API 请求、权限校验、数据校验、全文检索索引都在这一层处理;前端叫weknora-web,是一个可以直接通过浏览器访问的 SPA 管理界面;数据库承担持久化,我在这里选用 PostgreSQL。三个服务各司其职,互不干扰。
为什么不把三者揉进一个容器里?从运维角度讲,分开镜像可以独立换版本。比如前端只是样式和交互改了,只需要重新发布weknora-web,后端和数据库完全不动;反过来,如果底层数据库驱动有安全补丁,也只需要替换数据库镜像。从排障角度讲,容器隔离后你一眼就能从日志里看出问题在哪一层,不会出现“连不上数据库,但以为是前端的锅”这种排查地狱。
由于 WeKnora 原本就是按 REST 接口设计的,前端和后端之间只通过 HTTP 通信,所以这套结构即使不用 Docker,也能比较自然地拆到多台机器上。用 Docker Compose 只是在单机场景下帮你把网络、存储、启动顺序都标准化了,后面迁移、重装都会轻松很多。
1.2 为什么选择 Docker Compose 而不是单个容器
用一个docker run命令去启动 WeKnora 不是不行,但问题很快就会暴露:你要分别记住后端镜像、前端镜像、数据库镜像各自的参数,端口如果忘了映射,页面就白屏;容器重启策略没加,服务器一重启服务就全飘;数据卷写漏了,升级一次数据就没了。Docker Compose 强就强在能把这一整套声明成一份 YAML 文件,让别人照着文件就能复现你的环境。
Compose 还解决了一个很关键的问题:服务依赖之间的启动顺序。WeKnora 的后端在启动时会检查数据库连接,如果数据库还没有就绪,后端会报错退出。Compose 配合healthcheck能做“等数据库健康后再启动后端”的依赖控制,而不是简单地把几个容器一起扔起来靠运气。
对单机部署来说,Compose 的另一个好处是内置了跨容器网络。你不需要知道宿主机 IP 是多少,后端直接通过weknora-db这个容器名就能访问数据库。容器名在同一个 Compose 网络里相当于可解析的主机名,这比配置什么127.0.0.1可靠得多。
1.3 环境清单与端口规划
先明确一下资源要求。WeKnora 本身不算很吃资源,但数据索引和全文检索对内存有一定要求。
| 项目 | 最低配置 | 推荐配置 | 说明 |
|---|---|---|---|
| CPU | 1 核 | 2 核及以上 | 前端构建和检索并发都需要计算资源 |
| 内存 | 2 GB | 4 GB | 数据库缓存、后端索引、前端页面同时运行 |
| 磁盘 | 20 GB | 50 GB SSD | 镜像、数据库文件、上传媒体都会占用空间 |
| 系统 | Linux x86_64 | 同左 | ARM 架构部分镜像需另行确认 |
端口规划上,数据库5432只在 Compose 内部使用,不建议映射到宿主机;后端 API 我映射到宿主机的8300;前端 Web 映射到8080。如果你本机 8080 已经被占用,可以改宿主机侧端口,比如18080:80,但后端端口规划要和各处配置保持一致。
操作系统层面还需要完成 Docker 与 Docker Compose 的安装。常见的 Linux 发行版用包管理器安装即可,Docker 服务安装完成后记得执行sudo systemctl enable --now docker,确保开机自启。部署环境准备好之后,下面这份 compose 配置就可以直接用了。
2. docker-compose 配置文件逐段拆解
2.1 项目目录与 .env 文件
先创建项目目录,我习惯放在/opt/weknora下,这样配置、备份、日志都在一起:
mkdir -p /opt/weknora cd /opt/weknora接着创建一个.env文件,专门保存可变的环境变量。这样做的好处是docker-compose.yml本身可以提交到代码仓库,即使要发给同事复用,也不至于把数据库密码带到代码里。我的.env最初长这样:
WEKNORA_DB_NAME=weknora WEKNORA_DB_USER=weknora WEKNORA_DB_PASSWORD=change-this-password WEKNORA_JWT_SECRET=please-generate-a-long-random-string WEKNORA_VERSION=3.2.1需要特别提醒的是,WEKNORA_DB_PASSWORD和WEKNORA_JWT_SECRET不要用项目或者公司名称拼出来,容易被爆破。密码我建议直接用工具随机生成,比如:
openssl rand -base64 24JWT 密钥用openssl rand -hex 32生成一个 64 位十六进制字符串,用来对登录态做签名。如果你偷懒用固定字符串,以后只要有人猜到密钥,就可以伪造任意用户身份,这个代价不值得省。
2.2 数据库服务的健康检查与依赖顺序
docker-compose.yml里第一个服务是数据库。我选用postgres:16-alpine,体积小,内存占用低。PostgreSQL 官方镜像支持通过环境变量在首次初始化时创建数据库和用户,容器启动时如果发现数据卷为空,就会自动执行初始化脚本。
services: weknora-db: image: postgres:16-alpine container_name: weknora-db environment: POSTGRES_DB: ${WEKNORA_DB_NAME} POSTGRES_USER: ${WEKNORA_DB_USER} POSTGRES_PASSWORD: ${WEKNORA_DB_PASSWORD} volumes: - db_data:/var/lib/postgresql/data networks: - weknora_net healthcheck: test: ["CMD-SHELL", "pg_isready -U ${WEKNORA_DB_USER} -d ${WEKNORA_DB_NAME}"] interval: 10s timeout: 5s retries: 10 restart: unless-stopped单独加一个healthcheck是很关键的一步。Compose 的老版本里,depends_on只能决定启动顺序,不能判断数据库是否真的能接受连接。少了健康检查,后端的首次启动大概率会撞上“数据库还在初始化,你来了我还没准备好”的情况。加了健康检查之后,Compose 会等到pg_isready返回成功才继续拉起后端服务。
2.3 API 服务的数据卷和密钥变量
后端服务的配置里,最需要重视的是数据卷和密钥变量。数据卷需要挂两个:一个是存储上传的媒体文件,另一个如果后端有本地缓存目录也一并挂进去。我这里统一挂载到一个名为weknora_data的卷里。
weknora-server: image: weknora/server:${WEKNORA_VERSION} container_name: weknora-server depends_on: weknora-db: condition: service_healthy environment: WEKNORA_DB_HOST: weknora-db WEKNORA_DB_PORT: 5432 WEKNORA_DB_NAME: ${WEKNORA_DB_NAME} WEKNORA_DB_USER: ${WEKNORA_DB_USER} WEKNORA_DB_PASSWORD: ${WEKNORA_DB_PASSWORD} WEKNORA_JWT_SECRET: ${WEKNORA_JWT_SECRET} WEKNORA_LOG_LEVEL: info ports: - "8300:8300" volumes: - weknora_data:/data networks: - weknora_net restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8300/healthz"] interval: 15s timeout: 5s retries: 5这里解释一下WEKNORA_DB_HOST为什么要写成weknora-db:Compose 会为所有服务创建私有网络,服务之间可以直接使用“容器名”作为主机名进行访问,所以后端不需要知道宿主机 IP,也不需要用一条难记的动态 IP 去填连接串。这正是 Compose 比一串docker run省心的核心原因。
2.4 Web 服务与网络配置
前端本质上是一个静态资源站点,运行在容器内的 Nginx 上。它需要通过WEKNORA_API_BASE_URL知道后端 API 的地址。这里需要根据你的访问方式灵活调整:如果前端和后端都只在服务器本地访问,可以填http://127.0.0.1:8300;如果将来要暴露给外部用户,建议通过反向代理统一入口。
weknora-web: image: weknora/web:${WEKNORA_VERSION} container_name: weknora-web depends_on: - weknora-server environment: WEKNORA_API_BASE_URL: http://127.0.0.1:8300 ports: - "8080:80" networks: - weknora_net restart: unless-stopped volumes: db_data: weknora_data: networks: weknora_net: driver: bridge网络驱动选默认的bridge就够用了。bridge网络下层是 NAT 模式,对外只暴露你映射的端口,服务之间通过内部网络通信更安全,不会把数据库端口直接敞开到公网。
3. 部署前的安全配置与网络出口
3.1 生成随机密码与密钥
真正动手部署之前,先把安全相关的几串随机值准备好。数据库密码、JWT 密钥、管理员初始密码,这三项每一样都不建议手打。终端里生成随机字符串的常见命令如下:
openssl rand -base64 24 openssl rand -hex 32第一个命令生成数据库密码,第二个命令生成 JWT 密钥。生成之后写进.env文件,并顺手把.env加入.gitignore。我见过不少部署事故,最后定位都是.env被提交到仓库、密钥泄露导致的。配置文件的泄露风险比代码漏洞更隐蔽,也更致命。
数据库端口不建议映射到宿主机。如果确实需要远程连数据库做管理,建议通过堡垒机或只允许内网访问。默认情况下,没有ports映射的容器服务,外部网络无法直接访问,这是很重要的默认安全边界。如果你在服务器上看到0.0.0.0:5432这样的监听,就要警惕是不是有人为了图方便把数据库暴露出去了。
3.2 前端 API 地址到底怎么填
关于WEKNORA_API_BASE_URL的值,最容易踩坑。很多人在浏览器里访问管理台时,页面能打开,但登录后却一直转圈或报跨域错误,基本都是因为前端容器里配置的 API 地址对用户不可见。
这里要先分清两个视角。容器内视角:如果前端从容器内部访问后端,应该用http://weknora-server:8300,因为只有在这个网络里weknora-server才是一个可解析的主机名。浏览器视角:如果用户是在宿主机上用浏览器访问http://服务器IP:8080,前端发出的 AJAX 请求实际上是由浏览器去发起的,所以WEKNORA_API_BASE_URL必须填浏览器能够访问到的地址,也就是http://服务器IP:8300或者后端的域名。
这也就解释了一个现象:如果把WEKNORA_API_BASE_URL填成http://weknora-server:8300,后端容器之间可以通信,但用户的浏览器解析不了这个主机名,页面就会加载失败。所以对外访问场景下,要么填公网或内网 IP,要么通过反向代理把/api路径转发到后端。
3.3 反向代理配置示例
生产环境里一般不会直接把8300端口裸露给用户,更稳妥的方案是用 Nginx 做反向代理,对外只保留 80/443 端口。下面是一个适合放在 Nginxserver配置块里的例子:
server { listen 80; server_name knowledge.example.org; 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; } location /api/ { proxy_pass http://127.0.0.1:8300/api/; proxy_set_header Host $host; } }加了反向代理之后,前端的WEKNORA_API_BASE_URL可以填成http://knowledge.example.org/api。这样用户在浏览器里只有一个入口,后端端口不需要对外开放,数据库端口更是只存在于 Compose 内部网络,攻击面会小很多。如果直接用 HTTPS,记得把证书路径和跳转规则一并加上。
4. 从零到可用的完整实操记录
4.1 拉取镜像并启动数据库
配置文件和.env都准备好后,进入项目目录。第一次部署先拉取镜像,可以直观地看到镜像下载是否顺利:
cd /opt/weknora docker compose pull镜像就绪之后,先只启动数据库,单独观察初始化过程:
docker compose up -d weknora-db docker compose logs -f weknora-db正常情况下,你会看到 PostgreSQL 完成初始化、准备接收连接等日志。如果这里就报错,比如数据目录权限不足、端口冲突,先解决掉再继续。数据库启动稳定后,再执行健康检查:
docker compose ps输出里STATUS一栏如果显示(healthy),说明数据库已经具备接受连接的条件,可以继续启动后端服务。
4.2 启动主服务并执行迁移
数据库健康后,启动后端和前端:
docker compose up -d docker compose ps首次启动时,后端容器会尝试初始化数据库表结构。我这里遇到过一次表结构迁移不完成的状况,日志里报告relation "app_metadata" does not exist。这种情况下不要反复重启后端,而是需要手动执行一次迁移命令。WeKnora 的镜像里内置了一个管理命令行工具,迁移命令格式通常是:
docker compose exec weknora-server weknora-cli migrate --up迁移完成之后重新启动后端:
docker compose restart weknora-server执行迁移这个步骤看起来不起眼,却是最容易掉链子的地方。很多项目拆成多服务之后,都会把数据库迁移放到容器启动时自动执行,但如果迁移脚本不是幂等的,多容器同时启动就会互相干扰。手动执行迁移的好处是你可以清楚看到每一步在做什么,日志也更容易定位。
4.3 验证后端健康状态
后端启动后,先不要急着开前端页面,用命令确认一下 API 是否正常返回:
curl -s http://127.0.0.1:8300/healthz正常情况下会返回一个表示健康状态的 JSON,比如:
{"status":"ok","version":"3.2.1","time":"2025-01-15T08:00:00Z"}如果你看到这个响应,说明后端进程、数据库连接、迁移状态这三件事都通过了。如果 curl 报连接失败,需要检查后端容器是否还活着,以及日志最后几行报了什么错:
docker compose logs --tail=50 weknora-server每次部署时我习惯把“后端健康检查”作为一个独立的验收节点。它和“页面能打开”是两回事,前者说明服务链路本身没问题,后者还可能被前端配置干扰。先把后端验证过,出问题时就容易缩小范围。
4.4 创建管理员并登录管理台
接口通了之后,接下来创建管理员账号。WeKnora 的管理台的第一版界面上会引导你填写管理员信息,但命令行的方式更可控,也更适合脚本化。使用内置 CLI 创建管理员:
docker compose exec weknora-server weknora-cli user create --admin \ --username admin \ --email admin@example.org \ --password 'YourStrongPassw0rd'创建成功后,浏览器访问http://服务器IP:8080,应该能看到登录页。输入管理员账号密码登录后,就完成了整个部署链路的第一步。注意,如果你把WEKNORA_API_BASE_URL配置成127.0.0.1:8300,那么登录请求会由浏览器发到服务器本机的8300端口,这个行为只有在浏览器就在服务器本机上时才能成功。远程访问时必须按 3.2 节的方案把地址改对。
4.5 模拟项目 X:创建知识库和第一个资源
登录进来之后,很多新手会卡在界面一堆按钮上不知道怎么下手。其实核心的路径就是“创建项目 → 创建资源类型 → 添加资源”。我以内部知识库为例,创建一个叫作“模拟项目 X”的库,在里面录入第一条结构化数据。
在管理台的“项目”页面点击“新建项目”,填写项目名称“模拟项目 X”,保存后在项目详情页里创建资源类型。比如你维护的资料需要包含标题、日期、责任人三个字段,那就定义三个对应的属性。等资源类型定义好,再回到“资源”页面新增一条记录。
创建第一个资源的过程,会明显感受到它和普通关系型数据库表单的区别:资源类型是动态定义的,字段可以随时扩展,查询接口按项目隔离。这个设计很适合资料持续增长且结构会演变的场景,也是很多人选择 WeKnora 的核心原因。虽然项目叫“模拟项目 X”,但建库的逻辑和你实际要用的内部知识库完全一样。
5. 常见报错与排障速查
5.1 数据库容器反复重启
现象是docker compose ps里weknora-db的状态一直是restarting或exited,日志里有类似“data directory has invalid permissions”的内容。这个大概率是挂载的宿主机目录权限问题,如果某个数据卷之前是被别的用户创建过的,容器内的 postgres 用户写不进去。
先停掉容器,查看卷的实际持有者,再确定 UID/GID 是否兼容:
docker compose stop weknora-db docker run --rm -v db_data:/data alpine ls -ln /data如果权限确实不对,最简单的办法是重建卷。重建前确认不需要恢复里面的旧数据,然后执行docker compose down -v把卷删除,重新部署。不要在一台已经跑过旧版本的机器上直接复用同名数据卷,版本变更导致的数据目录结构差异也会引发这种问题。
5.2 后端日志显示 can't connect to database
这个报错很典型。第一种可能是数据库还没完全就绪,后端就抢跑了,但你已经在 compose 里配置了condition: service_healthy,所以这种情况比较少。第二种可能是数据库密码不匹配,.env改了之后没有重新创建容器。修改环境变量后,Compose 不会自动重建已存在的容器,你需要执行:
docker compose up -d --force-recreate weknora-server第三种可能是数据库容器名被改过,导致后端找不到weknora-db。检查一下docker network inspect weknora_weknora_net,确认weknora-db还在这个网络里。网络层面的问题排查起来比较绕,建议先把 Compose 网络整个删除重建,一般来说都能解决。
5.3 登录后被踢回登录页
后端健康、前端页面正常,但登录成功后马上又跳回登录页。我遇到这种情况时,第一反应是看浏览器的开发者工具里有没有设置 Cookie 失败。WeKnora 的登录态通过 JWT 保存,如果WEKNORA_JWT_SECRET改了,旧 token 就会全部失效,需要清理浏览器存储后再登录。
还有一种情况经常被忽视:服务器系统时间和容器时间不同步。JWT 的exp字段是按签发时间计算的,如果宿主机时间比容器快几分钟或者慢几分钟,就会出现“明明刚登录,却提示登录已过期”。遇到这种问题,先检查两边时间:
date docker compose exec weknora-web date系统时间不一致时,在宿主机上启用 NTP 时间同步,再重启容器即可。时间类问题很隐蔽,排障时如果前面都正常,记得看一眼时钟。
5.4 上传文件 413 或找不到存储目录
上传较大的文件时,如果 Nginx 返回413 Request Entity Too Large,说明默认的client_max_body_size太小。在前端镜像的 Nginx 配置或你自建的反向代理里加上:
client_max_body_size 100m;如果上传接口返回“storage directory not exist”之类的错误,检查weknora-server挂载的weknora_data卷是否真的被使用了。很多时候容器里路径写死成/data,但宿主机映射目录没有建好,镜像里用非 root 用户运行时就没有写权限。解决方案是在宿主机创建目录并授权,或者干脆使用命名卷,让 Docker 自己管理权限。
5.5 忘记管理员密码
忘记管理员密码不能靠删除数据库表解决,那会把整个库都搞坏。比较稳妥的方式是用 CLI 重设密码:
docker compose exec weknora-server weknora-cli user reset-password \ --username admin \ --password 'new-strong-password'重设之后立刻用新密码登录,然后到管理台把默认的管理员邮箱信息补全。如果连用户名都不记得,可以先查一下用户列表:
docker compose exec weknora-server weknora-cli user list这个方法比直接改数据库安全,命令行工具会同步更新安全的密码哈希格式,直接UPDATE表字段很容易把密码哈希弄成不可用的状态。
6. 备份、升级与日常维护
6.1 一键备份脚本
数据安全永远是自托管系统的底线。我写了一个简单的备份脚本,放在/opt/weknora/backup.sh,每周执行一次。它主要做三件事:备份 PostgreSQL 数据库、备份上传文件卷、导出当前部署配置。
#!/usr/bin/env bash set -euo pipefail BACKUP_DIR="/opt/weknora/backups" STAMP=$(date +%Y%m%d-%H%M%S) mkdir -p "$BACKUP_DIR" docker compose exec -T weknora-db \ pg_dump -U weknora weknora \ > "$BACKUP_DIR/db-$STAMP.sql" docker run --rm \ -v weknora_data:/data \ -v "$BACKUP_DIR:/backup" \ alpine tar czf "/backup/data-$STAMP.tar.gz" -C /data . cp /opt/weknora/docker-compose.yml "$BACKUP_DIR/compose-$STAMP.yml" cp /opt/weknora/.env "$BACKUP_DIR/env-$STAMP.env" echo "backup finished: $BACKUP_DIR"备份脚本执行前,记得先chmod +x backup.sh。数据库备份用pg_dump而不是直接复制数据目录,是因为热备份直接复制 Postgres 数据目录容易产生不一致。restore 的时候则可以结合docker compose exec执行psql导入,保证恢复过程可控。
6.2 版本升级操作顺序
升级 WeKnora 最忌讳的是直接docker compose pull && docker compose up -d一把梭。正确顺序是先备份,再拉新镜像,最后按依赖顺序重建容器。
bash /opt/weknora/backup.sh docker compose pull docker compose up -d --no-deps weknora-db docker compose up -d --no-deps weknora-server docker compose up -d --no-deps weknora-web--no-deps的作用是只重建指定服务,避免把所有依赖容器都拖起来。升级后要重点验证三点:后端健康检查是否通过、旧数据是否完好、新版本功能是否正常。如果升级过程中发现迁移出错,马上回滚到旧版本镜像,用备份恢复数据,不要硬着头皮继续修。
6.3 日志切割与磁盘控制
容器默认会把日志写到宿主机的 Docker 数据目录里,如果不加限制,访问量稍微大一点就可能把磁盘塞满。Compose 里给每个服务都加上日志配置,是成本最低的磁盘保护手段:
logging: driver: json-file options: max-size: "20m" max-file: "5"加上之后,每个容器最多保留 5 个 20MB 的日志文件,超过的部分会被自动清理。对日常排障来说,20MB 滚动的日志文件已经足够回看问题,也不用担心服务器因为日志文件无限增长而告警。
回过头来想,WeKnora 的 Docker 部署难度并不高,真正容易让人翻车的恰恰是环境变量填错、健康检查没配置、数据卷权限不对这几个细节。我在实际操作里最深的体会是:每次变更前先备份,迁移命令手动执行,别怕多敲几条 docker compose 命令。这套流程跑通一次之后,之后的升级和维护都会变得很顺,如果你想把它扩展到多个节点,再做一套负载均衡和共享存储也不迟。