跳转至

容器化与 Docker:从 Dockerfile 到 Compose

这一章(和 13 一起)把前面八章造出来的应用装进集装箱。 上篇讲 Docker 本体: 为什么需要它、镜像和容器到底差在哪、两个真实的多阶段 Dockerfile 逐行讲、 命令怎么敲、CI 怎么自动推镜像;然后用 Docker Compose 把数据库、Redis、 MailHog、前后端七个服务一键拉起。主线例子:一条 docker compose up, 从空机器到整个工单系统在跑。

1. 顶层全景

  为什么容器:虚拟机太重(内核跑两遍)→ Linux namespaces 隔离、宿主内核复用
    ↓
  五个概念:镜像(蓝图)/ 容器(实例)/ 层(不可变、可缓存)/ 构建参数 / 环境变量
    ↓
  两个 Dockerfile:后端多阶段(rust:nightly 构建 → debian-slim 运行)
                  前端多阶段(Wasm 读不到 env → lighttpd 反代 /backend)
    ↓
  docker build → docker run(端口映射、--rm、--env-file)
    ↓
  CI:GitHub Actions 登录 Docker Hub、Buildx、build-push
    ↓
  Docker Compose:services / desired state / health checks / volumes
    ↓
  编排(orchestration)定义 + YAGNI:别一上来就 Kubernetes

  图说:单机容器 → 多容器编排 → 集群编排,三级火箭逐级点。

2. 历史背景:从「在我机器上能跑」说起

先纠正一个概念:服务器不是一台机器,是一个角色——硬件或软件扮演的角色。 最早这个角色贴着裸金属:部署 = 往专用机器上拷文件;机器搬进机房,拷变成传, 本质没变1。

麻烦随之而来:为服务器优化的机器和日常电脑差异越来越大,环境冲突不断—— 那句著名的「works on my machine(在我机器上能跑)」就此诞生, 这不是借口,是参与各方的真实灾难2。

第一个解法是把应用连配置打包成轻量虚拟机(VM):能跑就行,但代价高—— 虚拟化要模拟 CPU、内存,还要再养一个完整操作系统。 「只要它在一处能跑,就必须到处能跑」的需求,不该用这么贵的方案买3。

3. Docker 的原理:namespaces 与层的复用

虚拟化的冗余在哪: 硬件之上是固件和操作系统;hypervisor(KVM/VMware/Hyper-V) 再虚拟一套硬件,上面再装一个操作系统——如果宿主和客户机是同一种系统, 内核等关键部件就在同一硬件上跑了两遍。能不能既复用宿主的系统部件、又不牺牲隔离? Linux 内核的 namespaces 正是答案:提供隔离性与复用性4。

但 namespaces 本来就难用得只有专家敢碰——Docker 的贡献是把 namespaces 包成人人会用的简单接口5。这就引出容器化的定义:

容器化 = 把应用包成标准化、可部署、可管理的统一格式;虚拟化细节被藏起来, 对环境来说一切如常;操作系统的重复部分被复用,而存储、进程、内存各自隔离。6

Docker 的五个核心概念(书里按出场顺序):7

概念 一句话
镜像(image) 环境的蓝图 + 全部材料;不可变;由层构成;从 registry(如 Docker Hub)拉
容器(container) 按蓝图装配出来的运行实例;为单一目的调好;用完即弃
层(layers) 每次改动/加装 = 新一层,底层镜像不动;层数越少越好(合并命令、多阶段构建)
构建参数(ARG) 构建期变量;依赖一变只需改一处;多阶段里每段要重新定义
环境变量(ENV/.env) 运行期配置;构建和运行都能传;.env 文件可以成批带

镜像与容器的关系,书里一句定音:「镜像是设计,容器是按设计的实现」—— 只能基于镜像建容器;镜像不可变以保值,容器易造以值得丢弃。8

Dockerfile 就是把上面全部概念写在项目根的一个文本文件里; 镜像构建好后,随时能实例化容器9。

4. 后端 Dockerfile:多阶段构建逐行讲

ARG BASE_IMAGE=rustlang/rust:nightly          # 1  将来要换基础镜像,只改这一行
FROM ${BASE_IMAGE} AS base                     # 2  拉 nightly 镜像,命名 base 供后面引用
WORKDIR /src                                   # 3  之后命令的工作目录
COPY backend ./backend/                        # 4-11 五个 workspace 成员 + 根 src/tests
COPY entity ./entity/                          #    + Cargo.lock/Cargo.toml 全部拷进来
COPY extra_migrations ./extra_migrations/
COPY migration ./migration/
COPY shared ./shared/
COPY src ./src/
COPY tests ./tests/
COPY Cargo.lock Cargo.toml ./

FROM base AS build-server                      # 13 新阶段基于 base(构建失败不用重拷源码)
RUN cargo build --release                      # 14 生产构建:优化、无调试符号

FROM debian:bookworm-slim as prod              # 16 运行镜像:瘦得只剩二进制
RUN apt-get update && apt install -y openssl   # 17 装 OpenSSL;命令合并成一层
COPY --from=build-server /src/target/release/ticketing /bin/   # 18 只把二进制搬过来
ENTRYPOINT [ "/bin/ticketing" ]                # 19 启动命令

逐行要点:ARG 是好习惯(会变的东西前置);FROM 之后的几乎每一行都是一个新层—— 所以要多阶段构建把「只用来编译的东西」全部扔掉10;apt-get update && install 合并成一条是为了少造层;最终产物从 debian:bookworm-slim 起跑, 容器里只有应用二进制,别无他物11。

5. 前端 Dockerfile:Wasm 的硬约束与反代解法

前端镜像前半段同款:装构建依赖(build-essential、cmake、clang…)、 装 stable 工具链 + wasm32 目标 + trunk、trunk build --release。 变招在最后:运行镜像用 jitesoft/lighttpd——因为「前端应用只是一堆静态文件, 自己不会服务自己;lighttpd 替它服务,还替它把 /backend 指到真后端」12。

为什么要反代?这是 Wasm 的硬约束: Wasm 跑在沙箱里,读不到环境文件—— 后端地址没法运行期注入,只能构建期硬编码。硬编码了又想留改动的余地,怎么办? 假定后端永远在本机的 /backend 下(哪怕它其实在别处),由代理兑现这个假定。 lighttpd 配置文件里两行是关键:

  server.error-handler-404 = "/index.html"        ← 所有未知路径都交给 Yew 应用自己处理
  $HTTP["url"] =~ "^/backend" { … proxy… }        ← /backend 开头的请求转发给真后端
      proxy.header 里把 URL 里的 "/backend" 前缀剥掉再转发

  图说:前端请求 <服务器>:<端口>/backend/projects/1
        → lighttpd 改写成 <BACKEND_HOST>:<BACKEND_PORT>/projects/1
  404 兜底同理:不配置的话,lighttpd 会把路径当磁盘文件找,Yew 路由直接报废。

买到的自由:BACKEND_HOST/BACKEND_PORT 是 lighttpd 的环境变量,部署期随便改, Wasm 二进制一行不用重编13。

6. 命令、CI 与 Compose

构建与运行(命令逐个讲):14

docker build -t <用户名>/ticketing-backend .            # -t 起名字;句点是 Dockerfile 所在目录
docker build -f Dockerfile.frontend -t <用户名>/ticketing-frontend .   # -f 指定另一个 Dockerfile

docker run -d --rm -p 8000:8000 --name ticketing-backend \
           --env-file .docker.env <镜像> env
# -d 后台跑;--rm 停了即删(容器用完即弃,不靠手工养);
# -p 宿主:容器 端口映射;--name 起名;--env-file 整文件传环境变量;-e 单个传

CI 自动化(GitHub Actions): push 到 master/testing 触发,两个 job 各四步: checkout → 登录 Docker Hub(账号密码走 secrets,红字:敏感数据永不硬编码) → Setup Buildx → build-push(指定 context、Dockerfile、push: true、tag …:latest)。 README 照例挂徽章15。

Docker Compose:多容器的指挥官。 单独容器不够用——应用还依赖数据库等外部组件; Compose 能把整套环境(含依赖)定义成一份文件、一键起齐16。

四个概念:

概念 是什么 文件里的痕迹
Services(服务) 可独立启动/重启/伸缩的计算资源 七个服务:MariaDB、Adminer(可选)、PostgreSQL(给 fang)、Redis、MailHog、backend、frontend
Desired state(期望状态) 你描述「想看到它运行的样子」,偏离了就拉回来 restart: always
Health checks(健康检查) Compose 靠应用自报判断健康;怎么报自定 Postgres 的 pg_isready,每秒一次、超时 5 秒、重试 10 次
Volumes(卷) 独立于容器生命周期的持久化抽象;容器死了数据还在 MariaDB/Postgres 的 bind 卷(库文件留在宿主)

七个服务一张网: depends_on 排依赖序(后端依赖四个基建,前端依赖后端); 前端服务的环境变量 BACKEND_HOST=ticketing-backend——注意用的是服务名, Compose 内建 DNS 让容器互相按名字找到17。

编排(orchestration)在此定义: 「通过自动化生命周期管理、健康检查和各组件的伸缩,来管理容器化应用环境」—— 靠的正是期望状态这个思想18。

判断(我们的,不是书里的): 章末那条 YAGNI 忠告是全书最值钱的运维观: 「大多数应用单个容器就跑得很好;先单容器,观测到真需求再上 Compose, 再上 Swarm/K8s——K8s 一开始大概率是过度设计,还会偷走更要紧任务的开发资源。」 如果错,会错在: 如果组织已有 K8s 平台团队、或合规强制要求容器编排标准, 「从小做起」反而要多付一次迁移税。判断依据是:有没有现成的平台能力替你付这笔税。

7. 主线例子走一遍:一条命令起全家桶

  git clone → cp .env 样例 → docker compose up
      │  ① Compose 按 depends_on 起七个服务;MariaDB/Postgres 的 bind 卷找到旧数据
      ▼
  postgres 健康检查 pg_isready 通过,backend 才开始连得上调度库
      │  ② 后端容器:Rust 二进制跑在 debian-slim 里,8000 端口映射到宿主
      ▼
  前端容器:lighttpd 在 8080 服务静态文件;/backend 反代到 ticketing-backend:8000
      │  ③ 浏览器开 localhost:8080 —— 整套工单系统活在本机,一条命令
      ▼
  改代码 → git push → CI 构建推 Docker Hub → 服务器 docker pull && up -d
  ——从开发机到生产的全链路,没有一步手工装环境。

  图说:Compose 文件就是「整套环境的图纸」;CI 让图纸的产物自动更新。
  (服务名、端口出自原书 compose 文件。)

8. 作者的判断与证据(分开标)

有出处的: namespaces 原理、五个概念定义、多阶段构建的理由、 Wasm 读不到 env 的约束、Compose 四概念,均为原书原文47101216。

作者自己的评估:

  • 前端用 lighttpd 反代是作者选的「工作流解」,Nginx/Caddy 同样可行(书里未比较);
  • 「K8s 大概率是过度设计」的 YAGNI 立场(见判断块);
  • 教学镜像 rustlang/rust:nightly 偏大;生产可用 cargo-chef 做依赖层缓存——书里未提。

9. 边界与局限

  • 没有讲镜像扫描与安全: 第 3 章的 cargo-deny 管依赖,容器层的漏洞扫描(trivy 等)没接;
  • compose 只到单机多容器: 多机部署留给 Swarm/K8s(见 13 篇);
  • 密钥管理仍靠 .env 文件:生产应上 secret manager,书里只在 object_store 那章点过;
  • 健康检查只有 Postgres 一个示例,应用自身的 /health 端点被建议但未实现。

10. 可带走的

主线例子一行走完: docker compose up → 七服务按依赖启动 → 健康检查放行 → 前端 8080/lighttpd → /backend 反代后端 8000 → push 触发 CI 推镜像。

  1. 容器化的动机一句话: 虚拟机把内核跑两遍太贵,namespaces 隔离 + 宿主复用是正解。
  2. 镜像不可变、容器用完即弃;层数越少镜像越小。
  3. 多阶段构建 = 编译的垃圾不进产房:构建镜像与运行镜像分开,只搬二进制。
  4. Wasm 应用拿不到环境变量——部署期可变配置用「反代 + 路径前缀」兑现。
  5. SPA 托管必须配 404 → index.html,否则深链接全灭。
  6. docker run 的常识: --rm(容器不靠手工养)、-p 宿主:容器、-e/--env-file。
  7. CI 推镜像四步: checkout → 登录(凭据走 secrets)→ Buildx → build-push。
  8. Compose 四概念:服务、期望状态、健康检查、卷;服务名就是内网 DNS 名。
  9. 编排 = 生命周期 + 健康 + 伸缩的自动化,本质还是「期望状态」。
  10. YAGNI 运维版: 单容器 → Compose → Swarm → K8s,逐级升级,别跳级。

5.5 Compose 文件逐服务解剖(七个服务各管什么)

原书的 compose 文件是整章的收束——七个服务就是过去八章基建的「全家福」。 逐个看它们各自承担什么、配置里的每个键为什么存在:19

MariaDB(主库): image: mariadb(官方镜像)+ command: --init-file /data/init.sql (启动时执行初始化 SQL——bind 卷把 extra_migrations/mariadb 挂到容器 /data, 建库建账号一步到位)+ restart: always(期望状态:永远活着)+ env_file: ./.docker-compose.env(密码等配置整个文件注入,不进版本库)+ 两个卷:一个匿名卷(/var/lib/mysql)存数据本体,一个 bind 卷送初始化脚本 + ports: 3306:3306 映射到宿主(开发期用数据库客户端直连方便)20。

Adminer(可选): 数据库网页客户端,8088 映射出来—— 「观察应用数据状态的便利工具,没有它系统照样转」21。

PostgreSQL(给 fang 的调度库): 第 8 章埋的伏笔在这里兑现: fang 绑死 PostgreSQL,所以全家桶里单开一台 postgres:14-alpine。 两个细节值得抄:健康检查 test: ["CMD-SHELL", "pg_isready"]、 interval: 1s、timeout: 5s、retries: 10——每秒探一次、5 秒算超时、 失败 10 次才判死重启;初始化脚本 bind 到官方约定的 /docker-entrypoint-initdb.d(镜像首次启动自动执行该目录 SQL)。 另有 POSTGRES_HOST_AUTH_METHOD=password、PGDATA 指定数据目录—— 这个服务的配置密度是七个里最高的,因为它最不「即插即用」22。

Redis: 最省事的一个:image: redis:7-alpine + 一行 command (redis-server --save 60 1 --loglevel warning——每 60 秒有一次写就快照持久化, 告警级日志)。键值库没有任何初始化负担,这就是第 8 章选它当队列的原因23。

MailHog: ports 把 1025/8025 都映射出来——SMTP 端口给应用连, 网页端口给人看信;restart: always24。

ticketing-backend: 唯一用我们自己镜像的服务(image: viktordaroczi/ticketing)。 depends_on 列四个基建(mariadb、postgres、redis、mailhog)—— Compose 按依赖顺序启动,但注意:depends_on 只保证启动顺序,不保证服务就绪 (Postgres 那套健康检查就是为此存在的;后端自己没有 readiness 检查, 靠 restart 兜底,这是文件里一个可以再精进的地方)。hostname: ticketing-backend 让前端能用这个名字找到它25。

ticketing-frontend: 依赖后端;environment 里两行把 BACKEND_HOST=ticketing-backend、BACKEND_PORT=8000 显式注入—— 这就是第 5 节反代解法的运行期一半:lighttpd 拿这两个值去连后端; 8080 映射出来是对外的唯一门面26。

判断(我们的,不是书里的): 这个 compose 文件的教学价值不在「能跑」, 而在它把前八章每个技术决策都显式化了:MariaDB 是主库、PostgreSQL 是调度库、 Redis 是队列、MailHog 是邮件——每一句 depends_on 都是一张架构图的边。 读别人的 compose 文件是摸一个系统架构的最快办法,写自己的 compose 文件是 检验架构清不清楚的最狠办法。 如果错,会错在: 把 compose 当「启动脚本」看的人会嫌它啰嗦; 但当它失宠那天,往往是因为系统长到了需要真正的编排(下一篇)。

6.5 构建进阶:三个容易忽略的细节

.dockerignore(书里散落的规矩,汇总在这里): 镜像构建的 COPY 会把构建上下文整个发给 Docker 守护进程—— target/(几 GB 的编译产物)、.git/、.env 全都会被拷进去, 又慢又泄密。.dockerignore 就是给 COPY 看的黑名单, 内容大致是 .git、target、.env、frontend/dist、frontend/node_modules。 「镜像里永不包含敏感信息」这条铁律,一半靠它执行27。

层缓存的脾气: Docker 构建缓存是逐层失效的——某一层变了, 它后面所有层都重建。所以 Cargo 项目的 Dockerfile 有一个经典的省力排法 (书里没展开,补充在这里,机制与书中「层」的概念完全一致): 先只 COPY Cargo.toml/Cargo.lock → cargo fetch 或空构建依赖 → 再 COPY 源码 → 真正构建。 依赖没变时,前面几层全命中缓存,改一行代码只重编最后两层; 而原书 Dockerfile 把源码和 manifest 一起拷,任何 .rs 变动都会让 cargo build --release 从头编译全部依赖——教学能接受,编译一次十几分钟, CI 里就会肉疼。生产做法是引入 cargo-chef 之类的工具把「依赖编译」单独分层。28

标签策略: 示例里 CI 一律推 :latest——每次覆盖,回滚只能靠重推。 至少加一条版本号标签(如 :1.4.2)再叠 :latest, 服务器 up -d 时钉版本号,回滚 = 改回上一个标签。 书里用 latest 是演示简化,别带进生产。29

6.6 Composerize 与 Compose 合并的小工具

书末给了一个顺手的工具:Composerize——把 docker run 命令翻译成 compose 文件(信息本来都在命令里);多个 compose 文件也可以用 -f a.yml -f b.yml 合并成一个栈一次拉起。 机器生成的文件要人工过一遍再上线,字段命名和默认值经常要微调30。

7. 作者的判断与证据(分开标)

有出处的: namespaces 原理、五个概念定义、多阶段构建的理由、 Wasm 读不到 env 的约束、Compose 四概念、七服务配置,均为原书原文或工具事实4710121619。

作者自己的评估:

  • 前端用 lighttpd 反代是作者选的「工作流解」,Nginx/Caddy 同样可行(书里未比较);
  • 「K8s 大概率是过度设计」的 YAGNI 立场(见 §6 判断块);
  • 「Composerize 生成后要人工微调」是经验之谈30;
  • 补充(不在书里,来自通用知识):cargo-chef 分层、版本号标签策略, 是对原书 Dockerfile/CI 演示的生产化修正。

8. 边界与局限

  • 没有讲镜像扫描与安全: 第 3 章的 cargo-deny 管依赖,容器层的漏洞扫描(trivy 等)没接;
  • compose 只到单机多容器: 多机部署留给 Swarm/K8s(见 13 篇);
  • 密钥管理仍靠 .env 文件:生产应上 secret manager,书里只在 object_store 那章点过;
  • 健康检查只有 Postgres 一个示例,应用自身的 /health 端点被建议但未实现;
  • 后端容器没有 readiness 检查,depends_on 只保顺序不保就绪(见 §5.5 判断块)。

9. 可带走的

主线例子一行走完: docker compose up → 七服务按依赖启动 → 健康检查放行 → 前端 8080/lighttpd → /backend 反代后端 8000 → push 触发 CI 推镜像。

  1. 容器化的动机一句话: 虚拟机把内核跑两遍太贵,namespaces 隔离 + 宿主复用是正解。
  2. 镜像不可变、容器用完即弃;层数越少镜像越小。
  3. 多阶段构建 = 编译的垃圾不进产房:构建镜像与运行镜像分开,只搬二进制。
  4. Wasm 应用拿不到环境变量——部署期可变配置用「反代 + 路径前缀」兑现。
  5. SPA 托管必须配 404 → index.html,否则深链接全灭。
  6. docker run 的常识: --rm(容器不靠手工养)、-p 宿主:容器、-e/--env-file。
  7. CI 推镜像四步: checkout → 登录(凭据走 secrets)→ Buildx → build-push; 敏感数据永不硬编码进项目文件。
  8. Compose 四概念:服务、期望状态、健康检查、卷;服务名就是内网 DNS 名。
  9. 编排 = 生命周期 + 健康 + 伸缩的自动化,本质还是「期望状态」。
  10. YAGNI 运维版: 单容器 → Compose → Swarm → K8s,逐级升级,别跳级。
  11. .dockerignore 是半条安全红线; Cargo 项目把 manifest 与源码分层拷,救的是编译缓存。
  12. 别只推 :latest; 版本号标签是回滚的锚。

10. 原文地图

主题 原书章 原文位置
服务器是角色不是机器 Containerization and Orchestration text/74-fm-containerization-with-docker.txt:13(搜「not a machine but a role」)
「在我机器上能跑」 同上 text/74-fm-containerization-with-docker.txt:16(搜「excuse」)
VM 的开销 同上 text/74-fm-containerization-with-docker.txt:28(搜「hypervisor」)
namespaces 与复用 同上 text/74-fm-containerization-with-docker.txt:31(搜「namespaces」)、:34(搜「simplified interface」)
容器化定义 同上 text/74-fm-containerization-with-docker.txt:4(搜「standardized and uniform」)
镜像/容器/层/ARG/ENV 同上 text/74-fm-containerization-with-docker.txt:44(搜「immutable」)、:50(搜「Layers」)、:63(搜「Dockerfile」)
后端 Dockerfile 逐行 同上 text/74-fm-containerization-with-docker.txt:76(搜「BASE_IMAGE」)、:115(搜「cargo build --release」)、:121(搜「bookworm-slim」)
命令合并少造层 同上 text/74-fm-containerization-with-docker.txt:163(搜「combining commands」)
前端 Dockerfile 与 lighttpd 同上 text/74-fm-containerization-with-docker.txt:241(搜「lighttpd」)、:260(搜「workaround」)
404 兜底与 /backend 改写 同上 text/74-fm-containerization-with-docker.txt:320(搜「error-handler-404」)、:323(搜「proxy.server」)、:387(搜「translate」)
docker build/run 参数 同上 text/74-fm-containerization-with-docker.txt:402(搜「docker build」)、:426(搜「docker run」)、:435(搜「disposable」)
CI 工作流四步 同上 text/74-fm-containerization-with-docker.txt:518(搜「login-action」)、:545(搜「build-push-action」)、:658(搜「never hardcode」)
Compose 的动机 同上 text/74-fm-containerization-with-docker.txt:696(搜「comes into the picture」)
Services 定义 同上 text/74-fm-containerization-with-docker.txt:947(搜「computation resource」)
Desired state 同上 text/74-fm-containerization-with-docker.txt:986(搜「intervenes」)
Health checks 同上 text/74-fm-containerization-with-docker.txt:808(搜「pg_isready」)、:995(搜「dedicated lightweight endpoint」)
Volumes 同上 text/74-fm-containerization-with-docker.txt:1001(搜「outlive any container」)
编排定义 同上 text/74-fm-containerization-with-docker.txt:1049(搜「automating lifecycle」)
YAGNI 忠告 同上 text/74-fm-containerization-with-docker.txt:1062(搜「smallest possible scale」)
Composerize 同上 text/74-fm-containerization-with-docker.txt:1035(搜「Composerize」)
七服务 compose 文件 同上 text/74-fm-containerization-with-docker.txt:703(搜「name: ticketing」)、:709(搜「mariadb」)、:832(搜「redis」)、:865(搜「ticketing-backend」)

6.8 Dockerfile 逐指令参考(每条指令干什么、何时失效)

把两个 Dockerfile 里出现的每条指令单独建档——这是「原书的教法」里最 值得原样接住的部分,因为 Docker 的错误信息永远指向这些词:31

指令 干什么 书里的使用点
ARG 构建期变量,只在构建时存在 BASE_IMAGE(将来换基础镜像只改一行)、SERVER_URL(前端的后端地址假定)
FROM…AS 拉基础镜像并给阶段起别名 base / build-server / build-client / prod 四段;多阶段靠它串
WORKDIR 之后所有命令的工作目录 /src;前端版用 ENV SRCDIR 间接引用
COPY 宿主机 → 镜像(新层) 源码五成员 + Cargo 清单;--from=build-server 只搬二进制
RUN 镜像里执行命令(新层) cargo build --release;apt install(合并成一条)
ENV 镜像内的持久环境变量 SRCDIR、SERVER_URL、PORT=8080
ENTRYPOINT 容器启动命令 后端 /bin/ticketing

层缓存的脾气(实务版): 每条指令产出的层,内容哈希不变就复用; 一变,它和它之后的所有层重建。所以「先 COPY manifest → 再 COPY 源码」 的排法能让依赖编译层跳过——原书 Dockerfile 没做这层优化(教学从简), CI 里编译一次十几分钟时就会肉疼;第 6.5 节给了修正方向32。

6.9 前端镜像的 Wasm 约束再展开(为什么必须是 lighttpd)

这个约束值得单独建档,因为它不是 Docker 问题,是 Wasm 沙箱问题: 编译进 Wasm 的应用「不允许访问 Wasm 运行时之外的文件」——.env 这类环境文件在浏览器沙箱里根本不存在。运行期配置注入这条路被焊死, 只剩构建期硬编码一条路;但硬编码又不甘,于是有了反代方案:33

  构建期:SERVER_URL=/backend 编进 Wasm(假定的后端位置)
  运行期:lighttpd 拿环境变量(BACKEND_HOST/BACKEND_PORT)兑现这个假定
          /backend/*  →  剥掉前缀  →  转发给真后端
          其它路径    →  静态文件;404 → index.html(Yew 路由接管)

  图说:假定留在二进制里,兑现放在容器外——二进制一次构建,到处部署。

lighttpd 配置的四行关键(原书 Code listing 9.3):模块行(mod_proxy 必开); server.error-handler-404 = "/index.html"(没有这行,深链接全灭—— lighttpd 会把 /ticket/42 当磁盘路径找);$HTTP["url"] =~ "^/backend" 块把匹配流量转给 env.BACKEND_HOST:env.BACKEND_PORT;proxy.header 里 map-urlpath 剥掉 /backend 前缀、upgrade => enable 放行 WebSocket 升级 (第 12 章的 WS 靠它)34。

6.10 Compose 文件的逐键速查(七个服务的共同骨架)

把 compose 文件里反复出现的键一次讲清(值都出自原书 docker-compose.yml):35

键 干什么 书中样例
image 用哪个镜像(不自建) mariadb、postgres:14-alpine、redis:7-alpine、自建 ticketing
container_name 容器名(人的视角) ticketing-mariadb…
hostname 容器内主机名(同伴视角) ticketing-backend(前端反代指向它)
restart: always 期望状态的最低表达 除构建外的所有服务
env_file 整个文件注入环境变量 ./.docker-compose.env
environment 逐条覆盖环境变量 前端的 BACKEND_HOST=ticketing-backend
ports 宿主:容器 端口映射 3306:3306、8000:8000、8080:8080
volumes 持久化(匿名卷/bind 卷) 数据库数据 + 初始化脚本
depends_on 启动顺序(不保证就绪!) backend 依赖四基建
healthcheck 自报健康,供调度判断 postgres 的 pg_isready 每秒一探

depends_on 与 healthcheck 的配合(书里藏着的坑): Compose 只按 depends_on 排启动顺序,不等服务就绪——Postgres 容器活着 ≠ 能接受连接; 所以后端真正依赖的是「Postgres 健康检查通过」,而 compose 里这层语义 要靠 condition: service_healthy(原书文件未用,属可精进处)。 「应用自身的 /health 端点」书里也建议了但同样未实现——两处都是骨架 之上的自然加厚点36。

6.11 从「跑起来」到「可交付」:本章未做但点了名的四件事

书里在 Docker 一节没做、但明说了「该做」的四件事,收在一起就是 生产化的 checklist:37

  1. 应用自身的健康端点——「最佳实践是做一个轻量的专用端点报告健康: 应用版本 + 数据库连接状态」(Compose 的 healthcheck 机制在等它);
  2. 镜像漏洞扫描——CI 里推镜像之前过一遍容器层扫描(trivy 类), 与第 3 章 cargo-deny(依赖层)互补;
  3. secret 管理升级——.env 文件只配活在开发机;生产换 secret manager (与第 8 章 object_store 那节的红线同一条);
  4. 标签策略——latest 是演示简化;「至少加版本号标签再叠 latest, 回滚 = 改回上一个标签」(第 6.5 节展开)。

镜像与容器的关系再敲一次(考试级): 「镜像是蓝图 + 全部建材,不可变, 存于 registry;容器是按蓝图装配的运行实例,为单一目的调好,用完即弃。」 docker run 的 --rm 就是「用完即弃」的纪律表达——容器死了不心疼, 数据在卷里,配置在环境变量里,一切可重建38。

6.12 编排衔接段(为 13 篇铺路)

书末「Orchestration」一节用一段话完成换场:「编排 = 自动化地管理容器化 应用的环境:生命周期、健康检查、各组件的伸缩」;它靠什么?—— 还是期望状态:「引入期望状态的概念(前面讨论过),接下来把它展开到 更多细节。」下一篇的 Swarm 与 K8s,就是同一思想在集群规模上的两次实现: Swarm 用 stack 文件 + manager/worker 小步走到多机,K8s 用 20 个 YAML 把每个细节都变成可声明的对象39。

2.1 历史线的完整版(容器出现前的三代部署)

书里用很短的篇幅给了部署史的三代,值得排成一条线:40

第一代:裸金属拷贝。 服务器是「一个角色」而不是一台机器(裸金属 = 没装虚拟化的物理机);部署 = 拷文件。 机器为服务器优化后与开发机差异越来越大,「在我机器上能跑」由此诞生—— 这不是程序员的借口,是异构环境的必然产物。

第二代:虚拟机打包。 把应用连同整个操作系统装进 VM,「能跑就能到处跑」 实现了,但账单很贵:hypervisor 虚拟出整套硬件,客户 OS 与宿主 OS 的 内核在同一块硅上跑两遍;只为隔离一个应用,要养一个完整的操作系统。 「只要它在任何地方能跑,它就必须在任何地方能跑」的需求本身没错, 错的是买法。

第三代:namespaces + Docker。 Linux 内核早就给了答案——namespaces 把进程的存储、网络、进程表隔离开,同时复用宿主内核;「不再有完全功能 的第二个操作系统」。但 namespaces 难用到「通常只有专家敢碰」; Docker 的历史贡献就是给 namespaces 做了人人都会的接口—— 书里那句「这不是 Docker 发明的,是它的普及之功」值得当作技术史常识记住41。

3.1 五个概念的成对关系(考试级的三组对照)

镜像 vs 容器: 设计图 vs 按图装配的实例;不可变以保值 vs 易造以值得丢弃; 「镜像存于 registry 供复用,容器 from 镜像 instantiate」。

层 vs 镜像: 镜像是层的堆叠;每层是「一次改动」的不可变快照, 底层永远不动;层数越多镜像越大——所以有了两条纪律:合并 RUN 命令 少造层,多阶段构建把「只用于构建的层」全部扔掉。

ARG vs ENV: ARG 只在构建期存在(多阶段里每段要重定义,书里的坑); ENV 进了镜像、运行期还在,能构建时传也能运行时覆盖,.env 文件可以 成批喂42。

容器 vs 卷: 容器用完即弃,数据不能跟着死;卷是独立于容器生命周期的 持久化抽象——「容器挂了,卷还在;下一个容器接着用」。bind 卷(数据库 文件留在宿主)与命名卷(由 Docker 管理)都服务于同一件事:可丢弃的 计算 + 不丢弃的数据43。

期望状态 vs 实际状态: Compose/Swarm/K8s 的共同心跳——你声明 「我想看到它 running」,系统持续对账;偏离就拉回(restart: always、 healthcheck 杀起重建)。第 13 篇会看到这个思想被复制到集群规模, 一行字都没改44。

5.1 后端 Dockerfile 的逐行账(带「为什么」的完整版)

  1  ARG BASE_IMAGE=rustlang/rust:nightly        # 将来换镜像只改这一行(最佳实践:会变的前置)
  2  FROM ${BASE_IMAGE} AS base                  # nightly 做基础;起别名供后面 FROM 引用
  3  WORKDIR /src                                # 之后命令的cwd
  4-11 COPY ×8                                  # 五个 workspace 成员 + 根 src/tests + 清单;
                                                # 「与 Dockerfile 同目录」是 docker build 句点的契约
  13 FROM base AS build-server                  # 新阶段基于 base:构建失败不用重拷源码(层缓存)
  14 RUN cargo build --release                  # 生产构建:优化、去调试符号
  16 FROM debian:bookworm-slim as prod          # 运行镜像:瘦到只剩二进制 + 一个 OpenSSL
  17 RUN apt-get update && apt install -y openssl  # 两条合一 = 少造一层
  18 COPY --from=build-server /src/target/release/ticketing /bin/   # 只搬产物;构建垃圾不进产房
  19 ENTRYPOINT ["/bin/ticketing"]              # 容器启动命令

  图说:三段分工 = 依赖层(base)/ 编译层(build-server)/ 运行层(prod);
  前两段在出产后全部丢弃 —— 「multi-stage build:先造一个只为构建的容器,事后丢弃」。

书里对 nightly 的一句坦白: 基础镜像选 nightly 是因为那时某些 Wasm 工具链要它;版本号是「写书时的状态」——原书自己提醒「出版时可能 已过时,代码仓库里有更新版」。所有具体版本号都当快照读45。

5.2 前端 Dockerfile 的逐行账(与后端的四处不同)

  1-2 ARG BASE_IMAGE / SERVER_URL               # 两个构建参数:基础镜像 + 后端地址假定
  4  ENV SRCDIR=/src                             # WORKDIR 的值也做成变量
  10 FROM base AS build-client                   # 第二阶段另起别名
  11-12 ARG SERVER_URL / ENV SERVER_URL=…        # ★坑:ARG 不跨阶段,必须重新声明再转 ENV
  13-17 RUN apt 装编译链 && rustup stable && wasm32 目标 && trunk
  19 RUN trunk build --release                   # 产出 dist/ 静态文件
  21 FROM jitesoft/lighttpd as prod              # ★不同:运行镜像不是 slim OS,是现成的静态服务器
  22 ENV PORT=8080
  23-24 COPY dist/ → /var/www/html/ + COPY lighttpd.conf
  (无 ENTRYPOINT —— lighttpd 镜像自带启动命令)

  图说:前端镜像没有「应用进程」,它是一叠静态文件 + 一个会反代的 Web 服务器。

「应用不服务自己」这句话值得背: 「我们的前端应用只是一堆静态文件, 仅此而已;它不服务自己——lighttpd 替它服务,还替它把 /backend 指到 真后端。」前后端镜像的形制差异(有 ENTRYPOINT 的二进制 vs 静态文件 + 服务器)正是「两端部署与伸缩方式不同」这句架构判断的落地46。

6.13 镜像构建与运行的命令全账(含 CI 的四个 action)

本地命令的完整参数语义(原书 docker build/run 两行的逐词):47

  docker build -t viktordaroczi/ticketing-backend .
  #   -t      标签 = 用户名/仓库名(将来 push 到 Docker Hub 的地址就是它)
  #   句点    构建上下文 = Dockerfile 与源码所在目录;
  #           COPY 能看到的文件全在这个句点里(.dockerignore 管黑名单)
  docker build -f Dockerfile.frontend -t …/ticketing-frontend .
  #   -f      显式指定 Dockerfile(默认只认 ./Dockerfile)

  docker run -d --rm -p 8000:8000 --name ticketing-backend \
             --env-file .docker.env viktordaroczi/ticketing-backend env
  #   -d        后台(detach),命令行立刻还给你
  #   --rm      停了即删 —— 容器用完即弃,不手工养
  #   -p H:C    宿主:容器 端口映射;无此项容器内部端口不可达
  #   --name    容器名(列表里好认;不起就随机名)
  #   --env-file 整文件注入环境变量;-e KEY=VAL 单条注入(前端镜像的用法)
  #   尾部 env  镜像起后执行的第一条命令(ENTRYPOINT 之外再叠)

  图说:build 造的是「类」,run 造的是「实例」;--rm 是「实例不养老」的纪律。

CI 工作流的四个 action 各管一段(uses: 引用的都是官方现成品):48

步骤 action 干什么
Checkout actions/checkout@v4 把仓库拉到 CI 机器
登录 docker/login-action@v3 用 secrets.DOCKERHUB_USERNAME / _TOKEN 登录 registry
构建环境 docker/setup-buildx-action@v3 装 Buildx(多架构/缓存优化的构建器)
构建并推 docker/build-push-action@v5 context=.、file=./Dockerfile(或 .frontend)、push=true、tags=…:latest

红字再引: 「with 的参数就是各 action 期待的输入;比如把 Docker Hub 凭据 传给构建。记住:项目文件里永不硬编码任何敏感数据」——凭据走 GitHub Secrets,打日志也会自动打码49。

6.14 Compose 的更多机制(healthcheck、卷、依赖的三层语义)

healthcheck 五个键(test/interval/timeout/retries,原书 Postgres 样例):test 是探针命令(Postgres 自带 pg_isready);interval: 1s 每秒一探;timeout: 5s 单次超时;retries: 10 失败上限——超过就杀容器、 起新实例(期望状态的对账动作)。「怎么判断健康由你定;最佳实践是应用 自带轻量 /health 端点报版本 + 数据库连通性」50。

卷的两形态与取舍: bind 卷(数据库文件留在宿主目录,人能直接摸)、 命名卷(由 Docker 管理,人只引用名字)。书里 MariaDB/Postgres 用了双卷: 匿名卷存数据 + bind 卷送初始化 SQL——「容器可以读写它们,但即使失败 也毁不掉卷」。数据的安全边界 = 卷的生命周期独立于容器51。

depends_on 的三层语义(超书注记,机制仍属 Compose 通用知识): 默认只排启动顺序;condition: service_started(等它启动)、 service_healthy(等健康检查过)、service_completed_successfully (等一次性任务成功)。原书文件停在默认档,生产应至少对数据库升到 service_healthy——这正是 healthcheck 一节存在的意义52。

6.15 「读 compose 文件 = 读架构」的完整示范(七服务的依赖图)

            ┌──────────────┐
            │   Adminer    │ 8088(可选,观察数据)
            └──────┬───────┘
                   │ 连
   ┌───────────┐   │    ┌────────────┐
   │  MariaDB  │◄──┴───►│  backend   │ 8000
   │  (主库)   │        │  (Axum API)│
   └───────────┘        └─────┬──────┘
   ┌───────────┐              │ 反代 /backend
   │ PostgreSQL│──────────────┤
   │ (fang调度)│              ▼
   └───────────┘        ┌────────────┐
   ┌───────────┐        │  frontend  │ 8080(唯一门面)
   │   Redis   │────────┤ (lighttpd)│
   │ (队列缓存)│        └────────────┘
   └───────────┘
   ┌───────────┐
   │  MailHog  │ 1025/8025(SMTP + 网页)
   └───────────┘

  图说:backend 是枢纽 —— 四路基建进、一路前端出;
  compose 的 depends_on 就是这张图的边,ports 就是对外开放的门。

这张图的读法,就是第 6 篇(后端定型)到第 11 篇(并行通信)全部内容 的 30 秒版——「配置即架构」的实例:每一条边(依赖)都能在文件里 找到一行,每一扇门(端口)都能找到映射53。

6.16 本章主线的最终复盘(从裸金属到一条命令)

  裸金属时代      拷文件;「在我机器上能跑」成灾
    → VM 时代     打包即 OS;内核跑两遍,太贵
      → Docker    namespaces 复用宿主内核;镜像不可变、容器用完即弃
        → 多阶段构建  编译垃圾不进产房(rust:nightly 构建 → debian-slim 运行)
          → Compose   七服务一张网;services/desired state/healthcheck/volumes
            → CI      push 即构建推送;徽章对外报健康
              → 编排(Swarm/K8s,见 13 篇)  期望状态思想原样放大到集群

  图说:每一级都在重复同一句话 —— 把「环境」从人的记忆里搬到文件里。

这一章对全书的意义: 第 3–8 章造的应用,到此成为一件可以在任何 机器上重建的制品——「它在一处能跑,处处能跑」不再是愿望,是 docker compose up 的副产品。而「环境即文件」也正是第 13 篇编排的 入场券:Swarm 与 K8s 读的还是文件,只是文件的粒度从「一个栈」细化到 「每个对象」54。

6.17 前端构建链在镜像里的完整还原(为什么装那一串系统包)

前端 build-client 阶段那行 apt install build-essential libssl-dev cmake clang pkg-config openssl 不是摆设,逐个给理由(依据原书清单与 Rust 生态的通行事实):build-essential = C 工具链(cargo 的 build scripts 要编译少量 C);libssl-dev/openssl = rustls 之外的 TLS 选项与链接; cmake/clang/pkg-config = 某些 crate 的本地构建依赖(openssl-sys 是 典型)。trunk 则需要 stable 工具链 + wasm32 目标(nightly 基底镜像里 另外装)。「装好这一套,trunk build --release 才产得出 dist/」—— 前端镜像的构建层比后端重得多,这正是「两端分别构建、分别伸缩」的 微观证据55。

SERVER_URL 的三次转手(一个 ARG 的旅程):Dockerfile 顶部声明 → 跨阶段重新声明(不重新声明就取不到,书里的坑)→ 转 ENV 进 build-client 环境 → trunk 构建时编进 Wasm。每一手都「交接签字」,少一手就在运行期 变成默认值56。

6.18 与 13 篇的交接清单(Docker 章交给编排章的四件行李)

  1. 七服务的 compose 文件——Swarm 直接复用(同规格),K8s 靠 Kompose 翻译;
  2. 期望状态的心法——restart: always 是它在单机的样子;deploy 块、 liveness 探针是它在集群的样子;
  3. secrets 的种子——.env 时代结束于 Swarm 的 secret 挂载, 再升级为 K8s 的可编程 secret 管理;
  4. CI 的镜像产物——两条流水线推上 Docker Hub 的 :latest, 是两种编排器的共同食材。57

7. 本章一页复习(容器化上半场的最终形态)

  裸金属 → VM(内核两遍) → Docker(namespaces + 复用)
  概念五件:镜像(不可变蓝图)/ 容器(用完即弃)/ 层(少造)/ ARG / ENV
  两个 Dockerfile:后端多阶段(rust 构建 → slim 运行)
                  前端多阶段(Wasm 无 env → lighttpd 反代 /backend)
  命令两板斧:docker build -t … . / docker run -d --rm -p H:C --env-file …
  CI 四步:checkout → login(secrets)→ buildx → build-push
  Compose 四概念:services / desired state / healthchecks / volumes
  七服务:MariaDB + Adminer + PostgreSQL + Redis + MailHog + backend + frontend

  图说:每条竖线都通向第 13 篇 —— Swarm 与 K8s 只是把这份文件换个规模重读。

交接清单: compose 文件直接给 Swarm 复用;expect-state 心法在 deploy 块与探针里续命;secrets 从 env_file 进化到挂载再进化到 K8s 对象;CI 的 :latest 是两种编排器的共同食材。58

19. 本篇卷尾(一句话版)

容器化章的全部贡献,是把「环境」从人的记忆里搬进文件:Dockerfile 管 一个组件,compose 管整套系统,CI 管文件产物的持续更新—— 「它在一处能跑,处处能跑」从此是一句陈述句,不是一句愿望。59

20. 本篇卷尾补记(compose 即架构的读法)

读别人系统的 compose/k8s 文件,是摸架构最快的办法;写自己的, 是检验架构清不清楚的最狠办法——每条 depends_on 是一条边、每个 published 端口是一扇门。本章的七服务文件,不妨当作这张读法的练习题。60


  1. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:13,搜「not a machine but a role」)。 ↩

  2. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:16,搜「work on my machine」与「not an acceptable excuse」)。 ↩

  3. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:28,搜「hypervisor」;:19,搜「runs anywhere」)。 ↩

  4. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:28,搜「kernel, are running twice」;:31,搜「namespaces offer isolation」)。 ↩↩↩

  5. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:34,搜「simplified interface」)。 ↩

  6. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:4,搜「standardized and uniform format」)。 ↩

  7. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:40,搜「blueprint」)、:50(搜「Layers」)、:53(搜「Arguments」)、:58(搜「Environment variables」)。 ↩↩↩

  8. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:44,搜「immutable」)、:47(搜「ephemeral」)。 ↩

  9. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:63,搜「Dockerfile」)。 ↩

  10. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:143,搜「multi-stage」)、:152(搜「do not start over」)。 ↩↩↩

  11. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:158,搜「nothing else」)、:163(搜「combining commands」)。 ↩

  12. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:276,搜「nothing more, nothing less」)。 ↩↩↩

  13. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:260,搜「cannot read environment files」)、:387(搜「without having to recompile」)。 ↩

  14. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:402,搜「docker build」)、:426(搜「docker run」)、:435(搜「disposable」)、:441(搜「env-file」)。 ↩

  15. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:518,搜「login-action」)、:545(搜「build-push-action」)、:658(搜「never hardcode any sensitive data」)。 ↩

  16. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:696,搜「Compose」)、:941(搜「services」)。 ↩↩↩

  17. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:950,搜「seven services」)、:919(搜「BACKEND_HOST」)。 ↩

  18. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:1049,搜「automating lifecycle management」)。 ↩

  19. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:703,搜「name: ticketing」)、:709(搜「mariadb」)、:718(搜「init-file」)、:748(搜「adminer」)、:766(搜「postgres」)、:832(搜「redis」)、:844(搜「mailhog」)、:865(搜「ticketing-backend」)、:901(搜「ticketing-frontend」)。 ↩↩

  20. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:709,搜「mariadb」)、:742(搜「3306:3306」)。 ↩

  21. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:959,搜「convenience database client」)。 ↩

  22. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:805,搜「healthcheck」)、:829(搜「14-alpine」)。 ↩

  23. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:841,搜「save 60 1」)。 ↩

  24. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:853,搜「1025:1025」)。 ↩

  25. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:871,搜「depends_on」)、:868(搜「hostname」)。 ↩

  26. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:919,搜「BACKEND_HOST=ticketing-backend」)。 ↩

  27. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:70,搜「never want to include sensitive information」)。「.dockerignore 黑名单」这层机制是通用知识(补充,不在书里)。 ↩

  28. 补充(不在书里,来自通用知识):层缓存逐层失效与「manifest 先拷」分层法是 Docker 构建的通行做法;cargo-chef 是 Rust 社区实现该模式的工具。 ↩

  29. 补充(不在书里,来自通用知识)::latest 覆盖式标签的回滚问题与「版本号 + latest 双标签」策略是发布管理的通行做法。 ↩

  30. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:1035,搜「Composerize」与「review the generated file」)。 ↩↩

  31. 指令语义为 Docker 通用知识;书中用法见 text/74-fm-containerization-with-docker.txt:76 起各代码清单。 ↩

  32. 层缓存机制为 Docker 通用知识(补充,不在书里);原书 Dockerfile 见 text/74-fm-containerization-with-docker.txt:76。 ↩

  33. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:260,搜「cannot read environment files」)。 ↩

  34. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:284,搜「mod_proxy」)、:320(搜「error-handler-404」)、:323(搜「proxy.server」)、:344(搜「upgrade」)。 ↩

  35. 键与值见原书 docker-compose.yml(text/74-fm-containerization-with-docker.txt:703 起)。 ↩

  36. 「depends_on 不保就绪」「建议自建 /health」为我们的工程注记;健康检查机制本身见 text/74-fm-containerization-with-docker.txt:992。 ↩

  37. 健康端点建议见 text/74-fm-containerization-with-docker.txt:995;secret 红线见 text/65-fm-ticket-attachments.txt:366;标签策略见本书第 6.5 节(补充,不在原书)。 ↩

  38. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:44,搜「immutable」)、:47(搜「ephemeral」)、:435(搜「disposable」)。 ↩

  39. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:1049,搜「automating lifecycle」)、:1052(搜「desired state」)。 ↩

  40. 历史线见 text/74-fm-containerization-with-docker.txt:13(服务器是角色)、:16(works on my machine)、:22(VM 开销)。 ↩

  41. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:28,搜「kernel, are running twice」)、:34(搜「specialists」)。 ↩

  42. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:50,搜「Layers」)、:53(搜「Arguments」)、:58(搜「Environment variables」)。 ↩

  43. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:1001,搜「outlive any container」)。 ↩

  44. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:986,搜「desired state」)。 ↩

  45. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:76,搜「BASE_IMAGE」)、:1508(搜「might be well outdated」)。 ↩

  46. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:276,搜「nothing more, nothing less」)。 ↩

  47. 命令与参数语义见 text/74-fm-containerization-with-docker.txt:402、:426。 ↩

  48. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:518,搜「login-action」)、:533(搜「setup-buildx-action」)、:545(搜「build-push-action」)。 ↩

  49. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:658,搜「never hardcode」)。 ↩

  50. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:808,搜「pg_isready」)、:995(搜「dedicated lightweight endpoint」)。 ↩

  51. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:1001,搜「outlive any container」)、:1004(搜「bind volumes」)。 ↩

  52. condition 三档为 Compose 通用知识(补充,不在原书);原书文件见 text/74-fm-containerization-with-docker.txt:865。 ↩

  53. 依赖图为我们对原书 docker-compose.yml 的图示化;文件见 text/74-fm-containerization-with-docker.txt:703 起。 ↩

  54. 意义为我们的归纳;「if it runs anywhere, it must run everywhere」见 text/74-fm-containerization-with-docker.txt:19。 ↩

  55. 包清单见 text/74-fm-containerization-with-docker.txt:220;各包的构建角色为 Rust 生态通行知识(补充,不在书里)。 ↩

  56. 出处:「Containerization with Docker」(text/74-fm-containerization-with-docker.txt:263,搜「propagate this argument」)、:268(搜「environment variable from it」)。 ↩

  57. 交接清单为我们的对账;compose 文件见 text/74-fm-containerization-with-docker.txt:703。 ↩

  58. 复习图与交接为我们的归纳;衔接见 13-orchestration.md §3.4–§3.5。 ↩

  59. 卷尾为我们的归纳;「if it runs anywhere」见 text/74-fm-containerization-with-docker.txt:19。 ↩

  60. 读法见本篇 §6.15;七服务文件见原书 text/74-fm-containerization-with-docker.txt:703。 ↩