很多人学 Docker Compose,学到最后只会一句:
docker compose up -d
看起来像会了,实际上离"能上线、能稳定、能维护、能回滚"的标准部署,还差很远。
真正的线上部署,不是把容器拉起来就完事。你要考虑的东西远比启动命令多:
- 项目目录怎么组织
- 环境变量怎么管理
- 数据卷怎么挂载
- 数据库和缓存怎么接
- Nginx 怎么反向代理
- 日志怎么控制
- 服务重启怎么办
- 更新版本怎么回滚
- 机器重启后能不能自恢复
- 哪一步出问题,应该先查哪里
说白了,很多"Docker Compose 部署失败",根本不是 Compose 不行,而是部署流程压根没标准化。
一、什么是 Docker Compose,为什么部署时必须学会它
Docker 解决的是"把应用装进容器里跑"的问题。 而 Docker Compose 解决的是"多个容器如何一起协作"的问题。
一个真实项目,往往不只一个服务:
- Web 应用
- 数据库
- Redis
- Nginx
- 定时任务
- 后台管理服务
- 文件存储服务
如果你靠一个一个 docker run 去启动,先不说麻烦,光是网络、环境变量、挂载、端口、重启策略,就足够把你绕晕。
Compose 的意义就在这里:你把这些服务写进一个 docker-compose.yml 文件里,统一定义镜像、端口、环境变量、数据卷、网络、启动顺序、健康检查、日志策略,然后通过一条命令完成整套服务编排。
所以,Docker 是零件,Docker Compose 才是部署工程。
二、上线前先搞清楚:什么才叫"标准部署"
很多新手对"上线"的理解太粗糙了。
真正的标准部署,至少要满足下面几点:
- 重启后能自动恢复 - 服务器重启后,服务不能靠你手动一个个再拉起来
- 配置和代码分离 - 环境变量、数据库密码、域名配置,不能硬写死在镜像里
- 数据持久化 - 数据库、上传文件、日志目录,不能随着容器销毁一起消失
- 反向代理清晰 - Nginx、应用、数据库之间的边界要明确
- 便于维护和升级 - 新版本上线不应该靠瞎改容器,更不能一升级就全站瘫痪
- 出问题可排查 - 日志、健康检查、端口、网络、挂载,都要有标准查看路径
一句话概括:真正的部署,不是把服务跑起来,而是把服务跑稳。
三、标准的 Docker Compose 部署架构长什么样
最常见、最实用的生产部署结构:
用户请求
↓
Nginx(反向代理、静态资源、HTTPS)
↓
App(你的 Web 应用)
↓
MySQL / PostgreSQL(数据库)
↓
Redis(缓存 / 会话 / 队列)
这一套架构,是绝大多数中小型项目最常见的部署形态。
四、服务器环境准备
正式开始之前,先准备一台 Linux 服务器。常见系统都可以:
- Ubuntu 22.04 / 24.04
- Debian 12
- Rocky Linux / AlmaLinux
建议最低配置:
- CPU:2 核
- 内存:2GB 起步
- 磁盘:40GB 起步
- 系统:64 位 Linux
安装 Docker
Ubuntu / Debian 示例:
sudo apt update
sudo apt install -y docker.io docker-compose-plugin
启动并设置开机自启:
sudo systemctl enable docker
sudo systemctl start docker
检查版本:
docker --version
docker compose version
五、项目目录怎么设计,决定你以后会不会乱
很多人的部署目录像垃圾堆。配置混着代码,日志混着数据,备份目录到处乱飞。出问题时连该去哪儿看都不知道。
推荐你从一开始就按标准结构来:
/opt/myapp/
├── docker-compose.yml
├── .env
├── app/
│ ├── Dockerfile
│ ├── package.json
│ ├── src/
│ └── ...
├── nginx/
│ ├── default.conf
│ └── certs/
├── data/
│ ├── mysql/
│ ├── redis/
│ └── uploads/
├── logs/
│ ├── nginx/
│ └── app/
└── backup/
这个结构的好处非常直接:
- app/ 放业务代码
- nginx/ 放反代配置
- data/ 放持久化数据
- logs/ 放日志
- backup/ 放备份
- .env 放环境变量
- docker-compose.yml 作为总控文件
部署最怕乱。目录一乱,排查就开始变成赌博。
六、先写应用镜像:Dockerfile 才是镜像的根
Compose 负责编排,镜像本身还是靠 Dockerfile 来定义。
下面给一个 Node.js Web 项目的示例 Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
这个 Dockerfile 的逻辑很清晰:
- 使用 Node 20 Alpine 作为基础镜像
- 设置工作目录为 /app
- 先复制依赖描述文件
- 安装生产依赖
- 再复制项目代码
- 暴露 3000 端口
- 启动应用
为什么先复制 package*.json
因为这样可以利用 Docker 构建缓存。如果代码改了,但依赖没改,安装依赖这一层就不用重新跑,构建速度快很多。
别忘了 .dockerignore
很多人 Dockerfile 写得不错,结果构建目录一锅粥,全被带进镜像。
推荐加一个 .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose.yml
.env
logs
data
backup
否则你的镜像会越来越胖,构建越来越慢,纯属给自己找事。
七、编写 .env 文件:配置不要写死在 Compose 里
部署项目时,最忌讳把密码、数据库地址、密钥直接写死在配置文件里。
应该使用 .env 文件统一管理环境变量。
示例:
APP_NAME=myapp
APP_PORT=3000
NODE_ENV=production
DB_HOST=db
DB_PORT=3306
DB_NAME=myapp
DB_USER=myapp_user
DB_PASSWORD=your_strong_password
REDIS_HOST=redis
REDIS_PORT=6379
TZ=Asia/Shanghai
这样做有几个好处:
- 配置和代码分离
- 不同环境可复用同一套 Compose
- 改配置不一定要改镜像
- 更方便迁移和维护
八、核心来了:编写 docker-compose.yml
下面给你一套典型的生产级基础模板。
version: "3.9"
services:
app:
build:
context: ./app
dockerfile: Dockerfile
container_name: myapp_app
restart: unless-stopped
env_file:
- .env
environment:
TZ: Asia/Shanghai
PORT: 3000
DB_HOST: db
DB_PORT: 3306
DB_NAME: myapp
DB_USER: myapp_user
DB_PASSWORD: your_strong_password
REDIS_HOST: redis
REDIS_PORT: 6379
expose:
- "3000"
depends_on:
- db
- redis
volumes:
- ./data/uploads:/app/uploads
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 3s
retries: 5
logging:
options:
max-size: "10m"
max-file: "3"
db:
image: mysql:8.0
container_name: myapp_db
restart: unless-stopped
environment:
TZ: Asia/Shanghai
MYSQL_DATABASE: myapp
MYSQL_USER: myapp_user
MYSQL_PASSWORD: your_strong_password
MYSQL_ROOT_PASSWORD: your_root_password
command:
[
"--default-authentication-plugin=mysql_native_password",
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci"
]
volumes:
- ./data/mysql:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pyour_root_password"]
interval: 10s
timeout: 5s
retries: 10
logging:
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
container_name: myapp_redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- ./data/redis:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
logging:
options:
max-size: "10m"
max-file: "3"
nginx:
image: nginx:1.27-alpine
container_name: myapp_nginx
restart: unless-stopped
depends_on:
- app
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
- ./logs/nginx:/var/log/nginx
logging:
options:
max-size: "10m"
max-file: "3"
九、这份 Compose 配置,关键点到底在哪
很多人会抄配置,但不明白为什么这么写。不懂原理,出了问题就只会发呆。
下面把关键点拆开说。
1. restart: unless-stopped
restart: unless-stopped
作用:容器异常退出后自动重启;服务器重启后也会自动恢复。 这几乎是生产环境必备。
2. env_file
env_file:
- .env
作用:从 .env 文件统一加载配置。这样你切换环境时,改 .env 就行,不必改 Compose 主文件。
3. expose 和 ports 的区别
expose:
- "3000"
expose 只在容器网络内部可见,不对宿主机开放。
ports:
- "80:80"
ports 才是映射到宿主机,对外暴露。
所以:
- App 服务通常只给 Nginx 访问,用 expose
- Nginx 要让外部访问,用 ports
4. depends_on 不是"服务一定可用"的保证
depends_on:
- db
- redis
它只能保证启动顺序,不保证数据库已经 ready。
这也是为什么还要加 healthcheck,并且应用自己最好也要带重试机制。
很多项目的问题就出在这里:应用比数据库起得快,然后启动瞬间连接失败,容器开始疯狂重启。
5. volumes 负责持久化
volumes:
- ./data/mysql:/var/lib/mysql
如果没有这个挂载,MySQL 数据会放在容器层里。容器一删,数据跟着走,你就知道什么叫"秒变事故现场"。
6. logging 用来防止日志把磁盘打爆
logging:
options:
max-size: "10m"
max-file: "3"
非常关键。Docker 默认日志文件如果不限制,写嗨了能把磁盘直接撑满。磁盘一满,最先发疯的不是你,是数据库和系统服务。
十、Nginx 反向代理怎么写才像回事
nginx/default.conf 示例:
server {
listen 80;
server_name your-domain.com www.your-domain.com;
client_max_body_size 50m;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
这里最核心的一句是:
proxy_pass http://app:3000;
注意不是 127.0.0.1,不是服务器公网 IP,而是 Compose 服务名 app。
在 Compose 网络里,服务之间互访靠的就是服务名。
为什么很多人会配出 502
因为他们把 Nginx 写成了:
proxy_pass http://127.0.0.1:3000;
然后 Nginx 又跑在容器里。结果这个 127.0.0.1 指向的是 Nginx 容器自己,不是 App 容器。打不通,502,理所当然。
Docker 部署里,很多报错不是玄学,是你根本没搞懂"谁的 localhost"。
十一、正式启动:从构建到运行的标准命令
进入项目根目录:
cd /opt/myapp
先检查 Compose 配置是否能正确渲染:
docker compose config
这一条强烈建议养成习惯。很多 YAML 缩进、变量替换、格式错误,在这里就能提前发现。
构建镜像
docker compose build
如果你不需要重新构建,只拉镜像:
docker compose pull
启动服务
docker compose up -d
查看运行状态
docker compose ps
查看日志
docker compose logs -f
如果只看某个服务:
docker compose logs -f app
docker compose logs -f nginx
docker compose logs -f db
十二、第一次上线后,必须做的 8 个检查
很多人把服务拉起来就关窗口了。这不叫部署完成,这叫把事故预埋好了。
上线后至少做下面这些检查:
1. 检查容器状态
docker ps -a
确认没有 Exited、Restarting 之类的异常状态。
2. 检查应用健康接口
curl http://127.0.0.1:3000/health
如果应用没健康接口,建议补一个。生产环境没有健康检查,等于闭着眼开车。
3. 检查 Nginx 转发
curl -I http://127.0.0.1
看是否正常返回 200、301、302 等预期状态码。
4. 检查数据库连接
进入应用容器里,看程序是否能正常连库:
docker exec -it myapp_app sh
必要时测试环境变量:
env | grep DB_
5. 检查数据卷是否落地
ls -lah ./data/mysql
ls -lah ./data/uploads
ls -lah ./data/redis
看到文件真实写进宿主机,才算持久化成功。
6. 检查端口监听
ss -ltnp
确认 80、443、3306、6379 等端口状态是否符合预期。
7. 检查磁盘空间
df -h
docker system df
部署当天就看这两条,远比等磁盘爆了再看体面得多。
8. 检查重启恢复能力
执行一次:
docker compose restart
或者重启服务器后验证服务是否自动恢复。
如果重启完站点就没了,那你这套部署就是纸糊的。
十三、HTTPS 怎么上
如果你是正式站点,HTTPS 基本是必需项。最常见做法有两种:
方案一:宿主机申请证书,挂载进 Nginx 容器
你可以用 certbot 在宿主机申请证书,然后把证书目录挂载进 Nginx。
Compose 里已经预留了:
- ./nginx/certs:/etc/nginx/certs:ro
然后在 Nginx 配置中启用 443:
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
方案二:使用专门的自动证书方案
比如:
- Nginx Proxy Manager
- Traefik
- Caddy
这类工具更自动化,但对新手来说复杂度也会提升。
十四、应用更新时,正确的上线流程是什么
别再 SSH 上去一通乱改了。标准上线应该是这样的:
1. 拉取最新代码
git pull origin main
2. 重新构建镜像
docker compose build app
3. 重启应用服务
docker compose up -d app
或者整套更新:
docker compose up -d --build
4. 观察日志
docker compose logs -f app
5. 验证页面、接口、数据库连接
确认没问题再离开。
十五、回滚怎么做,才不至于上线翻车后满地找牙
上线永远不是"成功 or 结束",而是"成功 or 回滚"。
如果你没有回滚方案,说明你根本没准备好上线。
最基础的回滚策略
1. 镜像版本化
别永远使用:
image: myapp:latest
建议:
image: myapp:1.0.0
image: myapp:1.0.1
image: myapp:1.0.2
一旦新版本出事,直接改回旧 tag 再启动。
2. 数据和镜像分离
镜像升级,不应该动数据库数据目录。否则你回滚代码时,数据结构已经变了,照样翻车。
3. 上线前备份数据库
MySQL 示例:
docker exec myapp_db mysqldump -uroot -pyour_root_password myapp > ./backup/myapp_$(date +%F_%H-%M-%S).sql
别嫌麻烦。真正的数据事故,都是嫌麻烦的人制造出来的。
十六、Compose 部署最常见的 10 个坑
下面这些坑,几乎是高频事故区。
1. 容器启动了,但服务没监听 0.0.0.0
如果你的应用只监听:
127.0.0.1:3000
那外部和其他容器都可能访问不到。
应该监听:
0.0.0.0:3000
2. 容器里乱用 localhost
容器里的 localhost 是当前容器自己。你要访问数据库服务,应该写:
db:3306
不是:
127.0.0.1:3306
3. 没做数据卷挂载
数据库数据、上传文件、缓存持久化没挂载,删个容器直接清零。这不叫线上系统,这叫一次性用品。
4. 没有限制日志大小
日志文件无限长大,磁盘被打满,整个系统跟着抽风。
5. 端口冲突没排查
80、443、3306 常年都是高危端口。
检查命令:
ss -ltnp | grep :80
ss -ltnp | grep :443
6. 环境变量名字写错
程序要的是 DATABASE_URL,你写成 DB_URL。程序要的是 REDIS_HOST,你写成 REDIS_URL。Docker 没错,Compose 没错,纯属你自己瞎配。
7. 目录权限不对
容器里的非 root 用户写不了宿主机挂载目录,就会报权限错误。
必要时检查:
ls -lah ./data
docker exec -it 容器名 id
8. depends_on 当成健康检查
depends_on 不是 ready 检查。数据库容器启动了,不代表数据库已经可以接连接。
9. 直接把数据库端口暴露公网
ports:
- "3306:3306"
不是不能用,但生产环境里最好只在必要时开放,并配好安全策略。否则等于把数据库大门半开着。
10. 只有部署,没有监控
你至少要有:
- 日志查看方式
- 磁盘检查方式
- 容器状态检查方式
- 备份方案
没有这些,迟早出事。
十七、线上故障时,应该先打哪几条命令
真出问题时,别慌,也别一通瞎改。
先按顺序打:
docker compose ps
docker ps -a
docker compose logs -f
docker inspect myapp_app
ss -ltnp
df -h
docker system df
如果是 Nginx 502,就进 Nginx 容器测后端:
docker exec -it myapp_nginx sh
wget -qO- http://app:3000/health
如果 Nginx 容器里都打不通后端,那就别再怪域名、浏览器、CDN 了。问题就在你的容器网络或者应用本身。
十八、给新手的一套最稳上线流程
如果你刚开始做 Docker Compose 部署,我建议你按下面这个顺序来,不要乱跳步骤。
第一步:本地先跑通 Dockerfile 先确保应用镜像能独立启动。
第二步:再接入 Compose 先让 App + DB + Redis 在 Compose 里协同正常。
第三步:再加 Nginx 别一开始就上 HTTPS、负载均衡、复杂转发,先把最基础的反代跑通。
第四步:验证健康检查和日志 别等出事后才第一次看日志。
第五步:验证重启恢复 重启服务、重启机器,都要确认是否自动恢复。
第六步:上线前备份 尤其是数据库,有备份你才配谈上线。
第七步:更新流程标准化 以后每次更新都固定:拉代码 → 构建 → 启动 → 看日志 → 验证页面 → 结束。
十九、一份更适合多数项目的上线检查清单
你每次正式上线前,都可以照着过一遍。
基础环境
- [ ] Docker 已安装
- [ ] Docker Compose 插件可用
- [ ] 服务器防火墙和安全组配置正确
- [ ] 域名解析已指向服务器
应用层
- [ ] Dockerfile 可构建成功
- [ ] 应用监听 0.0.0.0
- [ ] 健康检查接口可访问
- [ ] 环境变量齐全
Compose 层
- [ ] docker compose config 无报错
- [ ] 服务依赖关系明确
- [ ] 数据卷已挂载
- [ ] 自动重启已配置
- [ ] 日志轮转已配置
网络层
- [ ] Nginx 反代目标正确
- [ ] App 只暴露给内部网络
- [ ] 外部只开放必要端口
- [ ] HTTPS 证书就绪
数据层
- [ ] 数据库已初始化
- [ ] 上传目录权限正确
- [ ] Redis 持久化策略明确
- [ ] 备份脚本已准备
运维层
- [ ] 日志查看方式已确认
- [ ] 更新流程已标准化
- [ ] 回滚方案已准备
- [ ] 磁盘空间足够
二十、结语:Compose 不是高级玩法,它是上线的基本功
很多人把 Docker Compose 想得太简单,以为它就是个方便启动容器的小工具。其实不是。
对于绝大多数中小项目来说,Docker Compose 本身就是最实用的一套上线编排方案。
它不花哨,但够稳。它不神秘,但足够专业。
它最大的价值不是"把服务拉起来",而是让你的部署有结构、有秩序、有可维护性。
记住一句最重要的话:
能跑起来,只是开始;能稳定上线,才算部署。
别再把 Compose 当成一条命令。你真正要学会的,是它背后的整套部署逻辑:
- 目录怎么分
- 配置怎么管
- 数据怎么存
- 服务怎么连
- 反代怎么写
- 出问题怎么查
- 升级怎么回滚
这些东西学明白了,Docker Compose 才真的开始为你干活。否则你只是把一堆问题,从宿主机搬进了容器里。
问题求助
没能解决你的问题?直接问我
如果你遇到任何技术问题无法解决,可以在这里提交求助。我会尽快查看并回复你。
支持作者
如果这篇文章帮到了你,可以支持我
扫码打赏,支持我持续更新原创排障文章。

