一、从手动部署到编排的痛点

之前维护一个内部管理系统,包含Nginx反向代理、Spring Boot后端和PostgreSQL数据库。每次部署都要手动执行docker run命令,还得记住端口映射、环境变量,最痛苦的是数据库容器重启后数据丢失。有次升级后端代码,忘了先启动数据库,结果应用启动失败,排查了半小时才发现是连接超时。

后来决定用Docker Compose解决这些问题。我的需求很明确:
- 三个容器能一起启动、一起停止
- 数据库数据必须持久化
- 应用要等数据库就绪后再启动
- 容器间通信不用暴露端口到宿主机

二、环境与版本说明

我用的版本组合经过多次验证,稳定兼容:
- Docker Engine: 24.0.7
- Docker Compose: v2.23.3
- PostgreSQL镜像: postgres:15.4-alpine(90MB,比debian版小300MB)
- Spring Boot镜像: openjdk:17-slim + 项目jar包
- Nginx镜像: nginx:1.25.2-alpine

宿主机是Ubuntu 22.04 LTS,4核8G配置。这个组合在生产环境跑了4个月,没出过兼容问题。

三、网络与存储方案设计

网络方案我选择了自定义bridge网络而不是默认网络,原因是:
1. 默认网络不支持容器间DNS解析(虽然新版支持了,但自定义更清晰)
2. 可以控制哪些服务能互相访问

存储方面,三个挂载点:
- pg_data 卷:存储PostgreSQL数据文件
- ./nginx/conf.d 绑定挂载:实时修改Nginx配置
- ./logs 绑定挂载:收集应用日志

健康检查的设计是关键。PostgreSQL用pg_isready命令检查,Spring Boot用HTTP请求/actuator/health,Nginx检查/health端点。

四、核心实现:docker-compose.yml详解

先看完整配置,然后逐段解释关键部分:

version: '3.8'

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/24

volumes:
  pg_data:
    driver: local

services:
  db:
    image: postgres:15.4-alpine
    container_name: app_db
    restart: unless-stopped
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret123}
      POSTGRES_DB: app_db
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d:ro
    networks:
      backend:
        aliases:
          - db_host
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    deploy:
      resources:
        limits:
          memory: 512M

  app:
    build: ./backend
    image: myapp-backend:1.2.0
    container_name: app_backend
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db_host:5432/app_db
      SPRING_DATASOURCE_USERNAME: app_user
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-secret123}
    volumes:
      - ./logs:/app/logs
      - ./uploads:/app/uploads
    networks:
      - backend
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 768M

  nginx:
    image: nginx:1.25.2-alpine
    container_name: app_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./frontend/dist:/usr/share/nginx/html:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - backend
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s

五、关键配置的踩坑与优化

坑1:健康检查命令的差异
PostgreSQL的pg_isready在容器里默认不在PATH中,我第一版写的test: ["CMD", "pg_isready"...]直接报错。后来改用CMD-SHELL并加上完整路径,或者用shell执行方式。最终用CMD-SHELL配合pg_isready -U app_user -d app_db,注意要指定用户和数据库名。

坑2:启动顺序的假象
depends_on如果不加condition: service_healthy,只会等容器启动,不会等服务就绪。我踩过这个坑:应用容器启动了,但数据库还在初始化,Spring Boot连接失败重启。加上健康检查条件后,问题解决。注意start_period参数很关键,给服务留出初始化时间,避免误判。

坑3:卷挂载的权限问题
PostgreSQL容器默认用postgres用户(UID 999)运行,挂载到宿主机的pg_data卷如果权限不对,容器启动会失败。我的解决方法是手动创建目录并设置权限:

mkdir -p /var/lib/docker/volumes/app_pg_data/_data
chown -R 999:999 /var/lib/docker/volumes/app_pg_data/_data

或者更简单,在compose文件里加user: "999:999"

优化:网络别名(aliases)的使用
我给db服务加了aliases: db_host,这样应用连接数据库时用db_host而不是db,好处是即使容器名变了,应用代码不用改。实际上Compose网络默认支持服务名解析,但显式声明更清晰。

资源限制的实践
deploy.resources.limits限制内存,防止某个容器占用全部内存。实际测试:Spring Boot容器限制768M后,GC表现更稳定,没有出现过OOM。PostgreSQL限制512M对中小项目够用。

六、性能数据与运维效果

改造后我做了对比测试:

指标 手动部署 Compose编排
全量启动时间 45秒 18秒
单容器重启恢复 15秒 5秒
数据持久化 需要手动备份 自动卷存储
部署命令数 6条 1条

最明显的变化是排查问题的成本降低了。以前要检查各容器网络通不通,现在统一在一个网络里,DNS解析自动处理好。健康检查的状态通过docker compose ps就能看到:

NAME                SERVICE             STATUS                  PORTS
app_db              db                  Up 12 minutes (healthy) 5432/tcp
app_backend         app                 Up 10 minutes (healthy) 8080/tcp
app_nginx           nginx               Up 10 minutes (healthy) 0.0.0.0:80->80/tcp

有一次服务器重启,所有容器自动恢复,数据库数据完好,整个恢复时间不到30秒。这在以前是不可想象的——手动部署时我至少需要10分钟来检查各个服务状态。

七、总结与建议

这套配置已经稳定运行4个月,中间经历过3次代码更新、2次数据库迁移。总结几点经验:

  1. 健康检查一定要配置,特别是依赖链上的服务。start_period要按服务启动时间来设置,太短会误报,太长会拖延故障发现。
  2. 卷挂载优先级:数据库数据用命名卷,配置文件用绑定挂载。命名卷方便迁移和备份,绑定挂载方便调试。
  3. 敏感信息用环境变量,我用的.env文件配合${DB_PASSWORD:-secret123}语法,默认值只在开发环境使用。
  4. 版本锁定很重要,镜像tag不要用latest,生产环境必须指定具体版本。

如果你刚开始用Compose,建议从我这个配置改起,去掉不需要的服务,加上自己的业务逻辑。遇到问题先看docker compose logs,大部分错误都是配置语法或路径问题。最后提醒一句:docker compose config命令可以校验配置文件的语法,写完先跑一下这个,能省很多排查时间。