如何理解 docker-compose.yml
可以把 Docker Compose 想成:一份「一键启动清单」。不用分别 docker run 三次,写在一个 YAML 里,一条命令就能同时拉起 PostgreSQL、Redis 和 Node API。
bash
docker compose up -d # 启动
docker compose down # 停止并删除容器(pgdata 卷里的数据还在)
docker compose ps # 看状态(含 healthy)
docker compose logs postgres # 看某个服务的日志
整体结构
text
docker-compose.yml
├── services ← 要跑哪些容器
│ ├── postgres ← 数据库
│ ├── redis ← 缓存
│ └── api ← Node API
└── volumes ← 声明持久化存储(给 postgres 用)
Compose 会自动给三个容器建一张默认虚拟网络,容器之间用服务名 互访(如 postgres:5432、redis:6379)。api 在容器内连 PG/Redis 用服务名;在宿主机 上执行 migrate 或本地 npm run dev 时,仍用 localhost + 下面配置的映射端口。
服务 1:postgres
yaml
services:
postgres:
image: postgres:18
container_name: nodejs-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 你的密码 # 与 .env 里 DATABASE_URL 密码一致
POSTGRES_DB: nodejs_study
volumes:
- pgdata:/var/lib/postgresql # Volume:容器删了数据还在
ports:
- "5433:5432" # 宿主机可连,方便 migrate
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d nodejs_study"]
interval: 5s
timeout: 5s
retries: 5
| 字段 | 值 | 含义 |
|---|---|---|
| image | postgres:18 | 官方 PostgreSQL 18,不用自己写 Dockerfile |
| container_name | nodejs-postgres | 固定容器名,Docker Desktop 里好认 |
| environment | 用户名/密码/库名 | 仅首次启动 初始化库;密码需与 .env 里 DATABASE_URL 一致 |
| volumes | pgdata:/var/lib/postgresql/ | 数据存命名卷,删容器不丢 |
| ports | 5433:5432 | 宿主机 5433 → 容器 5432;本机连 localhost:5433 |
| healthcheck | pg_isready ... | 定期探测数据库是否就绪 |
连接串(Node 在宿主机):
text
postgresql://postgres:你的密码@localhost:5433/nodejs_study
端口用 5433 而非 5432,是为避免与本机已安装的 PostgreSQL 冲突。
healthcheck 命令拆解:
yaml
test: ["CMD-SHELL", "pg_isready -U postgres -d nodejs_study"]
| 部分 | 含义 |
|---|---|
| CMD-SHELL | 用 shell 执行后面字符串 |
| pg_isready | PostgreSQL 自带工具,检查能否连接 |
| -U postgres | 用用户 postgres |
| -d nodejs_study | 检查库 nodejs_study 是否就绪 |
探测成功 → healthy;失败 → 继续重试,多次失败后 → unhealthy。
healthcheck 时间参数(Redis 段配置相同,不再重复):
| 参数 | 含义 |
|---|---|
| interval: 5s | 每 5 秒测一次 |
| timeout: 5s | 单次命令最多等 5 秒 |
| retries: 5 | 连续多次失败后标记为 unhealthy |
healthcheck 不是必填;不写也能跑。api 服务已配置 depends_on: condition: service_healthy,会等 postgres、redis 变为 healthy 后再启动------避免 API 连上尚未就绪的数据库。
服务 2:redis
yaml
redis:
image: redis:7
container_name: nodejs-redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
| 字段 | 含义 |
|---|---|
| image: redis:7 | 官方 Redis 7 |
| ports: 6379:6379 | 宿主机 6379 直连容器 6379 |
| 无 volumes | 开发够用;删容器后缓存丢失(Redis 多为临时数据) |
| healthcheck | redis-cli ping,确认 Redis 已响应 |
连接串:
text
redis://localhost:6379
healthcheck 命令拆解:
yaml
test: ["CMD", "redis-cli", "ping"]
| 部分 | 含义 |
|---|---|
| CMD | 直接执行程序,不经过 shell |
| redis-cli | Redis 官方客户端 |
| ping | 发 PING,正常应返回 PONG |
时间参数含义见 Postgres 一节。Redis 启动快,healthcheck 这里主要是方便 docker compose ps 看状态;Postgres 的 healthcheck 对「等库就绪再 migrate」更关键。
服务 3:api
yaml
api:
build: .
container_name: nodejs-api
ports:
- "8080:8080"
environment:
DATABASE_URL: postgresql://postgres:你的密码@postgres:5432/nodejs_study?schema=public
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
| 字段 | 作用 |
|---|---|
| build: . | 用项目根目录 Dockerfile 构建镜像 |
| postgres:5432 | Compose 内部 DNS,解析到 postgres 容器 |
| redis:6379 | 同上,解析到 redis 容器 |
| ${JWT_SECRET} | 从项目根 .env 读取,Compose 自动做变量替换 |
| depends_on + healthy | 等 PG、Redis 就绪后再启动 API |
密码与 postgres 服务的 POSTGRES_PASSWORD 保持一致。
服务 4:worker
yaml
worker:
build: .
container_name: nodejs-worker
command: npm run worker:email
environment:
REDIS_URL: "redis://redis:6379"
depends_on:
redis:
condition: service_healthy
| 字段 | 作用 |
|---|---|
| build: . | 与 api 同一 Dockerfile |
| command: ... | 覆盖 Dockerfile 的 CMD,跑 Worker 而不是 API |
| REDIS_URL | 用服务名 redis,不是 localhost |
| 无 ports | Worker 不对外暴露端口 |
卷声明:volumes
yaml
volumes:
pgdata: # 声明命名卷
这里只是声明 命名卷 pgdata,真正挂载在 postgres 的 volumes 里。容器删了,数据还在 Docker 管理的存储里。首次 up 会初始化空库;pgdata 已有数据时再次启动不会重置。
和 docker run 的对应关系
这份 Compose 文件大致等价于:
bash
# PostgreSQL
docker run -d --name nodejs-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=... \
-e POSTGRES_DB=nodejs_study \
-v pgdata:/var/lib/postgresql/data \
-p 5433:5432 \
postgres:18
# Redis
docker run -d --name nodejs-redis \
-p 6379:6379 \
redis:7
# API(需 postgres、redis 已就绪,且与它们在同一个 Docker 网络)
docker build -t nodejs-api .
docker run -d --name nodejs-api \
--network <与 postgres/redis 相同的网络> \
-p 8080:8080 \
-e DATABASE_URL="postgresql://postgres:你的密码@postgres:5432/nodejs_study?schema=public" \
-e REDIS_URL="redis://redis:6379" \
-e JWT_SECRET="..." \
nodejs-api
Compose 的好处:配置集中、可版本管理、一条命令启停多个服务;网络、depends_on 与健康检查也会自动编排。
运行时是什么样子
text
你的电脑 (Host)
浏览器 / curl
│
└── localhost:8080 ──► nodejs-api (Node API 容器)
│
├── postgres:5432 ──► nodejs-postgres (PostgreSQL)
└── redis:6379 ──► nodejs-redis (Redis)
宿主机上单独操作(如 migrate、npm run dev)仍用映射端口:
localhost:5433 ──► nodejs-postgres
localhost:6379 ──► nodejs-redis
环境变量说明
.env文件在宿主机上,dockerignore中设置了,不会进入 Docker 镜像/容器里。environment是容器内的正式环境变量,优先级高于镜像里的.env/ dotenv(就算.env文件存在); compose 里DATABASE_URL、REDIS_URL直接覆盖了本地.env的对应值。JWT_SECRET: ${JWT_SECRET}的读取发生在 Docker Compose 在宿主机上解析配置文件这一步,不是容器里的 Node 去读.env。
端口映射
8080:8080,前面是外面访问端口;改成 "8081:8080" 后,从本机访问 API 要用 8081,容器内仍是 8080。
Worker 为什么需要容器
worker 需要容器,是因为:
- 它是独立进程 --- 和 API 分开跑,API 挂了 worker 还能继续处理队列(反过来也一样)。
- 需要同一套代码和依赖 --- ioredis、emailWorker.ts、emailQueue.ts 都在项目里,必须进容器才能跑。
build: .只是声明「用这个 Dockerfile 造运行环境」 --- Compose 通常只 build 一次,两个 service 复用同一张镜像;worker 用command覆盖默认的 CMD。
command 覆盖默认启动命令
command 就是覆盖 Dockerfile 的默认启动命令,但有个重要前提:
同一份镜像 → 可以起多个容器 → 每个容器可以有不同的启动命令
关系可以这样理解:
text
Dockerfile build → 镜像(含 Node、代码、依赖,默认 CMD = npm start)
│
┌───────────────┴───────────────┐
▼ ▼
api 容器 worker 容器
(没写 command) command: npm run worker:email
用镜像默认 CMD 覆盖 CMD
→ npm start → npm run worker:email
→ tsx ./index.ts → tsx src/workers/emailWorker.ts
Worker 连接 Redis 逻辑
emailWorker.ts被 tsx 当作入口执行import { redis } from "../redis.ts"会立刻执行redis.ts里的代码redis.ts用process.env.REDIS_URL创建 ioredis 连接- 在 Docker Compose 里,worker 容器的环境变量是
REDIS_URL=redis://redis:6379 - ioredis 通过 TCP 连到内部网络里名为
redis的服务(Redis 容器)
dotenv/config
dotenv/config 的作用是:进程启动时把 .env 文件里的变量读进 process.env。它应该放在入口文件(程序最先跑起来的那个文件),而不是放在被 import 的工具模块里。