系列说明: 这是「Docker Compose 避坑指南」三篇系列的第二篇。上一篇讲了 YAML 语法、依赖控制和环境变量 10 个基础坑,还没看过的建议先补一下。本篇进入网络配置、数据卷管理和多环境切换,这几块踩了坑往往要在运行时才暴露,比基础坑更难排查。

上一篇的坑,绝大多数在 docker compose up 的时候就能发现。这一篇要讲的坑不一样——它们往往在「能跑起来」之后才暴露:服务间连不上、数据丢了、生产拉了开发配置……等你发现的时候,已经耽误了。

一共 8 个坑,直接开始。


环境说明

项目 版本
Docker Engine 24.x / 25.x / 26.x
Docker Compose v2.x(docker compose 命令)
操作系统 Ubuntu 22.04 / CentOS 7·8

一、网络配置坑

坑 1:服务间用 `localhost` 互联,连接拒绝

报错现象:

Connection refused: localhost:6379
connect ECONNREFUSED 127.0.0.1:3306

根本原因:

容器里的 localhost 指向的是容器自身的网络接口,不是宿主机,也不是其他容器。每个容器都是独立的网络命名空间。

解决方案:

同一个 Compose 网络内,服务之间用服务名作为主机名,Docker 内置 DNS 会自动解析:

services:
  app:
    environment:
      REDIS_HOST: redis        # 用服务名,不是 localhost
      MYSQL_HOST: db           # 用服务名

  redis:
    image: redis:7-alpine

  db:
    image: mysql:8.0
# 应用代码里对应修改
redis_client = redis.Redis(host='redis', port=6379)
db_conn = pymysql.connect(host='db', port=3306)

验证服务名解析是否正常:

# 进入容器,直接 ping 其他服务名
docker compose exec app ping db
docker compose exec app ping redis

坑 2:`expose` 和 `ports` 混淆,端口对外不通

问题场景:

以为用了 expose 就能从宿主机访问,实际外部完全连不上。

根本原因:

两者作用完全不同:

# expose:只在 Docker 容器内部网络暴露端口
# 其他容器可以访问,宿主机和外部无法访问
expose:
  - "8080"

# ports:将容器端口映射到宿主机端口
# 外部可以通过宿主机 IP 访问
ports:
  - "8080:8080"              # 所有网卡监听
  - "127.0.0.1:8080:8080"   # 只监听本机,不对公网暴露

解决方案:

按照服务的访问需求选择:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"       # 对外服务,需要 ports
      - "443:443"

  app:
    image: myapp:latest
    expose:
      - "3000"        # 只供 nginx 内部代理,不需要对外,用 expose

  db:
    image: postgres:16
                      # 数据库什么都不写,只在容器网络内可访问(最安全)

生产环境中,数据库、Redis、内部 API 等服务不应该暴露端口到宿主机。只有最终对外的网关(Nginx/Traefik)才需要 ports


坑 3:默认网络导致跨项目服务名污染

根本原因:

不显式定义网络时,Compose 会创建一个叫 项目名_default 的网络。同一台机器上多个 Compose 项目,如果你把外部网络搞混,可能访问到其他项目的同名服务。

另一个常见场景:两个 Compose 项目需要共享某个基础设施服务(如公共的 Redis),但服务之间网络不通。

解决方案:

显式定义网络,按职责分层隔离:

services:
  app:
    networks:
      - frontend
      - backend        # app 同时加入两个网络

  db:
    networks:
      - backend        # db 只加入 backend,无法从 frontend 直接访问

  nginx:
    networks:
      - frontend       # nginx 只负责前端流量

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true     # internal: true,该网络无法访问外网,增强安全性

需要跨项目共享网络时,使用外部网络:

# 先手动创建共享网络
docker network create shared-infra
# 两个 compose 项目都引用这个外部网络
networks:
  shared-infra:
    external: true

二、数据卷配置坑

坑 4:bind mount 权限不对,容器写入报 Permission denied

报错现象:

Permission denied: /data/mysql
mkdir: cannot create directory '/var/lib/mysql': Permission denied

根本原因:

容器内的数据库进程通常以特定用户运行(MySQL/PostgreSQL 用 UID=999 的用户),但宿主机挂载目录的属主是 root 或当前用户(UID 不同),容器内进程没有写权限。

解决方案:

方案一:提前在宿主机设置正确的属主:

# MySQL/PostgreSQL 容器内用户 UID 通常为 999
sudo mkdir -p /data/mysql
sudo chown -R 999:999 /data/mysql

方案二(更推荐):改用 named volume,让 Docker 自动处理权限问题:

services:
  db:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql    # named volume,Docker 自动初始化权限

volumes:
  mysql_data:
    driver: local

查看 named volume 存放位置:

docker volume inspect projectname_mysql_data

坑 5:开发时挂载代码目录,生产环境忘了去掉

根本原因:

开发时为了热重载,把本地源码 bind mount 进容器:

# 开发用,方便修改代码实时生效
volumes:
  - ./src:/app/src

这段配置如果跟着上了生产,容器运行的是宿主机 ./src 目录的代码,而不是镜像里打包好的代码。代码更新了镜像,生产容器却还在跑旧代码。

解决方案:

用 Compose 文件覆盖机制,开发专用配置单独放,不混入基础文件:

# compose.yaml(基础配置,开发生产共用,不含 volumes 挂载)
services:
  app:
    image: myapp:${APP_VERSION:-latest}
    environment:
      NODE_ENV: ${NODE_ENV:-production}
# compose.override.yaml(开发专用,docker compose up 自动加载)
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app/src        # 只在开发环境挂载源码
    environment:
      NODE_ENV: development
    ports:
      - "3000:3000"

生产环境启动时明确指定文件,不加载 override:

# 生产:明确指定,不会自动加载 override
docker compose -f compose.yaml -f compose.prod.yaml up -d

坑 6:`docker compose down` 加了 `-v` 把数据库数据一起删了

报错现象:

这个没有报错,数据库直接空了。

根本原因:

docker compose down        # 停止并删除容器,volume 保留(安全)
docker compose down -v     # 停止并删除容器 + volume(数据全丢!)

很多人跟着教程敲 -v 成了习惯,某天在生产机器上也这么敲了。

解决方案:

  1. 养成习惯,清楚 -v 的含义,生产环境不要随手加
  2. 重要数据定期备份,不要依赖 volume 作为唯一存储

备份 named volume 数据:

# 备份 MySQL 数据
docker compose exec db mysqldump -u root -p${MYSQL_ROOT_PASSWORD} --all-databases > backup.sql

# 备份整个 volume(通用方法)
docker run --rm \
  -v projectname_mysql_data:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/mysql_backup.tar.gz /data

三、多环境切换坑

坑 7:多份完整 Compose 文件管理多环境,公共配置改了漏同步

常见的错误结构:

项目/
├── docker-compose.dev.yml    # 完整的开发配置
├── docker-compose.prod.yml   # 完整的生产配置(大量重复内容)
└── docker-compose.test.yml   # 完整的测试配置

改了数据库版本,3 个文件都得改,改漏一个就出问题。

解决方案:

改用「基础文件 + 差异覆盖」结构,公共配置只写一遍:

项目/
├── compose.yaml              # 基础配置(所有环境共用)
├── compose.override.yaml     # 开发覆盖(本地自动加载)
├── compose.prod.yaml         # 生产覆盖
└── compose.staging.yaml      # 测试环境覆盖
# compose.yaml(基础,干净,无环境差异)
services:
  app:
    image: myapp:${APP_VERSION:-latest}
    networks:
      - backend
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16.2-alpine    # 版本只在这里写一次
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10
    networks:
      - backend

networks:
  backend:
# compose.override.yaml(开发专用)
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app/src
    environment:
      NODE_ENV: development
      LOG_LEVEL: debug
    ports:
      - "3000:3000"

  db:
    ports:
      - "5432:5432"         # 开发时暴露给本机调试工具
    environment:
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myapp_dev
# compose.prod.yaml(生产专用)
services:
  app:
    restart: unless-stopped
    environment:
      NODE_ENV: production
      LOG_LEVEL: warn
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 1G

  db:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: myapp
    volumes:
      - /data/postgres:/var/lib/postgresql/data
# 开发:自动加载 compose.yaml + compose.override.yaml
docker compose up

# 生产:明确指定,不加载 override
docker compose -f compose.yaml -f compose.prod.yaml up -d

坑 8:CI/CD 和本地行为不一致,「本地好好的,流水线挂了」

根本原因:

本地有 compose.override.yaml 自动被加载,CI 环境没有;或者本地 .env 有某个变量,CI 没配;导致同一份代码两边行为不同。

解决方案:

CI/CD 脚本中明确指定所有文件和变量来源,杜绝隐式加载:

#!/bin/bash
# deploy.sh — 生产部署脚本

set -euo pipefail

docker compose \
  -f compose.yaml \
  -f compose.prod.yaml \
  --env-file .env.prod \
  pull

docker compose \
  -f compose.yaml \
  -f compose.prod.yaml \
  --env-file .env.prod \
  up -d --remove-orphans

GitHub Actions 示例:

# .github/workflows/deploy.yml
- name: Deploy
  env:
    POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
    APP_VERSION: ${{ github.sha }}
  run: |
    docker compose \
      -f compose.yaml \
      -f compose.prod.yaml \
      up -d --build --remove-orphans

快速检查清单

网络配置

x服务间通信用服务名,不用 localhost

x数据库等内部服务不配置 ports(只用 expose 或不写)

x按职责显式定义网络,避免所有服务混在同一网络

    数据卷

    x数据库等需要持久化的服务挂载了 named volume

    x开发用的 bind mount 不出现在生产配置里

    x明确 docker compose down -v 的危险,生产环境不随手加 -v

    x重要数据有定期备份策略

      多环境

      x使用基础文件 + 差异覆盖,公共配置不重复

      xCI/CD 脚本明确指定所有 -f--env-file,不依赖自动加载

      x.env.example 和生产实际变量保持同步


        常见问题 Q&A

        Q1:named volume 和 bind mount 怎么选?

        场景 推荐
        数据库数据持久化 named volume(权限、可移植性好)
        开发时挂载源码热重载 bind mount(需要直接读宿主机文件)
        配置文件注入 bind mount(单个文件,只读)
        生产环境数据持久化 named volume 或指定宿主机路径
        # 只读挂载配置文件
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro    # :ro 表示只读
        

        Q2:如何排查两个容器之间的网络问题?

        # 查看容器连接的网络
        docker inspect container_name | grep -A 30 '"Networks"'
        
        # 查看某个网络内的所有容器
        docker network inspect projectname_backend
        
        # 进入容器内部测试连通性
        docker compose exec app sh
        ping db
        curl http://nginx:80/health
        telnet redis 6379
        

        Q3:compose.override.yaml 是否应该提交到 Git?

        视团队情况:

        • 提交:适合团队开发环境高度统一,override 就是标准开发配置的情况
        • 不提交:适合每个人本地环境不同,override 是个人定制的情况。这时提供 compose.override.yaml.example 模板,每人自己复制一份

        小结

        网络和数据卷的坑,本质上都是对「容器隔离」理解不到位——容器有自己的网络命名空间,有自己的文件系统,和宿主机不是一回事。把这个模型在脑子里建立清楚,大部分问题就能在写配置的时候预判到。

        下一篇预告: 健康检查写法细节 + 上生产前必须处理的 8 个安全、稳定性问题——镜像版本、日志膨胀、资源限制、非 root 运行……踩过一次都是教训,下篇直接告诉你怎么绕开。

        更多《软件安装&避坑&实践》系列,公众号有图文详解

        关注公众号:IT安装手册

        Logo

        腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

        更多推荐