用 Docker 部署真实应用与滚动更新
前面几章把镜像、编排准备好了,本章回答真正棘手的问题:怎么把新版本换上去,并且换的过程中用户不报错。核心思路只有一条——先把新版本跑起来并检查健康,再把流量切过去,最后才清理旧版本。顺序反了,就会出现“服务重启中,请稍候”的页面。
完整部署流程
| 步骤 | 命令 | 通过标准 |
|---|---|---|
| 1 构建镜像 | docker compose build --pull | 构建退出码 0 |
| 2 推送或保存镜像 | docker push / docker save | 远端能看到该 tag |
| 3 备份数据 | mysqldump 导出并校验 | 备份文件非空且能解压 |
| 4 数据库迁移 | docker compose run --rm web migrate | 迁移脚本成功退出 |
| 5 启动新版本 | docker compose up -d --build | 容器状态 Up (healthy) |
| 6 本地验证 | 在服务器上 curl 127.0.0.1:8000/healthz | 返回 200 |
| 7 切换流量 | 改 Nginx upstream 或重建容器 | 公网访问正常 |
| 8 观察 | 看日志与错误率 5~10 分钟 | 无新增错误 |
首次部署
# 1) 目录准备
sudo mkdir -p /srv/tools-site && cd /srv/tools-site
# 放入 docker-compose.yml、.env、nginx/ 配置目录
# 2) 校验配置与变量替换
docker compose config -q && echo compose-ok
# 3) 构建并启动(首次会拉基础镜像,耐心等)
docker compose build --pull && docker compose up -d
# 4) 等待健康检查通过,看到 healthy 后继续
watch -n2 'docker compose ps'
日常发布
cd /srv/tools-site
git pull # 或从 CI 同步产物
docker compose build --pull # 只重建变化的层
docker compose up -d --build # 重建有变化的容器
docker compose ps && docker compose logs --since 2m web
| 命令 | 行为 | 什么时候用 |
|---|---|---|
up -d | 只重建镜像或配置变化的服务 | 改了 .env 或 compose 配置 |
up -d --build | 先 build 再重建 | 代码变了 |
up -d --force-recreate web | 强制重建指定服务 | 镜像 tag 未变但内容变了 |
up -d --no-deps web | 不动依赖服务 | 只发应用,不动数据库 |
restart web | 不重建容器,只重启进程 | 临时恢复,不用于发布 |
零停机的两种做法
| 做法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 双容器 + Nginx upstream 切换 | 新旧两套容器同时在线,改 upstream 后 reload | 切换瞬间完成,回滚只需改回 upstream | 需要两份资源,端口要提前规划 |
| 蓝绿目录 / 蓝绿项目 | 新版本整体部署到独立目录或独立 compose 项目 | 隔离彻底,适合大版本 | 资源翻倍,数据迁移要算准 |
直接 up -d | Docker 先停旧容器再起新容器 | 最简单 | 有几十秒到几分钟的不可用窗口 |
# 蓝绿容器:用两个 compose 项目名跑两套,端口错开
docker compose -p tools-blue up -d --build # web 映射 127.0.0.1:8001
docker compose -p tools-green up -d --build # web 映射 127.0.0.1:8002
# 健康检查通过后切流量:只改 upstream 指向,再平滑 reload
sudo nginx -t && sudo systemctl reload nginx
# Nginx 侧:upstream 里保留主备两台,切换就是调整注释顺序
upstream myapi {
server 127.0.0.1:8001 max_fails=2 fail_timeout=5s; # 蓝
server 127.0.0.1:8002 backup; # 绿,切换时把 backup 移到上面
keepalive 32;
}
server {
listen 443 ssl;
location / {
proxy_pass http://myapi;
proxy_next_upstream error timeout http_502 http_503;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
proxy_next_upstream 让单次请求在上游出错时自动重试另一台,这是“切换期间不报错”的关键一行。
数据迁移注意
迁移前必须备份,且备份要验证可恢复,不要只看文件存在。
mkdir -p /data/backup
docker compose exec -T mysql mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --routines --triggers --events appdb \
| gzip > /data/backup/appdb-$(date +%F-%H%M).sql.gz
gzip -t /data/backup/appdb-*.sql.gz && ls -lh /data/backup/ | tail -3
迁移原则:只做向后兼容的变更(加字段、加索引、加表),删字段与改类型放到下一个版本;这样旧版本容器在回滚后依然能读新库。
回滚
# compose 里把镜像写成 image: myapi:${IMAGE_TAG:-1.4.0}
export IMAGE_TAG=1.3.9
docker compose up -d --no-deps --force-recreate web
docker compose ps && curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/healthz
| 镜像 tag 策略 | 说明 |
|---|---|
禁止只用 latest | 无法判断当前跑的是哪个版本,也无法精确回滚 |
| 用版本号 + git sha | 形如 myapi:1.4.0-a3f9c21,出问题能定位到提交 |
| 服务器保留最近 3~5 个 | 回滚够用,又不至于把磁盘塞满 |
| 回滚只换 tag 不改代码 | 现场不动,最快恢复;修好再发新版本 |
验证方法
# 公网入口与本地上游分别验证
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/healthz
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/healthz
# 当前实际运行的镜像 tag,确认切过去了
docker inspect --format '{{.Config.Image}}' tools-site-web-1
# 旧容器是否已清理,避免端口与资源冲突
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
up -d 不带 --build | 跑的还是旧镜像 | 代码变更一律 --build |
只看容器 Up 不看 healthy | 应用连不上库就已经接流量 | 健康检查里真连一次依赖 |
| 迁移脚本不可回滚 | 回滚后旧代码读不了新表 | 只做向后兼容变更 |
| 忘记备份就迁移 | 数据损坏无法恢复 | 迁移前 dump 并 gzip -t 校验 |
只用 latest tag | 无法定位与回滚 | 版本号 + commit sha |
| 回滚时连代码目录一起回退 | 现场越改越乱 | 回滚只换镜像 tag |
小结:发布的标准动作是构建、备份、迁移、起新版本、验证健康、切流量、观察,缺一步就会出事故;零停机靠“双容器 + upstream 切换”或蓝绿两套项目实现,用 proxy_next_upstream 兜住切换瞬间的错误;迁移只做向后兼容的变更,回滚只换镜像 tag,服务器上永远保留最近几个可用版本。