如何为项目编写一份好的 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
读者不了解前端技术也没关系,我将尽量讲解在前端项目使用容器打包中的一些优化思路。
为了将项目打包成容器我们需要一下步骤:
- 为项目寻找一个基础镜像,需要包含 Node.js 运行环境。
- 将项目代码复制到镜像中。
- 安装项目依赖。
- 构建项目。
- 最后运行项目。
那么我们将以上步骤使用 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。接下来我们看一下如何进行优化吧!
优化思路
一、使用多阶段构建解耦编译环境和运行环境。
我们的第一版镜像内部大致包含了以下内容:
- Node.js 运行时环境
- 项目源代码
- 项目依赖包
- 构建产物
- 一个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.json
和package-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
体积没有变换,但是在项目依赖没有更新时,我们优化了构建速度。