能启动容器以后,下一步是弄清楚状态。很多问题都来自把“镜像”“已停止容器”和“正在运行的容器”混成一件事。
镜像层是只读的,可以被多个容器共享。每个容器在镜像层上增加一个可写层,用来记录运行期间的文件变化。停止容器不会删除这个可写层,删除容器才会删除它。
因此下面四个动作彼此独立:
docker pull:获取镜像。docker create:基于镜像创建容器,但不启动。docker start:启动已有容器。docker rm:删除容器,不会顺带删除镜像。镜像不是一个每次都被完整复制的大文件,而是一组按顺序叠放的只读层。创建容器时,Docker 在它们上方增加该容器独享的可写层。应用看到的是合并后的目录树,所以使用体验像普通文件系统,但底层仍保留层次。
当容器修改一个来自镜像层的文件时,存储驱动通常先把相关内容复制到可写层再修改,这类机制常被称为 copy-on-write。好处是多个容器能共享基础层;代价是数据库这类高频写入数据不适合依赖容器可写层,应该使用数据卷。
容器 A 可写层 容器 B 可写层
│ │
└──── 镜像层 3 ─────┘
镜像层 2
镜像层 1停止容器只是停止主进程,可写层仍跟容器对象一起保留;删除容器才会一起删除这层。镜像层只在镜像引用被删除且没有其他对象需要时才可能被回收。
run 的步骤先拉取镜像:
$ docker pull alpine:3.22
3.22: Pulling from library/alpine
Status: Image is up to date for alpine:3.22创建一个容器,让它每两秒打印一次时间:
$ docker create --name lifecycle-demo alpine:3.22 \
sh -c 'while true; do date -u; sleep 2; done'
4e3b7f...此时容器状态是 created:
$ docker ps -a --filter name=lifecycle-demo \
--format 'table {{.Names}}\t{{.Status}}'
NAMES STATUS
lifecycle-demo Created启动后查看日志:
$ docker start lifecycle-demo
lifecycle-demo
$ docker logs --tail 2 lifecycle-demo
Wed Jul 15 02:41:10 UTC 2026
Wed Jul 15 02:41:12 UTC 2026容器对象在 create 时就存在;start 只负责启动它。日志保存在容器对象中,所以停止后仍能读取。
$ docker stop lifecycle-demo
lifecycle-demo
$ docker inspect --format '{{.State.Status}} exit={{.State.ExitCode}}' lifecycle-demo
exited exit=0exit=0 表示进程正常结束。再次启动不会创建新容器:
$ docker start lifecycle-demo
lifecycle-demo
$ docker inspect --format '{{.State.Status}} restarts={{.RestartCount}}' lifecycle-demo
running restarts=0RestartCount 统计 Docker 根据重启策略自动重启的次数,手动执行 docker start 不会增加它。
理解“重启”和“重建”的差异后,再观察容器 ID:
$ docker inspect --format '{{.Id}}' lifecycle-demo
4e3b7f...
$ docker restart lifecycle-demo
lifecycle-demo
$ docker inspect --format '{{.Id}}' lifecycle-demo
4e3b7f...两次 ID 相同,说明 restart 只是让同一个容器的进程停下再启动。若修改了端口、挂载、运行用户等“创建时配置”,重启不会重写这些属性,必须删除并按新配置重新创建容器。Compose 后面会替我们判断何时需要重建。
常见状态包括:
“容器在运行”只证明 PID 1 存在,不证明业务能正确响应。第 10 章会用健康检查补上这一层判断。
容器有名字和 ID;镜像常用仓库名与标签表示,例如 alpine:3.22。标签是可移动的别名,不是不可变版本证明。需要精确锁定镜像内容时,应记录镜像摘要:
$ docker image inspect alpine:3.22 \
--format '{{index .RepoDigests 0}}'
alpine@sha256:14358309a308...sha256:... 摘要由镜像内容决定。团队在生产配置中使用摘要,可以避免同名标签后来指向不同内容。
lifecycle-demo。1.0 或 stable,它可以移动。同一个镜像内容可以有多个标签,给镜像增加标签不会复制所有层。反过来,同一个标签以后也可能指向新内容。因此开发命令中标签很方便,审计和可复现部署则更看重摘要。
执行下面的只读检查,可以同时看到镜像 ID、标签和摘要:
$ docker image inspect alpine:3.22 \
--format 'id={{.Id}} tags={{json .RepoTags}} digests={{json .RepoDigests}}'
id=sha256:... tags=["alpine:3.22"] digests=["alpine@sha256:..."]刚在设备上构建但尚未推送的镜像可能没有 RepoDigests,因为 Registry 还没有为该仓库引用返回分发摘要。这不是镜像损坏。
先删除容器,再按需删除镜像:
$ docker rm -f lifecycle-demo
lifecycle-demo
$ docker image rm alpine:3.22
Untagged: alpine:3.22如果仍有容器引用该镜像,镜像删除会失败。这是一种保护:先确认容器是否还需要,再处理镜像。
Docker 对象之间存在引用关系:容器引用镜像,容器可以连接网络并挂载数据卷,标签引用镜像内容。删除失败往往是在提醒你仍有上层对象依赖它。
推荐的项目级清理顺序是:
这个顺序的核心不是语法,而是让“运行对象”先解除对“可复用对象”和“持久对象”的引用。与其执行大范围 prune,不如给项目对象稳定命名,再按名字清理。
不要把 docker system prune -a --volumes 当作日常清理命令。它会删除所有未使用镜像、容器、网络和数据卷,范围比当前项目大。课程最后会使用带项目名的 Compose 清理命令,只处理课程资源。
假设执行以下流程:
$ docker create --name state-demo alpine:3.22 sh -c 'echo hello > /note.txt'
$ docker start -a state-demo
$ docker start -a state-demo先预测:第二次启动会不会创建新容器?/note.txt 是否还在?答案是容器 ID 不变,可写层也不变;主进程会再次执行并覆盖同一个文件。你可以这样核对:
$ docker inspect --format '{{.Id}} status={{.State.Status}}' state-demo
$ docker cp state-demo:/note.txt -
hello
$ docker rm state-demodocker cp 能从已停止容器读取文件,因为容器可写层仍存在。删除容器后,这份未挂载到卷的数据才消失。这个小实验把“停止保留、删除丢失”的原理落到了可观察状态上。
下一节不再使用别人准备的应用镜像。我们会创建任务 API 的源码和 Dockerfile,第一次构建自己的镜像。