【Go 与云原生】让一个 Go 项目脱离原生的操作系统——我们开始使用 Docker 制造云容器进行时
文章目录
- 前言
- Docker 的技术边界
- Docker 与虚拟机的区别
- 同一个镜像是可以运行成多个不同的容器,做不同的事情
- 打包 Docker 镜像
- 前端程序的 Dockerfile
- 后端程序的 Dockerfile
- 近距离的观察 Docker 镜象
- 镜像里面包含容器运行时的信息
- `UpperDir` 的作用
- `WorkDir` 的作用(处理文件操作的事务性)
- 想象图景
- 运行镜像,将其变成 Docker 容器运行时
- 近距离观察 Docker 容器
- 结尾:先删除镜像,后删除容器
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
前言
我们在上一篇文章 《先从 Go 对与云原生的依赖关系讲起,再讲讲 一个简单的 Go 项目热热身》 中介绍了一个零声教育的商品信息服务项目案例,由于在代码中使用了 GRPC 框架,我们把项目变成了一个前后端分离、信息流紧凑结构化的程序。这个 go 程序是可以通过 docker 打包成镜像以达到脱离其操作系统环境的目的,而且极其之简单,因为 go 程序真不像 C/C++ 程序那样每次启动都需要程序员主动去连接动态库,它是一次过把所有的依赖都打包进了同一个程序之中。
因此,我想说明 C/C++ 程序的 Docker 镜像打包,完全可以做。但是需要程序员对库依赖文件具有高度敏锐的识别,程序员需要把所有依赖文件都找齐,比如我们使用了 Linux 的 epoll 事件驱动库,那就需要把这个文件找出来… 。这是一个非常费力的过程,要求程序员对操作系统有很高水平的认知和理解。
进入正题,我们在本篇文章要把之前做的商品信息服务系统打包成 Docker 镜像,然后运行成一个容器。通过一个具体的案例,我们是可以深刻领会 Docker 寂静是一个什么样的软件了。另外,docker 的下载请自行解决。

Docker 的技术边界
docker是容器化技术,针对的是应用及应用所依赖的环境做容器化。遵循单一原则,一个容器只运行一个主进程。多个进程都部署在一个容器中,弊端很多。比如更新某个进程的镜像时,其他进程也会被迫重启,如果一个进程出问题导致容器挂了,所有进程都将无法访问。再根据官网的提倡的原则而言,容器 = 应用 + 依赖的执行环境,而不是像虚拟机一样,把一堆进程都部署在一起。
docker解决了什么问题:
- 解决了应用程序本地运行环境与生产运行环境不一致的问题
- 解决了应用程序资源使用的问题,docker会一开始就为每个程序指定内存分配和CPU分配
- 让快速扩展、弹性伸缩变得简单
Docker 与虚拟机的区别
我们虚拟机是需要在配置的时候通过光盘映像文件安装操作系统(比如 Ubuntu,Centos 等),但是 Docker 只需要把所有依赖文件安装好了就行,整个 docker 容器就只运行一个程序即可,但虚拟机操作系统需要做到 “面面俱到”,因而整体规模很大。
Hypervisor(虚拟机监控器,Virtual Machine Monitor,简称 VMM)是一种软件、硬件或固件,用于创建和运行虚拟机(VM)。它允许多个操作系统共享单一的物理主机,通过将主机的硬件资源(如 CPU、内存、存储和网络)虚拟化,使每个虚拟机都能独立运行,仿佛它们各自拥有独立的物理机。
对于 Docker 来说,它有 Docker 引擎管理镜像、存储数据持久化、网络区隔、容器运行时的资源分配。这个倒是和 Hypervisor 区别不大。Docker 容器镜像之内,就是一个小型王国,它有操作系统的一些性质,比如 “域名解析”、环境变量、路径、终端等等。

同一个镜像是可以运行成多个不同的容器,做不同的事情
镜像构建只创建只读的 LowerDir 层,每次运行容器时才会创建新的 UpperDir 和 WorkDir,同一个镜像的多个容器实例有各自独立的 UpperDir。

打包 Docker 镜像
将 Go 程序打包成 Docker 镜像,并不是那么难,因为 go 程序并没有那么的以来操作系统,也可以说是与操作系统生殖隔离(不同操作系统的差别并不大),实在是上手学习云原生部署的好对象。这点远远优于 C/C++ 程序,因为 c/c++ 是高度依赖于操作系统的各个架构与底层源代码的,比如 epoll 和 io-uring 等库文件都是 Linux 的底层代码,因而盘根错节,剪不断理还乱。
另外,我会先介绍 Dockerfile 的语法
| Docker 语法命令 | 作用 |
|---|---|
| FROM | 设置镜像使用的基础镜像 |
| MAINTAINER | 设置镜像的作者 |
| RUN | 编译镜像时运行的脚步 |
| CMD | 设置容器的启动命令 |
| LABEL | 设置镜像标签 |
| EXPOSE | 设置镜像暴露的端口 |
| ENV | 设置容器的环境变量 |
| ADD | 编译镜像时复制上下文中文件到镜像中 |
| COPY | 编译镜像时复制上下文中文件到镜像中 |
| ENTRYPOINT | 设置容器的入口程序 |
| VOLUME | 设置容器的挂载卷 |
| USER | 设置运行 RUN CMD ENTRYPOINT的用户名 |
| WORKDIR | 设置 RUN CMD ENTRYPOINT COPY ADD 指令的工作目录 |
| ARG | 设置编译镜像时加入的参数 |
| ONBUILD | 设置镜像的ONBUILD 指令 |
| STOPSIGNAL | 设置容器的退出信号量 |
前端程序的 Dockerfile
我们先来看我们为前端程序准备的 Dockerfile

我们发现这个 Dockerfile 里面有两段文字两个 FROM,实质上该镜像的构建过程分成两个阶段 stage,我们查看上面那个 Dockerfile 文件语法表,便可以解读
第一阶段
1、(基础)下载镜像 golang:1.20,本地没有的话,就远程拉取,就此进入第一阶段,记作 stage0。
2、(运行)设置 Docker 镜像容器内的环境变量 GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct
3、(复制)把当前路径下的所有文件都复制到容器的 /src/0voiceGateway 路径上
4、(进入某个容器镜像的某个目录)进入某个容器镜像的 /src/0voiceGateway 目录
5、(运行)编译 go 程序,跟我们上一篇文章类似, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o 0voiceGateway .第二阶段
1、(基础)下载镜像 alpine:3.18,本地没有的话,就远程拉取,就此进入第二阶段,记作 stage1。
2、(复制)把当前文件夹下的 ./curl-amd64 可执行文件复制到容器里面,并且指定路径与文件名 /usr/bin/curl
3、(运行)将容器内的文件 /usr/bin/curl 设置可执行权限
4、(进入某个容器镜像的某个目录)进入某个容器镜像的 /app 目录
5、(复制)把当前路径下的 config.yaml 文件都复制到容器的 /app/config.yaml 路径上
6、(复制)把第一个阶段的编译成型文件 /src/0voiceGateway/0voiceGateway 移动到当前镜像的 /app 目录下
7、(入口程序)当我们把这个镜像启动成 docker 容器运行时的时候,第一步就是要执行这个文件
打包前端程序的镜像需要用到这个命令,docker build 是 docker 的镜像构架命令,-t 是 --tag 的简写,用来给即将构建出的镜像取“名字+标签”(name:tag)。后面的 0voice-gateway:v0.5.0 就是镜像+标签,. 就是当前文件路径下的 Dockerfile。
docker build -t 0voice-gateway:v0.5.0 .第一阶段生成镜像就是中间镜像,如果这个命令没有指明生成哪一个,就一定是最后一个阶段的镜像作为最终镜像如果我们运行下面这个命令,那就不会再是默认的最后一个阶段变成最终镜像
$ docker build -t [输出镜像名字];[标签] --target [阶段名字] [Dockerfile的文件名]
于是,终端输出会显示整个构建过程,他是分阶段构建的,我们从 [stage0 3/5] ADD ./ /src/0voiceGateway 和 [stage1 6/6] COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./ 等等可以看出是分阶段构建,因为它有阶段名字与该阶段下的命令,还介绍了该阶段下运行的命令运行了多长时间。
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/0voiceGateway$ docker build -t 0voice-gateway:v0.5.0 .
[+] Building 351.7s (17/17) FINISHED docker:default=> [internal] load build definition from Dockerfile 0.2s=> => transferring dockerfile: 1.29kB 0.2s=> WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1) 0.2s=> WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 7) 0.2s=> [internal] load metadata for docker.io/library/alpine:3.18 0.0s=> [internal] load metadata for docker.io/library/golang:1.20 46.5s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [stage0 1/5] FROM docker.io/library/golang:1.20@sha256:8f9af7094d0cb27cc783c697ac5ba25efdc4da35f8526db21f7aebb0b0b4f18a 0.0s=> [stage1 1/6] FROM docker.io/library/alpine:3.18 0.0s=> CACHED [stage0 2/5] RUN go env -w GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct 0.0s=> [internal] load build context 18.3s=> => transferring context: 20.67MB 18.2s=> [stage0 3/5] ADD ./ /src/0voiceGateway 1.7s=> CACHED [stage1 2/6] ADD ./curl-amd64 /usr/bin/curl 0.0s=> CACHED [stage1 3/6] RUN chmod +x /usr/bin/curl 0.0s=> CACHED [stage1 4/6] WORKDIR /app/ 0.0s=> [stage1 5/6] ADD ./config.yaml /app/config.yaml 0.5s=> [stage0 4/5] WORKDIR /src/0voiceGateway 0.0s=> [stage0 5/5] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o 0voiceGateway . 284.1s=> [stage1 6/6] COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./ 0.2s => exporting to image 0.2s => => exporting layers 0.2s => => writing image sha256:d5542708ce1bf33c6ddc8fa5da784ae957306de83cf263b0a597084a4016f8f1 0.0s => => naming to docker.io/library/0voice-gateway:v0.5.0 0.0s 2 warnings found (use docker --debug to expand):- FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)- FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 7)
后端程序的 Dockerfile
我们先来看我们为后端程序准备的 Dockerfile(这个文件的解读也和上面的一样,大家跟着我的步骤来就行了)

打包前端程序的镜像需要用到这个命令
docker build -t goods:v0.5.0 .
于是,终端输出会显示整个构建过程(解读过程也跟前面的一样)
qiming@k8s-master1:~/share/CTASK/docker/code/0voice-crm/Goods$ docker build -t goods:v0.5.0 .
[+] Building 344.4s (17/17) FINISHED docker:default=> [internal] load build definition from Dockerfile 0.9s=> => transferring dockerfile: 498B 0.4s=> [internal] load metadata for docker.io/library/alpine:3.18 0.0s=> [internal] load metadata for docker.io/library/golang:1.20 50.1s=> [internal] load .dockerignore 0.0s=> => transferring context: 2B 0.0s=> [stage0 1/5] FROM docker.io/library/golang:1.20@sha256:8f9af7094d0cb27cc783c697ac5ba25efdc4da35f8526db21f7aebb0b0b4f18a 0.0s=> [internal] load build context 0.6s=> => transferring context: 14.94MB 0.6s=> [stage1 1/6] FROM docker.io/library/alpine:3.18 0.0s=> CACHED [stage0 2/5] RUN go env -w GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct 0.0s=> [stage0 3/5] ADD ./ /src/goods 9.8s=> [stage0 4/5] WORKDIR /src/goods 0.0s=> [stage0 5/5] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o goods . 277.7s=> CACHED [stage1 2/6] ADD ./grpc_health_probe-linux-amd64 /usr/bin/grpc_health_probe 0.0s => CACHED [stage1 3/6] RUN chmod +x /usr/bin/grpc_health_probe 0.0s => CACHED [stage1 4/6] WORKDIR /app/ 0.0s => CACHED [stage1 5/6] ADD ./config.yaml /app/config.yaml 0.0s => [stage1 6/6] COPY --from=stage0 /src/goods/goods ./ 0.2s => exporting to image 2.1s => => exporting layers 1.7s=> => writing image sha256:714feb39b527f08ebb24816e1ce9901dd97ecf42e7d5418da45aed48afbc162e 0.1s=> => naming to docker.io/library/goods:v0.5.0 0.1s3 warnings found (use docker --debug to expand):- FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)- FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 7)- MaintainerDeprecated: Maintainer instruction is deprecated in favor of using label (line 10)
近距离的观察 Docker 镜象
构建完镜像后,我们就可以通过 docker inspect 去近距离观察一个镜像,发现它的信息真的很多,但也无须着急,因为真正重要的也就是 GraphDriver 和 RootFS 两个字段的内容。
qiming@k8s-master1:~$ docker inspect 0voice-gateway:v0.5.0
[{"Id": "sha256:d5542708ce1bf33c6ddc8fa5da784ae957306de83cf263b0a597084a4016f8f1","RepoTags": ["0voice-gateway:v0.5.0"],"RepoDigests": [],"Parent": "","Comment": "buildkit.dockerfile.v0","Created": "2025-11-08T18:53:43.29937097Z","DockerVersion": "","Author": "","Architecture": "amd64","Os": "linux","Size": 34735574,"GraphDriver": {"Data": {"LowerDir": "/var/lib/docker/165536.165536/overlay2/r20wlwiybeqx425ote1slwbiu/diff:/var/lib/docker/165536.165536/overlay2/5o0xzo91v6dhwrrcf00m9r749/diff:/var/lib/docker/165536.165536/overlay2/t82x15extk3uuxhlrb8raiyya/diff:/var/lib/docker/165536.165536/overlay2/p645ylzduapfzr0yvm48k956s/diff:/var/lib/docker/165536.165536/overlay2/b9c3d4bf2bafc80dbf6081405bc051132bdbc6ab82eb989646a5777426c3c22e/diff","MergedDir": "/var/lib/docker/165536.165536/overlay2/73wyqndlla6e3d7dd0gwuw0eq/merged","UpperDir": "/var/lib/docker/165536.165536/overlay2/73wyqndlla6e3d7dd0gwuw0eq/diff","WorkDir": "/var/lib/docker/165536.165536/overlay2/73wyqndlla6e3d7dd0gwuw0eq/work"},"Name": "overlay2"},"RootFS": {"Type": "layers","Layers": ["sha256:f44f286046d9443b2aeb895c0e1f4e688698247427bca4d15112c8e3432a803e","sha256:aaf1c64fc2a8e5857597d97ede0eb18beedc86d05b6e2bdda3b62571ea9d3af7","sha256:fe695ece85edd2ab4d1c0c6115d8febce80f51d2cab1d1b656440b5754c16678","sha256:be4ad4a66fe7c00a1c5d51ad3915879114f774399a111d4db5971659d11959b6","sha256:6e819136eca5d4fa602bd1e8476786cdd429715c74c33b8536f5d25e554abad0","sha256:b4cc20de0a7c46f6ca41cc8f3579f56291936d90dd70735608b967b26d6eb76f"]},"Metadata": {"LastTagTime": "2025-11-08T18:53:43.50436954Z"},"Config": {"Cmd": null,"Entrypoint": ["./0voiceGateway"],"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Labels": null,"OnBuild": null,"User": "","Volumes": null,"WorkingDir": "/app/"}}
]
我们需要注意到 GraphDriver 字段的子字段 Data 的 LowerDir 字段,即 GraphDriver.Data.LowerDir 字段,它里面有 5 个路径。这 5 个路径对应 Dockerfile 中的以下构建步骤:
LowerDir[0]: /var/lib/docker/.../r20wlwiybeqx425ote1slwbiu/diff↓
LowerDir[1]: /var/lib/docker/.../5o0xzo91v6dhwrrcf00m9r749/diff ↓
LowerDir[2]: /var/lib/docker/.../t82x15extk3uuxhlrb8raiyya/diff↓
LowerDir[3]: /var/lib/docker/.../p645ylzduapfzr0yvm48k956s/diff↓
LowerDir[4]: /var/lib/docker/.../b9c3d4bf2bafc80dbf6081405bc051132bdbc6ab82eb989646a5777426c3c22e/diff
需要明确,任何镜像构建命令,没有指定哪一个阶段作为最终镜像,那就一定是最后一个阶段作为构建镜像的依据,即这个镜像的 5 个 LowerDir 就是仅由红色框的命令生成的

第1层(最底层):alpine:3.18 基础镜像
# LowerDir[4] - 基础操作系统层
/var/lib/docker/.../b9c3d4bf2bafc80dbf6081405bc051132bdbc6ab82eb989646a5777426c3c22e/diff# 内容:Alpine Linux 3.18 的最小文件系统
/bin, /etc, /lib, /usr, /var, ...
第2层:添加 curl 二进制文件
# LowerDir[3] - 对应 Dockerfile 第8行
ADD ./curl-amd64 /usr/bin/curl# 内容:/usr/bin/curl 文件
# 这创建了一个新层,包含您添加的 curl 二进制文件
第3层:设置 curl 权限
# LowerDir[2] - 对应 Dockerfile 第9行
RUN chmod +x /usr/bin/curl# 内容:/usr/bin/curl 文件的权限元数据变化
# 虽然看起来是"修改",但在分层文件系统中这会创建新层
第4层:设置工作目录和添加配置文件
# LowerDir[1] - 对应 Dockerfile 第10-11行
WORKDIR /app/
ADD ./config.yaml /app/config.yaml# 内容:
# - 工作目录元数据
# - /app/config.yaml 配置文件
第5层:从 stage0 复制构建结果
# LowerDir[0] - 对应 Dockerfile 第12行
COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./# 内容:/app/0voiceGateway 可执行文件
# 这是从第一阶段构建的 Go 二进制文件
被优化的指令:
- FROM golang:1.20 as stage0 - 这是第一阶段,不包含在最终镜像中
- ENTRYPOINT [“./0voiceGateway”] - 元数据指令,不创建文件系统层
完整的镜像结构就是 GraphDriver.Data.MergedDir 字段下的那个
+------------------------------------------------+
| 最终镜像(stage1)的 Merged View |
+------------------------------------------------+
| UpperDir (可写层,如果有容器修改) |
+------------------------------------------------+
| LowerDir[0]: 0voiceGateway 二进制文件 | ← COPY --from=stage0
+------------------------------------------------+
| LowerDir[1]: /app/config.yaml + WORKDIR | ← ADD config.yaml
+------------------------------------------------+
| LowerDir[2]: curl 文件权限设置 | ← RUN chmod +x
+------------------------------------------------+
| LowerDir[3]: curl 二进制文件 | ← ADD curl-amd64
+------------------------------------------------+
| LowerDir[4]: alpine:3.18 基础系统 | ← FROM alpine:3.18
+------------------------------------------------+
另外,RootFS 字段也很有意思
"RootFS": {"Type": "layers","Layers": ["sha256:f44f286046d9443b2aeb895c0e1f4e688698247427bca4d15112c8e3432a803e","sha256:aaf1c64fc2a8e5857597d97ede0eb18beedc86d05b6e2bdda3b62571ea9d3af7","sha256:fe695ece85edd2ab4d1c0c6115d8febce80f51d2cab1d1b656440b5754c16678","sha256:be4ad4a66fe7c00a1c5d51ad3915879114f774399a111d4db5971659d11959b6","sha256:6e819136eca5d4fa602bd1e8476786cdd429715c74c33b8536f5d25e554abad0","sha256:b4cc20de0a7c46f6ca41cc8f3579f56291936d90dd70735608b967b26d6eb76f"]
}
核心区别
GraphDriver.Data.LowerDir:显示的是实际磁盘上的分层目录结构(Overlay2 驱动视角)RootFS.Layers:显示的是镜像的链式层级关系(镜像元数据视角)
第1-2层:来自 golang:1.20 基础镜像
# golang:1.20 本身就是一个多层的镜像
Layers[0]: "sha256:f44f286046d9443b2aeb895c0e1f4e688698247427bca4d15112c8e3432a803e"
Layers[1]: "sha256:aaf1c64fc2a8e5857597d97ede0eb18beedc86d05b6e2bdda3b62571ea9d3af7"
# 这些是 golang:1.20 基础镜像的层,虽然最终镜像不包含文件内容,
# 但镜像元数据中保留了这些层的引用
第3-6层:来自 Dockerfile 构建
# 对应您的 Dockerfile 指令
Layers[2]: "sha256:fe695ece85edd2ab4d1c0c6115d8febce80f51d2cab1d1b656440b5754c16678"# FROM alpine:3.18 基础层Layers[3]: "sha256:be4ad4a66fe7c00a1c5d51ad3915879114f774399a111d4db5971659d11959b6" # ADD ./curl-amd64 /usr/bin/curlLayers[4]: "sha256:6e819136eca5d4fa602bd1e8476786cdd429715c74c33b8536f5d25e554abad0"# RUN chmod +x /usr/bin/curl + WORKDIR /app/ + ADD ./config.yaml /app/config.yamlLayers[5]: "sha256:b4cc20de0a7c46f6ca41cc8f3579f56291936d90dd70735608b967b26d6eb76f"# COPY --from=stage0 /src/0voiceGateway/0voiceGateway ./
虽然最终镜像(stage1)不包含 stage0 的文件内容,但镜像元数据中仍然保留了这些层的引用:
// 镜像元数据结构
type ImageConfig struct {RootFS struct {Type string `json:"type"`Layers []string `json:"layers"` // 所有引用到的层,包括构建阶段的引用} `json:"rootfs"`History []History `json:"history"` // 构建历史记录
}
您可以通过以下命令验证:
# 查看镜像的历史记录,会显示所有层的创建信息
docker history your-image-name
终端输出可以看到
qiming@k8s-master1:~$ docker history 0voice-gateway:v0.5.0
IMAGE CREATED CREATED BY SIZE COMMENT
d5542708ce1b 13 hours ago ENTRYPOINT ["./0voiceGateway"] 0B buildkit.dockerfile.v0
<missing> 13 hours ago COPY /src/0voiceGateway/0voiceGateway ./ # b… 20.2MB buildkit.dockerfile.v0
<missing> 13 hours ago ADD ./config.yaml /app/config.yaml # buildkit 356B buildkit.dockerfile.v0
<missing> 4 weeks ago WORKDIR /app/ 0B buildkit.dockerfile.v0
<missing> 4 weeks ago RUN /bin/sh -c chmod +x /usr/bin/curl # buil… 3.58MB buildkit.dockerfile.v0
<missing> 4 weeks ago ADD ./curl-amd64 /usr/bin/curl # buildkit 3.58MB buildkit.dockerfile.v0
<missing> 8 months ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
<missing> 8 months ago ADD alpine-minirootfs-3.18.12-x86_64.tar.gz … 7.36MB buildkit.dockerfile.v0
RootFS.Layers 有 6 个层的原因是:
- 包含了多阶段构建的所有层引用,包括构建阶段(stage0)的层
- 镜像元数据记录了完整的构建历史和层依赖关系
- 内容寻址存储机制要求记录所有被引用的层哈希
而 GraphDriver.Data.LowerDir 只有 5 个路径是因为:
- 只包含实际的文件系统层(最终镜像中存在的文件)
- 反映了 Overlay2 存储驱动的实际磁盘结构
这种设计确保了:
- 层的高效共享(多个镜像可以引用相同的底层)
- 构建历史的完整性(可以追溯完整的构建过程)
- 存储的空间效率(实际只存储需要的文件数据)
镜像里面包含容器运行时的信息
我补充说明,GraphDriver.Data 字段的 UpperDir 和 WorkDir 是容器运行时的概念。镜像构建只创建只读的 LowerDir 层,每次运行容器时才会创建新的 UpperDir 和 WorkDir,同一个镜像的多个容器实例有各自独立的 UpperDir。
UpperDir 的作用
1、容器的可写层(比如我们在容器里面编译一个文件,那这个文件就会放在这个文件夹下)
# 当您运行容器时:
docker run -it your-image /bin/sh# Docker 会为这个容器实例创建一个 UpperDir
# 所有在容器内的修改都存储在这里
2.、写时复制(Copy-on-Write)
// 当容器修改文件时的流程:
func handleFileModification(filename string) {// 1. 检查文件是否在 UpperDir 中已存在if !existsInUpperDir(filename) {// 2. 如果不存在,从 LowerDir 复制到 UpperDircopyFromLowerToUpper(filename)}// 3. 在 UpperDir 中进行修改modifyFileInUpperDir(filename)
}
WorkDir 的作用(处理文件操作的事务性)
当容器进行文件操作时,WorkDir 用于:
- 文件重命名的原子操作
- 文件删除的标记处理
- 硬链接的临时处理
- 确保文件系统操作的一致性
类似于我们所理解的 C/C++ 原子操作,确保文件一致性不会被打破。实际工作流程是
# 当删除文件时:
1. 在 WorkDir 中创建 "whiteout" 文件(标记删除)
2. 在 MergedDir 中隐藏原文件
3. 确保操作是原子的和一致的
完整的 Overlay2 运行时结构
+------------------------------------------------+
| MergedDir (合并视图) |
| 容器看到的统一文件系统,包含所有修改 |
+------------------------------------------------+
| UpperDir (可写层) |
| - 新建的文件: /app/test.txt |
| - 修改的文件: /app/config.yaml (复制后修改) |
| - 删除标记: /etc/nginx/nginx.conf.wh..wh..opq |
+------------------------------------------------+
| WorkDir (工作目录) |
| - 临时文件操作 |
| - 原子操作准备 |
+------------------------------------------------+
| LowerDir[0-4] (只读镜像层) |
| - 基础镜像文件 |
| - 构建时添加的文件 |
+------------------------------------------------+
想象图景
Docker 的文件系统可以这样想像

运行镜像,将其变成 Docker 容器运行时
上文,我们构建完镜像之后,通过 docker inspect 观察到了镜像的多层构建细节,从而理解了 Docker 的文件系统架构,最终理解了它的运行规律。接下来,我们通过这段命令
docker run -d --name [容器命名] -p [监听宿主机的端口]:[容器内网络条件的端口] [镜像名字]:[标签]
来运行我们生成的两个镜像,生成容器运行时。
qiming@k8s-master1:~$ docker run -d --name qiming-goods -p 50051:50051 goods:v0.5.0
f0d04556bb8baffa6cf8ba09367998d7490264e47c2bf41ef057d0d51ba5e740
qiming@k8s-master1:~$ docker run -d --name qiming-0voice-gateway -p 8081:8081 0voice-gateway:v0.5.0
ee891850ff17a063f48c409e8842b9b11ca4237fc4bedbbc0ec36980158eafd7
--name qiming-0voice-gateway 是命名。-p 是端口映射,它非常之关键,因为没有它,就不能正常的监听宿主机的端口,他只会监听自己容器小空间内的端口,换言之是默认与容器内的程序通信,而非与宿主机通信。-d 是让容器以守护进程的方式运行。守护进程是在后台运行的计算机程序,它的特点是:
- 不直接与用户交互
- 没有控制终端
- 长期运行提供服务
- 通常在系统启动时自动运行
在上面的命令行中的一长串字符是 Docker 容器的完整 ID。
现在我们来检验一下,是否在正常运行,可以通过 docker ps 命令现实 docker 引擎正在管理的容器进程,注意 CONTAINER ID 只是显示了前面那一段,意思意思而已,ID 是同一个的。
qiming@k8s-master1:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ee891850ff17 0voice-gateway:v0.5.0 "./0voiceGateway" 6 seconds ago Up 5 seconds 0.0.0.0:8081->8081/tcp, [::]:8081->8081/tcp qiming-0voice-gateway
f0d04556bb8b goods:v0.5.0 "./goods" About a minute ago Up About a minute 0.0.0.0:50051->50051/tcp, [::]:50051->50051/tcp qiming-goods
而且事实上,他也是可以运行的

近距离观察 Docker 容器
我们可以通过这段命令
docker exec -it [容器名] [容器内的可执行文件]
进入容器这个小型的文件系统
qiming@k8s-master1:~$ docker exec -it qiming-0voice-gateway sh
/app # ls
0voiceGateway config.yaml
/app #
docker exec 命令的选项 -it 实质可分为 -i 和 -t
-i即--interactive:保持标准输入流(STDIN)打开,也就是不会立刻退出,保持交互性-t即--tty:分配一个伪终端(pseudo-TTY),也就是我们看到的终端
这个 docker 容器真的就是一个小型文件系统,他有很多的操作系统小工具(就是那个 alpine 镜像引进的),大家跟着我实验就好了
/app # cd ..
/ # ls
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ #
结尾:先删除镜像,后删除容器
退出容器,使用 exit 命令
/ # exit
qiming@k8s-master1:~$
停止容器 docker stop [容器名]
qiming@k8s-master1:~$ docker stop qiming-goods
qiming-goods
qiming@k8s-master1:~$ docker stop qiming-0voice-gateway
qiming-0voice-gateway
关掉容器 docker rm [容器名]
qiming@k8s-master1:~$ docker rm qiming-goods
qiming-goods
qiming@k8s-master1:~$ docker rm qiming-0voice-gateway
qiming-0voice-gateway
删除镜像 docker rmi [镜像名]:[标签]
qiming@k8s-master1:~$ docker rmi 0voice-gateway:v0.5.0
Untagged: 0voice-gateway:v0.5.0
Deleted: sha256:d5542708ce1bf33c6ddc8fa5da784ae957306de83cf263b0a597084a4016f8f1
qiming@k8s-master1:~$ docker rmi goods:v0.5.0
Untagged: goods:v0.5.0
Deleted: sha256:714feb39b527f08ebb24816e1ce9901dd97ecf42e7d5418da45aed48afbc162e
检查容器情况、镜像的删除情况情况分别用 docker ps 和 docker images,都删干净了
qiming@k8s-master1:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
qiming@k8s-master1:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ghcr.io/flannel-io/flannel v0.27.4 e83704a17731 5 weeks ago 91.4MB
ghcr.io/flannel-io/flannel-cni-plugin v1.8.0-flannel1 bb28ded63816 5 weeks ago 10.8MB
alpine 3.18 802c91d52981 8 months ago 7.36MB
golang 1.22.12 2fce09cfad57 9 months ago 823MB
registry.aliyuncs.com/google_containers/kube-apiserver v1.28.2 cdcab12b2dd1 2 years ago 126MB
registry.aliyuncs.com/google_containers/kube-scheduler v1.28.2 7a5d9d67a13f 2 years ago 60.1MB
registry.aliyuncs.com/google_containers/kube-controller-manager v1.28.2 55f13c92defb 2 years ago 122MB
registry.aliyuncs.com/google_containers/kube-proxy v1.28.2 c120fed2beb8 2 years ago 73.1MB
busybox latest a416a98b71e2 2 years ago 4.26MB
quay.io/jetstack/cert-manager-webhook v1.12.0 5059a762fd74 2 years ago 48.9MB
quay.io/jetstack/cert-manager-controller v1.12.0 eb0fa758c994 2 years ago 63.7MB
quay.io/jetstack/cert-manager-cainjector v1.12.0 8ec112cada1b 2 years ago 41.6MB
registry.aliyuncs.com/google_containers/etcd 3.5.9-0 73deb9a3f702 2 years ago 294MB
gcr.io/kubebuilder/kube-rbac-proxy v0.14.1 1da78bf35ce7 2 years ago 55.6MB
nginx 1.22.1 0f8498f13f3a 2 years ago 142MB
registry.aliyuncs.com/google_containers/coredns v1.10.1 ead0a4a53df8 2 years ago 53.6MB
registry.aliyuncs.com/google_containers/pause 3.9 e6f181688397 3 years ago 744kB
gcr.io/distroless/static nonroot ea2dc773e32d N/A 2.45MB
