问题通常不是“起不来”

很多 Docker Compose 问题看起来都很玄。明明 docker compose up -d 没报错,容器也都是 running,应用却连不上 MySQL;改了初始化 SQL,重启十次还是旧表结构;Redis 密码换了,程序日志里还在报旧密码认证失败。

我刚开始用 Compose 搭本地环境时,也把“容器启动成功”当成“环境启动成功”。后来才发现,这是两个概念。容器能跑,只说明进程没立刻退出;服务是否可用、配置是否生效、数据卷里有没有历史包袱,还是要另外检查。

这篇不是 Docker 命令大全,只整理我在本地开发里最常遇到的几类脏环境问题,以及我现在会怎么处理。

先看一个常见 compose 文件

假设项目依赖 MySQL 和 Redis:

services:

  mysql:

    image: mysql:8.0

    container_name: demo-mysql

    environment:

      MYSQL_ROOT_PASSWORD: root123

      MYSQL_DATABASE: demo

      MYSQL_USER: demo

      MYSQL_PASSWORD: demo123

    ports:

      - "3306:3306"

    volumes:

      - mysql_data:/var/lib/mysql

      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro

    healthcheck:

      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"]

      interval: 5s

      timeout: 3s

      retries: 20

  redis:

    image: redis:7

    container_name: demo-redis

    command: ["redis-server", "--requirepass", "redis123"]

    ports:

      - "6379:6379"

    healthcheck:

      test: ["CMD", "redis-cli", "-a", "redis123", "ping"]

      interval: 5s

      timeout: 3s

      retries: 20

volumes:

  mysql_data:

这个文件不复杂,但已经埋了几个容易误判的点。

初始化 SQL 为什么没重新执行

MySQL 官方镜像只会在数据目录为空时执行 /docker-entrypoint-initdb.d/ 下的脚本。也就是说,第一次启动之后,mysql_data 这个 volume 里已经有数据了。你再改 init.sql,然后重启容器,MySQL 不会重新初始化。

所以改初始化脚本后,我会明确选择一种处理方式。

如果只是想保留数据,手动执行迁移脚本:

docker exec -i demo-mysql mysql -uroot -proot123 demo < sql/patch.sql

如果本地数据可以丢,直接删掉 volume:

docker compose down -v

docker compose up -d

这里的 -v 很关键。只执行 docker compose down 会删容器和网络,但不会删命名 volume。旧数据还在,旧问题也还在。

我一般不把这条命令写进一键启动脚本里,因为它会清数据。更稳妥的做法是单独写一个 reset-local-env.ps1 或 reset-local-env.sh,名字里把 reset 写清楚。

服务互联别写 localhost

容器里的 localhost 指的是容器自己,不是你的宿主机,也不是另一个容器。这个坑太常见了。

如果应用也跑在 Compose 网络里,连接 MySQL 应该写服务名:

spring:

  datasource:

    url: jdbc:mysql://mysql:3306/demo?useSSL=false&allowPublicKeyRetrieval=true

    username: demo

    password: demo123

连接 Redis 也一样:

spring:

  data:

    redis:

      host: redis

      port: 6379

      password: redis123

如果应用跑在宿主机,才用 localhost:3306、localhost:6379。这两个场景要分清楚。很多“我本地能连,容器里不能连”的问题,最后都落在这里。

depends_on 不是健康检查

不少人会这样写:

services:

  app:

    depends_on:

      - mysql

      - redis

这只能保证 mysql、redis 容器先启动,不保证服务已经准备好。MySQL 第一次初始化时可能要十几秒,应用抢先连接,启动失败并不奇怪。

Compose 新版本可以配合 healthcheck 使用:

services:

  app:

    build: .

    depends_on:

      mysql:

        condition: service_healthy

      redis:

        condition: service_healthy

但我还是建议应用自己也做重试。基础设施脚本只能减少问题,不能替应用兜底。尤其是 Java、Go 这类长期运行服务,数据库短暂不可用时直接退出,体验很差。

端口冲突别靠猜

本地经常已经装了 MySQL,Compose 又映射 3306:3306,启动时可能直接失败,也可能你连到的根本不是容器里的库。

我现在更喜欢给项目环境换一个不常用端口:

ports:

  - "13306:3306"

宿主机连接就用:

mysql -h 127.0.0.1 -P 13306 -udemo -pdemo123 demo

容器内服务互联仍然用 mysql:3306。宿主机端口和容器内端口不是一回事,写清楚能少很多误会。

我常用的排查顺序

容器启动后,我不会只看 docker ps。一般按这个顺序走:

docker compose ps

docker compose logs mysql --tail=80

docker compose logs redis --tail=80

docker inspect demo-mysql --format='{{json .State.Health}}'

docker volume ls | grep mysql

如果怀疑应用连错地址,再进容器里测网络:

docker compose exec app sh

nc -vz mysql 3306

nc -vz redis 6379

没有 nc 的镜像可以临时起一个调试容器:

docker run --rm -it --network demo_default nicolaka/netshoot

网络名可以通过 docker network ls 看。别凭感觉改配置,先确认请求到底有没有打到目标服务。

给团队留一份说明

本地环境最怕每个人靠记忆操作。我的做法是在仓库里放一个很短的 docs/local-env.md,只写四件事:

• 第一次启动命令;

• 如何查看 MySQL、Redis 连接信息;

• 如何重置本地数据;

• 常见错误对应的排查命令。

不需要写成教程,能让新同事少问两次就够了。

小结

Compose 的价值是把环境变得可复制,但它不会自动让环境变干净。命名 volume、服务名、healthcheck、端口映射,这几个点只要混在一起,问题就会变得很像“玄学”。

我的经验是:先承认本地环境会脏,再给它设计清理方式。能重置、能检查、能解释,比单纯“能启动”更重要。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐