跳到主要内容

构建一个自己的镜像

基础概念

pAh9sYT.png

Dockerfile: 构建镜像的说明文件

Dockerfile 是用于描述如何构建 Docker 容器镜像的文本文件,它由一系列指令和配置信息构成。Docker 依次读取和执行这些配置和指令,从而一步步组装出目标 Docker 容器和镜像。通过 Dockerfile 可以实现自动化地构建镜像,确保在不同的环境中都可以复现相同的容器。

Dockerfile 中的指令可以指定从哪个基础镜像开始构建、复制文件到镜像中、安装软件包、设置环境变量、暴露端口、运行命令等等。每个指令都会在镜像的构建过程中创建一个新的镜像层,这些层构成了最终镜像的结构。这种分层结构让镜像的构建更加高效,同时也方便了镜像的复用和共享。

Image 镜像: 一个特殊的文件系统

众所周知,操作系统分为内核空间与用户空间。对于 Linux 而言,内核在启动后,会挂载 rootfs (根文件系统)为其提供用户空间支持。而 Docker 中的镜像,就相当于是一个特殊的 rootfs 文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

Docker 在设计时就充分利用 Union FS 技术,将其设计为分层存储的架构

镜像实际是由多层文件系统联合组成。Docker 镜像实际上是被一层层地构建起来的,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。 比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

分层存储的特征还使得镜像的复用和定制变的更为容易。我们甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

Container容器: 镜像运行时的实体

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的「类」和「实例」一样。**镜像是静态的定义,而容器是镜像运行时的实体。**容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。我们在前面提到,镜像使用的是基于 Union FS 的分层存储,而容器也是如此。

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据 ,也就是要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)或绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)进行读写,其性能和稳定性更高。数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此, 使用数据卷后,容器可以随意删除和重新运行 ,数据却不会丢失。

Registry 注册表: 镜像的集中存储和分发服务

镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。所以说:镜像仓库是 Docker 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。

Docker Registry 公开服务 是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。

最常用、最知名的公共 Registry 服务是 Docker 官方维护的 Docker Hub ,这也是 Docker 默认的 Registry,拥有大量的高质量的官方镜像。除此之外,还有由其他企业提供和维护的第三方 Registry,例如 Amazon Elastic Container Registry(ECR)Azure Container Registry (ACR)GitHub Container RegistryQuay.ioGitLab container registry 等。

制作一个自己的镜像

下面是一个简单的 Dockerfile 示例,用于将一个 Python 程序封装为 Docker 容器: 示例可以在这里克隆 https://github.com/lulaide/docker-example/blob/main/example_flask_app/main.py

# 使用 uv 的 python 3.12 镜像作为基础镜像
FROM astral/uv:python3.12-bookworm-slim

# 设置工作目录
WORKDIR /app

# 复制当前目录下的所有文件到工作目录
COPY . .

# 安装应用程序依赖
RUN uv sync

# 暴露应用程序需要的端口
EXPOSE 5000

# 定义容器启动时运行的命令
ENTRYPOINT ["uv", "run", "main.py"]

# 可覆盖的默认参数
CMD [ "--name", "World" ]

下面是对 Dockerfile 指令的解释

  • FROM:指定基础镜像,用于构建新的镜像。通常从官方镜像或其他已有的镜像开始构建。
  • WORKDIR:设置工作目录,后续的指令都会在该目录下执行。
  • COPY 或 ADD:将本地文件复制到镜像中的指定目录。
  • RUN:在镜像中执行命令,用于安装软件包、更新系统等操作。
  • EXPOSE:声明容器运行时需要暴露的端口,供外部访问。
  • CMD 或 ENTRYPOINT:定义容器启动时执行的默认命令或程序。

这里详细讲一下 ENTRYPOINTCMD 的区别:

指令是什么作用是否能被 docker run 参数覆盖
ENTRYPOINT容器的主命令指定容器启动后一定要执行的“主程序”不能被覆盖(除非使用 --entrypoint
CMD默认参数 或 默认命令给 ENTRYPOINT 传参 或 作为默认命令会被覆盖

构建镜像

编写完 Dockerfile 后,可以使用 docker build 命令构建镜像。在 Dockerfile 所在的目录下执行以下命令:

docker build -t flask-app:1.0.0 .

这里的 flask-app 是镜像名称。 -t 参数用于指定镜像的名称和标签,这里的 1.0.0 就是这个镜像的标签(通常使用版本号作为镜像标签),最后的 . 指定在当前工作目录下寻找 Dockerfile 并由此构建镜像。

Dockerfile 的文件名不必须为 Dockerfile,也不一定要放在构建上下文的根目录中。通过在运行 docker build 命令时使用 -f--file 参数,可以指定任何位置的任何文件作为 Dockerfile。

当然,大多数人还是会使用默认的文件名 Dockerfile,并将其置于镜像构建时的工作目录中,这样做可以简化构建命令。

运行容器

构建完成镜像后,可以使用 docker run 命令运行容器:

docker run -p 5000:5000 flask-app:1.0.0

这里的 -p 参数用于将容器的端口映射到宿主机的端口,这样就可以通过宿主机的 5000 端口访问容器内的应用程序。

访问宿主机的 5000 端口,就可以看到运行在容器内的 Flask 应用程序了。

Hello, World!

可以通过修改 docker run 命令的参数,来传递不同的参数给容器内的应用程序。例如:

docker run -p 5000:5000 flask-app:1.0.0 --name Lulaide

可以看到 world 被替换成了 Lulaide:

Hello, Lulaide!

推送镜像

如果我们构建的镜像没有问题,就可以将其推送到镜像仓库中。

docker push 命令用于将本地的 Docker 镜像上传到指定的 Registry/Hub。

推送前需要给镜像打上正确的标签,标签格式为 <registry>/<repository>:<tag> 。例如此示例的 tag:

docker tag flask-app:1.0.0 ghcr.io/lulaide/example_flask_app:latest

登入指定的 Registry/Hub(默认为 Docker Hub):

# Docker Hub
docker login
# GHCR
docker login ghcr.io

然后使用 docker push 命令将镜像推送到指定的 Registry/Hub:

docker push ghcr.io/lulaide/example_flask_app:latest

这样,其他用户就可以从该 Registry/Hub 上拉取并使用这个镜像了。

多阶段构建镜像

多阶段构建(Multi-stage Build)是 Docker 提供的一种优化镜像构建过程的技术。通过多阶段构建,可以在一个 Dockerfile 中定义多个构建阶段,每个阶段可以使用不同的基础镜像和构建指令。最终只将需要的文件从各个阶段复制到最终的镜像中,从而减小最终镜像的体积,提高构建效率。 https://github.com/lulaide/docker-example/blob/main/example_gin_app/Dockerfile

# 使用 golang 作为构建阶段的基础镜像
FROM golang:1.25 AS builder

WORKDIR /app

COPY . /app/

# 编译可执行文件
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./main.go
FROM debian:bookworm-slim

# 从构建阶段复制编译好的二进制文件
COPY --from=builder /app/server .

EXPOSE 8080

CMD ["./server"]

最终镜像中就只有一个可执行文件 server,没有任何编译时的依赖和中间文件,从而大大减小了镜像的体积。

docker run -p 8080:8080 ghcr.io/lulaide/example_gin_app:latest