Docker第一次运行:从CLI到容器生命周期
本页不是命令速查表,而是一节从零开始的动手课。你会亲自启动第一个容器,并在每一步理解:Docker CLI把请求发给谁、镜像从哪里来、容器为什么本质是进程、端口为什么要发布、日志从哪里产生、停止后数据还在不在,以及删除容器和删除镜像分别删除什么。
安装尚未完成时先阅读 Docker安装、权限与升级;需要查全部命令和生产安全边界时阅读 Docker命令从对象模型到生产操作。
学习目标
学完后应能:
- 区分Docker CLI、daemon、Registry、镜像和容器。
- 解释
docker run为什么等于pull、create和start等多个阶段。 - 使用名称、Container ID、Image ID、Tag和Digest准确识别对象。
- 区分前台、后台、attach和exec。
- 画出宿主机端口到容器端口的请求路径。
- 查看状态、日志、进程、资源、端口、网络和挂载。
- 区分stop、start、restart、rm和image rm的生命周期影响。
- 证明容器可写层会随容器删除,Volume生命周期独立于容器。
- 按证据定位“容器启动就退出”和“服务访问不到”。
一、Docker到底解决什么问题
没有容器时,一套Java服务交付到服务器可能依赖:
操作系统库版本
JDK具体版本和更新号
启动脚本
Jar包
配置文件
时区与字符集
系统用户和目录权限
外部依赖地址人工安装容易造成环境差异。Docker把应用运行所需的文件系统内容和启动声明组织成镜像,再以受控方式创建容器进程。
flowchart TD
A["应用代码与依赖"] --> B["Dockerfile构建镜像"]
B --> C["Registry保存和分发镜像"]
C --> D["Docker Engine拉取镜像"]
D --> E["镜像只读层"]
E --> F["创建容器可写层、网络和挂载"]
F --> G["启动容器主进程"]Docker减少环境差异,但不会自动解决:
- 数据库数据备份。
- 应用自身Bug。
- 分布式高可用。
- 镜像漏洞和Secret泄露。
- CPU、内存和磁盘容量规划。
- 线上监控、告警和故障恢复。
二、六个核心对象先建立关系
| 对象 | 是什么 | 例子 | 生命周期 |
|---|---|---|---|
| Dockerfile | 构建镜像的声明文件 | FROM、COPY、ENTRYPOINT | 属于源码仓库 |
| Image | 只读文件层与运行配置 | nginx:stable-alpine | 可被多个容器共享 |
| Container | 镜像创建出的进程及运行配置 | first-nginx | 主进程退出后进入Exited |
| Registry | 保存和分发镜像 | Docker Hub、企业Registry | 远程制品系统 |
| Network | 容器网络与服务发现 | bridge、Compose网络 | 独立于普通容器对象 |
| Volume | Docker管理的持久数据 | first-nginx-data | 默认独立于容器 |
关系:
flowchart TD
A["Dockerfile"] --> B["docker build"]
B --> C["Image"]
D["Registry"] --> E["docker pull"]
E --> C
C --> F["docker create"]
F --> G["Container元数据与可写层"]
G --> H["docker start"]
H --> I["运行中的主进程"]
J["Network"] --> G
K["Volume"] --> G容器不是“缩小版虚拟机”。Linux容器本质上仍是宿主机进程,只是通过namespace隔离视图,通过cgroup限制和统计资源,通过镜像层提供根文件系统。完整原理见 容器运行底层原理。
三、实验前先确认你连接的是谁
Docker采用客户端—服务端模型:
flowchart TD
A["docker CLI"] --> B["当前Docker context"]
B --> C["Unix Socket、Named Pipe或远程连接"]
C --> D["dockerd"]
D --> E["containerd与OCI runtime"]
D --> F["镜像、网络和Volume"]执行:
docker context show
docker context ls
docker version
docker info3.1 docker version看什么
- Client:当前CLI版本。
- Server:连接到的Docker Engine版本。
- 只有Client信息、Server报错:CLI存在,但daemon未启动、权限不足或context错误。
3.2 docker info看什么
- 容器和镜像数量。
- Storage Driver。
- Docker Root Dir。
- Cgroup Driver和版本。
- 默认日志驱动。
- Registry与网络信息。
多台主机或Docker Desktop环境中,先确认context,避免在错误环境拉镜像、启动或删除容器。
四、第一个实验:hello-world背后发生什么
执行:
docker run --name first-hello hello-world如果本地没有镜像,典型过程是:
flowchart TD
A["CLI发送Container Create请求"] --> B{"本地是否有hello-world镜像"}
B -- "没有" --> C["daemon向Registry查询镜像Manifest"]
C --> D["按Digest下载缺失镜像层"]
D --> E["校验并保存镜像内容"]
B -- "已有" --> F["复用本地镜像"]
E --> G["创建容器元数据和可写层"]
F --> G
G --> H["创建namespace与cgroup"]
H --> I["启动镜像声明的主进程"]
I --> J["程序打印文本后正常退出"]
J --> K["容器状态变成Exited 0"]你会看到容器打印欢迎信息后命令返回。这不是启动失败。hello-world 的主程序设计就是打印信息后退出;容器生命周期由主进程决定,主进程正常结束后容器进入Exited。
查看:
docker ps
docker ps -a --filter "name=first-hello"
docker inspect first-hello \
--format 'Status={{.State.Status}} ExitCode={{.State.ExitCode}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}'
docker logs first-hello预期理解:
docker ps默认只显示运行中的容器,所以看不到它。docker ps -a能看到Exited容器。- ExitCode 0通常表示程序正常结束。
- 容器退出后仍保留元数据、可写层和日志,直到被删除。
hello-world成功能证明什么
大致证明:
- CLI能连接daemon。
- daemon能找到或拉取镜像。
- 本地镜像存储可写。
- containerd/runtime能创建进程。
- stdout日志链路可用。
它不能证明:
- 端口发布正常。
- 自定义网络DNS正常。
- Volume持久化正常。
- CPU和内存限制正常。
- Compose正常。
- Java应用能运行。
五、镜像名称、Tag、Digest和ID
查看镜像:
docker image ls
docker image inspect hello-world例如:
nginx:stable-alpinenginx:Repository。stable-alpine:Tag,是可变的人类可读引用。- Digest:镜像清单内容摘要,常见形式
sha256:...。 - Image ID:本地镜像配置对象的内容ID。
Tag不是版本内容本身,同一个Tag以后可能指向新内容。生产发布应记录经过验证的镜像Digest和构建来源,不能只说“使用latest”。
六、第二个实验:后台运行Nginx
先拉取:
docker pull nginx:stable-alpine启动:
docker run -d \
--name first-nginx \
--label lab=docker-first-run \
-p 127.0.0.1:18080:80 \
nginx:stable-alpine逐项解释:
| 参数 | 含义 | 不写会怎样 |
|---|---|---|
run | 创建并启动新容器 | 不是启动旧容器 |
-d | 后台运行,CLI不持续附着标准输出 | 前台运行时终端会附着容器输出 |
--name first-nginx | 设置可读名称 | Docker自动生成随机名称 |
--label | 添加业务归属标签 | 后续筛选和安全清理更困难 |
-p 127.0.0.1:18080:80 | 宿主机本地18080发布到容器80 | 外部不能通过该宿主机端口访问 |
nginx:stable-alpine | 创建容器的镜像引用 | run缺少必需镜像参数会失败 |
这里绑定 127.0.0.1,只用于本机实验,避免默认暴露到所有宿主机网卡。生产是否对外开放由负载均衡、防火墙和网络架构决定。
七、docker run不是“运行一个镜像文件”
逻辑上:
docker run
=
必要时docker pull
+ docker create
+ docker start
+ 可选attach创建阶段固定:
- 镜像引用和实际Image ID。
- Entrypoint与Cmd合并结果。
- 环境变量。
- 端口绑定。
- 网络连接。
- Volume和Bind Mount。
- CPU、内存、PID限制。
- 日志驱动。
- 重启策略。
因此修改Compose或run参数后只执行 docker restart,不会重新创建这些配置。需要用新创建参数时要受控重建容器。
检查实际创建结果:
docker inspect first-nginx
docker inspect first-nginx \
--format 'Image={{.Config.Image}} ImageID={{.Image}} Path={{.Path}} Args={{json .Args}}'八、端口映射完整路径
flowchart TD
A["curl 127.0.0.1:18080"] --> B["宿主机TCP 18080"]
B --> C["Docker端口发布与NAT"]
C --> D["first-nginx容器IP:80"]
D --> E["Nginx进程监听0.0.0.0:80"]
E --> F["返回HTTP响应"]验证:
docker ps --filter "name=first-nginx"
docker port first-nginx
curl -i http://127.0.0.1:18080/EXPOSE 80只是镜像元数据声明,不会自动创建宿主机端口。真正发布由 -p 或Compose ports完成。
如果端口访问失败,逐层检查:
- 容器是否Running。
- HostConfig是否有端口绑定。
- 宿主机18080是否监听/转发。
- Nginx是否监听容器80。
- 是否绑定到正确宿主机地址。
- 防火墙、安全组和代理是否拦截。
九、名称、容器ID和PID不是一回事
docker ps --filter "label=lab=docker-first-run"
docker inspect first-nginx \
--format 'ContainerID={{.Id}} HostPid={{.State.Pid}} Name={{.Name}}'- 容器名称:用户友好的Docker对象名称。
- Container ID:Docker容器对象ID。
- Host PID:容器主进程在当前宿主机PID Namespace中的进程号。
- 镜像ID:创建容器所用镜像内容对象,和Container ID不同。
不要把 docker ps 的Container ID当成Linux PID。
十、日志从哪里来
flowchart TD
A["Nginx主进程及worker"] --> B["stdout和stderr"]
B --> C["Docker日志驱动"]
C --> D["docker logs"]查看:
docker logs --timestamps --tail 100 first-nginx
docker logs --timestamps --since 5m -f first-nginx另一个终端执行:
curl http://127.0.0.1:18080/应该看到访问日志。docker logs只读取日志驱动接收的stdout/stderr,不会自动扫描容器里的任意文件。应用只写 /app/logs/app.log 时,docker logs可能没有业务日志。
十一、前台、后台、attach和exec
| 方式 | 做什么 | 是否创建新进程 | 风险 |
|---|---|---|---|
| 前台run | 创建容器并附着主进程IO | 主进程本身 | Ctrl+C可能发送信号影响容器 |
-d | 后台运行 | 不额外创建诊断进程 | 需用logs观察输出 |
docker attach | 连接主进程已有IO | 否 | 按键和信号可能影响主进程 |
docker exec | 在运行容器内启动额外进程 | 是 | 结果不完全等于主进程启动环境 |
执行只读诊断:
docker top first-nginx
docker exec first-nginx nginx -t
docker exec first-nginx sh -c 'id && ls -lah /usr/share/nginx/html'容器退出后不能exec,因为没有现存容器环境供新进程加入,但通常仍可inspect和logs。
十二、状态为什么会变化
flowchart TD
A["Created"] --> B["Running"]
B --> C["Paused"]
C --> B
B --> D["Exited"]
D --> B
B --> E["Restarting"]
E --> B
D --> F["Removed"]常见状态:
| 状态 | 含义 |
|---|---|
| Created | 容器对象已创建,主进程尚未成功运行 |
| Running | 主进程正在运行 |
| Paused | cgroup进程被冻结,不等于正常停止 |
| Exited | 主进程已经结束,元数据仍在 |
| Restarting | 重启策略正在反复尝试启动 |
| Dead | Docker无法正常完成清理的异常状态 |
详细查看:
docker inspect first-nginx \
--format 'Status={{.State.Status}} Running={{.State.Running}} Exit={{.State.ExitCode}} OOM={{.State.OOMKilled}} Error={{.State.Error}}'十三、stop、start和restart实验
停止:
docker stop first-nginx典型原理:
flowchart TD
A["docker stop"] --> B["daemon向PID 1发送停止信号"]
B --> C["应用停止接收新工作并清理"]
C --> D{"宽限期内是否退出"}
D -- "是" --> E["容器进入Exited"]
D -- "否" --> F["发送SIGKILL强制结束"]再次启动同一容器:
docker start first-nginx它复用同一个容器的创建配置和可写层。验证页面仍可访问:
curl -i http://127.0.0.1:18080/重启:
docker restart first-nginxrestart相当于停止后再次启动同一容器,不会自动pull新镜像,也不会采用后来修改的端口和挂载配置。
十四、容器可写层实验
在容器中创建文件:
docker exec first-nginx sh -c 'echo container-layer > /tmp/lifecycle.txt'
docker exec first-nginx cat /tmp/lifecycle.txt
docker diff first-nginx停止再启动:
docker stop first-nginx
docker start first-nginx
docker exec first-nginx cat /tmp/lifecycle.txt文件仍在,因为stop/start没有删除容器可写层。
删除容器前先确认对象:
docker inspect first-nginx \
--format 'Name={{.Name}} Image={{.Config.Image}} Status={{.State.Status}} Mounts={{json .Mounts}}'
docker stop first-nginx
docker rm first-nginx重新用同一镜像创建:
docker run -d \
--name first-nginx \
--label lab=docker-first-run \
-p 127.0.0.1:18080:80 \
nginx:stable-alpine再检查:
docker exec first-nginx sh -c 'test -f /tmp/lifecycle.txt && echo exists || echo missing'预期是missing。删除旧容器会删除旧容器可写层;使用同一镜像创建的是新容器,不会继承旧可写层。
十五、Volume为什么能跨容器保留
创建带实验标签的Volume:
docker volume create \
--label lab=docker-first-run \
first-nginx-data创建一个一次性容器写入Volume:
docker run --rm \
-v first-nginx-data:/data \
nginx:stable-alpine \
sh -c 'echo volume-data > /data/message.txt'另一个新容器读取:
docker run --rm \
-v first-nginx-data:/data:ro \
nginx:stable-alpine \
cat /data/message.txtflowchart TD
A["写入容器"] --> B["挂载first-nginx-data"]
B --> C["Volume中的message.txt"]
A --> D["容器因--rm被删除"]
C --> E["新读取容器挂载同一Volume"]
E --> F["仍能读取volume-data"]Volume生命周期默认独立于普通容器,但Volume不是备份:它通常仍在同一宿主机,可能被误删、磁盘损坏或随 down -v 删除。数据库还需要一致性备份和恢复演练。
十六、镜像和容器删除边界
| 操作 | 删除什么 | 默认不删除什么 |
|---|---|---|
docker stop | 不删除对象,只停止主进程 | 容器元数据、可写层、镜像、Volume |
docker rm | 容器元数据和可写层 | 镜像、普通命名Volume |
docker image rm | 镜像引用及可释放内容 | 容器、Volume;被容器引用时通常拒绝 |
docker volume rm | Volume及其数据 | 镜像和容器可写层 |
删除操作前必须确认名称、ID、标签、引用和数据归属。不要把全局prune当作入门清理命令。
本实验只清理明确名称和标签的对象:
docker inspect first-nginx --format '{{json .Config.Labels}}'
docker stop first-nginx
docker rm first-nginx
docker volume inspect first-nginx-data
docker volume rm first-nginx-data保留镜像也没有问题,它可供后续实验复用。
十七、资源使用怎么观察
重新创建容器后可执行:
docker stats --no-stream first-nginx
docker top first-nginx
docker inspect first-nginx \
--format 'Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} PidsLimit={{.HostConfig.PidsLimit}}'如果Memory等值为0,通常表示创建时没有显式设置该限制,不代表宿主机资源无限。生产必须为Java、MySQL、Redis和Nginx分别预算资源。
十八、容器启动后马上退出怎么查
先不要反复restart,执行:
docker ps -a --filter "name=<容器名>"
docker inspect <容器名> \
--format 'Status={{.State.Status}} Exit={{.State.ExitCode}} OOM={{.State.OOMKilled}} Error={{.State.Error}} Path={{.Path}} Args={{json .Args}}'
docker logs --timestamps --tail 300 <容器名>常见原因:
| 证据 | 可能原因 |
|---|---|
| Exit 0 | 程序按设计执行完;后台服务入口可能写错 |
| Exit 1 | 应用配置、参数或业务启动异常 |
| Exit 126 | 文件存在但无执行权限等 |
| Exit 127 | 找不到命令或PATH错误 |
| Exit 137 | 收到SIGKILL,结合OOMKilled和events判断 |
| 日志提示配置文件不存在 | 挂载路径、工作目录或启动参数错误 |
| Restarting | 重启策略正在掩盖持续启动失败 |
Exit 137不等于一定是Java Heap OOM,也可能是容器总内存超限、手工kill或stop超时后的强杀。
十九、服务访问不到怎么逐层查
flowchart TD
A["确认容器Running"] --> B["确认应用监听容器端口"]
B --> C["确认Docker端口绑定"]
C --> D["确认宿主机监听与NAT"]
D --> E["确认防火墙和安全组"]
E --> F["确认HTTP、TLS和应用响应"]命令:
docker ps -a --filter "name=first-nginx"
docker logs --tail 200 first-nginx
docker port first-nginx
docker inspect first-nginx --format '{{json .NetworkSettings.Ports}}'
curl -v http://127.0.0.1:18080/错误类型有不同含义:
- DNS失败:名字无法解析。
- Connection refused:目标可达但该端口没有监听或被主动拒绝。
- Timeout:流量被丢弃、路由错误、服务卡住或超时边界触发。
- TLS错误:TCP可能已通,但证书、协议或SNI失败。
- HTTP 4xx/5xx:已经到达HTTP处理层,不应继续只查“网络”。
二十、最小Dockerfile只理解边界
FROM eclipse-temurin:8-jre
WORKDIR /app
COPY target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]逐项含义:
FROM:指定基础镜像,JDK 8还必须关注具体8u更新和容器感知能力。WORKDIR:设置后续指令和默认运行工作目录。COPY:把构建上下文中的Jar写入镜像层。EXPOSE:声明容器服务端口,不自动发布宿主机端口。ENTRYPOINT:定义主程序;Exec form有利于信号直接到JVM。
这只是认识Dockerfile,不是生产模板。JDK 8与JDK 17、非root、多阶段构建、分层Jar、Secret、Heap和容器内存预算见 Dockerfile完整原理 与 Spring Boot容器化。
二十一、商业项目中对象如何对应
以订单服务为例:
Git commit
→ CI构建order-service.jar
→ Dockerfile构建镜像
→ 镜像Tag和Digest推送Registry
→ 部署平台按镜像创建容器
→ 配置和Secret在运行时注入
→ 网络连接MySQL、Redis和MQ
→ 日志、指标和链路追踪进入监控系统
→ 健康后接入流量商业发布必须能回答:
- 当前容器对应哪个commit和镜像Digest。
- 配置和Secret从哪里来。
- 数据是否落在Volume或外部数据库。
- CPU和内存限制是多少。
- 健康检查检查了什么。
- 如何优雅停止和回滚。
- 删除容器是否会丢业务数据。
二十二、面试标准回答
Docker镜像和容器有什么区别
镜像是只读文件层和运行配置组成的模板,可以被多个容器共享;容器是基于镜像创建的运行对象,增加自己的元数据、可写层、网络和挂载,并以主进程生命周期决定Running或Exited。删除容器会删除其可写层,但通常不删除镜像和普通命名Volume。
docker run背后发生什么
CLI向daemon发送创建请求;本地缺镜像时daemon从Registry获取Manifest并按Digest下载镜像层,然后准备容器可写层、namespace、cgroup、网络、端口和Volume,合并Entrypoint/Cmd并通过containerd和OCI runtime启动主进程。逻辑上run约等于必要时pull,再create和start,并可选择attach。
为什么容器启动后马上退出
容器生命周期由主进程决定,主进程正常执行完会Exit 0,异常则可能是配置、命令、权限、依赖或OOM。应先用ps -a、inspect读取Status、ExitCode、OOMKilled、Path和Args,再看logs与events,不能看到Exited就反复restart。
EXPOSE和-p有什么区别
EXPOSE是镜像元数据,声明应用预期监听的容器端口,不自动开放宿主机端口;
-p 宿主机地址:宿主机端口:容器端口才创建端口发布。端口映射存在也不代表应用一定监听或健康,仍要逐层验证。
stop后文件为什么还在,rm后为什么没了
stop只停止容器主进程,不删除容器对象和可写层,start同一容器时文件还在;rm删除容器元数据和可写层,用同一镜像重新run得到的是新可写层。需要跨容器保留的数据应放Volume或外部存储,但Volume仍不是备份。
二十三、学习验收
不看答案完成:
- 用context、version和info证明CLI连接的是哪台daemon。
- 运行hello-world并解释它为什么Exited 0。
- 画出缺少本地镜像时run的pull、create、start全过程。
- 启动Nginx并解释每个run参数。
- 从浏览器或curl画出宿主机18080到容器80的链路。
- 区分名称、Container ID、Image ID和宿主机PID。
- 使用ps、inspect、logs、top、stats和port收集证据。
- 区分前台、后台、attach和exec。
- 证明stop/start保留可写层,rm/recreate不保留旧可写层。
- 使用两个一次性容器证明Volume跨容器保留数据,并解释Volume为什么不是备份。
- 制造一个启动即退出的容器,按ExitCode、日志和命令定位。
- 制造一个错误端口映射,按监听、发布、宿主机和协议逐层定位。
- 解释Dockerfile中EXPOSE为什么不会自动开放端口。
- 写出商业项目中commit、镜像Digest、容器、配置、Secret和监控的对应关系。
