Docker Compose 常见报错排查笔记(含 nginx+mysql+redis 可运行模板)
前段时间帮同事看一个 docker compose up -d 起不来的项目前后折腾了大半天最后发现是 YAML 里混了一个 Tab。借这个机会把这两年用 Compose 踩过的坑整理一下顺便把常用的模板存个档下次直接抄。先说一个前提本文用的是Compose v2命令是 docker compose中间空格v1 的 docker-compose连字符已经在 2023 年停止维护网上老教程里那种 version: 3.8 开头的写法在新版会打警告不再推荐。环境项目版本Docker Engine24.0.7Docker Composev2.24.5OSUbuntu 22.04 / Windows 11 Docker Desktop 4.29查看自己版本docker compose version # Docker Compose version v2.24.5先讲一个万能习惯写完先 config不管写多短的 compose.yml启动前先跑一次docker compose config这条命令做两件事校验 YAML 语法缩进、Tab、引号错乱都会在这里报出来还会指出具体行号展开所有变量插值和 extends 继承输出的是 Compose 实际会使用的最终配置。比如你写了 ${DB_PASSWORD}config 会告诉你这个变量最终被替换成了什么或者是不是空字符串——很多配置看着没问题但服务起不来的情况跑一次 config 就露馅了。如果报错输出大概长这样yaml: line 12: found character that cannot start any tokenline 12 就是出错行号直接跳过去改。坑 1YAML 缩进这是新手 90% 的第一个坑。YAML 有几个硬规则只能用空格不能用 Tab——用 Tab 直接 found character that cannot start any token同一层级缩进数必须一致惯例是2 空格冒号后面必须有空格image: nginx ✅image:nginx ❌列表项 - 后面也要空格错误示范services: nginx: image: nginx ports: # ← 这一行是 Tab - 8080:80正确写法services: nginx: image: nginx ports: - 8080:80VSCode 装个 YAML 插件Red Hat 出的那个底部状态栏能切空格/Tab缩进错误也会实时高亮比事后 config 校验省事。坑 2字段名拼错Compose 对未知字段是静默忽略的除非开了严格模式拼错了不报错但配置不生效。最常见的错写正确volumevolumesenviromentenvironmentportportscontainer_namecontainer_name这个反而经常被写成 containernamedepend_ondepends_on排查方法还是 docker compose config展开后的输出会告诉你 Compose实际读到了什么。如果你写了 enviroment展开的输出里就找不到对应的环境变量一眼看穿。坑 3挂载路径绑定挂载bind mount路径踩坑分平台Linux / Mac相对路径以 compose.yml所在目录为基准不是你执行命令的目录。services: nginx: volumes: - ./conf/nginx.conf:/etc/nginx/nginx.conf:ro - ./logs:/var/log/nginx:ro 表示只读挂载配置文件建议都加防止容器内进程误改宿主机文件。Windows路径分隔符推荐用正斜杠 /反斜杠在 YAML 里要转义很麻烦。volumes: - D:/projects/myapp/conf:/etc/nginx/conf:ro # ✅ # - D:\projects\myapp\conf:/etc/nginx/conf # ❌ 反斜杠容易被吃掉Docker Desktop 还要注意挂载的盘符必须在 Settings → Resources → File Sharing 里共享过否则报 Mount denied。Windows 11 上 C 盘默认共享D/E 盘要手动加。排查挂载失败docker compose logs nginx # 或者进容器看 docker compose exec nginx ls -la /etc/nginx/如果宿主机文件没挂进来容器里看到的是镜像自带的默认文件不是你的配置。坑 4depends_on 只等启动不等就绪这个是原文提到但没给解法的坑展开讲。depends_on 的默认行为是等被依赖的容器进入 running 状态就返回不管服务本身有没有准备好接受连接。MySQL 容器启动到能接受 TCP 连接中间有 10~30 秒的初始化时间这段时间 Web 服务连过去就是 Connection refused。错误示范services: web: image: myapp depends_on: - mysql mysql: image: mysql:8启动顺序是 mysql 先起但 web 起来的时候 mysql 还没监听 3306应用初始化连接池直接崩。正确写法推荐用 healthcheck condition: service_healthyservices: mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: app healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -p${MYSQL_ROOT_PASSWORD}] interval: 5s timeout: 3s retries: 20 start_period: 30s volumes: - mysql_data:/var/lib/mysql web: image: myapp depends_on: mysql: condition: service_healthystart_period: 30s 是给 MySQL 初始化的宽限期这段时间内 healthcheck 失败不算 unhealthy。retries: 20 interval: 5s 意味着最多等 100 秒够用。注意condition: service_healthy 是 Compose v2.1 支持的语法v1 的 docker-compose 老版本可能不认报 depends_on contains an invalid type。坑 5环境变量 ${VAR} 与 shell 变量混淆Compose 里 ${VAR} 有两层解析很多人分不清第一层Compose 插值——发生在读取 yml 时Compose 从宿主机 shell 环境或同目录下的 .env 文件里取值替换到 yml 里。services: mysql: environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PW} # ← Compose 会从宿主机/.env 取 DB_ROOT_PW第二层容器内 shell 解析——如果值里带了 $$两个美元符Compose 会转义成单个 $最终传给容器里的 shell 去解析。services: app: command: bash -c echo $$HOSTNAME # ← 打印容器内的 HOSTNAME不是宿主机的写一个 $ 就是 Compose 插值写两个 $$ 就是留给容器内 shell——这个区别踩过一次就记一辈子。推荐做法敏感配置全放 .env同目录Compose 自动读并且 .env 加进 .gitignore# .env DB_ROOT_PWs3cret DB_USERapp DB_NAMEapp # .gitignore .env提交一个 .env.example 到仓库写明需要哪些变量别人 clone 下来复制成 .env 自己填。坑 6匿名卷导致重启丢数据services: mysql: image: mysql:8 volumes: - /var/lib/mysql # ← 匿名卷这种写法叫匿名卷Compose 会创建一个随机名字的 Docker volume。问题在于docker compose down不会删匿名卷默认行为docker compose down -v会删所有卷包括匿名的你换台机器部署或者 down -v 一次数据全没正确做法显式声明命名卷services: mysql: image: mysql:8 volumes: - mysql_data:/var/lib/mysql # ← 命名卷 volumes: mysql_data: # ← 顶层声明命名卷有固定名字down 不会删docker volume ls 能看到方便备份和迁移。完整可运行模板nginx mysql redis下面这份是我常用的模板直接 docker compose up -d 就能起包含了上面所有的最佳实践# compose.yml Compose v2不需要 version 字段 services: nginx: image: nginx:1.25-alpine container_name: nginx ports: - 80:80 - 443:443 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/logs:/var/log/nginx - ./nginx/html:/usr/share/nginx/html:ro depends_on: - app restart: unless-stopped networks: - frontend - backend app: image: myapp:latest container_name: app environment: DB_HOST: mysql DB_PORT: 3306 DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} DB_NAME: ${DB_NAME} REDIS_HOST: redis REDIS_PORT: 6379 depends_on: mysql: condition: service_healthy redis: condition: service_healthy restart: unless-stopped networks: - backend mysql: image: mysql:8.0 container_name: mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: ${DB_NAME} MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASSWORD} TZ: Asia/Shanghai command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --default-authentication-pluginmysql_native_password volumes: - mysql_data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d:ro # 首次启动执行的 SQL healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -p${DB_ROOT_PASSWORD}] interval: 5s timeout: 3s retries: 20 start_period: 30s restart: unless-stopped networks: - backend redis: image: redis:7-alpine container_name: redis command: [redis-server, --requirepass, ${REDIS_PASSWORD}, --appendonly, yes] volumes: - redis_data:/data healthcheck: test: [CMD, redis-cli, -a, ${REDIS_PASSWORD}, ping] interval: 5s timeout: 3s retries: 10 restart: unless-stopped networks: - backend volumes: mysql_data: redis_data: networks: frontend: driver: bridge backend: driver: bridge internal: true # backend 网络不通外网只有 nginx 能进出配套的 .envDB_ROOT_PASSWORDChangeMe_Root_2026 DB_USERapp DB_PASSWORDChangeMe_App_2026 DB_NAMEapp REDIS_PASSWORDChangeMe_Redis_2026这份模板里几个细节值得单独说两个网络分离frontend 让 nginx 对外backend 设了 internal: truemysql/redis 只在内部网络里宿主机端口不暴露安全性直接提升一档restart: unless-stopped宿主机重启后容器自动拉起除非你手动 stop 过。比 always 更符合直觉TZ: Asia/Shanghai不设时区容器内默认 UTC日志时间戳会差 8 小时查问题很痛苦MySQL 初始化脚本目录/docker-entrypoint-initdb.d 里的 .sql、.sh 会在数据卷为空时首次启动执行用来建表灌初始数据很方便。注意如果 mysql_data 卷已经有数据脚本不会重跑排查工具速查现象命令看某个服务日志docker compose logs -f --tail200 mysql进容器排查docker compose exec mysql bash看容器状态docker compose ps看容器详细信息docker inspect container_id重启单个服务docker compose restart app重建单个服务docker compose up -d --build app停止并清理保留数据卷docker compose down停止并清理含数据卷docker compose down -v ⚠️ 慎用down -v 那个 -v 一按命名卷全删生产环境执行前三思。小结Compose 出问题基本就三类YAML 语法缩进、拼写、路径/变量解析挂载、${}、.env、服务时序depends_on 不等于就绪。养成三个习惯能规避 80% 的坑写完先 docker compose config 校验敏感值放 .env 不入库有状态服务必写 healthcheck依赖方用 condition: service_healthy上面那份 nginxmysqlredis 模板我自己用了两年改改环境变量和挂载路径就能套到新项目上需要的直接抄。