fyang 的博客

首页 文章

如何为项目编写一份好的 Dockerfile

前言

时至今日绝大多数项目都采用容器来进行分发,一份 Dockerfile 几乎是每个现代项目的标配。容器的构建过程也越来越复杂,那么如何编写一份Dockerfile 呢?我将通过一个实际的项目作为实例来分享一下我的经验。

注:本文不对 Dockerfile 的基础语法进行讲解,而是针对如何编写一份好的 Dockerfile 提供一些实用的建议。

项目示例

我准备了一个前端项目,它是使用 vite 初始化的静态网站。项目结构如下:

fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ tree --gitignore 
.
├── index.html
├── package.json
├── package-lock.json
├── public
│   └── vite.svg
├── README.md
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

5 directories, 16 files

读者不了解前端技术也没关系,我将尽量讲解在前端项目使用容器打包中的一些优化思路。

为了将项目打包成容器我们需要一下步骤:

  1. 为项目寻找一个基础镜像,需要包含 Node.js 运行环境。
  2. 将项目代码复制到镜像中。
  3. 安装项目依赖。
  4. 构建项目。
  5. 最后运行项目。

那么我们将以上步骤使用 Dockerfile 来描述将得到一下内容:

# base image
FROM node:22 # 基础镜像包含 Node.js 22 运行环境

# build dist
WORKDIR /app # 设置工作目录
COPY . . # 将项目代码复制到镜像中
RUN npm install && npm run build # 安装项目依赖并构建项目

# deploy
EXPOSE 8080 # 暴露端口
RUN npm install -g http-server # 安装 web 服务器
CMD ["http-server", "dist"] # 运行 web 服务器

我们将它保存至 Dockerfile.1 文件中,并执行以下命令来构建镜像:

fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ docker build -f Dockerfile.1 -t vite-demo:1 .
[+] Building 1.8s (10/10) FINISHED                                                                                           docker:default
 => [internal] load build definition from Dockerfile.1                                                                                 0.0s
 => => transferring dockerfile: 272B                                                                                                   0.0s
 => [internal] load metadata for docker.io/library/node:22                                                                             1.8s
 => [internal] load .dockerignore                                                                                                      0.0s
 => => transferring context: 350B                                                                                                      0.0s
 => [1/5] FROM docker.io/library/node:22@sha256:c7fd844945a76eeaa83cb372e4d289b4a30b478a1c80e16c685b62c54156285b                       0.0s
 => [internal] load build context                                                                                                      0.0s
 => => transferring context: 2.41kB                                                                                                    0.0s
 => CACHED [2/5] WORKDIR /app                                                                                                          0.0s
 => CACHED [3/5] COPY . .                                                                                                              0.0s
 => CACHED [4/5] RUN npm install && npm run build                                                                                      0.0s
 => CACHED [5/5] RUN npm install -g http-server                                                                                        0.0s
 => exporting to image                                                                                                                 0.0s
 => => exporting layers                                                                                                                0.0s
 => => writing image sha256:9f07cbddbb09aca962509f75158f9c31006cb8814cc302e8748974ac5e0621db                                           0.0s
 => => naming to docker.io/library/vite-demo:1                                                                                         0.0s

尝试运行一下:


fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ docker run --rm -i -t vite-demo:1 
Starting up http-server, serving dist

http-server version: 14.1.1

http-server settings: 
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
  http://127.0.0.1:8080
  http://172.17.0.3:8080
Hit CTRL-C to stop the server

项目顺利运行起来了,我们打的包应该没有问题,那么来看一下镜像吧:

fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ docker images -f reference=vite-demo
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
vite-demo    1         9f07cbddbb09   4 hours ago   1.21GB

容器体积 1.21GB。接下来我们看一下如何进行优化吧!

优化思路

一、使用多阶段构建解耦编译环境和运行环境。

我们的第一版镜像内部大致包含了以下内容:

  1. Node.js 运行时环境
  2. 项目源代码
  3. 项目依赖包
  4. 构建产物
  5. 一个Web服务器(http-server)

但实际上,我们只需要构建后的静态文件和一个 Web服务器 即可。 因此我们可以使用多阶段构建来优化镜像体积,我们修改 Dockerfile 内容如下:

# 阶段一 安装依赖、编译项目
# 基础镜像包含 Node.js 22 运行环境
FROM node:22 AS build-stage 
# 设置工作目录
WORKDIR /app 
# 将项目代码复制到镜像中
COPY . . 
# 安装项目依赖并构建项目
RUN npm install && npm run build 

# 阶段二 运行项目
# 基础镜像包含 Node.js 22 运行环境
FROM node:22-alpine
# 将构建产物复制到镜像中
COPY --from=build-stage /app/dist /app/dist
# 设置工作目录 
WORKDIR /app 
# 暴露端口
EXPOSE 8080 
# 安装 web 服务器
RUN npm install -g http-server 
# 运行 web 服务器
CMD ["http-server", "dist"] 

我们将它保存到 Dockerfile.2 文件中并重新构建镜像:

fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ docker build -f Dockerfile.2 -t vite-demo:2 .
[+] Building 12.2s (12/12) FINISHED                                                                                          docker:default
 => [internal] load build definition from Dockerfile.2                                                                                 0.0s
 => => transferring dockerfile: 701B                                                                                                   0.0s
 => [internal] load metadata for docker.io/library/node:22                                                                             1.8s
 => [internal] load .dockerignore                                                                                                      0.0s
 => => transferring context: 350B                                                                                                      0.0s
 => [internal] load build context                                                                                                      0.0s
 => => transferring context: 3.11kB                                                                                                    0.0s
 => CACHED [build-stage 1/4] FROM docker.io/library/node:22@sha256:c7fd844945a76eeaa83cb372e4d289b4a30b478a1c80e16c685b62c54156285b    0.0s
 => CACHED [build-stage 2/4] WORKDIR /app                                                                                              0.0s
 => [build-stage 3/4] COPY . .                                                                                                         0.0s
 => [build-stage 4/4] RUN npm install && npm run build                                                                                 4.5s
 => [stage-1 2/4] COPY --from=build-stage /app/dist /app/dist                                                                          0.0s 
 => [stage-1 3/4] WORKDIR /app                                                                                                         0.0s 
 => [stage-1 4/4] RUN npm install -g http-server                                                                                       5.4s 
 => exporting to image                                                                                                                 0.2s 
 => => exporting layers                                                                                                                0.2s 
 => => writing image sha256:669980cac4c222fc5ac78624ab2cc159fb8b6f688f4e35e5a59f9eaf6875d230                                           0.0s 
 => => naming to docker.io/library/vite-demo:2                                                                                         0.0s 

再看一下构建后的镜像大小:

fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ docker images -f reference=vite-demo
REPOSITORY   TAG       IMAGE ID       CREATED              SIZE
vite-demo    2         a83e392daed8   About a minute ago   166MB
vite-demo    1         9f07cbddbb09   4 hours ago          1.21GB

可见,我们通过多阶段构建将容器体积从1.21GB减少到了166MB。同时在第二阶段我们选择了node-alpine作为基础镜像,它拥有更小的体积。当然,我们还可以选择 nginx 或者 caddy 等其他 Web服务器 作为基础镜像,从而进一步优化镜像体积。

二、最小化容器层与源代码的偶合度。

容器体积我们已经优化到足够小了,那么我们的 Dockerfile 就足够好了吗? 当我们的项目变得庞大,依赖越来越多时,你就会发现,我们每次构建过程都异常缓慢。 由于容器的多层特性,每条指令都会生成一层新的容器层,最终的镜像文件系统是一层一层叠加上去的,而为了优化构建速度,构建工具通常会进行多层缓存。 在 Dockerfile.2 中我们首先使用 COPY . . 拷贝了源代码到容器内部,然后使用RUN npm install安装依赖。 那么我们修改任意源代码内容时都会导致COPY . .层的缓存失效进而导致后面每次都要重复执行npm install。 但是npm install仅依赖package.jsonpackage-lock.json文件,在其他文件内容改动时理论上是不需要重新安装依赖的。 因此,我们可以继续优化得到 Dockerfile.3

# 阶段一 安装依赖、编译项目
# 基础镜像包含 Node.js 22 运行环境
FROM node:22 AS build-stage 
# 设置工作目录
WORKDIR /app 
# 仅复制 package.json 和 package-lock.json 文件
COPY package.json package-lock.json .
# 安装项目依赖
RUN npm install
# 将项目代码复制到镜像中
COPY . . 
# 安装项目依赖并构建项目
RUN npm run build 

# 阶段二 运行项目
# 基础镜像包含 Node.js 22 运行环境
FROM node:22-alpine
# 将构建产物复制到镜像中
COPY --from=build-stage /app/dist /app/dist
# 设置工作目录 
WORKDIR /app 
# 暴露端口
EXPOSE 8080 
# 安装 web 服务器
RUN npm install -g http-server 
# 运行 web 服务器
CMD ["http-server", "dist"] 

构建之后的镜像体积如下:

fyang in 🌐 fyang-dev02 in Codes/demos/vite-demo via  v22.14.0 
❯ docker images -f reference=vite-demo
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
vite-demo    2         a83e392daed8   25 minutes ago   166MB
vite-demo    3         a83e392daed8   25 minutes ago   166MB
vite-demo    1         9f07cbddbb09   5 hours ago      1.21GB

体积没有变换,但是在项目依赖没有更新时,我们优化了构建速度。