$ ls ~yifei/notes/

Dockerfile 基础和多阶段构建

Posted on:

Last modified:

Dockerfile 列出了构建一个 docker image 的可复现步骤。比起一步一步通过 docker commit 来制作一个镜像,dockerfile 更适用于 CI 自动测试等系统。

Dockerfile 命令

  • FROM,指定基础镜像,如 python:3.10
  • MAINTAINER,已废弃,建议使用 LABEL
  • LABEL key=value,标签
  • EXPOSE,标记需要暴露的端口
  • USER,运行的用户
  • WORKDIR,进程的工作目录
  • ENV k1=v1 k2=v2,设定环境变量,可以一次设定多个。
  • VOLUME,卷
  • COPY,复制文件到镜像文件系统
  • RUN,运行 shell 命令来更新镜像文件系统
  • ENTRYPOINT,容器启动的入口,默认是 sh -c
  • CMD,容器启动进程使用的命令

几个比较容易混淆的命令

大部分命令都是可以「望文生义」的,只有个别几个比较让人迷惑。

COPY vs ADD

ADD 命令比较复杂,支持 tar 包和网络等复杂行为,一般用不到。

COPY 命令和 Linux 上的 cp 命令的行为是不同的。COPY 命令会自动创建文件路径。另外,COPY 只能从当前目录复制,而不能使用 ../ 这些父级目录,因为只有当前目录作为构建上下文传递给了 docker daemon。

另外,COPY 的行为在各种情况下都很一致,而 cp 则比较混乱。

如果目标文件夹存在,Linux 上的 cp 有时会把源文件夹复制到目标文件夹中成为子目录,有时又会 复制源文件夹的内容,非常让人迷惑。更糟的是,macOS 上的 cp 行为也和 Linux 上有差异。

COPY 命令始终会复制源文件夹的内容到目标文件夹中。具体来说:

  • 在 Linux 上,cp -R foo/ bar/, 会产生 bar/foo 这种目录。想要复制内容需要使用 cp -R foo/* bar/
  • 在 macOS 上,cp -R foo/ bar/,只会复制内容。
  • cp -R . /app, 只会复制当前目录的内容
  • COPY foo/ bar/, COPY . /app 都是针对内容,行为一致。

可以看出 COPY 命令本身是很简单且一致的,但是我们可能会被头脑中混乱的 cp 影响而产生困惑。

ENTRYPOINT vs CMD

ENTRYPOINT 指定了 Docker 镜像要运行的二进制文件(当然也包括参数),而 CMD 则指定了运行 这个二进制文件的参数。不过因为默认 entrypoint 是 sh -c,所以实际上 CMD 才是运行的命令。

另外,如果 docker run 中包含命令行参数,会替换掉 CMD 的内容。如果使用 /bin/bash 作为 docker run 的参数,便可以进入容器中的 Shell,并查看编译出的文件系统究竟是什么样的。

个人倾向于只使用 CMD,而不使用 ENTRYPOINT

VOLUME 指令

Dockerfile 中的 volume 指定了一个匿名的 docker volume,也就是说在 docker run 的时候,docker 会把对应的目录 mount 到一个匿名的卷。当然如果使用 -v 参数指定了 mount 到哪个目录,或者 是指定了卷名,那就不会采用匿名的卷了。

最佳实践

首先是指令的排序,我们都知道 FROM 一般放在最前边,那么其他指令怎么排序呢?这里有几条规则:

  • 静态指定放在最前边,包括:EXPOSE, VOLUME, CMD, ENTRYPOINT, WORKDIR 等。因为他们很少变化。
  • 动态指定放在后边,比如 COPY, RUN 等
  • 首先 COPY 依赖描述文件(e.g. package.json),然后安装(npm install),最后再 COPY 整个项目 进去,因为代码的变更要比依赖频繁,这样可以充分利用缓存,减少构建时间。
  • RUN 命令要合并起来,减少镜像层数
FROM ubuntu

WORKDIR /opt
EXPOSE 3000
VOLUME ["/data"]
ENTRYPOINT ["node"]
CMD ["server.js"]

COPY package.json /opt/package.json
RUN apt-get update && \
    apt-get install curl && \
    npm install --production

COPY . /opt

ARG BUILD_REVISION=master
ENV REVISION=${BUILD_REVISION}

多阶段构建

对于需要编译的项目,实际上我们只需把编译后的文件复制到 docker 镜像中即可,而不需要把编译 使用的所有依赖都放进去。这时候可以使用多阶段构建,也就是把 docker build 分为编译和生产两个 阶段。

这里的关键在于使用 FROM ASCOPY --from 两个命令。以前端项目为例,我们使用 node 镜像 编译出

# Compile
FROM node AS builder

WORKDIR /build
COPY package.json package-lock.json ./
RUN npm install
COPY . /build
RUN npm run build

# Put content into nginx
FROM nginx

WORKDIR /app
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /build/dist /usr/share/nginx/html

对于 Python 这种无需编译的语言,其实也可以借助多阶段构建来缩减镜像大小。

FROM python:3.10 as builder

WORKDIR /venv
COPY requirements.txt ./
# 注意这里的 --copies 选项,使用这个选项后,venv 会复制文件到虚拟环境中,而不是使用链接。
RUN python -m venv --copies /venv && \
    . ./bin/activate && \
    pip install -r requirements.txt

# 注意这里使用了 slim 镜像
FROM python:3.10-slim

ENV PATH=/venv/bin:$PATH
WORKDIR /app

COPY --from=builder /venv /venv
COPY . /app

CMD ["python", "server.py"]

在完整镜像中包含了许多构建工具,生产环境中并不需要这些工具,所以可以使用 slim 镜像。

最后需要说明的一点,不要轻易使用 alpine 镜像。虽然 alpine 体积更小,但是因为使用了 musl, 性能可能相对 glibc 编译的镜像要差不少,为了一点点磁盘空间去花费更多的 CPU 资源,一般是 不值得的。

参考

  1. https://stackoverflow.com/a/34245657/1061155
  2. https://stackoverflow.com/questions/41935435/understanding-volume-instruction-in-dockerfile
  3. https://stackoverflow.com/questions/26110828/should-i-use-dockerfiles-or-image-commits
  4. https://medium.com/@esotericmeans/optimizing-your-dockerfile-dc4b7b527756
  5. https://dev.to/ackshaey/macos-vs-linux-the-cp-command-will-trip-you-up-2p00
  6. https://buddy.works/tutorials/optimizing-dockerfile-for-node-js-part-1
  7. https://buddy.works/tutorials/optimizing-dockerfile-for-node-js-part-2
  8. https://gabnotes.org/lighten-your-python-image-docker-multi-stage-builds/
  9. https://www.docker.com/blog/how-to-use-the-official-nginx-docker-image/
WeChat Qr Code

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.

友情链接: MySQL 教程站