一、为什么需要编排:手工部署的混乱与痛点

上个月接手一个老项目,四个服务:前端静态资源、后端API、PostgreSQL数据库、Redis缓存。之前都是手工部署,每次发版流程是这样的:

  1. 先启动数据库,等30秒确认能连上
  2. 再启动Redis,确认端口通
  3. 然后启动后端,等它注册到Nacos
  4. 最后配置Nginx反代

每次都要在多个终端来回切换,敲十几条docker run命令,还要记住各种-v-p--network参数。最崩溃的是有一次升级数据库镜像,忘了挂载数据卷,容器一删数据全没了。这还不算完,服务之间互相等待的时序问题反复出现,后端启动时数据库还没就绪,连接池报错。

这就是我写这篇文章的原因:用Docker Compose把整个部署流程代码化、可重复化。下面直接上干货。

二、环境与版本:不要再问为什么用旧版本

先说环境,别用什么奇奇怪怪的版本,以下是我验证过的组合:

  • Docker Engine:24.0.7(2023年12月发布)
  • Docker Compose:v2.24.2(通过docker-compose-plugin安装)
  • 宿主机:Ubuntu 22.04 LTS,4核8G
  • 镜像版本:nginx:1.25.3-alpine、openjdk:17-slim、postgres:16.1-alpine、redis:7.2.3-alpine

重要提醒:Compose V2已经内置在Docker CLI中,直接用docker compose(没有横杠)而非docker-compose。我见过太多人还在用老版本V1,语法都不兼容了。

三、方案设计:网络、卷、健康检查、启动顺序的决策

在设计编排方案时,我定了四条原则:

  1. 网络隔离:所有服务放在同一个自定义bridge网络中,不暴露数据库端口到宿主机(只有Nginx映射80端口)。这样数据库和Redis只在内部网络可达,安全性好。
  2. 数据持久化:PostgreSQL使用命名卷(named volume),Redis用AOF持久化,把appendonly配置挂载到匿名卷。
  3. 健康检查:每个服务都配healthcheck,确保依赖的服务真正可用,而不是端口能通就算成功。
  4. 启动顺序:使用depends_on.condition: service_healthy,让Compose等待依赖服务健康后再启动后续服务。

四、核心实现:docker-compose.yml完整解析

先看完整配置,然后我逐个拆解:

version: "3.9"

services:
  postgres:
    image: postgres:16.1-alpine
    container_name: blog-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: blogdb
      POSTGRES_USER: bloguser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pg_data:/var/lib/postgresql/data
    networks:
      - blog_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U bloguser -d blogdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    ports:
      - "5432:5432"  # 仅开发环境注释掉,生产环境删除此行

  redis:
    image: redis:7.2.3-alpine
    container_name: blog-redis
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    networks:
      - blog_net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3
    ports:
      - "6379:6379"  # 同样,生产环境删除

  backend:
    build: ./backend
    container_name: blog-backend
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/blogdb
      SPRING_DATASOURCE_USERNAME: bloguser
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      SPRING_DATA_REDIS_HOST: redis
      SPRING_DATA_REDIS_PORT: 6379
    volumes:
      - ./logs:/app/logs
    networks:
      - blog_net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 60s

  nginx:
    image: nginx:1.25.3-alpine
    container_name: blog-nginx
    restart: unless-stopped
    depends_on:
      backend:
        condition: service_healthy
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./frontend/dist:/usr/share/nginx/html:ro
    networks:
      - blog_net
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  pg_data:
    driver: local
  redis_data:
    driver: local

networks:
  blog_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

4.1 网络配置:为什么指定subnet

自定义网络默认桥接模式,但我手动指定了172.28.0.0/16,好处是固定网段,方便在防火墙或监控中精确匹配IP。而且服务名就是DNS名称,比如后端连接数据库直接用postgres:5432,不需要知道IP。

4.2 卷挂载:数据安全和日志管理

PostgreSQL的pg_data卷是命名的,即使docker compose down删除容器,数据依然保留。Redis同理。注意我没有把Redis的appendonly.aof单独挂载,而是整个/data目录,因为Redis会在这个目录下生成dump.rdb和appendonly.aof两个文件。

后端日志目录挂载到宿主机的./logs,方便用logrotate或ELK收集。Nginx配置和前端静态文件用bind mount方式,这样改配置不用重建镜像,nginx -s reload就行。

4.3 健康检查:别用telnet,用业务命令

很多人喜欢用nc -z localhost 5432或者curl检查端口,这在某些场景下会误判。比如PostgreSQL刚启动时端口可能已经监听,但实际还没准备好接受连接。PostgreSQL官方镜像自带pg_isready,Redis自带redis-cli ping,这才是正确的方式。Spring Boot的actuator health端点会检查数据库连接池,确保数据库连接可用。

4.4 启动顺序:depends_on的坑

Compose V2的depends_on默认只等待容器启动(started),不等待服务就绪。必须显式加上condition: service_healthy。我用的是:

depends_on:
  postgres:
    condition: service_healthy
  redis:
    condition: service_healthy

这意味着backend容器会等到postgres和redis都健康后才启动。同理,nginx等到backend健康后才启动。

五、踩坑记录:三个真实问题与解决方案

5.1 坑一:健康检查超时导致服务永不启动

第一次配置时,我把PostgreSQL的start_period设成了5秒,结果容器启动慢(要初始化数据目录),5秒内没通过健康检查,Compose直接标记为unhealthy,backend永远等不到。解决方案是把start_period放宽到20秒,interval设为10秒,retries设为5。start_period不算在retries里,它是容错窗口。

5.2 坑二:Spring Boot启动太慢,Nginx等不起

Spring Boot 3.2启动约25秒,但我同时配置了数据库连接池初始化、Redis连接、定时任务预热,导致健康检查60秒后才通过。Nginx那边interval是10秒,retries是3次,30秒就宣告失败。后来把Nginx的depends_on也加上condition: service_healthy,并且将backend的start_period调至90秒,问题解决。

5.3 坑三:环境变量泄露

配置里有数据库密码,直接用明文写在yaml里。后来改成.env文件:

# .env
DB_PASSWORD=MyS3cureP@ssw0rd

然后在compose文件里用${DB_PASSWORD}引用。并且把.env加进.gitignore,避免上传到代码仓库。

5.4 优化:资源限制

给每个服务加了deploy.resources.limits,避免某个服务吃光内存:

deploy:
  resources:
    limits:
      memory: 512M
      cpus: "0.5"

实测加了限制后,整体内存占用从1.8G降至1.5G,性能影响几乎无感。

六、效果数据:部署效率对比

用这套Compose配置后,我在同一台机器上做了三次测试:

指标 手工部署 Docker Compose
首次部署时长 3分20秒 45秒(镜像已拉取)
服务重启恢复 2分15秒 12秒
配置变更生效 需逐台操作 一行命令搞定
数据持久化失败率 曾发生数据丢失 0次

更重要的是,现在团队任何人拿到代码仓库,只要装好Docker,执行docker compose up -d就能获得一套完整可运行的开发环境。不是嘴上说说,是真的做到了基础设施即代码

七、总结与建议

Docker Compose不是万能的,但对于中小型项目的容器化编排,它比Kubernetes简单太多,足够满足90%的场景。我的建议是:

  1. 每个服务都必须配置健康检查,这是编排可靠性的基石。
  2. 命名卷和bind mount要分清:数据库用命名卷,配置文件用bind mount。
  3. depends_on必须用condition: service_healthy,否则只是表面顺序。
  4. 善用.env文件管理敏感信息,别硬编码密码。
  5. 生产环境把数据库端口从ports中移除,只保留内部网络访问。

如果你还没有把项目容器化,或者还在手工docker run,强烈建议花半天时间迁移到Compose。这可能是你今年做的性价比最高的技术投资。有问题欢迎评论区交流,我看到了会回复。