Please enable Javascript to view the contents

Docker-Dockerfile的最佳实践

 ·  ☕ 8 分钟

重要

1. 概念说明

  • 构建Context说明
    执行docker build命令时,当前工作目录称为构建上下文。默认情况下,dockerfile也在当前目录,但可以使用文件标志(-f)指定不同的位置。 无论dockerfile实际在哪里,执行构建命令的当前目录中的文件和目录的所有递归内容都将作为Context发送到Docker Daemon程序。

操作镜像大小安全构建性能可读性
尽可能使用官方Docker镜像;使用明确的镜像标签
RUN、COPY、ADD命令会创建layer,其他指令不会增加layer
始终在同一运行语句中结合运行 apt-get update 和 apt-get install
使用.dockerignore排除构建不需要的文件。
将长或复杂的RUN语句拆分到多行上,用反斜杠分隔。
以非root用户运行;uid>10000;使用静态 UID 和 GID
使用静态 UID 和 GID
只在CMD中存储参数
使用COPY代替ADD
采取多阶段构建, 以减少最终运行镜像大小,环境clean。

2. 最佳实践

尽可能使用官方Docker镜像

相比自己构建镜像安装sdk来说,使用适合的官方镜像是首选。因为官方镜像经过了数百万用户的优化和测试。找不到合适的官方镜像或者官方基础镜像包含漏洞时,再考虑自己从头创建镜像。

例如,相较于手动安装SDK,如下

1
2
3
4
5
6
7
8
9
FROM ubuntu
RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor > microsoft.asc.gpg \
&& sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ \
&& wget -q https://packages.microsoft.com/config/ubuntu/18.04/prod.list \
&& sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list \
&& sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg \
&& sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list
RUN sudo apt-get install dotnet-sdk-3.1

更推荐使用官方镜像

1
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster

Alpine并不一定是最佳选择

Alpine镜像由于受到严格控制,具备镜像小,安全性高的特点,在绝大多少情况下被推荐使用(Docker官方推荐)。

但无脑使用Alpine也并不可取,也要意识到以下两个问题:

使用明确的镜像标签,而非latest

基于lastest镜像构建的缺点如下:

  • Docker 镜像编译不一致。latest 标签,那么每次构建都会拉取一个最新构建的Docker镜像。引入这种非确定性的行为会影响编译的可重复性。
  • node Docker 映像基于成熟的操作系统,其中包含运行 Node.js 网络应用时可能需要也可能不需要的各种库和工具。这有两个缺点。首先,更大的映像意味着更大的下载量,除了增加存储需求外,还意味着需要更多时间下载和重新构建映像。其次,这意味着您有可能将所有这些库和工具中可能存在的安全漏洞引入到映像中。

使用 major.minor,而不是 major.minor.patch,以确保您始终使用特定的图像版本:

保持你的构建正常工作(最新版本意味着你的构建在未来可能会任意损坏,而 major.minor 版本则意味着这种情况不会发生)
在您构建的新镜像中包含最新的安全更新。

始终在同一RUN语句中运行apt-get update和apt-get install

在 RUN 中单独使用 apt-get update 会导致缓存问题和后续的 apt-get install指令失败。

这与 Docker 使用的缓存机制有关。在构建镜像时,Docker 会将初始指令和修改后的指令对比,指令未修改的会使用之前构建的缓存。例如:

apt-get update单独放到一个RUN语句中:

1
2
3
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl

之后修改dockerfile,增加nginx

1
2
3
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl nginx

因为RUN apt-get update未发生变化,apt-get update 就不会被执行,而是使用之前构建的缓存。由于没有运行apt-get update,构建可能无法获得最新版本的 curl 和 nginx 软件包。

下面是推荐的Dockerfile写法:

1
2
3
4
5
6
RUN apt-get update && apt-get install -y \
    curl \
    git \
    build-essential  \
    && rm -rf /var/lib/apt/lists/*
# Debian和Ubuntu的官方镜像会自动执apt-get clean,因此无需明确调用。

限制镜像layers数量

Dockerfile中的每一条RUN指令最终都会在最终镜像中创建一个额外的layer。限制layer的数量,以保持镜像的轻量级。

以非root用户运行

以非 root 用户身份运行容器可大大降低从容器到主机提权的风险。详情参考Docker文档

对于基于 Debian 的映像,可以通过下面命令从容器中移除 root 用户:

1
2
3
4
5
RUN groupadd -g 10001 paas && \
   useradd -u 10000 -g paas paas \
   && chown -R paas:paas /app

USER 10001

从容器中移除root后,会存在权限问题,比如安装软件没有权限,可以将USER 10001命令放到这些命令之后;再比如,如果没有 root 权限,应用程序无法在 80 端口上运行,因此必须更改端口>1024。

UID>=10000

低于 10000 的 UID 在多个系统上都存在安全风险,因为如果有人设法在 Docker 容器外提升权限,他们的 Docker 容器 UID 可能会与权限更高的系统用户的 UID 重叠,从获取更高的权限。为了避免这种安全隐患,建议以高于 10000 的 UID 运行进程。

使用静态UID和GID

运行容器时,需要为容器所拥有的文件设置文件权限。如果镜像没有设置静态UID/GID,就必须从运行的容器中提取该信息,然后才能在主机上分配正确的文件权限。最好为所有镜像设置一个固定的静态UID/GID。建议使用 10000:10001,这样可以减少不必要的运维复杂度。

只在CMD中存储参数

如果 CMD 包含命令名称,在运行容器时就必须猜出命令名称,以便传递参数等。

ENTRYPOINT作为命令名称:

1
ENTRYPOINT ["/sbin/tini", "--", "myapp"]

CMD只是命令的参数:

1
CMD ["--foo", "5", "--bar=10"]

运行容器时参数传递的方式更人性化,而无需猜测其名称,例如

1
docker run IMAGE --some-argument

使用COPY代替ADD

使用 ADD 的唯一例外是 tar 自动提取功能
ADD local-file.tar.xz /usr/share/files

禁止ADD指定URL,这样可能会导致MITM攻击或恶意数据源。此外,ADD会隐式解压本地压缩包,这可能会导致路径遍历和 Zip Slip漏洞

应避免这样做:

1
ADD https://example-url.com/file.tar.xz /usr/share/files

使用多阶段构建

在使用 Dockerfile 构建应用程序时,会创建许多仅在构建时需要的工件。这些工件可以是编译所需的开发工具和库等软件包,也可以是运行单元测试所需的依赖项、临时文件、密钥等。

在基础镜像中保留这些人工制品(可能用于生产)会导致 Docker 镜像大小增大,这不仅会严重影响下载时间,还会因为安装了更多软件包而增加攻击面。您使用的 Docker 镜像也是如此–您可能需要特定的 Docker 镜像来构建,但不需要它来运行应用程序的代码。

Golang 就是一个很好的例子。要构建 Golang 应用程序,你需要 Go 编译器。编译器生成的可执行文件可以在任何操作系统上运行,无需依赖,包括从头镜像。

这也是 Docker 具有多阶段构建功能的一个很好的原因。该功能允许你在构建过程中使用多个临时镜像,只保留最新的镜像和你复制到其中的信息。这样,你就有了两个镜像:

第一个镜像–非常大的镜像,捆绑了许多依赖项,用于构建应用程序和运行测试。

第二个镜像–在大小和库数量方面都非常小的镜像,只有在生产中运行应用程序所需的工件副本。

3. 常见配置备忘

3.1 安装包

3.1.1 apt包安装

  1. 使用RUN apt-get update && apt-get install -y 确保dockerfile安装最新的软件包版本。(如果将命令分成两行RUN, 会导致缺少缓存,apt-get install失败)。
  2. apt-get clean命令清理/var/cache/apt/archives目录下的内容,且在官方Ubuntu/Debian镜像会在安装后自动清理,所以不需要显示使用。
1
2
3
4
5
6
7
8
## 国内加速-设置阿里源
RUN  sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list

RUN apt-get update && apt-get install -y --no-install-recommends \
    package-bar \
    package-baz \
    package-foo  \
 && rm -rf /var/lib/apt/lists/*

3.1.2 apk安装

1
2
3
4
5
## 国内加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk upgrade --update \
    && apk add -U tzdata \
    && rm -rf /var/cache/apk/*

3.2 设置时区

时区原理以及具体说明,参考理解容器时区问题的根源及常见解决方法

3.2.1 alpine发行版

1
2
3
4
FROM alpine:3.19.1
ENV TZ=Asia/Shanghai

RUN apk add --no-cache tzdata

3.2.2 Ubuntu发行版

ubuntu\debian\centos默认安装tzdata,因此只需设置环境变量,如果缺少tzdata包,请参考理解容器时区问题的根源及常见解决方法设置

1
2
FROM ubuntu:22.04
ENV TZ=Asia/Shanghai

3.3 创建用户

3.3.1 alpine

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM reg.svc.com/paas/alpine:3.11.5
LABEL MAINTAINER = "Hex"

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache ca-certificates

# Create user paas
RUN addgroup -S paas \
 && adduser paas -u 10001 -S paas -G paas \
 && su paas -s /bin/sh -c "mkdir -p /home/paas/config" \
 && su paas -s /bin/sh -c "mkdir -p /home/paas/data"
USER 10001

3.4 多阶段构建

3.4.1 Alpine-golang

 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
36
37
# Build image (Golang)
FROM golang:1.13-alpine3.11 AS build-stage
ENV GO111MODULE on
ENV GOPROXY https://goproxy.io
ENV CGO_ENABLED 0

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache gcc git make

WORKDIR /src
COPY . .

RUN go mod download
RUN go build -a -ldflags '-extldflags "-static"' -o paas-api

# Final Docker image
FROM reg.svc.com/paas/alpine:3.11.5 AS final-stage
LABEL MAINTAINER = "Hex"

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache ca-certificates

# Create user paas
RUN addgroup -S paas && adduser paas -u 10001 -S paas -G paas && su paas -s /bin/sh -c "mkdir -p /home/paas/config" && su paas -s /bin/sh -c "mkdir -p /home/paas/data"

# must be numeric to work with Pod Security Policies:
# https://kubernetes.io/docs/concepts/policy/pod-security-policy/#users-and-groups
USER 10001
WORKDIR /home/paas/
COPY --from=build-stage /src/paas-api .

COPY --chown=paas:paas config /home/paas/config
COPY --chown=paas:paas data /home/paas//data
COPY helm /usr/local/bin/helm
COPY kubectl /usr/local/bin/kubectl

ENTRYPOINT ["/home/paas/paas-api", "server","--config=./config/config.yaml","--port=8081","--cors=false"]

3.4.2 Ubuntu-golang

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# Build image (Golang)
FROM golang:1.17.8-alpine3.15 AS build-stage
ENV GO111MODULE on
ENV GOPROXY https://goproxy.cn
ENV CGO_ENABLED 0

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache gcc git make

WORKDIR /src
COPY . .

RUN go mod download
RUN go build -a -ldflags '-extldflags "-static"' -o devops-api

# Final Docker image
FROM reg.svc.com/paas/ubuntu:20.04 AS final-stage
LABEL MAINTAINER = "Hex"

RUN  sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN set -x; \
    apt-get update && apt-get install -y --no-install-recommends \
    tzdata \
    jq \
    telnet \
    dnsutils \
    iputils-ping \
    net-tools \
    netcat \
    ca-certificates \
    curl \
    vim-tiny \
    git \
    openssl \
    ldap-utils \
    tree \
 && rm -rf /var/lib/apt/lists/*

RUN mkdir -p /home/paas

WORKDIR /home/paas/
COPY --from=build-stage /src/paas-api .

ENV ROOT /tmp
ENV HDFS_ALGO_PATH /algos/
ENV HADOOP_CONF_DIR /etc/hadoop/conf
ENV TZ=Asia/Shanghai

COPY ./pkg/api/middleware/builder/ /tmp/builder

COPY config /home/paas/config
COPY data /home/paas/data
COPY ./bin/ /usr/local/bin/

#EXPOSE 8081
ENTRYPOINT ["/usr/local/bin/entrypoint"]

Reference

Docker官方文档-Dockerfile最佳实践

10 Docker Image Security Best Practices

10 best practices to build a Java container with Docker
10 best practices to containerize Node.js web applications with Docker

分享

Hex
作者
Hex
CloudNative Developer

目录