第4章 Docker Compose编排:YAML语法、服务定义、依赖管理、环境变量注入

聊到Docker Compose,我得先坦白一件事。我刚接触容器编排那会儿,觉得Dockerfile已经够用了,干嘛还要多学一个工具?直到我在一个微服务项目里手动启动了十几个容器,端口冲突、启动顺序乱套、环境变量配错……折腾了一整天。嗯,从那以后,Compose就成了我工具箱里的标配。

说白了,Docker Compose就是让你用一份YAML文件,把多个容器的启动、网络、依赖、环境变量全部管起来。你想想看,一个命令docker-compose up,所有服务就绪,多省心。

4.1 YAML语法基础——别让缩进坑了你

YAML的语法其实很简单,但越简单的东西越容易出幺蛾子。我个人习惯用两个空格做缩进,坚决不用Tab。为什么?因为YAML解析器对Tab的态度,就像我对周一早上的闹钟——完全不兼容。

⚠️ 我曾经踩过的坑: 有一次线上部署,Compose文件怎么都解析失败,排查了半小时,结果发现是编辑器自动把空格转成了Tab。从那以后,我都在编辑器里开启「显示空白字符」功能。

来看一个最基本的YAML结构:

# 键值对
version: '3.8'
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"

# 列表(用短横线)
volumes:
  - ./data:/app/data
  - ./logs:/app/logs

# 多行字符串(用 | 保留换行)
config: |
  server {
    listen 80;
    server_name example.com;
  }

记住三个要点:

  • 缩进必须一致——要么全用空格,要么全用Tab,别混用
  • 冒号后面要加空格——key: value,不是key:value
  • 列表项用短横线+空格——- item

4.2 服务定义——把每个容器说清楚

服务定义是Compose文件的核心。每个services下面的条目,就对应一个容器。我一般这样组织:

version: '3.8'

services:
  # 前端服务
  frontend:
    image: myapp-frontend:1.2.0
    container_name: app-frontend
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - API_URL=http://backend:8080
    depends_on:
      - backend
    restart: unless-stopped

  # 后端服务
  backend:
    build: ./backend
    image: myapp-backend:latest
    ports:
      - "8080:8080"
    volumes:
      - ./backend/logs:/app/logs
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - DB_USER=appuser
      - DB_PASSWORD=${DB_PASSWORD}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # 数据库
  db:
    image: postgres:15-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

这里有几个关键点:

  • image vs build:用image直接拉取现成镜像,用build从Dockerfile构建。我建议开发阶段用build,生产环境用image配合CI/CD推送的镜像。
  • container_name:给容器起个固定的名字,方便用docker logsdocker exec操作。但注意,如果做水平扩展,就别指定这个名字了。
  • restart策略unless-stopped是我最常用的——除非你手动停了它,否则挂了自动重启。

4.3 依赖管理——谁先启动,谁后启动

微服务之间通常有依赖关系。比如前端依赖后端,后端依赖数据库。如果启动顺序乱了,前端连不上后端,直接报错。

depends_on就是干这个的。但这里有个坑——depends_on只保证启动顺序,不保证服务就绪。什么意思呢?比如数据库容器启动了,但PostgreSQL还没完成初始化,这时候后端去连它,照样连不上。

✅ 最佳实践:配合healthcheck使用condition: service_healthy。这样Compose会等数据库的健康检查通过后,才启动后端服务。

services:
  backend:
    depends_on:
      db:
        condition: service_healthy

  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s  # 给数据库30秒的初始化时间

我记得有一次生产环境出问题,就是因为没加start_period。数据库第一次启动要初始化数据,花了40秒,健康检查在30秒内重试了6次都失败,Compose直接标记为unhealthy,导致后端一直没启动。加了start_period后,问题就解决了。

4.4 环境变量注入——别把密码写死在文件里

环境变量是容器化应用传递配置的标准方式。但怎么管理这些变量,学问可不少。

方式一:直接在文件中写死

environment:
  - DB_HOST=localhost
  - DB_PORT=5432

这种方式只适合本地开发。千万别在生产环境这么干——你的数据库密码、API密钥都暴露在代码仓库里了。

方式二:引用宿主机的环境变量

environment:
  - DB_PASSWORD=${DB_PASSWORD}
  - API_KEY=${API_KEY}

启动时从宿主机环境变量读取。但问题来了——你需要在每台机器上都设置这些变量,维护起来很麻烦。

方式三:使用.env文件(我最推荐的方式)

在Compose文件同目录下创建一个.env文件:

# .env 文件
DB_PASSWORD=MyS3cretP@ss
API_KEY=abc123xyz
REDIS_HOST=redis.example.com

然后在docker-compose.yml里直接引用:

services:
  backend:
    environment:
      - DB_PASSWORD=${DB_PASSWORD}
      - API_KEY=${API_KEY}
      - REDIS_HOST=${REDIS_HOST}

启动时,Compose会自动读取.env文件里的变量。而且.env文件可以加入.gitignore,避免提交到代码仓库。

💡 我的习惯:在项目里放一个.env.example文件,里面列出所有需要的变量名和示例值。新同事拉取代码后,复制一份改成.env,填上真实值就行。这样既安全又方便。

方式四:使用Docker Secrets(生产环境推荐)

对于敏感信息,比如数据库密码、TLS证书,我建议用Docker Secrets:

services:
  backend:
    secrets:
      - db_password
      - tls_cert

secrets:
  db_password:
    file: ./secrets/db_password.txt
  tls_cert:
    file: ./secrets/tls_cert.pem

Secrets会被挂载到容器的/run/secrets/目录下,应用从文件读取。这种方式比环境变量更安全——不会在进程列表、日志里泄露。

4.5 实战:一个完整的Compose文件

最后,我分享一个我在项目中实际用过的Compose文件模板。它包含了前面讲的所有要点:

version: '3.8'

services:
  # Nginx反向代理
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
      - static_volume:/static
    depends_on:
      - frontend
      - backend
    restart: unless-stopped

  # Vue前端
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.prod
    image: myapp/frontend:${VERSION:-latest}
    volumes:
      - static_volume:/app/dist
    environment:
      - VUE_APP_API_URL=https://api.example.com
    restart: unless-stopped

  # Django后端
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: myapp/backend:${VERSION:-latest}
    expose:
      - "8000"
    volumes:
      - static_volume:/app/static
      - media_volume:/app/media
    environment:
      - DJANGO_SETTINGS_MODULE=config.settings.${ENV:-production}
      - DB_HOST=db
      - DB_PORT=5432
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    secrets:
      - django_secret_key
    restart: unless-stopped

  # PostgreSQL数据库
  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

  # Redis缓存
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  static_volume:
  media_volume:

secrets:
  django_secret_key:
    file: ./secrets/django_secret_key.txt

这个模板涵盖了:

  • 多服务定义(Nginx、前端、后端、数据库、缓存)
  • 依赖管理(depends_on + healthcheck
  • 环境变量注入(.env文件 + Secrets)
  • 数据持久化(命名卷)
  • 构建与镜像分离(开发用build,生产用image)

你想想看,如果没有Compose,这些配置你得手动敲多少docker run命令?而且每次重启都要按顺序来,漏一个就全乱套。有了Compose,一个docker-compose up -d就搞定了。

📌 核心要点回顾:

  • YAML缩进用空格,别用Tab
  • 服务定义时,区分imagebuild的使用场景
  • 依赖管理要配合healthcheck,别只靠depends_on
  • 环境变量用.env文件管理,敏感信息用Secrets
  • 生产环境加上restart: unless-stopped保证高可用

好了,这一章的内容就到这里。下一章我们会聊Docker Swarm集群管理——到时候你会看到,Compose文件稍微改改,就能直接部署到Swarm集群上,实现真正的容器编排和高可用。