「Docker」Docker 系列文章2 - Dockerfile

简介

Dockerfile是一个文本文件,其中包含创建 Docker 镜像所需的步骤和指令。主要分为基础镜像信息、维护者信息、镜像操作指令和容器启动时指令四个部分,并支持以 # 开头的注释行。这些指令告诉 Docker 如何设置应用程序的运行环境,通常包括安装软件包、复制文件和设置环境变量的命令。

用户可以使用 Dockerfile 与 docker build 命令一起使用,用于创建 Docker 自定义镜像,然后可以用该镜像创建 Docker 容器。

构建镜像

例如,假设我们想要创建一个基于 go-gin 的应用程序,并使用 Docker 运行它。可以使用 Dockerfile 来指定应用程序所需的依赖项、工作目录和启动命令。

以下是执行步骤

  1. 初始化 go 项目:在 Go 项目目录中初始化 go.mod 文件。在终端中输入 go mod init 并回车,这将创建 go.mod 文件。

  2. 安装 go-gin 框架:在项目中安装 Gin 框架。在终端中输入 go get github.com/gin-gonic/gin 并回车,这将安装 Gin 框架。

  3. 编写使用 Gin 框架的 Go 代码,gin.go 文件代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package main

    import (
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
    )

    // 测试 http 框架 https://github.com/gin-gonic/gin
    func main() {
    // 默认返回一个已连接日志记录器和恢复中间件的引擎实例。
    r := gin.Default()
    // 绑定路由 /ping,访问后执行func的方法
    r.GET("/ping", func(c *gin.Context) {
    // 返回一个 json, 状态值为 200, H的内容为 map[string]
    c.JSON(http.StatusOK, gin.H{
    "message": "pong",
    })
    })

    // 在0.0.0.0:8080上侦听和服务(对于Windows“为 localhost:8080”)
    err := r.Run()
    if err != nil {
    fmt.Println("启动服务异常:", err)
    }
    }

  4. 创建一个 Dockerfile 文件,并编写文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # 使用安装了Go的基本映像
    FROM golang:latest

    # 维护者信息
    MAINTAINER docker_user wsxc_0617@sina.cn

    # 镜像的操作指令
    # 将工作目录设置为项目根目录
    WORKDIR /app

    # 将Go代码复制到容器的工作目录
    COPY . .

    # 设置 go module 模式,并设置代理服务
    ENV GO111MODULE=on
    ENV GOPROXY=https://goproxy.cn,direct

    # 构建Go代码
    RUN env GOOS=linux GOARCH=amd64 go build -o go-gin

    # 暴露应用程序的端口
    EXPOSE 8080

    # 当容器启动时运行Go代码
    ENTRYPOINT ["./go-gin"]
    • 在这个文件中,第一行必须需要使用 FROM 命令来指定要使用的基础镜像
    • 使用 COPY 命令将 Go 代码复制到镜像中
    • 使用 ENV 设置环境变量,这里设置 go module 的代理服务,避免 Go 依赖包下载失败
    • 使用 RUN go build 命令来构建 Go 代码,-o 表示指定打包后输出的文件名称
    • Dockerfile 中运行的 Go 代码。可以使用 CMDENTRYPOINT 命令来运行 Go 代码,一般是执行 go build 构建好的程序名
  5. 使用 docker build 命令来构建你的 Docker 镜像。在终端中输入 docker build -t <image-name> . 并回车,其中 <image-name> 是我们要为镜像指定的名称

    1
    docker build -t go-gin .
    • 在命令中 -t 指定镜像的名称和标记, 格式为 name:tag,如 go-gin:latest

    • 命令末尾的 . 表示构建上下文,即 Dockerfile 和需要包含在镜像中的文件的位置。也可以通过将 . 替换为包含Dockerfile的目录路径来指定不同的目录作为上下文。例如:

      1
      docker build -t myimage:latest /path/to/dir

      这将使用 /path/to/dir 目录中的 Dockerfile 和所需文件构建镜像。

      重要的是,上下文必须包括 Dockerfile 和构建镜像所需的所有文件。Docker 守护程序将使用上下文构建镜像,并为 Dockerfile 中的每条指令创建一个新层。
      命令参考:https://docs.docker.com/engine/reference/commandline/build/

    构建完成后,我们可以通过 docker images 查看构建好的镜像

    1
    docker images

    可以看到如下信息:

    1
    2
    REPOSITORY                       TAG           IMAGE ID       CREATED          SIZE
    go-gin latest d82303fc77be 34 seconds ago 1.13GB
  6. 使用 docker run 命令来运行你的 Docker 镜像。在终端中输入 docker run -p 8080:8080 <image-name> 并回车,其中 8080 是你希望运行应用程序的端口。
    我们运行 docker run -p 8080:8080 go-gin 来运行镜像。可以看到如下结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

    [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
    - using env: export GIN_MODE=release
    - using code: gin.SetMode(gin.ReleaseMode)

    [GIN-debug] GET /ping --> main.main.func1 (3 handlers)
    [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
    Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
    [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
    [GIN-debug] Listening and serving HTTP on :8080

    命令参考文档:https://docs.docker.com/engine/reference/run/

    通过浏览器访问 http://localhost:8080/ping 可以看到返回了信息 {"message":"pong"}

分段构建

「Dockerfile 构建镜像太大了怎么处理」

在 Dockerfile 中,可以使用多个构建阶段来进行分段构建。分段构建的目的是为了在构建 Docker 镜像的过程中,更加高效地使用资源。

在 Dockerfile 中,每个构建阶段都是一个单独的上下文,在每个构建阶段中执行的操作都是独立的。在每个构建阶段结束时,Docker 会将其上下文打包成一个新的镜像层,并将其与前面的镜像层合并。

我们在上一步操作中可以看到我们的代码量很小,但是构建出来的镜像却有 1GB 大小。主要是因为依赖包等内容也一并打包到构建的镜像中去了,实际上我们仅需编译好 Go 的二进制文件即可运行。因此可以通过 Dockerfile 的分段构建来进行镜像的瘦身。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# ---------- 构建 ---------- #
# 使用安装了Go的基本映像【并且设置别名】
FROM golang:latest as go_app_build
# 维护者信息
MAINTAINER docker_user wsxc_0617@sina.cn

# 镜像的操作指令
# 将工作目录设置为项目根目录
WORKDIR /app

# 将Go代码复制到容器
COPY . .

# 设置 go module的代理服务
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct

# 构建Go代码
RUN env GOOS=linux GOARCH=amd64 go build -o go-gin .


# ---------- 运行 ---------- #
# 运行环境设置为 alpine
FROM alpine:latest as go_app_run
# 【采用相同的目录为工作目录】
WORKDIR /app
# 【将构建镜像中编译好的二进制文件复制到运行镜像中】
#COPY --from=go_app_build /app .
COPY --from=0 /app .

# 显示应用程序的端口
EXPOSE 8080

# 当容器启动时运行Go代码
ENTRYPOINT ["./go-gin"]

在修改后的 Dockerfile 中,我们可以看到有两个构建阶段:“go_app_build” 和 “go_app_run”。

在 “go_app_build” 构建阶段中,我们执行了将 Go 代码复制到容器、设置 Go module 的代理服务以及构建 Go 代码的操作。

在 “go_app_run” 构建阶段中,我们执行了将编译好的二进制文件复制到运行镜像中的操作。

COPY 指令的 --from 选项允许你从其他构建阶段复制文件。可以使用 FROM 指令的名称来标识构建阶段,也可以使用数字来标识构建阶段。当使用数字标识构建阶段时,0 表示第一个构建阶段,1 表示第二个构建阶段,以此类推。

例如,在本例中,0 表示名为 “go_app_build” 的构建阶段,即在修改后的 Dockerfile 中的第一个构建阶段。

使用数字标识构建阶段的好处是,当我们添加、删除或重新排制构建阶段时,只需要更新相应的数字即可,而不需要更新所有的构建阶段名称。

例如,假设你在修改后的 Dockerfile 中再添加了一个构建阶段,那么只需要将这个新的构建阶段的标识符设置为 2,其他的构建阶段的标识符都不需要更改。这样可以避免出现因为构建阶段名称的更改而导致的潜在问题

我们使用 docker build -t go-gin2 . 命令,来构建名为 go-gin2 的镜像,并通过 docker images 查看构建好的镜像,结果如下:

1
2
REPOSITORY                       TAG           IMAGE ID       CREATED          SIZE
go-gin2 latest ddbe692c0268 56 minutes ago 36.1MB

可以看到我们构建出来的镜像比之前小了很多,主要因为

  1. 同一个 Dockerfile 文件的多个 FROM 是多个不同的image,不同的 image 之间数据隔离,包括环境变量,但是不同的 image 之间可以通过 --from 来传递数据

  2. 同一个 Dockerfile 的多个 FROM 只有最后一个FROM 才会生成最终的镜像,因此编译阶段的环境没有在第二阶段中,第二阶段只是引用了第一阶段的生成物:二进制文件,所以最终生成的镜像文件比写一个 FROM 要小得多

分段构建的优点是,当构建过程中的某个操作失败时,Docker 只需要重新构建失败的构建阶段,而不需要重新构建整个镜像。这样可以提高构建效率,并减少构建失败的风险。

总的来说,分段构建是一种非常有用的技术,可以帮助我们提高 Docker 镜像的构建效率,并减少构建失败的风险。建议在编写 Dockerfile 时尽量使用分段构建,以便更好地利用 Docker 的构建功能。

指令

Dockerfile 指令的一般格式为INSTRUCTION <arguments>,下面分别做详细介绍。

FROM

格式为FROM <image>FROM <image>:<tag>。第一条指令必须为FROM指令,在同一个Dockerfile中创建多个镜像时可以使用多个FROM指令(每个镜像一次)。

MAINTAINER

格式为MAINTAINER <name>,指定维护者信息。

RUN

格式为RUN <command>RUN ["executable", "param1", "param2"]。前者将在shell终端中运行命令,即/bin/sh -c;后者则使用exec执行。指定使用其他终端可以通过第二种方式实现。

1
RUN ["/bin/bash", "-c", "echo hello"]

每条RUN指令将在当前镜像基础上执行指定命令,并提交为新的镜像,当命令较长时可使用\来换行。

CMD

支持三种格式:

  • CMD ["executable", "param1", "param2"],使用exec执行,推荐方式。
  • CMD command param1 param2,在/bin/sh中执行,提供给需要交互的应用。
  • CMD ["param1", "param2"],提供给ENTRYPOINT的默认参数

指定启动容器时执行的命令,每个Dockerfile只能有一条CMD命令,如果指定了多条命令,只有最后一条会被执行。如果用户启动容器时指定了运行的命令则会覆盖掉CMD指定的命令。

EXPOSE

格式为EXPOSE <port> [<port>...],告诉Docker服务端容器暴露的端口号,供互联系统使用。

在启动容器时需要通过-P,Docker主机会自动分配一个端口转发到指定的端口;使用-p则可以具体指定哪个本地端口映射过来。

ENV

格式ENV <key> <value>,指定一个环境变量,会被后续RUN指令使用,并在容器运行时保持。

1
2
3
4
ENV PG_MAJOR    9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && ...
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

ADD

格式ADD <src> <dest>,复制指定的<src>到容器中的<dest>,其中<src>可以是Dockerfile所在目录的一个相对路径(文件或目录);也可以是一个URL;还可以是一个tar文件(自动解压为目录)。

COPY

格式COPY <src> <dest>,复制本地主机的<src>(为Dockerfile所在目录的相对路径,文件或目录)为容器中的<dest>,目标路径不存在时会自动创建。

ENTRYPOINT

配置容器启动后执行的命令,且不可被docker run提供的参数覆盖。有两种格式:

  • ENTRYPOINT ["executable", "param1", "param2"]
  • ENTRYPOINT command param1 param2,shell中执行。

每个Dockerfile中只能有一个ENTRYPOINT,当指定多个ENTRYPOINT时,只有最后一个生效。

VOLUME

格式VOLUME ["/data"],创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。

USER

格式USER daemon,指定运行容器时的用户名或UID,后续的RUN也会使用指定用户。

当服务不需要管理员权限时,可通过该命令指定运行用户,并可在之前创建所需要的用户。要临时获取管理员权限可使用gosu,而不推荐sudo

WORKDIR

格式WORKDIR /path/to/workdir,为后续的RUNCMDENTRYPOINT指令配置工作目录。

可以使用多个WORKDIR指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。

1
2
3
4
5
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
# 最终路径为/a/b/c

ONBUILD

格式ONBUILD [INSTRUCTION],配置当前所创建的镜像作为其他新创建的基础镜像时所执行的操作指令。

镜像image-A Dockerfile

1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

基于image-A创建新镜像,新的Dockerfile中使用FROM images-A指定基础镜像时,会自动执行ONBUILD指令内容,等价于在后面添加了两条指令。

1
2
3
4
5
FROM images-A

# Automatically run the following 等价于自动运行以下命令
ADD . /app/src
RUN /usr/local/bin/python-build --dir /app/src

使用ONBUILD指令的镜像推荐在标签中注明。例如ruby:1.9-onbuild

实用技巧:

如果你写 Dockerfile 时经常遇到一些运行错误,依赖错误等,你可以直接运行一个依赖的底,然后进入终端进行配置环境,成功后再把做过的步骤命令写道 Dockerfile 文件中,这样编写调试会快很多。

例如上面的底是golang:latest,我们可以运行docker run -it -d golang:latest bash,跑起来后进入容器终端配置依赖的软件,然后尝试跑起来自己的软件,最后把所有做过的步骤写入到 Dockerfile 就好了。

掌握好这个技巧,你的 Dockerfile 文件编写起来就非常的得心应手了。

扩展

怎样上传到阿里云私有仓库?

参考

官方文档

Docker安装与基本操作

Docker 快速入门-制作自己的镜像

go 构建最小的镜像