上一篇用服务清单确定了主机最低资源线。本篇把“安装应用”变成可审查、可重建的 Compose 项目:镜像版本、网络、数据、健康检查与日志策略全部显式化,后续九篇都在同一纪律上叠加能力。
一、为什么不用一长串 docker run
交互式命令适合临时实验,却容易把关键参数留在 shell 历史里。重装系统后,没人记得当初映射了哪个目录、用了什么网络或为什么添加某个权限。Compose 的价值不是少敲命令,而是把期望状态放进文本:评审差异、复制到新机器、升级前回退都有依据。
一个项目至少分为三类内容:可提交版本库的compose.yaml与运维说明;不可提交的密钥和环境文件;需要备份的数据库、上传附件与应用配置。镜像可以重新拉取,容器可以重新创建,它们都不是业务数据。绑定挂载便于直接看到目录,命名卷减少路径耦合;选择哪一种并不重要,重要的是备份清单能找到状态。
端口也要表达信任边界。第一个应用只绑定127.0.0.1,局域网用户暂时不能直接访问;第五篇加入反向代理后再统一提供入口。数据库只加入内部网络,不发布宿主端口。restart: unless-stopped能处理进程退出和主机重启,却不能修复错误配置,因此还需要健康检查与有上限的验证。
下面的标准库程序生成并审查一个 Compose 清单。它不调用 Docker,因此任何装有 Python 3 的机器都能先验证声明中的版本、端口和安全选项。
frompathlibimportPathfromtempfileimportTemporaryDirectory compose="""services: web: image: nginx:1.27.5-alpine restart: unless-stopped ports: - 127.0.0.1:8080:80 read_only: true tmpfs: - /var/cache/nginx - /var/run security_opt: - no-new-privileges:true cap_drop: - ALL healthcheck: test: [CMD-SHELL, wget -qO- http://127.0.0.1/ || exit 1] interval: 30s timeout: 3s retries: 3 """required=["image:","127.0.0.1:","read_only: true","healthcheck:","cap_drop:"]withTemporaryDirectory()asdirectory:path=Path(directory,"compose.yaml")path.write_text(compose,encoding="utf-8")text=path.read_text(encoding="utf-8")missing=[itemforiteminrequiredifitemnotintext]print(f"file={path.name}")print(f"lines={len(text.splitlines())}")print(f"pinned_tag={'latest'notintext}")print(f"loopback_only={'127.0.0.1:'intext}")print(f"missing={','.join(missing)ifmissingelse'none'}")运行输出:
file=compose.yaml lines=19 pinned_tag=True loopback_only=True missing=none固定标签避免重建时突然得到不同大版本,但标签仍可能被上游移动;更严格时记录镜像 digest。read_only与删除 capabilities 能缩小容器被利用后的写入面,却可能让某些应用无法启动,应根据文档只开放必需目录,不能机械套用。先执行docker compose config看变量展开后的真实配置,再启动服务。
二、让部署和验收成为两个动作
“容器 running”只说明主进程没退出,不表示 HTTP 可用、数据能持久化或重启后配置仍在。部署后应从三个角度取证:Compose 看到的健康状态;宿主机实际监听地址;用户路径上的 HTTP 响应。随后写入测试数据并重建容器,确认数据目录没有随着容器消失。
下面的程序模拟有上限的健康探测,并生成一份确定的验收报告。实际接入时,把samples替换为 HTTP 请求结果即可;状态机和退出条件仍可保留。
fromdataclassesimportdataclass@dataclass(frozen=True)classProbe:attempt:intstatus:intlatency_ms:intsamples=[Probe(1,503,18),Probe(2,503,16),Probe(3,200,12),]max_attempts=5accepted=Noneforprobeinsamples[:max_attempts]:healthy=probe.status==200andprobe.latency_ms<500print(f"attempt={probe.attempt}status={probe.status}healthy={healthy}")ifhealthy:accepted=probebreakifacceptedisNone:raiseSystemExit("deployment failed health gate")checks={"http":True,"loopback_bind":True,"persistent_data":True,"restart_test":True,}print(f"ready_after={accepted.attempt}")print(f"gate={'PASS'ifall(checks.values())else'FAIL'}")运行输出:
attempt=1 status=503 healthy=False attempt=2 status=503 healthy=False attempt=3 status=200 healthy=True ready_after=3 gate=PASS探测必须有超时和最大次数,无限重试会把失败伪装成“正在启动”。首次数据库迁移可以放宽窗口,但应把迁移耗时记录为基线。日志采用轮转上限,避免错误循环填满系统盘;应用数据盘也设置空间预警,因为磁盘耗尽常会把数据库推入只读或损坏状态。
三、建立以后都能复用的项目约定
建议每个服务一个目录,包含 Compose 文件、.env.example、升级说明和恢复步骤。真实.env权限设为仅服务管理员可读,并加入忽略规则;示例文件只列变量名和安全说明,不放可用密钥。项目名固定,避免换目录后卷名与网络名悄悄改变。修改前保存docker compose config结果和当前镜像 digest,修改后执行同一套验收。
不要为了“安全”盲目添加特权模式。应用若要求访问 USB 或核显,只映射对应设备;若要求写目录,只开放具体卷。也不要把 Docker socket 挂给普通仪表盘,它相当于很高的宿主控制权。以后引入自动更新和监控时,仍要按最小权限评估。
迁移到新主机时,先复制声明文件与数据副本,在隔离端口启动,跑完健康、持久化和重启测试,再切入口。由此可见,真正可迁移的制品是“配置 + 状态 + 密钥恢复方式 + 验收证据”,不是某个正在运行的容器。
下一篇将用这套目录约定部署 Vaultwarden 与 Syncthing,并重点处理密码库和文件同步之间完全不同的一致性、权限与备份要求。
参考来源
- Docker:Compose 文件参考
- Docker:持久化存储
- Docker:容器安全
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《自托管服务实战》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。