Docker Compose部署WeKnora:从环境规划到上线运维的完整指南 我第一次帮人部署WeKnora时对方给的服务器上已经跑了七八个业务系统里Python和Node版本乱成一锅粥。当时我们几乎没有犹豫直接用Docker部署WeKnora把服务、数据库、缓存全部容器化隔离不到半天就正常跑起来之后升级和迁移也省了不少心。这篇内容适合两类人一是第一次接触自托管知识类服务的开发者想快速搭起一套可用的环境二是想把这套流程固化下来、避免在宿主机上装一堆依赖的运维同学。如果你是老手可以直接跳到第4章看编排细节如果你是新手建议从头到尾读一遍尤其别跳过第2章的环境和目录规划那是我踩过坑之后才意识到值得提前做的事。1. 开始之前先搞清楚WeKnora的部署形态和资源底线1.1 你手里的WeKnora是单容器还是多服务我接触过的WeKnora版本并不是只有一种打包方式。有些版本官方给的是单容器镜像一个容器里打包了前端页面、后端API和内置数据库docker run一条命令就能起来有些版本则更接近生产形态拆分成了API服务、关系型数据库、缓存甚至对象存储。这个差异决定了后面做法的复杂度。最靠谱的判断方法不是猜而是打开官方镜像仓库说明看它给出的启动命令。如果文档里是docker run -p 端口:端口 镜像名那基本是单容器如果出现了docker-compose.yml示例、依赖PostgreSQL/Redis之类那就是多服务形态。这篇文章讲的是更通用的多服务编排你把其中的服务列表按实际版本增删即可。就算你拿到的版本跟这里不完全一样第2章、第3章、第6章和第7章的内容依然是通用的。1.2 为什么容器化部署这条路值得走我在服务器上手动部署过很多次应用对“装依赖”这件事特别有体会。WeKnora如果依赖Python、Node和一堆分词、检索相关的原生库那在宿主机上每装一次轻则污染别的项目重则把一个应用的运行库版本顶掉。用Docker之后镜像内部就是一个封闭的运行环境宿主机只需要有Docker引擎不需要关心镜像里的库到底跟系统自带的版本冲突不冲突。另外容器化带来的可回滚性很实在。手动部署时升级等于原地修改文件成功率全靠备份和运气容器化之后升级就是换镜像tag出问题切回旧tag几分钟内完成回滚。再加上compose文件本身就是环境清单团队里任何一个人拿到同一份配置都能复现出相同的部署这在多人协作时价值非常高。1.3 内存、CPU和磁盘的底线建议如果以多服务形态部署我的建议是至少2核4GB内存起步磁盘预留20GB以上。数据库和缓存本身的占用可能不大但镜像、日志、上传数据和备份都会慢慢撑大磁盘预留不足很容易把服务器跑满。下面是我近几年部署这类服务常用的一个配置参考资源最低要求建议要求CPU2核4核及以上内存4GB8GB磁盘20GB50GB以上并保留扩展空间Docker20.1020.10以上带Compose v2插件发行版差别不大但要注意服务器架构如果用的是ARM服务器得先确认镜像有没有对应的arm64版本。我有一个习惯在拉镜像之前先看一眼官方仓库的tag列表确认架构和版本都存在再写进compose这样能避免部署到一半才发现镜像不兼容。2. 环境检查与目录规划先做这两件事后面少踩一半坑2.1 用命令确认Docker状态别凭记忆开始部署前我习惯先跑三条命令docker --version docker compose version docker infodocker --version确认引擎版本docker compose version确认compose插件存在docker info可以看到存储驱动、磁盘空间和是否正常连接daemon。有些服务器上只装了旧版docker-compose命令中间有横杠写法是docker-compose up和现在的docker compose up有区别。这两个对应的安装方式不同如果你写带空格的命令报command not found先检查是不是只装了独立二进制。如果服务器开启了防火墙还需要确认你规划的Web端口在防火墙规则里放行。很多人在容器里一切正常外面却访问不了一查是安全组或防火墙没放行端口。这种事在云服务器上尤其常见还没法靠容器日志定位所以检查环境时就要顺手处理掉。2.2 目录规划把一切都收在同一个文件夹里我推荐在/opt下建一个独立目录比如/opt/weknora把compose文件、数据、日志、备份、环境变量都放进去。这里给一个我常用的结构/opt/weknora/ ├── data/ # 数据库数据、上传文件 ├── logs/ # 应用日志目录 ├── backup/ # 备份产物 ├── env/ # 存放服务自定义环境变量 ├── .env # compose读取的变量文件不进版本库 └── docker-compose.yml这样做的直接好处是后续备份只需要打包一个目录迁移服务器时把整个目录搬过去再执行docker compose up -d就能恢复。如果数据散落在各种匿名volume里时间一长很难定位也不知道哪个卷是哪个服务的。另外除非你对容器权限已经足够熟悉否则不要用root在宿主机上一路操作。可以创建一个专门的系统用户来管理这个部署目录后续所有命令都通过它执行至少能避免权限混乱带来的麻烦。2.3 端口规划只暴露必要端口数据库别露出去端口冲突是我遇到比较多的启动失败原因。在写compose之前先用ss -lntp看看打算用的端口是否已经被占用。这篇文章的示例里我习惯用8920作为Web访问端口端口不是固定的你可以按项目需要调整但不要随便挑一个正在被别的服务使用的端口。更重要的一个原则数据库和缓存端口不要映射到宿主机外。它们只应该通过容器网络内部互相访问。原因很简单端口一旦映射出去等于把数据库暴露在一个有可能被公网访问到的位置一旦配置疏漏后果很严重。需要远程调试时宁可进入容器里执行命令也不要图省事直接映射数据库端口。3. 镜像拉取与版本锁定为什么别拿latest当默认选项3.1 确认镜像名和版本这一步省不了从哪里拉镜像如果你是第一次部署先打开官方文档或镜像仓库页面确认仓库名、tags列表、是否有arm64版本。下面我的示例用weknora/server作为镜像名这只是个示意实际部署时请替换成官方给出的真实镜像名。这一步看起来很简单但真的会出问题。我遇到过一位用户他把文档里的示例镜像名看错一个字符拉了好几次都失败最后才发现仓库名写错。Docker对镜像名的匹配是精确的多一个少一个字符都会报manifest unknown之类错误所以拉取之前仔细核对一遍能省不少时间。3.2 拉取之后花十秒检查镜像信息拉取的命令很简单docker pull weknora/server:1.6.2拉完之后我会顺手看一下这个镜像的基本信息docker image inspect weknora/server:1.6.2 | grep -E Architecture|Os这步是为了确认拉下来的镜像是amd64还是arm64。如果服务器是ARM架构而镜像只有amd64版本运行的时候会因为架构不匹配直接报exec format error。与其等启动时报错不如拉取阶段就排除这个问题。我还见过有人在x86服务器上拉到了arm64镜像症状同样诡异连容器都起不来。3.3 锁定版本tag和镜像摘要生产部署里我非常不建议在compose里直接写latest。latest指向的镜像会变你今天部署和下周部署可能拉到不同版本下次docker compose pull还会把它更新成新版本你的部署就变成了“随缘部署”。锁定具体版本tag比如1.6.2至少在升级前你知道当前跑的是什么。更严谨的做法是记录镜像的digest。digest是镜像内容的哈希摘要比tag更可靠tag可以重新指向不同镜像digest则代表一个不可变的具体镜像。用docker images --digests weknora/server可以查看。我在正式环境里会在升级记录里写上旧tag、新tag和对应的digest出问题审计时一目了然。日常管理没必要每次都记但每次升级前看一眼是值得的。4. 编排docker-compose.yml把各个组件串成一条完整的服务链4.1 一份可以直接改用的compose示例下面是我部署时常用的一个骨架服务名和端口都是示意请按你实际部署的镜像和文档调整services: db: image: postgres:16-alpine container_name: weknora-db restart: unless-stopped environment: POSTGRES_USER: weknora POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: weknora TZ: Asia/Shanghai volumes: - ./data/db:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U weknora -d weknora] interval: 10s timeout: 5s retries: 10 networks: - weknora-net cache: image: redis:7-alpine container_name: weknora-redis restart: unless-stopped command: [redis-server, --appendonly, yes] volumes: - ./data/redis:/data networks: - weknora-net api: image: weknora/server:1.6.2 container_name: weknora-api restart: unless-stopped env_file: - ./env/weknora.env environment: TZ: Asia/Shanghai DB_HOST: db DB_PORT: 5432 DB_USER: weknora DB_PASSWORD: ${DB_PASSWORD} REDIS_HOST: cache ports: - 8920:8000 depends_on: db: condition: service_healthy cache: condition: service_started volumes: - ./data/uploads:/app/data/uploads - ./logs:/app/logs networks: - weknora-net networks: weknora-net: driver: bridge几点说明image字段里的weknora/server:1.6.2是示意镜像名实际请换成官方文档给的完整名称。容器内API端口8000是示意如果镜像实际监听其他端口ports右侧的容器内端口要改成对应值。${DB_PASSWORD}来自同目录下的.env文件不建议直接把密码写死在compose里。4.2 环境变量的组织.env文件和env_file怎么配合在部署目录下建一个.env文件DB_PASSWORD你的强密码 WEB_PORT8920 IMAGE_TAG1.6.2compose会自动读取同目录下的.env文件把里面定义的变量填充到配置里。api服务里的env_file则负责把应用本身需要的配置注入容器。这两者容易混淆.env是为compose文件本身服务的env_file是为容器内应用服务的。有一个好习惯把.env加入版本库的忽略名单永远不提交。compose文件本身可以提交里面只保留变量引用这样既方便协作也不会把密码散播出去。我见过有人不小心把数据库密码推到代码仓库里后面改密码、改备份、通知相关方折腾了一整天完全没必要。4.3 数据卷挂载和权限问题文件能存下来才算部署完成数据卷有三种选择bind mount、named volume、匿名volume。我推荐把关键数据都放到宿主机目录里也就是用bind mount。第2章规划的data和logs目录正好在这里派上用场。上传的文件如果挂载到Linux目录经常遇到一个权限问题宿主机目录属主是root容器内进程是非root用户结果写入失败。解决方式要么是chown -R 1000:1000 data logs把目录属主改成容器内用户的UID要么在compose的service里通过user字段直接指定与宿主机一致的UID。如果你所在环境开了SELinuxbind mount可能还需要加:Z标签否则容器内进程读不到文件。这些权限问题在日志里通常表现为Permission denied排查时先看属主再看SELinux基本能覆盖八成情况。4.4 启动顺序depends_on只是保险健康检查才是关键很多人在首次启动时看到api容器反复退出就以为是镜像问题其实只是因为数据库还没就绪。depends_on默认只控制启动顺序不控制“就绪”状态——数据库容器进程起来了但PostgreSQL还在初始化api连不上就会退出。所以我在db服务里加了healthcheck用pg_isready检测数据库是否真正可连api的depends_on中再用condition: service_healthy确保数据库健康后才启动api。如果官方镜像没有内置等待脚本你也可以在api的启动命令前面加一个等待脚本或者接受“进程退出后由restart策略拉起”但不推荐后者因为会制造一堆无意义的重启日志。5. 启动之后怎么验证日志、初始化和反代接入5.1 首次启动看什么docker compose up -d docker compose ps第一次启动不要急着下结论。镜像要拉取数据库要初始化前一两分钟日志可能看起来像没动静这很正常。用docker compose logs -f api持续跟进日志看到类似“database connection established”或“server started”之类的输出就算正常起来了。如果容器状态是Restarting别反复重启先停下来看日志docker compose logs --tail50 api。日志里通常已经写明了原因。盲目重启解决不了问题只会把同一个报错再看一遍。5.2 初始化管理员账号的两种常见路径WeKnora这类型应用首次初始化通常有两条路。有的镜像会在启动时往日志里打印一段临时管理员密码你需要在首次登录后立刻修改有的没有内置CLI首次打开Web页面会进入初始化向导要求设置管理员账号和基础参数。如果你拿到的版本属于前者用docker logs找到初始密码如果是后者直接浏览器打开http://服务器IP:端口就行。具体是哪种以官方文档为准。我记得第一次部署时盯着日志找密码没找到后来发现初始化入口在Web页面白白折腾了十分钟。5.3 验证服务是否真正可用容器状态是running不代表业务可用。我习惯分两步验证先在容器内部确认端口和健康接口再在宿主机通过映射端口访问。docker exec weknora-api curl -s http://127.0.0.1:8000/api/health curl -s http://127.0.0.1:8920/api/health如果容器内部正常、外部不通优先级最高的是检查防火墙和安全组其次是ports映射写没写对。健康接口的路径不同版本可能不一样你也要以实际镜像的说明为准但验证思路是一样的。5.4 接入反向代理别让内部端口直接面对公网就算你已经把Web端口映射到宿主机我仍然建议在它前面加一层Nginx。你只需要把容器端口映射到127.0.0.1比如ports写成127.0.0.1:8920:8000然后用Nginx代理到它。下面是一个可以起步的server块server { listen 80; server_name your-domain.example; location / { proxy_pass http://127.0.0.1:8920; 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; } }如果要上HTTPS建议用自动续签的免费证书客户端来管理证书Nginx里配置证书路径再开443端口定期更新由客户端自动完成。手工写定时任务刷证书的做法不是不行只是容易漏而且证书过期导致的异常比什么都难排查。6. 上线后的三个刚需功课备份、升级和日志治理6.1 数据库逻辑备份和数据目录备份分开做备份是部署里最容易被拖到“明天再做”的事但真等到出问题时明天就是灾难。我对数据类服务的备份策略是数据库用逻辑备份文件用目录打包。为什么数据库不能直接拷贝数据目录因为PostgreSQL在运行中直接复制数据文件很可能得到一个内部不一致的快照恢复时无法启动或数据损坏。pg_dump生成的SQL文件是某个时间点的一致逻辑视图恢复起来更可靠。我用下面这个脚本做一个简单的每日备份#!/usr/bin/env bash set -euo pipefail BACKUP_DIR/opt/weknora/backup DATE$(date %Y%m%d_%H%M%S) docker exec weknora-db pg_dump -U weknora -d weknora | gzip $BACKUP_DIR/weknora_db_$DATE.sql.gz tar czf $BACKUP_DIR/weknora_data_$DATE.tar.gz -C /opt/weknora data uploads logs find $BACKUP_DIR -type f -mtime 14 -delete如果你熟悉定时任务把它加到每天凌晨执行即可。恢复时先恢复数据库dump再解包文件目录步骤反过来就能完成一次数据回滚。注意备份文件会越来越大脚本里的find -mtime 14 -delete是把超过14天的旧备份删掉你按自己的保留周期调整。6.2 升级应用和回滚的标准化动作容器化之后升级流程可以固定成一套动作先备份再修改.env中的IMAGE_TAG或compose中的image版本然后docker compose pull拉新镜像最后docker compose up -d启动新版本。数据库是否需要同步升级要看官方说明。通常我会先只升应用观察一段时间确认数据库兼容没出问题再考虑数据库升级。回滚同样简单把版本tag切回旧版本docker compose up -d即可。但如果升级过程中数据库迁移脚本已经执行并且修改了表结构那么单纯切镜像可能不够你得把数据库恢复到升级前的备份。这也是为什么升级前必须备份而且备份要放在升级开始之前不是等出了问题再去想。6.3 日志和资源的长期治理容器没有配置日志轮转时Docker会把标准输出和错误不断积累到json日志文件里几个月后可能超过几十GB。在compose里给每个服务加上logging配置限制单个日志文件大小和保留数量是最省心的做法logging: driver: json-file options: max-size: 200m max-file: 5再加上内存限制可以防止某个服务异常占用导致整台服务器卡死deploy: resources: limits: memory: 2g这里要留意内存限制过低时容器会被OOM杀掉所以数值要按业务规模留有余量。如果用的Compose版本较老deploy下限制可能不生效换成mem_limit: 2g这种写法也行。设置之后用docker compose up -d重新创建容器才生效。7. 三个真实踩坑案例从现象到根因的排查链路7.1 容器反复重启为什么不能只看重启策略现象docker compose ps里api容器一直在Restarting。我遇到过一个很典型的案例某开发者在服务器上部署镜像拉下来了compose也启动了但api容器每隔十几秒就退出一次。他的第一反应是改restart策略把unless-stopped改成always结果还是重启只是更执着了。正确的排查链路应该是这样先看日志docker compose logs --tail50 api注意最后几行才是直接原因。根据日志内容判断如果是connection refused说明依赖服务没就绪或地址配置错如果是exec format error说明架构不匹配如果是Permission denied则回到权限问题。对照第4.4小节的healthcheck配置把启动顺序从“只管启动”改成“等到就绪”。这个案例最后的根因就是api容器在数据库尚未初始化完成时启动失败数据库健康后restart策略又把api拉起来问题核心不在于重启策略而在于“太早启动”。7.2 文件上传成功但保存失败目录属主不一致现象页面操作正常数据库里也有记录但只要涉及上传文件就提示保存失败。日志里的定位是Permission denied。排查链路在宿主机执行ls -l /opt/weknora/data/uploads。在容器内执行ls -l /app/data/uploads。比对两者属主UID发现容器内进程是UID为1000的用户宿主机目录属主是root。这事的根源是bind mount两侧UID不匹配。解决方式要么在宿主机把目录chown成容器内用户的UID要么在compose的service里用user字段指定宿主机端的UID。我的习惯是把部署目录的UID统一这样既不需要每个服务单独指定也能保证容器内进程能写。7.3 磁盘被容器日志撑爆现象服务一切正常但磁盘占用告警而且是在一个低频请求的系统里。排查链路du -sh /var/lib/docker/containers/*/快速看哪个容器目录占用最大。逐个查看*-json.log大小找到最大的日志文件。对比时间发现日志从部署之后就没轮转过。解决分为两步先用truncate -s 0清空那个日志文件路径注意不要直接rm否则需要先停容器再按第6.3小节的配置给compose加上max-size和max-file之后重新创建容器。这个案例里的服务本身没有问题问题出在没有给日志设上限等于让Docker无限记录。7.4 一些可以提前养成的部署习惯经历了这些之后我现在部署任何Docker服务都维持几个习惯compose文件放进版本管理数据目录和env文件绝不提交任何变更之前先跑一遍备份脚本容器端口宁可少映射也绝不图方便把数据库或缓存端口暴露出来。这些看起来都是小动作但能少收很多次“半夜叫醒服务”。我自己还会在每次升级前把当前镜像的tag和digest记一条笔记排查问题时至少能分清“是环境问题还是版本升级引入的问题”。