Docker Compose部署多服务架构怎么规划目录结构
Docker Compose 部署多服务架构,目录结构别等项目大了再补
Docker Compose 用起来很快,写一个 compose.yaml,服务一拉,端口一映射,项目就能跑。但实际使用中发现,真正容易出问题的不是 Compose 语法,而是目录结构。尤其是一个机器上跑 API、Web、MySQL、Redis、Nginx、队列、定时任务、日志采集这些服务时,目录如果一开始随手放,后面排障、迁移、备份、扩容都会很别扭。
比较常见的坑是这样的:compose.yaml 放在 /root 下面,MySQL 数据卷放在默认 volume,Nginx 配置在另一个目录,证书又在 /etc/letsencrypt,业务代码在 /data/app,日志散在容器里。前期看不出来,等到服务器要迁移、磁盘要扩容、服务要拆分时,就开始翻历史命令。
多服务架构下,目录结构要解决的不是“看起来整齐”,而是让部署、备份、回滚、排障、权限管理都有稳定入口。
先按项目边界划目录,不要按服务随便堆
一台服务器可能跑多个业务,比如官网、管理后台、支付回调、游戏 API、内部工具。如果直接在 /data/docker 下面堆 nginx、mysql、redis、api,时间长了很难判断哪个服务属于哪个项目。
更推荐按项目作为一级边界,例如:/opt/projects/shop、/opt/projects/game-api、/opt/projects/internal-tools。每个项目目录内部再放 compose、配置、数据、日志、脚本。这样迁移一个项目时,基本就是打包一个目录,配合数据库备份即可。
实际线上更常见的结构可以这样放:
/opt/projects/myapp
/opt/projects/myapp/compose.yaml
/opt/projects/myapp/.env
/opt/projects/myapp/services
/opt/projects/myapp/config
/opt/projects/myapp/data
/opt/projects/myapp/logs
/opt/projects/myapp/scripts
/opt/projects/myapp/backups
这里补充一点,目录名最好别用 docker-compose 这种泛名称。机器上项目一多,ps、日志、备份脚本里全是 docker-compose,会很难读。项目目录名和 Compose project name 保持一致,后面查容器名、网络名、volume 名都省事。
compose.yaml 放根目录,别藏太深
compose.yaml 建议放在项目根目录。原因很简单,运维人员进到项目目录后,第一眼要知道这个项目由哪些服务组成。compose.yaml 如果藏在 deploy/docker/prod 这种层级里,每次操作都要找路径,自动化脚本也容易写乱。
如果确实有多环境,比如 dev、test、prod,可以这样处理:根目录放 compose.yaml,环境差异放 compose.prod.yaml、compose.test.yaml。启动时用 docker compose -f compose.yaml -f compose.prod.yaml up -d。这样公共部分和环境差异能分开,配置也不会复制三份。
不建议每个服务都单独放一个 compose 文件,然后靠人记启动顺序。Compose 本身就是为多服务编排准备的,把同一个项目内的服务关系写在一处,网络、依赖、端口、volume 才容易看清。
.env 只放部署变量,不要当配置中心用
.env 很方便,但不要把所有配置都塞进去。它适合放部署层变量,比如镜像版本、端口、域名、数据库密码、Redis 密码、环境标识。应用内部复杂配置,例如 Nginx vhost、Prometheus 配置、业务 YAML,放到 config 目录更清楚。
例如 .env 里可以放:
APP_NAME=myapp
APP_ENV=prod
API_IMAGE=registry.example.com/myapp/api:2026-06-01
WEB_IMAGE=registry.example.com/myapp/web:2026-06-01
MYSQL_ROOT_PASSWORD=change_this
REDIS_PASSWORD=change_this
HTTP_PORT=80
HTTPS_PORT=443
多说一句,.env 里有密码时,不要直接提交到 Git。可以提交 .env.example,把字段列出来,真实值由部署机生成或通过 CI/CD 注入。很多事故不是服务被打穿,而是配置仓库里泄漏了数据库密码。
services 目录放构建上下文,不放运行数据
如果项目里有自定义镜像,比如 API、worker、前端构建、Nginx 镜像,可以放到 services 目录。常见结构是 services/api、services/web、services/nginx。每个服务目录内放 Dockerfile、启动脚本、构建依赖文件。
例如:
/opt/projects/myapp/services/api/Dockerfile
/opt/projects/myapp/services/api/entrypoint.sh
/opt/projects/myapp/services/web/Dockerfile
/opt/projects/myapp/services/nginx/Dockerfile
这里要分清一个边界:services 是镜像构建相关内容,不是容器运行时数据。MySQL datadir、Redis dump、上传文件、日志都不应该放在 services 下面。构建内容和运行数据混在一起,备份时会很痛苦,清理镜像时也容易误删数据。
config 目录按服务拆,配置要能肉眼定位
config 目录建议按服务拆分,比如 config/nginx、config/mysql、config/redis、config/supervisor。这样 compose.yaml 里的挂载路径也很直观。
例如 Nginx 可以这样放:
/opt/projects/myapp/config/nginx/nginx.conf
/opt/projects/myapp/config/nginx/conf.d/api.conf
/opt/projects/myapp/config/nginx/conf.d/web.conf
/opt/projects/myapp/config/nginx/certs
MySQL 可以这样放:
/opt/projects/myapp/config/mysql/my.cnf
/opt/projects/myapp/config/mysql/initdb.d
实际排障时,Nginx 502、MySQL 连接数打满、Redis maxmemory 配错,第一反应就是看配置。配置目录如果统一,别人接手时也能快速判断服务行为,不需要进容器里到处 cat。
data 目录只放持久化数据,权限要提前规划
data 目录是最需要谨慎的地方。数据库、Redis、对象存储模拟目录、上传文件都属于持久化数据。Compose 支持 named volume,也支持 bind mount。生产环境里,如果要方便备份和迁移,bind mount 会更直接。
常见结构如下:
/opt/projects/myapp/data/mysql
/opt/projects/myapp/data/redis
/opt/projects/myapp/data/uploads
/opt/projects/myapp/data/minio
MySQL 这类服务要注意 UID/GID。比如官方 mysql 镜像内部用户可能不是宿主机 root,目录权限不对时会报 Permission denied。不要看到容器起不来就 chmod -R 777。更稳的方式是确认镜像内运行用户,再 chown 到对应 UID。
另外,数据库数据目录不要和备份目录混在一起。data/mysql 是在线数据,backups/mysql 是备份文件。备份文件可以压缩、上传、清理;在线数据不能随便动。
logs 目录按服务落盘,别依赖 docker logs 扛全部场景
docker logs 对小项目够用,但在多服务架构里,经常会遇到日志量大、容器重建、排障窗口较长的问题。业务日志、Nginx access log、error log,建议挂载到 logs 目录。
例如:
/opt/projects/myapp/logs/nginx/access.log
/opt/projects/myapp/logs/nginx/error.log
/opt/projects/myapp/logs/api/app.log
/opt/projects/myapp/logs/worker/worker.log
日志量要有预估。一个中等访问量 API,如果每秒 100 条 access log,每条 300 字节,一天大约 2.5GB。Nginx、API、worker 都写日志时,60GB 系统盘很快就会被打满。实际部署时,logrotate 必须配,保留周期也要按业务定。比如 access log 保留 7 天,错误日志保留 30 天,审计日志单独归档。
scripts 目录放运维动作,命令不要散在聊天记录里
项目跑起来后,常用动作会越来越多:启动、停止、拉镜像、备份数据库、恢复数据库、清理日志、刷新证书、健康检查。如果这些命令只存在于某个人的历史记录里,交接时一定出问题。
scripts 目录可以放这些脚本:
/opt/projects/myapp/scripts/deploy.sh
/opt/projects/myapp/scripts/backup-mysql.sh
/opt/projects/myapp/scripts/restore-mysql.sh
/opt/projects/myapp/scripts/rotate-logs.sh
/opt/projects/myapp/scripts/check-health.sh
脚本不一定要写得很复杂,但要可重复执行。比如 backup-mysql.sh 里明确容器名、数据库名、备份输出目录、保留天数。不要每次手敲 docker exec mysqldump,凌晨排障时很容易输错库名。
backups 目录只做中转,别把它当长期归档
本机 backups 目录适合做短期中转,比如保留最近 3 到 7 天。长期备份应该放到对象存储、异地服务器或备份专用机器。尤其是 MySQL 和上传文件都在同一台宿主机时,硬盘损坏会让在线数据和备份一起消失。
目录可以这样放:
/opt/projects/myapp/backups/mysql
/opt/projects/myapp/backups/redis
/opt/projects/myapp/backups/uploads
备份文件命名建议带环境、服务、时间,例如 myapp-prod-mysql-20260601-030000.sql.gz。恢复时能看懂,不需要打开文件猜。
网络规划要写进 Compose,不要让容器全挤默认网络
Compose 默认会给项目创建一个网络,简单项目够用。但多服务架构里,最好按访问关系拆网络。比如 frontend 网络只给 Nginx 和 Web/API 使用,backend 网络给 API、MySQL、Redis、worker 使用。MySQL 不需要暴露到 frontend,更不需要映射到宿主机公网端口。
常见关系是:Nginx 暴露 80/443,API 只在内部网络开放 8080,MySQL 只给 API 和 worker 访问,Redis 只给 API 和 worker 访问。宿主机上不映射 MySQL 3306,减少被扫描的面。
实际使用中发现,很多云服务器被爆破不是因为应用漏洞,而是 MySQL、Redis、Elasticsearch 这类服务直接映射公网。Compose 里端口写 ports 就会暴露到宿主机,内部访问用 expose 或者只写容器端口即可。
镜像版本要固定,latest 在生产环境很危险
目录结构规划时,镜像版本也要纳入习惯。compose.yaml 里不要写 mysql:latest、redis:latest、nginx:latest。某次重新拉镜像后,版本变了,配置兼容性、数据格式、默认参数都可能变化。
更稳的写法是 mysql:8.0.36、redis:7.2.4、nginx:1.26.0。业务镜像用构建号或日期标签,例如 api:2026-06-01-1730。这样回滚时能明确回到哪个版本。
如果是 CI/CD 发布,.env 里放镜像 tag,deploy.sh 负责替换或读取。目录结构不只是文件怎么摆,也影响发布流程是否可控。
单机多服务时,服务器规格要按瓶颈选
Docker Compose 很适合单机多服务部署,但单机不是无限塞服务。规划目录时,也要顺手把资源边界想清楚。API、Nginx、Redis、worker 通常吃 CPU 和内存;MySQL 吃内存和磁盘 I/O;文件下载、图片分发、游戏补丁分发吃带宽。
举个常见场景:一个管理后台加 API,加 MySQL 和 Redis,日活不高但要求稳定,4C8G 会比 2C2G 舒服很多。MySQL buffer pool、Redis 缓存、系统 page cache 都需要内存,2G 内存跑起来没问题,但一遇到备份、构建镜像、日志压缩,swap 很容易飙。
如果业务面向海外用户,或者静态资源下载多,带宽会比 CPU 更先成为瓶颈。像美国节点、香港节点这类线路,实际体验差距很明显。购买服务器时可以结合访问区域选,如果你也在找这种适合 Docker Compose 多服务部署的云服务器,可以看看129云。比如美国精品大宽带-A型是 2C2G、60G SSD、1Gbps 峰值、500GB 流量,适合轻量 API、落地页、海外测试环境;香港大宽带-E型是 16C16G、300Mbps 峰值、1TB 流量,更适合多服务合并部署、跨境访问、前后端加数据库一体化场景。
如果业务容易被打,比如游戏登录服、活动页、支付回调入口,DDoS 防护也要考虑进去。泉州电信-B型提供 4C8G、60G SSD、20Mbps 峰值、100Gbps 单机防御,更适合电信用户为主、需要高防入口的场景。具体线路和防护策略可以咨询129云客服,电话 400-9177118。
一个项目目录里的 Compose 示例应该长什么样
下面这种结构在中小型项目里比较常见,服务关系清楚,备份和迁移也方便。
/opt/projects/myapp/compose.yaml
/opt/projects/myapp/.env
/opt/projects/myapp/config/nginx/conf.d/default.conf
/opt/projects/myapp/config/mysql/my.cnf
/opt/projects/myapp/data/mysql
/opt/projects/myapp/data/redis
/opt/projects/myapp/data/uploads
/opt/projects/myapp/logs/nginx
/opt/projects/myapp/logs/api
/opt/projects/myapp/scripts/deploy.sh
/opt/projects/myapp/scripts/backup-mysql.sh
/opt/projects/myapp/backups/mysql
compose.yaml 里服务可以包括 nginx、api、worker、mysql、redis。Nginx 加入 frontend 和 backend,API 加入 backend,MySQL 和 Redis 只加入 backend。宿主机只暴露 Nginx 的 80 和 443。
这里有个小细节,container_name 不一定要写。Compose 默认容器名带项目名和服务名,例如 myapp-api-1。写死 container_name 后,横向扩容副本会受限制,也容易和其他项目冲突。如果没有强依赖固定容器名,保留默认命名更灵活。
环境隔离不要靠改文件,靠文件叠加和变量
很多团队会维护 compose-dev.yaml、compose-prod.yaml 两份几乎一样的文件。时间一长,一边改了健康检查,另一边忘了;一边更新了 volume,另一边还是旧路径。更好的方式是公共配置放 compose.yaml,环境差异用 override 文件叠加。
开发环境可以挂载源码、开放调试端口;生产环境使用固定镜像、限制端口、挂载生产配置。比如 dev 暴露 API 8080,prod 不暴露 API,只让 Nginx 转发。这样环境差异明确,不会把调试端口带到公网。
.env 也可以分环境保存成 .env.dev、.env.prod,但真实生产密码仍然不要进代码仓库。部署时复制为 .env,或者由 CI/CD 在服务器生成。
证书和域名配置要独立出来
HTTPS 证书不要散落在 Nginx 容器里。证书应该在宿主机项目目录下有明确位置,例如 config/nginx/certs 或者统一放 /opt/certs,再挂载到 Nginx。自动续期脚本也要能找到证书路径。
如果同一台机器跑多个项目,可以选择统一证书目录,例如 /opt/certs/example.com/fullchain.pem,然后各项目 Nginx 只读挂载。这样证书续期集中处理,不用每个项目都跑一套 certbot。
需要注意权限,私钥文件不要给过宽权限。Nginx 容器只需要读证书,不需要写证书目录。
健康检查和依赖关系别只靠 depends_on
Compose 里的 depends_on 只能解决启动顺序,不代表服务已经可用。MySQL 容器启动了,不代表数据库已经可以连接;Redis 进程起来了,也不代表应用初始化已经完成。
生产环境建议给关键服务加 healthcheck。API 可以提供 /health,MySQL 用 mysqladmin ping,Redis 用 redis-cli ping。worker 启动前也可以在 entrypoint 里等待数据库可用。
目录层面可以把健康检查脚本放到 scripts 或 services 对应目录里,不要把复杂命令都写在 compose.yaml 里。compose.yaml 太长后,可读性会下降。
权限模型要简单,root 能不用就不用
很多 Compose 项目默认全用 root 跑,短期省事,长期麻烦。业务容器如果能用普通用户运行,就不要用 root。挂载目录时,提前规划 UID/GID,尤其是 uploads、logs、cache 这些需要写入的目录。
常见做法是在宿主机创建 deploy 用户,项目目录归 deploy 管理。数据库数据目录按镜像要求设置权限,Nginx 证书只读挂载,业务上传目录只给业务容器写。权限边界清楚后,即使某个服务被打穿,影响面也更小。
不要为了省事给整个 /opt/projects chmod -R 777。这个命令执行一次,后面很多安全问题就埋下了。
迁移服务器时,目录结构会直接决定工作量
如果目录规划清楚,迁移通常就是停服务、备份数据库、同步项目目录、调整 .env、拉镜像、启动服务、验证健康检查。rsync 同步 /opt/projects/myapp 时,config、data、logs、scripts 都在一个边界内,操作可控。
如果目录分散,迁移会变成找配置、找数据、找证书、找脚本。尤其是 Docker named volume,用 docker volume inspect 才知道真实路径,不熟的人很容易漏掉。
数据量较大时,不建议直接停机全量拷贝。可以先同步静态文件和配置,再做数据库备份恢复,或者用主从复制、逻辑备份降低停机窗口。比如 50GB MySQL 数据,用普通公网 20Mbps 传输,理论上就要 5 小时以上,还没算压缩、抖动和校验时间。带宽规划不够,迁移窗口会被拉得很长。
目录命名别追求花哨,排障时看得懂最重要
目录命名推荐使用 config、data、logs、scripts、backups、services 这种直白名称。不要一会儿 conf,一会儿 configs,一会儿 runtime,一会儿 storage。团队里每个人的理解不一样,越花哨越容易放错。
服务名也尽量和 Compose service 名一致。compose.yaml 里叫 api,目录就叫 api;服务叫 worker,日志目录也叫 worker。排障时从 docker compose ps 看到服务名,能直接定位到对应配置和日志目录。
别把宿主机当垃圾桶,清理策略也要写进项目
Docker Compose 项目跑久了,磁盘被吃满通常来自三类东西:日志、镜像、备份。日志不轮转,镜像不清理,备份不删除,任何 60G SSD 都撑不了太久。
建议在 scripts 里放清理脚本,但清理动作要保守。镜像可以清理 dangling image,备份按时间删除,日志交给 logrotate。不要在脚本里直接 docker system prune -a 不加过滤,可能把回滚需要的镜像也删掉。
磁盘监控也要配。哪怕只是 cron 每小时 df -h 检查,超过 80% 发告警,也比等 MySQL 写不进去再处理强。Compose 项目本身不复杂,复杂的是它长期运行后的状态管理。
多人协作时,README 要写部署入口,不写长篇理论
项目根目录放 README.md 很有必要。内容不用长,写清楚服务列表、端口、目录说明、启动命令、备份命令、恢复命令、常见排障入口即可。
例如写明:启动用 docker compose up -d,查看日志用 docker compose logs -f api,备份 MySQL 用 scripts/backup-mysql.sh,Nginx 配置在 config/nginx/conf.d,上传文件在 data/uploads。新人接手时,不需要从 compose.yaml 里一点点猜。
README 里也可以标注服务器规格、系统版本、Docker 版本、Compose 版本。Compose V1 和 Compose V2 命令不同,老环境里 docker-compose,新环境里 docker compose,脚本里要统一。
生产目录可以从这份结构起步
/opt/projects/myapp
/opt/projects/myapp/README.md
/opt/projects/myapp/compose.yaml
/opt/projects/myapp/compose.prod.yaml
/opt/projects/myapp/.env
/opt/projects/myapp/services/api
/opt/projects/myapp/services/web
/opt/projects/myapp/config/nginx
/opt/projects/myapp/config/mysql
/opt/projects/myapp/config/redis
/opt/projects/myapp/data/mysql
/opt/projects/myapp/data/redis
/opt/projects/myapp/data/uploads
/opt/projects/myapp/logs/nginx
/opt/projects/myapp/logs/api
/opt/projects/myapp/logs/worker
/opt/projects/myapp/scripts/deploy.sh
/opt/projects/myapp/scripts/backup-mysql.sh
/opt/projects/myapp/scripts/restore-mysql.sh
/opt/projects/myapp/scripts/check-health.sh
/opt/projects/myapp/backups/mysql
这个结构不要求所有目录一开始都有内容,但边界要先定下来。后面服务增加,比如加 RabbitMQ、Prometheus、Grafana、Elasticsearch,就按同样规则扩展:配置进 config,数据进 data,日志进 logs,脚本进 scripts。不要因为临时上线一个服务,就把配置扔到 /tmp 或 /root 下面。
权限、备份、日志轮转、证书续期这些动作,最好在服务上线当天就处理。等业务有流量后再改目录,风险会高很多。