你以为 docker-compose up -d 只是启动几个容器?大错特错。真正读懂 docker-compose.yml 的每一层含义,你才能在生产环境里游刃有余——网络到底怎么隔离、依赖为什么有时失效、卷挂载有哪些坑、环境变量如何优雅替换。这篇文章以一个 Java Spring Boot 应用(JAR 包) + PostgreSQL 数据库的真实场景为蓝本,逐行解析 docker-compose.yml 的所有核心字段,让你从“会抄”变成“会写”。
一、Compose 是什么?为什么值得深度剖析?
Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。用一个 YAML 文件配置所有服务、网络、卷,然后一行命令就能启动整个应用栈。但大多数人对它的理解停留在“照着模板改”,一旦遇到:
-
容器启动顺序错乱导致应用连不上数据库
-
环境变量没生效
-
数据卷权限问题
-
生产环境和开发环境配置混乱
就束手无策。本文从一个Java Jar 应用 + 远程 PostgreSQL 数据库的场景出发,把 docker-compose.yml 的每一个角落都翻出来讲透。
二、场景案例:一个典型的微服务雏形
假设我们有一个 Spring Boot 应用,打包成 app.jar,它需要连接一个 PostgreSQL 数据库(通过 Docker 运行,也可以理解为“拉取远程 PG 镜像”)。我们的目标是:
用 Dockerfile 构建 Java 应用的镜像。
用 docker-compose.yml 编排 Java 容器和 PostgreSQL 容器。
确保 Java 容器能通过服务名 postgres 访问数据库。
持久化 PostgreSQL 的数据,避免重启丢失。
三、从零构建 Java 应用镜像(Dockerfile)
在项目根目录创建 Dockerfile:
dockerfile
# 基础镜像:使用 OpenJDK 17 精简版
FROM openjdk:17-jdk-slim
# 维护者信息
LABEL maintainer="yourname@example.com"
LABEL description="Spring Boot App for Compose Demo"
# 设置工作目录
WORKDIR /app
# 将宿主机的 JAR 包复制到镜像内(构建时复制)
COPY target/app.jar /app/app.jar
# 创建非 root 用户运行(安全实践)
RUN addgroup –system javauser && \\
adduser –system –no-create-home –ingroup javauser javauser && \\
chown -R javauser:javauser /app
USER javauser
# 暴露应用端口(根据你的 Spring Boot 配置,比如 8080)
EXPOSE 8080
# 健康检查(可选,用于 Compose 的 depends_on 条件)
HEALTHCHECK –interval=30s –timeout=3s –start-period=5s –retries=3 \\
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
关键点解释:
-
COPY target/app.jar 假设你在构建镜像前已经通过 Maven/Gradle 打包好 JAR 到 target/ 目录。
-
创建非 root 用户是安全基线,防止容器内进程提权。
-
HEALTHCHECK 让 Compose 能检测应用是否真正就绪(而不仅仅是容器启动)。
四、docker-compose.yml 完整解剖(带逐行注释)
创建 docker-compose.yml:
yaml
# 1. 版本声明:决定支持的 Compose 特性,3.8 是目前主流
version: '3.8'
# 2. 定义所有服务(容器)
services:
# ———- PostgreSQL 服务 ———-
postgres:
# 拉取远程镜像(相当于从 Docker Hub 拉取 postgres:15-alpine)
image: postgres:15-alpine
# 容器名称(方便命令行操作)
container_name: my-postgres
# 重启策略:除非手动停止,否则总是重启
restart: unless-stopped
# 环境变量:设置数据库初始配置
environment:
POSTGRES_DB: myappdb # 自动创建的数据库名
POSTGRES_USER: myuser
POSTGRES_PASSWORD: secretpass
PGDATA: /var/lib/postgresql/data/pgdata # 数据目录(可自定义)
# 卷挂载:持久化数据库文件
volumes:
– postgres_data:/var/lib/postgresql/data
# 可选:初始化脚本挂载(首次启动自动执行)
– ./init-scripts:/docker-entrypoint-initdb.d
# 端口映射:宿主机端口:容器端口(可选,不映射也可供其他容器访问)
ports:
– "5432:5432"
# 网络:自定义网络,便于服务发现
networks:
– app-network
# 健康检查:确认数据库真正可用
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d myappdb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# ———- Java Spring Boot 应用 ———-
java-app:
# 构建上下文:当前目录(即 Dockerfile 所在位置)
build: .
# 也可以直接用 image: my-java-app:latest,但用 build 更灵活
container_name: my-java-app
restart: unless-stopped
# 依赖声明:等待 postgres 服务“启动”(不是就绪)
depends_on:
– postgres
# 但 depends_on 默认只等容器状态为 running,不关心健康。
# 使用 condition 可以等待健康检查通过(Compose 3.8+ 实验特性,需启用)
# 实际生产更推荐用 wait-for-it 脚本,此处演示 condition 语法:
depends_on:
postgres:
condition: service_healthy
# 环境变量:传递给 Spring Boot 应用
environment:
SPRING_PROFILES_ACTIVE: prod
# 关键:数据库连接 URL 使用服务名 postgres(因为同网络)
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/myappdb
SPRING_DATASOURCE_USERNAME: myuser
SPRING_DATASOURCE_PASSWORD: secretpass
# JVM 内存限制(容器内生效)
JAVA_OPTS: "-Xmx256m -Xms128m"
# 端口映射:将应用暴露到宿主机
ports:
– "8080:8080"
# 卷挂载:挂载日志或配置文件(可选)
volumes:
– ./logs:/app/logs
# 如果不想每次重新构建镜像就更新 jar,可以挂载 jar(开发时有用)
# – ./target/app.jar:/app/app.jar
# 网络:与 postgres 同一个网络
networks:
– app-network
# 资源限制(防止容器耗尽宿主机资源)
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 256M
# 3. 定义网络(方便服务间使用服务名通信)
networks:
app-network:
driver: bridge
# 如果希望使用外部已有网络,取消下面的注释
# external: true
# name: my-custom-network
# 4. 定义卷(持久化数据)
volumes:
postgres_data:
# 可以指定卷驱动和选项,默认 local
driver: local
五、深度拆解:每个字段为什么这么写?
5.1 version —— 选对版本很重要
-
'3.8' 是目前广泛支持的稳定版,支持 depends_on 的 condition、deploy 资源限制等。
-
如果你用旧版 2.x,则没有 deploy,且 depends_on 不支持健康等待。
-
建议:新项目一律 3.8 或更高(3.9、3.10 差别不大)。
5.2 services —— 心脏地带
image vs build
-
image: postgres:15-alpine 直接从仓库拉取。
-
build: . 会从当前目录的 Dockerfile 构建镜像。如果同时指定 image,则构建后还会打上该镜像名。
container_name —— 固定容器名
-
不指定则自动生成 项目名_服务名_序号。固定名称方便 docker exec -it my-java-app bash。
restart: unless-stopped
-
no:不自动重启
-
always:总是重启(包括守护进程重启)
-
on-failure:仅非正常退出时重启
-
unless-stopped:除了手动停止外都重启 —— 生产推荐
environment —— 注入环境变量
两种写法:
yaml
environment:
– SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/myappdb
– JAVA_OPTS=-Xmx256m
或者字典格式。注意变量值不要加引号除非包含特殊字符。
连接数据库的关键:jdbc:postgresql://postgres:5432/myappdb —— 其中 postgres 是服务名,因为两个容器在同一个自定义网络 app-network 中,Docker 内部 DNS 会解析服务名为容器 IP。
depends_on —— 启动顺序的误解与真相
很多人以为 depends_on 会等待数据库完全就绪再启动 Java 应用。错! 默认情况下,它只等待依赖容器的状态变为 running(即进程启动),不代表数据库可以接受连接。
解决方案:
-
条件等待(Compose 3.8+ 需开启 COMPOSE_EXPERIMENTAL=1):
yaml
depends_on:
postgres:
condition: service_healthy这需要 postgres 服务定义了 healthcheck。
-
更通用的方法:在应用启动脚本中轮询数据库(如使用 wait-for-it.sh)。示例:
dockerfile
COPY wait-for-it.sh /wait-for-it.sh
CMD ["/wait-for-it.sh", "postgres:5432", "–", "java", "-jar", "app.jar"]
ports —— 端口映射
-
"8080:8080" 宿主机端口:容器端口。
-
如果只写 "8080",则宿主机随机端口映射。
-
多端口可以列表形式。
volumes —— 数据持久化与共享
-
命名卷(如 postgres_data):由 Docker 管理,路径在 /var/lib/docker/volumes/,适合数据库等需要持久化的场景。
-
绑定挂载(如 ./logs:/app/logs):直接挂载宿主机相对路径,适合开发热加载或输出日志。
-
注意 权限:容器内进程的 UID 可能不是宿主机的用户,导致无法写入。可以用 :Z 或 :z 处理 SELinux,或者指定 user 参数。
networks —— 容器通信的桥梁
-
自定义网络 app-network,让两个容器通过服务名互相访问。
-
如果不定义网络,Compose 会默认创建一个 default 网络,服务名也可以互通,但自定义网络可以更精细控制(如设置 IPAM、限制外部访问)。
-
如果不需要对外暴露数据库端口,可以不写 ports,只让 Java 容器内部访问,提高安全性。
deploy —— 资源限制(仅用于 swarm 模式?)
-
在 docker-compose up 模式下,deploy 字段被忽略。它只在 docker stack deploy(Swarm 模式)下生效。
-
如果要在单机 Compose 中限制资源,使用:
yaml
cpus: '0.5'
mem_limit: 512m
mem_reservation: 256m注意 mem_limit 是旧版写法,新版建议用 deploy.resources 但仅在 swarm 有效。坑点:很多人搞混。单机模式使用 cpus 和 mem_limit 需要配合 –compatibility 或使用 docker-compose 1.29+ 的 –compatibility 标志。最佳实践:生产环境用 Swarm 或 K8s,单机开发暂不限制。
5.3 networks 和 volumes 的顶层定义
-
网络:app-network 可以被多个服务引用。driver: bridge 是默认单机桥接网络。
-
卷:postgres_data 如果不指定 driver,默认 local。可以通过 driver_opts 配置 NFS 等。
六、常用命令与调试技巧
bash
# 构建并启动所有服务(-d 后台运行)
docker-compose up -d
# 只构建不启动
docker-compose build
# 只拉取镜像(postgres 等)
docker-compose pull
# 查看日志(-f 实时跟踪)
docker-compose logs -f java-app
# 进入容器调试
docker-compose exec java-app sh
# 在 Java 容器内测试数据库连通性
docker-compose exec java-app curl http://postgres:5432 # 不应直接访问,仅测试网络
# 停止并删除所有容器、网络(但保留卷)
docker-compose down
# 停止并删除卷(数据丢失)
docker-compose down -v
# 重启单个服务
docker-compose restart java-app
# 查看服务状态
docker-compose ps
七、生产环境必加的配置项
7.1 环境变量文件(.env)
把敏感信息移到 .env 文件,避免写死在 YAML 中。
.env 内容:
text
DB_PASSWORD=secretpass
DB_USER=myuser
在 docker-compose.yml 中引用:
yaml
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
7.2 日志驱动(避免磁盘爆炸)
yaml
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
7.3 时区设置
yaml
environment:
TZ: Asia/Shanghai
volumes:
– /etc/localtime:/etc/localtime:ro
7.4 健康检查的 start_period
-
start_period: 10s 给予数据库启动的宽限期,期间健康检查失败不计入重试。
八、排坑指南(血泪经验)
坑1:Java 应用启动比数据库快导致连接失败
解决:使用 wait-for-it.sh 或 Spring Boot 的数据库重连机制(spring.datasource.hikari.connection-timeout 设置足够长)。
坑2:卷权限导致 PostgreSQL 无法写入
现象:容器反复重启,日志显示 permission denied。
解决:可以指定 user: "999:999"(postgres 官方镜像的 UID),或者让宿主机目录权限为 999。
坑3:网络不通,Java 应用报 UnknownHostException: postgres
检查:两个服务是否在同一个网络?是否拼写错误?用 docker-compose exec java-app ping postgres 测试。
坑4:端口冲突
宿主机 5432 或 8080 已被占用。修改 ports 左侧的宿主机端口,如 "5433:5432"。
坑5:depends_on 不生效,应用仍早于数据库启动
根本原因:depends_on 不等健康。必须配合 healthcheck + condition: service_healthy 或启动脚本轮询。
九、扩展:连接远程已有的 PostgreSQL(非容器)
如果你的 PostgreSQL 运行在外部服务器(不是本机容器),则不需要在 docker-compose.yml 中定义 postgres 服务。只需修改 Java 应用的 SPRING_DATASOURCE_URL 指向远程 IP。但为了网络连通性,需要注意:
-
Java 容器需要能访问远程数据库的 IP 和端口。
-
可以使用 extra_hosts 或宿主机网络模式 network_mode: host。
示例:
yaml
java-app:
build: .
network_mode: host # 直接使用宿主机网络栈,可通过 localhost 访问宿主机上的服务
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/myappdb
但 network_mode: host 会失去网络隔离,一般不建议。
十、总结:一份合格 Compose 文件的自我修养
一份高质量的 docker-compose.yml 应当满足:
明确版本,使用 3.8 及以上。
所有服务都有健康检查(至少关键依赖)。
敏感信息通过 .env 注入。
数据持久化使用命名卷。
自定义网络,确保服务发现。
日志限制,防止磁盘占满。
启动顺序保障(健康等待或脚本)。
资源限制(生产 Swarm 模式)。
掌握了这些,你就从“docker-compose 用户”升级为“Compose 配置架构师”。下一步,可以研究 docker-compose.override.yml 实现开发/生产环境分离,或者向 Kubernetes 迁移。
如果这篇文章帮你彻底啃下了 docker-compose.yml 的硬骨头,欢迎点赞、收藏、转发。评论区留下你遇到的 Compose 怪问题,我帮你排!
附录:完整的项目文件树
text
.
├── docker-compose.yml
├── .env
├── Dockerfile
├── target/
│ └── app.jar
├── logs/
│ └── (运行时生成)
└── init-scripts/
└── init.sql




