写在前面:前几节课搭了 React 前端、NestJS 后端、Nginx 代理,一个全栈项目零件齐了。但怎么把它们打包部署?答案是 Docker 。今天的课堂从一个
console.log('hello Docker!')开始,讲到 Dockerfile 标准配方、镜像的 build/push/pull 生命周期,再到 docker-compose 编排多容器------最后用 Milvus 向量数据库 做了实战:一个 docker-compose 文件拉起三个容器,各司其职。有趣的是,readme 用蜜雪冰城的 SOP 来类比 Dockerfile,但今天我要换个更宏大的视角------房地产开发。图纸、楼盘、小区物业,一个不少。以下所有代码和概念均来自课堂真实文件。
一、Dockerfile:施工图纸
图纸就是图纸
readme 原话:
"蜜雪冰城标准操作手册 SOP,写清'先加奶茶、再加奶、放三勺糖、摇匀',任何人照着做,出来味道都一样,就成了连锁店。"
"DOCKERFILE 是一个文本配方文件,里面写着一步步'做菜'的指令,Docker 照着做它就能自动做出一个一模一样的 Docker 镜像,运行。"
这个比喻精准到可怕。但我想换一个更"工程"的视角------Dockerfile 就是施工图纸。
施工图纸上写的是什么?
sql
1. 打地基(FROM node:18-alpine)
2. 砌墙(WORKDIR /app)
3. 装管线(COPY package*.json ./)
4. 进设备(RUN npm install)
5. 搬家具(COPY . .)
6. 通水通电(EXPOSE 3000)
7. 交付入住(CMD ["node", "index.js"])
任何施工队拿到这份图纸,建出来的楼一模一样------北京建的和上海建的一样,今天建的和明年建的也一样。这就是 Dockerfile 的核心价值:可重复的构建,消除环境差异。
Hello Docker:最简施工图
课堂的 index.js 只有一行:
javascript
console.log(`hello Docker! 我跑在容器里了`)
这行代码本身没什么好说的------但它在容器里运行,意味着背后有一份 Dockerfile 把 Node.js 运行环境、这行代码、启动命令打包成了一个镜像。你不需要在本机装 Node.js,不需要配环境变量,docker run 一下,它就跑起来了。
这就是容器化的威力------环境跟着代码走,不是代码去适应环境。
二、镜像生命周期:施工、挂牌、交房
readme 记录了镜像的完整生命流程:
"构建镜像:docker build -t my-hello . → docker login → docker push → docker pull。Dockerfile 是发布项目的标准项目之一。"
翻译成房地产语言:
| Docker 命令 | 房地产类比 | 干了什么 |
|---|---|---|
docker build -t my-hello . |
施工建楼 | 按图纸(Dockerfile)建楼(镜像) |
docker login |
开发商认证 | 登录镜像仓库 |
docker push |
挂牌上市 | 把镜像推到 Docker Hub / 私有仓库 |
docker pull |
买房 | 从仓库拉取镜像到本地 |
docker run |
入住 | 从镜像启动一个容器 |
施工:docker build
bash
docker build -t my-hello .
-t my-hello:给楼起个名字.:施工图纸在哪?当前目录的 Dockerfile
施工队(Docker 引擎)读图纸,一步步执行------装系统、装依赖、拷代码、设端口、定启动命令。建完之后,你得到一个镜像------一栋建好的、随时可以交付的大楼。
挂牌:docker push
bash
docker login # 开发商认证
docker push my-hello # 挂牌到 Docker Hub
镜像建好了,只存在你本机------别人用不了。docker push 把镜像推到镜像仓库(Docker Hub 或私有仓库),相当于开发商把楼盘信息挂到房产交易平台上,全世界都能看到、都能下载。
readme 特别强调了一句:
"Dockerfile 是发布项目的标准项目之一。"
以后项目的交付物不止有源代码、文档,还有 Dockerfile------有了图纸,任何人都能复刻你的环境。
买房:docker pull
bash
docker pull my-hello
加盟商(其他开发者或服务器)不需要自己施工,直接从仓库拉取镜像------拎包入住。镜像里包含了运行所需的一切:操作系统、运行时、依赖、代码。不需要装 Node.js,不需要 npm install,不需要配环境变量------一个镜像,到处运行。
三、Docker Compose:小区物业
单个容器好管------一栋楼嘛。但现实项目往往需要多个容器协同。比如课堂提到的全栈项目:
"todos 全栈项目:前端 react + ts + zustand,后端 nest.js Todo Module,nginx 80→3000 跨域。"
至少三个服务:前端、后端、Nginx。手动 docker run 三个容器?可以,但太累。而且启动顺序、网络连接、数据卷挂载全得手动管。
Docker Compose 就是小区物业------一个文件管好所有楼。
readme 对 docker-compose 的定位:
"安装 Milvus:由多个 image 组成的。docker-compose 文件,编排多个 image 工作流。"
一个 YAML 文件,定义所有服务、网络、数据卷。docker-compose up 一条命令,整个小区全部上线。
四、实战:Milvus 向量数据库------一个三栋楼的微型小区
课堂的 milvus-standalone-docker-compose.yml 是一个真实的 docker-compose 文件,拉起 Milvus 向量数据库。Milvus 不是单容器应用------它由三个服务组成。
小区全景图
yaml
version: '3.5'
services:
etcd: # 楼栋 A:物业档案室
minio: # 楼栋 B:地下仓库
standalone: # 楼栋 C:主楼
三栋楼,各司其职。让我们逐栋看。
楼栋 A:etcd------物业档案室
yaml
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.25
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
etcd 是一个分布式键值存储------Milvus 用它存元数据。就像小区的物业档案室,记录着"几号楼住了谁、车位分配情况、业主信息"。
关键配置解析:
| 配置 | 含义 | 房地产类比 |
|---|---|---|
image: quay.io/coreos/etcd:v3.5.25 |
用哪个图纸建 | 开发商选的施工图纸版本 |
environment |
楼内配置 | 每层楼的功能设置 |
ETCD_QUOTA_BACKEND_BYTES: 4294967296 |
4GB 存储配额 | 档案室最多放多少档案 |
volumes |
数据持久化 | 档案柜------楼拆了档案还在 |
command |
启动命令 | 物业上班要做什么 |
healthcheck |
健康检查 | 每月消防检查 |
interval: 30s |
每 30 秒查一次 | 检查频率 |
volumes 这行特别值得注意:
yaml
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
${DOCKER_VOLUME_DIRECTORY:-.} 是 shell 语法------如果设了环境变量 DOCKER_VOLUME_DIRECTORY 就用它,没设就用 .(当前目录)。数据存在宿主机的磁盘上,容器删了数据还在------楼拆了,档案柜搬到新楼继续用。
楼栋 B:minio------地下仓库
yaml
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2024-05-28T17-19-04Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 30s
timeout: 20s
retries: 3
MinIO 是一个 S3 兼容的对象存储------Milvus 用它存向量数据和索引文件。就像小区的地下仓库,存大件物品。
它开放了两个端口:
yaml
ports:
- "9001:9001" # 管理控制台
- "9000:9000" # API 接口
左边的 9001 是宿主机端口,右边的 9001 是容器内端口。宿主机:容器------你可以从浏览器访问 localhost:9001 打开 MinIO 的管理控制台。
账号密码都是 minioadmin------课堂环境用的默认值,生产环境绝对不能这样。
楼栋 C:milvus-standalone------主楼
yaml
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v3.0.0
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
MINIO_REGION: us-east-1
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
主楼是整个小区的核心------Milvus 向量数据库本体。
它怎么找到另外两栋楼的?靠环境变量:
yaml
environment:
ETCD_ENDPOINTS: etcd:2379 # 档案室在哪
MINIO_ADDRESS: minio:9000 # 仓库在哪
etcd:2379 和 minio:9000 用的是服务名(容器名)作为主机名------Docker Compose 自动创建了一个网络,三栋楼在同一个小区(网络)里,可以用楼栋名互相串门。
三栋楼的协作关系
最后两行是关键:
yaml
depends_on:
- "etcd"
- "minio"
depends_on 定义了启动顺序------主楼必须在档案室和仓库之后启动。你不能物业档案室还没建好就让人住进来。
但注意:depends_on 只保证启动顺序 ,不保证服务就绪 。etcd 容器启动了,但 etcd 服务可能还在初始化中。这就是 healthcheck 存在的意义------主楼的 start_period: 90s 给了 90 秒的宽限期,让它等另外两栋楼的服务真正就绪。
整个协作流程:
markdown
docker-compose up
↓
etcd 容器启动 ← 档案室先建
minio 容器启动 ← 仓库同时建
↓ healthcheck 通过
milvus 容器启动 ← 主楼开工,连上 etcd 和 minio
↓ start_period: 90s 等待
milvus 服务就绪 ← 19530 端口对外服务
小区网络
文件最后一行:
yaml
networks:
default:
name: milvus
三栋楼在同一个叫 milvus 的网络里。这个网络是 Docker Compose 自动创建的------容器之间用服务名通信(etcd:2379、minio:9000),不需要知道对方的 IP。物业内部通讯,靠楼栋名就行。
五、从单楼到小区:Docker 的两层抽象
今天的文件恰好展示了 Docker 的两层抽象:
第一层:Dockerfile → 单个镜像
readme 说的:
"DOCKERFILE 是一个文本配方文件,里面写着一步步'做菜'的指令。"
一个 Dockerfile 产出一个镜像------一栋楼。适合简单应用,比如课堂的 index.js,一个 Node.js 镜像就够了。
第二层:Docker Compose → 多容器编排
readme 说的:
"docker-compose 文件,编排多个 image 工作流。"
一个 docker-compose.yml 编排多个镜像------一个小区。适合复杂应用,比如 Milvus(三个服务协同)或全栈项目(前端 + 后端 + Nginx)。
| 层级 | 文件 | 产出 | 适用场景 |
|---|---|---|---|
| 第一层 | Dockerfile | 单个镜像 | 单服务应用 |
| 第二层 | docker-compose.yml | 多容器协作 | 多服务系统 |
现实项目几乎都是第二层------很少有只用一个容器的生产系统。Docker Compose 是连接"单个容器"到"完整系统"的关键桥梁。
六、回到全栈项目:Todo 的"楼盘化"
readme 最后提到了全栈项目:
"todos 全栈项目:前端 react + ts + zustand,后端 nest.js Todo Module,nginx 80→3000 跨域。"
现在回头看这个项目------如果用 Docker 部署,就是三个容器:
scss
docker-compose.yml
├── frontend (React + TS + Zustand 镜像)
├── backend (NestJS 镜像)
└── nginx (Nginx 镜像,80→3000 反向代理)
Nginx 容器开放 80 端口对外,内部通过 Docker 网络把 /api 请求转发到 backend 容器的 3000 端口------跨域问题在容器层面自然解决,不需要 Vite 代理了。前端容器跑静态文件,Nginx 容器统一代理。
每个容器都有自己的 Dockerfile------前端一份、后端一份、Nginx 一份。docker-compose.yml 把它们组装成一个完整系统。一张图纸建一栋楼,一个 compose 文件管一个小区。
这就是 readme 说的"Dockerfile 是发布项目的标准项目之一"的完整含义------不只是给你一个镜像,而是给你一套可复现的部署方案 。别人拿到你的 docker-compose.yml,docker-compose up 一条命令,整个系统就跑起来了。
七、Milvus 配置文件里的"地产开发"学问
回头看整个 milvus-standalone-docker-compose.yml,每个配置项都有对应的"地产"含义:
| 配置项 | 值 | 房地产含义 |
|---|---|---|
version: '3.5' |
compose 文件版本 | 小区规划规范版本 |
container_name |
milvus-etcd / milvus-minio / milvus-standalone | 每栋楼的门牌号 |
image |
etcd:v3.5.25 / minio:... / milvus:v3.0.0 | 施工图纸版本 |
environment |
ETCD_、MINIO_ | 楼内功能配置 |
ports |
"19530:19530"、"9001:9001" | 对外开门------左宿主右容器 |
volumes |
./volumes/xxx:/xxx | 地下室------楼拆了数据还在 |
command |
启动命令 | 物业上班第一件事 |
healthcheck |
test/interval/timeout/retries | 消防检查制度 |
start_period |
90s | 装修宽限期 |
depends_on |
etcd + minio | 前置工程------先建档案室再建主楼 |
networks |
milvus | 小区内部道路 |
一个 YAML 文件,56 行,把三个容器的镜像版本、环境变量、端口映射、数据持久化、健康检查、启动顺序、网络拓扑全部定义清楚。这不是运维,是架构设计。
PS:以前觉得 Docker 难,换个角度想------它就是房地产开发。Dockerfile 是图纸,镜像是楼盘,容器是有人住的房子,docker-compose 是小区物业。docker-compose up 一条命令,整个小区拔地而起。你还在手动 docker run 管单栋楼?