如何构建 Docker 镜像:从零开始的完整指南

云计算 云原生
在一个 Dockerfile 里,在左边的一切都是指令(Instruction),而在右边的一切都是赋予给指令的参数(Argement)。还有,要记着这个文件名称是 Dockerfile 它不需要任何的扩展名。

引言

在本文中,您将学习如何从头开始构建一个Docker镜像,并使用Dockerfile将您的应用程序部署并运行为一个Docker容器[1]。

如您所知,Docker 是一种用于打包、部署和运行应用程序的工具,它能够在轻量级容器中完成这些操作。如果您想了解Docker的基础知识,请参考《Docker 详解》[2]博客。

如果您还没有安装 Docker, 请检查 Docker Installation Guide[3]。

开始

Dockerfile 详解

一个 Docker 镜像最基础的建筑块是一个 Dockerfile。

一个 Dockerfile 是一个带有指令和参数的简易的文本文件。Docker 可以通过读取这些在 Dockerfile 里给出的指令自动构建镜像。

在一个 Dockerfile 里,在左边的一切都是指令(Instruction),而在右边的一切都是赋予给指令的参数(Argement)。还有,要记着这个文件名称是 Dockerfile 它不需要任何的扩展名。

图片图片

以下的表格中包含了重要的 Dockerfile 指令和它们的解释。

Dockerfile 指令

解释

FROM

可以从容器注册表拉取的基础镜像 ( Docker hub, GCR, Quay, ECR, 等等)

RUN

在镜像构建过程中执行的命令

ENV

在镜像中设置环境变量。它是构建过程中是可用的,同样在运行的容器中也是。如果您只想要在构建时间中使用它,请使用 ARG 指令

COPY

拷贝本地文件和目录到镜像中

EXPOSE

为 Docker 容器指定特定的要暴露的端口

ADD

它是 COPY 指令的功能更丰富的版本。它还允许从源 URL 复制并将 tar 文件自动提取到镜像中。但是,建议使用 COPY 命令而不是 ADD。如果要下载远程文件,请使用 curl 或使用 RUN 获取

WORKDIR

设置当前的工作目录。您可以在一个 Dockerfile 里面重复使用这个指令去设置一个不同的工作目录。如果您设置了 ENTRYPOINT,像 RUN,CMD,ADD,COPY,或者 ENTRYPOINT 这样的指令就会在你的这个目录里执行

VOLUME

它是用于创建或者挂载卷到 Docker 容器

USER

当运行容器时,设置用户名称和 UID 。你可以使用这个指令去设置一个非 root 的容器用户

LABEL

它是去指定 Docker 镜像的 metadata 信息

ARG

设置构建时,带有 Key 和 Value 的变量。当容器运行时,ARG 变量将不可用。如果你坚持想要在一个运行的容器中使用一个变量,请使用 ENV

SHELL

它被用于为了给其后的 RUN,CMD 和 ENTRYPOINT 去设置 shell 选项和默认 shell 。

CMD

它用于在一个运行的容器中执行一条命令。这里只能有一个 CMD, 如果有多个,它只让最后一个执行。它还可以被 Docker CLI 覆盖

ENTRYPOINT

当容器启动时,指定的命令将会执行。如果您不指定任何 ENTRYPOINT,它默认会是 /bin/sh -c 。您还可以使用 CLI 的 --entrypoint 覆盖 ENTRYPOINT。为了更多的信息请参考如下网址: https://devopscube.com/run-scripts-docker-arguments/

使用 Dockerfile 构建 Docker 镜像

在这一节,您将会学习使用一个在现实工作中使用的案例去构建一个 Docker 镜像。我们将会从头使用一个自定义的 Index 页面去创建一个 Nginx Docker 镜像。

以下的照片展示了镜像构建过程的工作流。

图片图片

跟随以下给出的步骤去构建一个 Docker 镜像。

这篇文章中被使用的 Dockerfile 和 configs 被托管在 Docker 镜像实例 Github repo[4] 上面。您可以克隆它以便参考。

步骤 1: 创建一个必须的文件和文档

创建一个名为 nginx-image 和一个名为 files 的文件夹

mkdir nginx-image && cd nginx-image
mkdir files

创建一个 .dockerignore 文件

touch .dockerignore

步骤 2: 创建一个模板 HTML 文件和 config 文件

当您构建一个为实时项目的 Docker 镜像时,它包含了代码或者应用配置文件。

用于演示目的,我们将要创建一个简单的 HTML 文件和 config 文件作为我们的 app 代码,再使用 Docker 打包。这是一个简单的 index.html 文件。如果您愿意,您可以创建您自己喜欢的。

cd 进入文件夹。

cd files

创建一个 index.html 文件。

vim index.html

复制以下的内容到 index.html 再保存这个文件。

<html>
  <head>
    <title>Dockerfile</title>
  </head>
  <body>
    <div class="container">
      <h1>My App</h1>
      <h2>This is my first app</h2>
      <p>Hello everyone, This is running via Docker container</p>
    </div>
  </body>
</html>

创建一个名字为 default 的文件

vim default

复制以下的的内容到 default 文件

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    
    root /usr/share/nginx/html;
    index index.html index.htm;

    server_name _;
    location / {
        try_files $uri $uri/ =404;
    }
}

步骤 3: 选择一个基础镜像

我们在 Dockerfile 中使用 FROM 命令,该命令指示 Docker 根据 Docker 中心或任何使用 Docker 配置的容器注册表上可用的镜像创建镜像。 我们称它为一个基础镜像。

它是和我们在云上如何从一个虚拟机镜像创建一个虚拟机是相似的。

选择一个基础镜像取决于我们的应用和选择的 OS 平台。在我们的例子中,我们选择 ubuntu:18.04 基础镜像。

为了避免潜在的风险,应总是为您的应用使用 official/org 批准的基础镜像。最后,我们已经添加了所有的已经认证的容器基础镜像的公共仓库,还有,当它来到生产使用案例时,总是使用 minimal 基础镜像类似 Alpine[5](仅仅5Mib) 或者 distroless images[6],Distroless alpine仅仅 2 MiB

步骤 4: 创建一个 Dockerfile

在 nginx-image 文件夹中创建一个 Dockerfile。

vim Dockerfile

这里是一份简单的 Dockerfile 为了我们能够好的继续。然后把这些添加到我们的 Dockerfile。

FROM ubuntu:18.04  
LABEL maintainer="contact@devopscube.com" 
RUN  apt-get -y update && apt-get -y install nginx
COPY files/default /etc/nginx/sites-available/default
COPY files/index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["/usr/sbin/nginx", "-g", "daemon off;"]

这里是每一步的解释:

  1. 1. 使用 LABEL 指令, 我们添加了关于维护者的 一些信息。他不是必须的指令哈。
  2. 2. FROM 指令将会从 Docker hub 拉取 Ubuntu 18.04 版本的镜像,在第三行,我们正在安装 Nginx 。
  3. 3. 然后,我们将 Nginx 的默认配置文件从本地文件目录拷贝到目标镜像目录。
  4. 4. 下一步,我们将 Index.html 文件从本地目录拷贝进目标镜像目录。它将会覆盖在 Nginx 安装过程中被创建的默认的 Index.html 文件。
  5. 5. 我们暴露了 80 端口,作为 Nginx 服务监听的 80 端口。
  6. 6. 最终,当 Docker 镜像启动,我们的 Nginx 服务会在运行过程中使用 CMD 指令。

在Docker 容器, 这个 daemon off; 指令会告诉 Nginx 停留在前端。这就意味着 Nginx 进程进会保持运行不会停止,直到你自己停止这个容器。它不允许 Nginx 的 自守护进程行为。-g 选项指定了一个指令给 Nginx 。

我们在前台运行该进程的原因是将控制台进程附加到标准输入、输出和错误。这意味着您可以看到来自 Nginx 进程的日志或消息。

步骤 4: 构建你的第一个 Docker 镜像

最终的文件夹和文件结构看起来像以下内容。

nginx-image
├── Dockerfile
└── files
    ├── default
    └── index.html

现在,我们要使用 Docker 命令构建我们的镜像。以下的命令会从相同的目录使用 Dockerfile 构建镜像。

docker build -t nginx:1.0 .

1. -t 是为了给这个镜像起个名字和指定你的标签

2. nginx 是这个镜像的名字

3. 1.0 是这个标签名称。如果你不添加任何标签,它默认的标签名称为 Latest

4. . 在末尾的 . 意味着我们会参考 Dokerfile 位置作为我们的 Docker 构建上下文。也就是我们现在的目录

图片图片

如果 Dockerfile 在另一个文件夹,那么你需要明确的把它指定出来,否则会找不到

docker build -t nginx /path/to/folder

现在,我们可以使用这个命令列出镜像

docker images

图片图片

我们在这里可以看见标签是1.0。 如果我们想要弄一个指定的标签,我们可以这样写 image-name:[tag] 。还是那句话,如果你不指定任何标签,默认会是 Latest 。

docker build -t nginx:2.0 .

一个单一的镜像可以有多个标签。这里有两种我们普遍认同的给镜像打标签的方法:

1. 稳定的 Tags – 我们可以继续拉取我们指定的标签,它会继续获得更新。标签总是不变的,但是镜像的内容可以改变。

2. 唯一的 Tags – 我们为每一个镜像使用一个不同和唯一的标签。有多种方式可以提供唯一标签,例如日期时间戳、构建编号、提交 ID 等。

当它到生产环境时,一个推荐给 Docker 镜像打标签的方法是语义版本控制(Semver) [7]

Docker 缓存了构建步骤,因此如果我们再次构建这个镜像,过程会移动地快一点。例如,它不会再次下载 Ubuntu 18.04 镜像

使用体积大的镜像会让容器的构建和部署时间变长。如果你想要学习更过关于优化 Docker 镜像,请参考 减少 Docker 镜像[8]指南。

步骤 5: 测试你的 Docker 镜像

现在,构建过镜像之后,我们将会运行这个 Docker 镜像。这个命令是:

docker run -d -p 9090:80 --name webserver nginx:1.0

这里

1. -d 这个表示让容器在后台运行

2. -p 这个为了指定端口,格式为 本地端口:容器端口

3. --name 指定容器的名称,webserver 是我们的名称

我们可以通过以下的命令检查这个容器

docker ps

图片图片

现在在浏览器中,如果你去到 http://[host-ip]:9090,您可以看到索引页,其中显示了我们添加到 docker 镜像中的自定义 HTML 页面中的内容。

图片图片

推送 Docker 镜像到 Docker Hub

推送我们的 Docker 镜像到 Docker hub[9],我们需要在 Docker hub 创建一个帐号。

从终端执行以下命令登录。它将会要求输入一个用户名和密码。也支持 Docker hub 凭证。

docker login

图片图片

登录进去之后,现在我们需要用 Docker hub 用户名给我们的镜像打标签,如下所示。

docker tag nginx:1.0 <username>/<image-name>:tag

例如,这里的 devopscube 是 Dockerhub 用户名。

docker tag nginx:1.0 devopscube/nginx:1.0

再次运行 docker images 命令,检查被打了标签的镜像将会显示在这里。

图片图片

现在,我们使用以下的命令推送我们的镜像到 Docker hub 。

docker push devopscube/nginx:1.0

现在,你可以在你的 Docker Hub 账户中检查这个镜像是否可用的。

图片图片

图片图片

在 Dockerfile 中使用 heredoc

Dockerfile 还支持 heredoc[10] 语法。如果你有多个 RUN 命令,那么你就可以使用 heredoc,如下所示。

RUN <<EOF
apt-get update
apt-get upgrade -y
apt-get install -y nginx
EOF

还有, 让我们聊聊你想要从 Dockerfile 执行的一个 Python 脚本,你可以使用以下的的语法。

RUN python3 <<EOF
with open("/hello", "w") as f:
    print("Hello", file=f)
    print("World", file=f)
EOF

你还可以使用 heredoc 语法去创建一个文件,这里是一个 Nginx 例子。

FROM nginx

COPY <<EOF /usr/share/nginx/html/index.html
<html>
  <head>
    <title>Dockerfile</title>
  </head>
  <body>
    <div class="container">
      <h1>My App</h1>
      <h2>This is my first app</h2>
      <p>Hello everyone, This is running via Docker container</p>
    </div>
  </body>
</html>
EOF

Dockerfile 的最好实践

这里是一些我们应该遵循 Dockerfile 的通常做法:

1. 使用一个 .dockerignore 文件去排除不必要的文件和目录,好增强我们的构建性能。

2. 只使用被信任的基础镜像,进行定期更新的镜像。

3. 在 Dockerfile 每一个指令都向 Docker 镜像添加了额外的一层。通过把指令合并,让镜像层尽量以最少的层去构建,有助于增强构建性能和时间。

4. 以一个非 ROOT 用户去运行,有助于更加安全。

5. 把镜像体积保持为最小:在你的镜像中,为了更快的部署, 要避免安装不必要的工具,以减少镜像的大小。使用尽可能小的镜像为了减少攻击面。

6. 使用特定标签覆盖镜像的最新标签,以避免随着时间的推移发生重大变化。

7. 当创建多个缓存的层时,它通常会影响到构建过程的效率,所以应避免使用多个 RUN 命令。

8. 永远不要往你的 Dockerfile 中共享和拷贝应用程序的凭证或者任何敏感的信息。如果你使用了它,请将其添加它到 .dockerignore。

9. 尽可能在末尾中使用 EXPOSE 和 ENV 命令。

10. 使用一个 linter: 使用一个像 hadolint[11] 的 linter 去检查你的 Dockerfile,这是为了常见的问题和最好的实践。

11. 每一个容器只使用一个单独进程: 每一个容器应该只运行一个单独的进程。这是为了让它更容易去管理和监控容器,还有帮助我们保持容器是轻量的。

12. 使用多阶段构建:使用多阶段构建去创建更小和更有效率的镜像。

潜在的 Docker 构建问题

1. 如果在 Dockerfile 里面有一个语法错误或者一个无效的参数,Docker build 命令将会有一个错误信息的失败。可以检查语法去解决这个。

2. 始终尝试使用 docker run 命令为容器命名。如果不指定名称,Docker 会自动分配一个随机名称,这可能会导致一些问题。

3. 端口冲突问题:有时会遇到类似 Bind for 0.0.0.0:8080 failed: port is already allocated 的错误,这是因为其他软件或服务正在使用该端口。可以通过 netstat 或 ss 命令检查端口占用情况,然后选择使用其他端口或停止占用端口的服务来解决此问题。

4. 依赖包下载失败:有时 Docker 会报错 Failed to download package [package-name],这通常是因为容器无法访问互联网或存在其他依赖问题。

Docker 镜像注册表

在步骤一中提到过,你应该始终选择官方认证的基础镜像作为应用的镜像。

以下表格列出了一些公共可用的容器注册表,你可以在这些注册表中找到官方认证的基础镜像和应用镜像:

Registry

Base Images

Docker

Docker hub base images[12]

Google Cloud

Distroless base images[13]

AWS

ECR public registry[14]

Redhat Quay

Quay Registry[15]

Docker Image vs. Containers

Docker 镜像是文件系统和应用依赖的快照。它是一个可执行的软件包,包含了运行应用所需的一切,比如应用代码、库、工具、依赖项和其他文件。你可以将其类比为虚拟机的黄金镜像。

Docker 镜像以堆叠在一起的只读层形式组织。

Docker 容器是 Docker 镜像的运行实例。就像从虚拟机镜像创建虚拟机一样,我们从容器镜像创建容器。当你从 Docker 镜像创建容器时,会在现有镜像层之上创建一个可写层。

Docker 镜像和容器之间的主要区别在于容器顶部的可写层。这意味着,如果你从一个镜像运行了五个容器,所有容器都会共享镜像中的相同只读层,而顶部的可写层对每个容器来说是独立的。

因此,当你删除容器时,其可写层也会被删除。

镜像可以独立于容器存在,而容器需要镜像才能运行。我们可以从同一个镜像创建多个容器,每个容器都有自己独立的数据和状态。

图片图片

Docker 镜像构建 FAQs

如何使用来自 Docker hub 以外的容器注册表的基础镜像?

默认情况下,Docker 引擎配置为使用 Docker Hub 作为容器注册表。因此,如果你只指定镜像名称,Docker 会从 Docker Hub 拉取镜像。然而,如果你想从其他容器注册表拉取镜像,则需要提供完整的镜像 URL。例如:FROM gcr.io/distroless/static-debian11。

什么是 Docker 构建上下文?

Docker 构建上下文是指 Docker 主机上的一个位置,其中包含构建过程中所需的所有代码、文件、配置和 Dockerfile。你可以使用一个点 [.] 来指定当前目录作为构建上下文,或者指定其他文件夹的路径。此外,Dockerfile 也可以与构建上下文位于不同的位置。

作为最佳实践,构建上下文中应仅包含必需的文件。否则,可能会导致不必要的文件被包含,从而使 Docker 镜像变得臃肿。

如何从一个 git 仓库构建 Docker 镜像?

你可以使用 docker build 命令结合 Git 仓库来构建 Docker 镜像。该 Git 仓库中必须包含 Dockerfile 和所需的文件,否则构建过程会失败。

下一个是什么呢?

同样地,你可以尝试构建多个 Docker 镜像。

例如,你可以尝试将一个Java 应用容器化[16] 并运行它。

为了进一步学习,你可以尝试容器化以下应用,从而掌握更多知识。

1. Python

2. NodeJS

3. Django

4. FastAPI

总结

在这篇文章中,我们讨论了如何构建 Docker 镜像,并使用 Dockerfile 将应用作为 Docker 容器运行。

我们详细介绍了 Dockerfile,并分享了一些编写 Dockerfile 的最佳实践。

作为一名 DevOps 工程师[17],在项目中应用 Docker 之前,深入理解 Docker 的最佳实践非常重要。此外,学习 Kubernetes[18] 也需要掌握构建容器镜像的工作流程。

Podman 是另一个容器管理工具。如果你想了解更多,可以参考 Podman 教程[19]。

结语

ok, guys, see you next time

原文:https://devopscube.com/build-docker-image/#Step_2_Create_a_sample_HTML_file_and_config_file

引用链接

[1] 容器:https://devopscube.com/what-is-a-container-and-how-does-it-work/

[2]《Docker 详解》:https://devopscube.com/what-is-docker/

[3]Docker Installation Guide:https://devopscube.com/how-to-install-and-configure-docker/

[4]Docker 镜像实例 Github repo:https://github.com/techiescamp/docker-image-examples/tree/main

[5]Alpine:https://hub.docker.com/_/alpine

[6]distroless images:https://github.com/GoogleContainerTools/distroless

[7]语义版本控制(Semver) :https://semver.org/

[8]减少 Docker 镜像:https://devopscube.com/reduce-docker-image-size/

[9]Docker hub:https://hub.docker.com/

[10]heredoc:https://tldp.org/LDP/abs/html/here-docs.html

[11]hadolint:https://devopscube.com/lint-dockerfiles-using-hadolint/

[12]Docker hub base images:https://hub.docker.com/search?badges=official

[13]Distroless base images:https://github.com/GoogleContainerTools/distroless

[14]ECR public registry:https://gallery.ecr.aws/?verified=verified&operatingSystems=Linux&page=1

[15]Quay Registry:https://quay.io/search

[16]Java 应用容器化:https://devopscube.com/dockerize-java-application/

[17]DevOps 工程师:https://devopscube.com/become-devops-engineer/

[18]Kubernetes:https://devopscube.com/learn-kubernetes-complete-roadmap/

[19]Podman 教程:https://devopscube.com/podman-tutorial-beginners/

责任编辑:武晓燕 来源: 云原生运维圈
相关推荐

2024-03-01 19:53:37

PyBuilderPython开发

2024-05-17 17:29:00

CurdlingPython开发

2018-04-18 07:01:59

Docker容器虚拟机

2010-02-22 09:39:52

HTML 5Web

2017-02-10 09:30:33

数据化运营流量

2024-07-31 08:14:17

2023-03-21 07:35:43

2024-02-23 09:00:00

编程语言编译器工具

2025-01-09 11:14:13

2022-03-30 08:24:25

操作系统内核开源软件

2011-04-29 10:46:32

iPhone开发入门iPhoneiOS

2018-04-16 16:31:56

前端开发从零开始

2020-07-02 15:32:23

Kubernetes容器架构

2024-12-06 17:02:26

2019-01-18 12:39:45

云计算PaaS公有云

2015-11-17 16:11:07

Code Review

2025-01-06 00:38:12

2018-05-28 13:12:49

深度学习Python神经网络

2024-05-15 14:29:45

2018-09-14 17:16:22

云计算软件计算机网络
点赞
收藏

51CTO技术栈公众号