Docker - 03 - docker-compose.yml

如何理解 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:5432redis: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 用户名/密码/库名 仅首次启动 初始化库;密码需与 .envDATABASE_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_URLREDIS_URL 直接覆盖了本地 .env 的对应值。
  • JWT_SECRET: ${JWT_SECRET} 的读取发生在 Docker Compose 在宿主机上解析配置文件这一步,不是容器里的 Node 去读 .env

端口映射

8080:8080,前面是外面访问端口;改成 "8081:8080" 后,从本机访问 API 要用 8081,容器内仍是 8080。

Worker 为什么需要容器

worker 需要容器,是因为:

  1. 它是独立进程 --- 和 API 分开跑,API 挂了 worker 还能继续处理队列(反过来也一样)。
  2. 需要同一套代码和依赖 --- ioredis、emailWorker.ts、emailQueue.ts 都在项目里,必须进容器才能跑。
  3. 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 逻辑

  1. emailWorker.ts 被 tsx 当作入口执行
  2. import { redis } from "../redis.ts" 会立刻执行 redis.ts 里的代码
  3. redis.tsprocess.env.REDIS_URL 创建 ioredis 连接
  4. 在 Docker Compose 里,worker 容器的环境变量是 REDIS_URL=redis://redis:6379
  5. ioredis 通过 TCP 连到内部网络里名为 redis 的服务(Redis 容器)

dotenv/config

dotenv/config 的作用是:进程启动时把 .env 文件里的变量读进 process.env。它应该放在入口文件(程序最先跑起来的那个文件),而不是放在被 import 的工具模块里。