用 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 -dDocker 先停旧容器再起新容器最简单有几十秒到几分钟的不可用窗口
# 蓝绿容器:用两个 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,服务器上永远保留最近几个可用版本。

笔记加载中…