单个容器好管,一堆容器就靠 Compose。但 Compose 里最容易踩的坑不是语法,是两件事:启动顺序和日志把磁盘吃光。这篇给一份能直接改用的编排示例,讲清 depends_on 的真实语义,最后附一份上线前的检查清单。
一、Compose 文件骨架
services: # 每个服务 = 一组相同配置的容器
app: ...
mysql: ...
networks: # 自定义网络,服务间按名字互访
app-net:
volumes: # 命名卷,数据持久化
mysqldata:现在的 Compose v2 不需要写 version: 键了,写了反而会告警。命令也从 docker-compose 变成了 docker compose(带空格)。
二、一份能直接改用的编排
services:
app:
image: registry.example.com/team/myapp:1.0.0 # 固定 tag,绝不用 latest
container_name: app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy # 等 MySQL 真正就绪,不是等启动
redis:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/app?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
SPRING_REDIS_HOST: redis
JAVA_OPTS: -XX:MaxRAMPercentage=75.0
ports:
- "127.0.0.1:8080:8080" # 只监听本机,外网走 Nginx
volumes:
- applogs:/var/log/app
networks: [app-net]
deploy:
resources:
limits:
cpus: "2.0"
memory: 2g
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 60s # 给慢启动留宽限
logging: &default-logging # 锚点,下面复用
driver: json-file
options:
max-size: "10m"
max-file: "3"
mysql:
image: mysql:8.0.36
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_pw
MYSQL_DATABASE: app
TZ: Asia/Shanghai
volumes:
- mysqldata:/var/lib/mysql
- ./conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro # 配置用 bind mount,只读
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks: [app-net]
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 60s
logging: *default-logging
redis:
image: redis:7.2-alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
networks: [app-net]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
logging: *default-logging
networks:
app-net:
driver: bridge
volumes:
mysqldata:
redisdata:
applogs:三、depends_on 只等"启动",不等"就绪"
这是 Compose 最大的误会。默认写法:
depends_on:
- mysql只保证 mysql 容器被创建并启动,不等它初始化完、不等它能接受连接。于是 Spring Boot 启动时数据库还没好,报 Communications link failure,然后应用自己退出。
三种解法,建议同时用:
condition: service_healthy:依赖方必须配healthcheck,Compose 会等它变成 healthy 再启动上游。- 应用侧重试:数据库连接池配置重连(HikariCP 本身会重试),或加
spring.datasource.hikari.initialization-fail-timeout=-1让启动不因数据库未就绪而失败。 - 重启策略兜底:
restart: unless-stopped,启动失败会再试,配合上面的健康检查形成闭环。
四、环境变量与密钥
environment:
JAVA_OPTS: -XX:MaxRAMPercentage=75.0
env_file:
- .env.app # 或 env_file 从文件读
# .env 文件(跟 docker-compose.yml 同级,自动加载)
TAG=1.0.0
MYSQL_ROOT_PASSWORD=xxxxxximage: myapp:${TAG:-1.0.0} # 变量替换,带默认值关于密钥的三条纪律:
- 不要把密码写进
environment——docker inspect能直接看到; - 不要写进 Dockerfile 的
ENV或用--build-arg传——会留在docker history里; - 用
secrets(Swarm)、*_FILE环境变量(MySQL/Redis 官方镜像都支持,如MYSQL_ROOT_PASSWORD_FILE),或者外部配置中心。
五、日志:别让 json.log 撑爆根分区
Docker 默认用 json-file 驱动,默认不做任何轮转。一个跑半年的服务,日志能把 200G 磁盘吃到 0 字节,然后全站写不进去。
logging:
driver: json-file
options:
max-size: "10m" # 单文件上限
max-file: "3" # 保留几个全局兜底配置(对所有容器生效),改 /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}sudo systemctl restart docker # 改完要重启 daemon(会影响在跑的容器)注意:daemon.json 的改动只对新创建的容器生效,已有的容器要重建。
| 驱动 | 适用 | 说明 |
|---|---|---|
| json-file | 单机、默认 | 必须配 max-size,否则就是定时炸弹 |
| local | 单机、要省空间 | 压缩格式,体积更小,但 docker logs 读的是二进制 |
| journald | 已用 systemd 的机器 | 交给 journalctl 统一管理 |
| fluentd / loki | 要集中收集 | 直接推到日志平台 |
| none | 完全不要日志 | 排查靠应用自己写文件 |
真实事故复盘见:根分区 100%:一个没配 max-size 的 json.log,把 200G 磁盘"吃"成了 0 字节。
六、常用命令
docker compose up -d # 后台启动
docker compose up -d --build # 改了 Dockerfile 后重新构建再启
docker compose ps # 看各服务状态与健康检查
docker compose logs -f --tail 100 app # 跟日志
docker compose restart app # 重启单个服务
docker compose down # 停并删除容器(保留卷)
docker compose down -v # 连卷一起删(危险)
docker compose config # 校验并展开最终配置,排查变量没生效很好用
docker compose exec app sh # 进容器
docker compose pull && docker compose up -d # 更新镜像七、上线检查清单
按重要性排序,每一项都对应一类真实事故:
- 镜像 tag 固定:
myapp:1.0.0,不用latest。否则回滚时你根本不知道要回到哪。 - 非 root 运行:Dockerfile 里
USER 10001。容器逃逸时 root 是最大助攻。 - 资源限制:
mem_limit/cpus/pids_limit。不限就是让一个容器能拖垮整台机。 - 日志轮转:
max-size+max-file,外加daemon.json全局兜底。 - 健康检查 + 合理的重启策略:
unless-stopped或on-failure:5,别裸用always。 - JVM 容器感知:
MaxRAMPercentage而非写死-Xmx,并留出堆外空间。 - 端口只监听本机:
127.0.0.1:8080:8080,公网入口统一走 Nginx。 - 数据用命名卷,配置用只读 bind mount,密钥走 secrets 或
*_FILE。 - 只读根文件系统:能做到的服务加
read_only: true+tmpfs: [/tmp]。 - 镜像漏洞扫描:
trivy image myapp:1.0.0,基础镜像也要定期更新。 - 时区统一:容器
TZ、JVMuser.timezone、JDBCserverTimezone三处都要对齐。 - 备份与回滚演练:
docker compose down改 tag 再up能不能起来,平时就要试过。
八、出问题先看这几条
docker compose ps # 谁 unhealthy 了
docker inspect app --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
dmesg | grep -i -E 'killed|oom' | tail # 内核 OOM 的直接证据
docker stats --no-stream # 谁在吃资源
docker compose logs --since 30m mysql | tail -50
docker system df -v # 磁盘是不是被日志/镜像吃满了
docker events --since 30m --filter container=app # 这半小时它经历了什么排查思路的更完整版本,以及 37 个真实案例的索引,在生产问题排查手记总目录里。
备忘
- Compose v2 命令是
docker compose,不需要version:键。 depends_on默认只等启动;要等就绪必须配healthcheck+condition: service_healthy。- 日志不配
max-size迟早撑爆磁盘,daemon.json全局兜底也别忘了。 - 密码不进 environment、不进 Dockerfile,用 secrets 或
*_FILE。 - 端口写
127.0.0.1:宿主机:容器,公网入口统一收口。 - 上线清单里最容易漏的是:时区三处对齐、回滚演练、镜像扫描。
一起聊聊