Skip to content

Docker第一次运行:从CLI到容器生命周期

本页不是命令速查表,而是一节从零开始的动手课。你会亲自启动第一个容器,并在每一步理解:Docker CLI把请求发给谁、镜像从哪里来、容器为什么本质是进程、端口为什么要发布、日志从哪里产生、停止后数据还在不在,以及删除容器和删除镜像分别删除什么。

安装尚未完成时先阅读 Docker安装、权限与升级;需要查全部命令和生产安全边界时阅读 Docker命令从对象模型到生产操作

学习目标

学完后应能:

  1. 区分Docker CLI、daemon、Registry、镜像和容器。
  2. 解释 docker run 为什么等于pull、create和start等多个阶段。
  3. 使用名称、Container ID、Image ID、Tag和Digest准确识别对象。
  4. 区分前台、后台、attach和exec。
  5. 画出宿主机端口到容器端口的请求路径。
  6. 查看状态、日志、进程、资源、端口、网络和挂载。
  7. 区分stop、start、restart、rm和image rm的生命周期影响。
  8. 证明容器可写层会随容器删除,Volume生命周期独立于容器。
  9. 按证据定位“容器启动就退出”和“服务访问不到”。

一、Docker到底解决什么问题

没有容器时,一套Java服务交付到服务器可能依赖:

text
操作系统库版本
JDK具体版本和更新号
启动脚本
Jar包
配置文件
时区与字符集
系统用户和目录权限
外部依赖地址

人工安装容易造成环境差异。Docker把应用运行所需的文件系统内容和启动声明组织成镜像,再以受控方式创建容器进程。

mermaid
flowchart TD
    A["应用代码与依赖"] --> B["Dockerfile构建镜像"]
    B --> C["Registry保存和分发镜像"]
    C --> D["Docker Engine拉取镜像"]
    D --> E["镜像只读层"]
    E --> F["创建容器可写层、网络和挂载"]
    F --> G["启动容器主进程"]

Docker减少环境差异,但不会自动解决:

  • 数据库数据备份。
  • 应用自身Bug。
  • 分布式高可用。
  • 镜像漏洞和Secret泄露。
  • CPU、内存和磁盘容量规划。
  • 线上监控、告警和故障恢复。

二、六个核心对象先建立关系

对象是什么例子生命周期
Dockerfile构建镜像的声明文件FROMCOPYENTRYPOINT属于源码仓库
Image只读文件层与运行配置nginx:stable-alpine可被多个容器共享
Container镜像创建出的进程及运行配置first-nginx主进程退出后进入Exited
Registry保存和分发镜像Docker Hub、企业Registry远程制品系统
Network容器网络与服务发现bridge、Compose网络独立于普通容器对象
VolumeDocker管理的持久数据first-nginx-data默认独立于容器

关系:

mermaid
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采用客户端—服务端模型:

mermaid
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"]

执行:

bash
docker context show
docker context ls
docker version
docker info

3.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背后发生什么

执行:

bash
docker run --name first-hello hello-world

如果本地没有镜像,典型过程是:

mermaid
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。

查看:

bash
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

查看镜像:

bash
docker image ls
docker image inspect hello-world

例如:

text
nginx:stable-alpine
  • nginx:Repository。
  • stable-alpine:Tag,是可变的人类可读引用。
  • Digest:镜像清单内容摘要,常见形式 sha256:...
  • Image ID:本地镜像配置对象的内容ID。

Tag不是版本内容本身,同一个Tag以后可能指向新内容。生产发布应记录经过验证的镜像Digest和构建来源,不能只说“使用latest”。

六、第二个实验:后台运行Nginx

先拉取:

bash
docker pull nginx:stable-alpine

启动:

bash
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不是“运行一个镜像文件”

逻辑上:

text
docker run
=
必要时docker pull
+ docker create
+ docker start
+ 可选attach

创建阶段固定:

  • 镜像引用和实际Image ID。
  • Entrypoint与Cmd合并结果。
  • 环境变量。
  • 端口绑定。
  • 网络连接。
  • Volume和Bind Mount。
  • CPU、内存、PID限制。
  • 日志驱动。
  • 重启策略。

因此修改Compose或run参数后只执行 docker restart,不会重新创建这些配置。需要用新创建参数时要受控重建容器。

检查实际创建结果:

bash
docker inspect first-nginx
docker inspect first-nginx \
  --format 'Image={{.Config.Image}} ImageID={{.Image}} Path={{.Path}} Args={{json .Args}}'

八、端口映射完整路径

mermaid
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响应"]

验证:

bash
docker ps --filter "name=first-nginx"
docker port first-nginx
curl -i http://127.0.0.1:18080/

EXPOSE 80只是镜像元数据声明,不会自动创建宿主机端口。真正发布由 -p 或Compose ports完成。

如果端口访问失败,逐层检查:

  1. 容器是否Running。
  2. HostConfig是否有端口绑定。
  3. 宿主机18080是否监听/转发。
  4. Nginx是否监听容器80。
  5. 是否绑定到正确宿主机地址。
  6. 防火墙、安全组和代理是否拦截。

九、名称、容器ID和PID不是一回事

bash
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。

十、日志从哪里来

mermaid
flowchart TD
    A["Nginx主进程及worker"] --> B["stdout和stderr"]
    B --> C["Docker日志驱动"]
    C --> D["docker logs"]

查看:

bash
docker logs --timestamps --tail 100 first-nginx
docker logs --timestamps --since 5m -f first-nginx

另一个终端执行:

bash
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在运行容器内启动额外进程结果不完全等于主进程启动环境

执行只读诊断:

bash
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。

十二、状态为什么会变化

mermaid
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主进程正在运行
Pausedcgroup进程被冻结,不等于正常停止
Exited主进程已经结束,元数据仍在
Restarting重启策略正在反复尝试启动
DeadDocker无法正常完成清理的异常状态

详细查看:

bash
docker inspect first-nginx \
  --format 'Status={{.State.Status}} Running={{.State.Running}} Exit={{.State.ExitCode}} OOM={{.State.OOMKilled}} Error={{.State.Error}}'

十三、stop、start和restart实验

停止:

bash
docker stop first-nginx

典型原理:

mermaid
flowchart TD
    A["docker stop"] --> B["daemon向PID 1发送停止信号"]
    B --> C["应用停止接收新工作并清理"]
    C --> D{"宽限期内是否退出"}
    D -- "是" --> E["容器进入Exited"]
    D -- "否" --> F["发送SIGKILL强制结束"]

再次启动同一容器:

bash
docker start first-nginx

它复用同一个容器的创建配置和可写层。验证页面仍可访问:

bash
curl -i http://127.0.0.1:18080/

重启:

bash
docker restart first-nginx

restart相当于停止后再次启动同一容器,不会自动pull新镜像,也不会采用后来修改的端口和挂载配置。

十四、容器可写层实验

在容器中创建文件:

bash
docker exec first-nginx sh -c 'echo container-layer > /tmp/lifecycle.txt'
docker exec first-nginx cat /tmp/lifecycle.txt
docker diff first-nginx

停止再启动:

bash
docker stop first-nginx
docker start first-nginx
docker exec first-nginx cat /tmp/lifecycle.txt

文件仍在,因为stop/start没有删除容器可写层。

删除容器前先确认对象:

bash
docker inspect first-nginx \
  --format 'Name={{.Name}} Image={{.Config.Image}} Status={{.State.Status}} Mounts={{json .Mounts}}'
docker stop first-nginx
docker rm first-nginx

重新用同一镜像创建:

bash
docker run -d \
  --name first-nginx \
  --label lab=docker-first-run \
  -p 127.0.0.1:18080:80 \
  nginx:stable-alpine

再检查:

bash
docker exec first-nginx sh -c 'test -f /tmp/lifecycle.txt && echo exists || echo missing'

预期是missing。删除旧容器会删除旧容器可写层;使用同一镜像创建的是新容器,不会继承旧可写层。

十五、Volume为什么能跨容器保留

创建带实验标签的Volume:

bash
docker volume create \
  --label lab=docker-first-run \
  first-nginx-data

创建一个一次性容器写入Volume:

bash
docker run --rm \
  -v first-nginx-data:/data \
  nginx:stable-alpine \
  sh -c 'echo volume-data > /data/message.txt'

另一个新容器读取:

bash
docker run --rm \
  -v first-nginx-data:/data:ro \
  nginx:stable-alpine \
  cat /data/message.txt
mermaid
flowchart 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 rmVolume及其数据镜像和容器可写层

删除操作前必须确认名称、ID、标签、引用和数据归属。不要把全局prune当作入门清理命令。

本实验只清理明确名称和标签的对象:

bash
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

保留镜像也没有问题,它可供后续实验复用。

十七、资源使用怎么观察

重新创建容器后可执行:

bash
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,执行:

bash
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超时后的强杀。

十九、服务访问不到怎么逐层查

mermaid
flowchart TD
    A["确认容器Running"] --> B["确认应用监听容器端口"]
    B --> C["确认Docker端口绑定"]
    C --> D["确认宿主机监听与NAT"]
    D --> E["确认防火墙和安全组"]
    E --> F["确认HTTP、TLS和应用响应"]

命令:

bash
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只理解边界

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容器化

二十一、商业项目中对象如何对应

以订单服务为例:

text
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仍不是备份。

二十三、学习验收

不看答案完成:

  1. 用context、version和info证明CLI连接的是哪台daemon。
  2. 运行hello-world并解释它为什么Exited 0。
  3. 画出缺少本地镜像时run的pull、create、start全过程。
  4. 启动Nginx并解释每个run参数。
  5. 从浏览器或curl画出宿主机18080到容器80的链路。
  6. 区分名称、Container ID、Image ID和宿主机PID。
  7. 使用ps、inspect、logs、top、stats和port收集证据。
  8. 区分前台、后台、attach和exec。
  9. 证明stop/start保留可写层,rm/recreate不保留旧可写层。
  10. 使用两个一次性容器证明Volume跨容器保留数据,并解释Volume为什么不是备份。
  11. 制造一个启动即退出的容器,按ExitCode、日志和命令定位。
  12. 制造一个错误端口映射,按监听、发布、宿主机和协议逐层定位。
  13. 解释Dockerfile中EXPOSE为什么不会自动开放端口。
  14. 写出商业项目中commit、镜像Digest、容器、配置、Secret和监控的对应关系。

关联知识点