最近更新一个 Docker 服务时,我遇到了一个很容易让人怀疑人生的问题。
镜像拉取成功,容器也重新启动了,整个过程没有任何报错:
bash
docker compose pull app
docker compose restart app
可打开页面一看,显示的仍然是 V1。
第一反应通常是:镜像没拉下来?Docker 缓存出了问题?要不要再重启一次?
其实 Docker 什么都没有做错。问题在于,pull 与 restart 操作的不是同一个对象:
pull更新本地镜像;restart重新启动已经存在的容器;- 让服务使用新镜像,需要根据新镜像重新创建容器。
我们可以把这个过程想成一次舞台换景:
剧院已经把新版布景运到了后台,但台前仍然搭着旧布景。此时把幕布拉上再拉开,只会让原来的舞台重新开场,不会自动把后台的新布景换上来。

*新版布景已经到达后台,但台前仍在使用旧布景------就像新镜像已经拉到本地,旧容器却没有被替换。*
一、先分清镜像和容器
镜像(Image)是一份标准化的软件包,包含运行程序需要的文件、依赖和默认配置。它有点像版本化的安装包,但这个类比只用来帮助入门:Docker 镜像还包含分层文件系统和启动元数据。
容器(Container)是根据某份镜像创建出来的隔离实例。创建时,Docker 会选定具体镜像,并结合环境变量、端口、挂载和启动命令准备运行环境。
两者最重要的区别是:
| 对象 | 负责什么 | 生命周期 |
|---|---|---|
| 镜像 | 提供程序、依赖和默认配置 | 创建后内容不可原地修改 |
| 容器 | 保存根据镜像创建出的实例状态 | 可以启动、停止、重启或删除 |
停止的容器仍然是容器。docker ps 只显示运行中的容器,docker ps -a 才会把已经停止的容器也列出来。
同一份镜像可以创建多个彼此独立的容器;删除一个容器不会删除镜像,更新镜像也不会自动改造已经创建好的容器。
#mermaid-svg-MGQMDQlwd3xJmWXR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MGQMDQlwd3xJmWXR .error-icon{fill:#552222;}#mermaid-svg-MGQMDQlwd3xJmWXR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MGQMDQlwd3xJmWXR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MGQMDQlwd3xJmWXR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MGQMDQlwd3xJmWXR .marker.cross{stroke:#333333;}#mermaid-svg-MGQMDQlwd3xJmWXR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MGQMDQlwd3xJmWXR p{margin:0;}#mermaid-svg-MGQMDQlwd3xJmWXR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR .cluster-label text{fill:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR .cluster-label span{color:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR .cluster-label span p{background-color:transparent;}#mermaid-svg-MGQMDQlwd3xJmWXR .label text,#mermaid-svg-MGQMDQlwd3xJmWXR span{fill:#333;color:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR .node rect,#mermaid-svg-MGQMDQlwd3xJmWXR .node circle,#mermaid-svg-MGQMDQlwd3xJmWXR .node ellipse,#mermaid-svg-MGQMDQlwd3xJmWXR .node polygon,#mermaid-svg-MGQMDQlwd3xJmWXR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MGQMDQlwd3xJmWXR .rough-node .label text,#mermaid-svg-MGQMDQlwd3xJmWXR .node .label text,#mermaid-svg-MGQMDQlwd3xJmWXR .image-shape .label,#mermaid-svg-MGQMDQlwd3xJmWXR .icon-shape .label{text-anchor:middle;}#mermaid-svg-MGQMDQlwd3xJmWXR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MGQMDQlwd3xJmWXR .rough-node .label,#mermaid-svg-MGQMDQlwd3xJmWXR .node .label,#mermaid-svg-MGQMDQlwd3xJmWXR .image-shape .label,#mermaid-svg-MGQMDQlwd3xJmWXR .icon-shape .label{text-align:center;}#mermaid-svg-MGQMDQlwd3xJmWXR .node.clickable{cursor:pointer;}#mermaid-svg-MGQMDQlwd3xJmWXR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MGQMDQlwd3xJmWXR .arrowheadPath{fill:#333333;}#mermaid-svg-MGQMDQlwd3xJmWXR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MGQMDQlwd3xJmWXR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MGQMDQlwd3xJmWXR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MGQMDQlwd3xJmWXR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MGQMDQlwd3xJmWXR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MGQMDQlwd3xJmWXR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MGQMDQlwd3xJmWXR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MGQMDQlwd3xJmWXR .cluster text{fill:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR .cluster span{color:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MGQMDQlwd3xJmWXR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MGQMDQlwd3xJmWXR rect.text{fill:none;stroke-width:0;}#mermaid-svg-MGQMDQlwd3xJmWXR .icon-shape,#mermaid-svg-MGQMDQlwd3xJmWXR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MGQMDQlwd3xJmWXR .icon-shape p,#mermaid-svg-MGQMDQlwd3xJmWXR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MGQMDQlwd3xJmWXR .icon-shape .label rect,#mermaid-svg-MGQMDQlwd3xJmWXR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MGQMDQlwd3xJmWXR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MGQMDQlwd3xJmWXR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MGQMDQlwd3xJmWXR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} pull
之前已经创建
重新创建
远端镜像 V2
本地镜像 V2
本地镜像 V1
旧容器
仍绑定 V1
新容器
使用 V2
二、pull 更新镜像,不替换容器
执行:
bash
docker pull nginx:alpine
Docker 会从远程仓库下载 nginx:alpine 当前指向的镜像,并更新本地的标签引用。
这里有一个容易混淆的细节:镜像内容由 image ID 或 digest 标识,创建后不可原地修改;但 nginx:alpine 这样的 tag 可以重新指向另一份镜像。
因此,下次 pull 之后,同一个 tag 可能已经指向 V2,但旧容器仍然绑定着创建时的 V1。
如果想亲眼确认,可以比较两者:
bash
# 容器创建时绑定的镜像 ID
docker inspect --format '{{.Image}}' web
# 当前 tag 指向的镜像 ID
docker image inspect --format '{{.Id}}' nginx:alpine
两个 ID 不同,说明新镜像已经到了,旧容器却还没有被替换。
Docker 不自动替换容器,其实是一种稳定性保证。否则一次普通的 pull 就可能直接改变正在运行的线上服务。
!important
镜像变了,只代表本地有了一个可用的新版本;容器重建后,才代表服务真正开始运行新版本。
三、restart 为什么不能完成升级
bash
docker restart web
restart 会停止并重新启动同一个容器。
容器没有换,绑定的镜像没有换,创建时确定的环境变量、端口映射和挂载定义也没有换。它只是让容器中的主进程重新运行一次。
所以 restart 适合这些情况:
- 进程临时卡死;
- 服务出现一次性异常;
- 已挂载的配置文件发生变化,而且程序会在启动时重新读取;
- 只是希望让同一个运行实例重新开始。
它不适合用来应用新镜像、新环境变量或新端口映射。
这些信息大多在容器创建时确定:
| 发生的变化 | 只执行 restart | 通常需要 |
|---|---|---|
| 拉取新镜像 | 仍使用旧镜像 | recreate |
| 修改环境变量 | 仍使用创建时的值 | recreate |
| 修改端口映射 | 原映射不变 | recreate |
| 修改 Dockerfile 或构建内容 | 旧镜像、旧容器都不变 | build 后 recreate |
| 修改已挂载的配置文件 | 文件已变化,但程序未必重读 | reload 或 restart |
四、reload、restart、recreate 的区别
这三个动作看起来相近,改变的层级却完全不同。
Reload:让应用重新读取配置
如果修改的是已经挂载进容器的配置文件,并且应用本身支持热加载,可以调用应用提供的 reload。例如 Nginx 可以重新读取部分配置。
reload 是应用程序自己的能力,不是 Docker 提供的一种通用容器状态。哪些配置能够热加载,需要查看具体程序的说明。
Restart:重新启动同一个容器
bash
docker compose restart app
它不会采用新的镜像,也不会应用 Compose 中新修改的环境变量、端口等创建期配置。
Recreate:创建替代容器
recreate 会根据当前镜像和当前创建期配置创建一个新容器,用它替换旧容器。这才是更新镜像、环境变量、端口或挂载定义后通常需要的动作。
回到舞台:restart 是落幕后重新开幕,台上还是原来的雨夜布景;recreate 则是先撤掉旧景,换上后台准备好的晴日新景,再重新开场。

可以把判断方法压缩成三句话: 进程临时异常,考虑 restart;应用支持重新读取配置,考虑 reload;镜像或创建期配置变化,就要 recreate。
需要注意,重建容器时,旧容器的可写层不会保留。重要数据应放在 Volume、Bind Mount 或外部存储中。Volume 独立于容器可写层,但它也不是备份,docker compose down -v 仍然可以删除命名卷。
五、让 Compose 完成正确的更新闭环
真实项目通常不只有一个容器,还可能包含 API、数据库、Redis 和 Nginx。这时可以让 Compose 根据声明的配置协调实际运行状态。
更新远端镜像服务时:
bash
docker compose pull app
docker compose up -d app
pull 明确拉取镜像;up -d 检查服务使用的镜像和配置。发现变化时,Compose 会停止并重建对应容器,同时保留已挂载的卷。它不会每次都无条件重建。
也可以明确要求启动前拉取:
bash
docker compose up -d --pull always app
如果修改了 Dockerfile 或构建上下文:
bash
docker compose up -d --build app
只有在明确希望无论是否检测到变化都替换容器时,才需要:
bash
docker compose up -d --force-recreate app
--force-recreate 不是默认万能修复。大多数情况下,普通的 compose up -d 会在检测到镜像或服务配置变化后完成重建。
阅读一份 compose.yml 时,可以按下面的顺序:
services:有哪些服务;image/build:程序来自现有镜像还是本地构建;ports:哪些端口暴露给宿主机;volumes:哪些数据脱离容器可写层保存;environment:服务带着什么参数启动;depends_on/networks:服务如何依赖和通信。
depends_on 的简短写法只保证依赖服务先启动,不保证它已经能接受请求。需要等待服务真正就绪时,要结合 healthcheck 和 condition: service_healthy,应用侧最好仍保留连接重试。
六、下次更新不生效,先问三个问题
现在回头看最开始的操作:
bash
docker compose pull app
docker compose restart app
第一条命令更新了本地镜像,第二条命令却只启动了原来的容器。服务继续运行旧版本,完全符合 Docker 的生命周期逻辑。
下次再遇到"明明更新了,为什么没有生效",先问自己:
- 我更新的是镜像、创建期配置,还是应用能够重新读取的文件?
- 我现在操作的是当前镜像,还是之前创建的旧容器?
- 这次需要重新读配置、重启同一容器,还是创建替代容器?
最后记住这一句就够了:
新布景到了后台,重新拉一次幕不会自动换景;要演新版,得先换景再开场。
对应到 Docker:Pull 是拿到新镜像,restart 是重启旧容器,recreate 才让服务换成按当前镜像与配置创建的新容器。