Docker Compose 避坑指南②:网络、数据卷、多环境,8 个坑绕过去少走半年弯路
Docker Compose 网络与数据卷配置避坑指南 本文是 Docker Compose 系列的第二篇,重点讲解网络配置、数据卷管理和多环境切换中的常见问题。网络方面,服务间通信应使用服务名而非 localhost,注意 ports 和 expose 的区别,以及避免默认网络导致的跨项目污染。数据卷配置中,需正确处理挂载目录权限,区分开发与生产环境的挂载需求,警惕 docker compose
系列说明: 这是「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 成了习惯,某天在生产机器上也这么敲了。
解决方案:
- 养成习惯,清楚
-v的含义,生产环境不要随手加 - 重要数据定期备份,不要依赖 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安装手册
更多推荐

所有评论(0)