目录

Dockerfile 教程:将自己的程序打包成镜像

Dockerfile 是一个纯文本文件,里面逐行写着“如何创建这个镜像”。Docker 会按照这些步骤执行,生成一个镜像,之后就能像 nginx 一样用 docker run 启动。

示例项目

创建一个文件夹 hello-node,里面放两个文件。

package.json:

{
  "name": "hello-node",
  "version": "1.0.0",
  "scripts": { "start": "node server.js" }
}

server.js:

const http = require("http");

const server = http.createServer((req, res) => {
  res.end("Hello from Docker!\n");
});

server.listen(3000, () => console.log("Listening on port 3000"));

你的电脑上不需要安装 Node.js。Node.js 会在镜像中,这正是 Docker 的用途。

编写 Dockerfile

在同一个文件夹中创建名为 Dockerfile 的文件(没有扩展名):

# 1. 以官方 Node.js 镜像为基础
FROM node:24-alpine

# 2. 设置容器内的工作目录
WORKDIR /app

# 3. 先复制依赖包清单并安装依赖包
COPY package*.json ./
RUN npm install --omit=dev

# 4. 再复制其余代码
COPY . .

# 5. 声明容器会使用的端口
EXPOSE 3000

# 6. 容器启动时运行的命令
CMD ["npm", "start"]

现在文件夹里有三个文件:

$ ls
Dockerfile
package.json
server.js

构建镜像:docker build

docker build -t hello-node .

-t hello-node 是镜像名称,最后的 . 表示使用当前文件夹作为构建上下文(build context),Docker 会把这个文件夹中的文件发送给构建引擎,COPY 命令只能复制构建上下文中的文件。

构建时终端会显示动态更新的进度,完成后的画面如下:

$ docker build -t hello-node .

[+] Building 9.9s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              1.9s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  3.6s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444 0B / 444B       7.9s
 => => sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc 1.26MB / 1.26MB  0.7s
 => => sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a 56.52MB / 56.52MB  1.3s
 => => sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b 3.85MB / 3.85MB  0.6s
 => => extracting sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b5      0.6s
 => => extracting sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a7f0      2.2s
 => => extracting sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc1      0.1s
 => => extracting sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444      0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 759B                                                              0.0s
 => [2/5] WORKDIR /app                                                                         0.0s
 => [3/5] COPY package*.json ./                                                                0.0s
 => [4/5] RUN npm install --omit=dev                                                           1.4s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         2.7s
 => => exporting layers                                                                        0.2s
 => => exporting manifest sha256:b311d231396fab904d11e3fbac8bade8302e8e7401fe14ba1e4a736f4fed  0.0s
 => => exporting config sha256:fa1ad1088d08bf3f3f035dfe9dbed3cf5a008492316926ba23fae3e58d6852  0.0s
 => => exporting attestation manifest sha256:0ea092cede0047cad91ad77043f79ef84c7cab9153913acf  0.0s
 => => exporting manifest list sha256:62664c2b6134a7c365eacb28019730c69b7d1d0fd855c559f6994c3  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        2.5s

每一行 => 都是一个步骤:

步骤对应的 Dockerfile 命令说明
load build definition—读取 Dockerfile
load metadata for …node:24-alpineFROM查询基础镜像的最新版本
[1/5] FROM …FROM node:24-alpine下载基础镜像(下面的 sha256:… 是每一层的下载进度,第一次会比较久)
load build context—发送构建上下文(这次只有 759B,因为只有 3 个小文件)
[2/5] WORKDIR /appWORKDIR创建并切换到 /app
[3/5] COPY package*.json ./COPY复制依赖包清单
[4/5] RUN npm install --omit=devRUN在镜像中安装依赖包
[5/5] COPY . .COPY复制其余代码
exporting to image—将结果保存为镜像并命名为 hello-node:latest

EXPOSE 和 CMD 只是设置,不会产生构建步骤。

运行

$ docker images hello-node
IMAGE               ID             DISK USAGE   CONTENT SIZE   EXTRA
hello-node:latest   62664c2b6134        245MB         62.2MB

$ docker run -d -p 3000:3000 --name hello hello-node
2c4745bd0187d9ba7f0e9af096519ce9a2ad4436395490f5af24cd142f3bef4c

$ curl -s http://localhost:3000
Hello from Docker!

$ docker logs hello

> hello-node@1.0.0 start
> node server.js

Listening on port 3000
浏览器显示 Hello from Docker!
在浏览器中打开 http://localhost:3000
  • 镜像大小约为 245 MB,其中大部分是 Node.js 本身(node:24-alpine)。
  • docker logs 中的 > node server.js 是 npm start 输出的信息,接下来才是程序自己的 Listening on port 3000。

修改代码后重新构建:缓存

将 server.js 中的文本改为 Hello from Docker! (v2),再构建一次:

$ docker build -t hello-node .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 294B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package*.json ./                                                         0.0s
 => CACHED [4/5] RUN npm install --omit=dev                                                    0.0s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:667a883ab027b5db474215519a69eef0b162988f9f6384d75c7e6e819ebc  0.0s
 => => exporting config sha256:c3795d6af309587aa9f021692bf628df45d1d38a5e12d05388447644423a01  0.0s
 => => exporting attestation manifest sha256:4598cd1b9ff7979933f7bc06c48df78bb906278618ad587a  0.0s
 => => exporting manifest list sha256:fb09b954543fd2fa9a8d93763672d6a0b8d3ac26e7751e3b3f69b79  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        0.0s

这次只用了 0.4 秒:

  • [2/5]、[3/5]、[4/5] 前面标有 CACHED:这几步和上次完全相同,直接使用上次的结果,无需重新运行 npm install。
  • 只有 [5/5] COPY . . 重新执行了,因为 server.js 有改动。
$ docker run --rm -d -p 3000:3000 --name hello hello-node
9d4e4fc490b7b2e5d4c4c5ba729359c63271cbfa7934f6fff011a4fd4d6e87ca

$ curl -s http://localhost:3000
Hello from Docker! (v2)

$ docker stop hello
hello

为什么先复制 package.json

Dockerfile 的每一行都会生成一层,Docker 会缓存这些层。某一行(以及它复制的文件)发生变化时,这一行之后的所有步骤都要重新执行。

如果一开始就 COPY . .,每次改动一行代码,npm install 的缓存都会失效,必须重新安装所有依赖包。先只复制 package*.json 再安装,只有依赖包清单发生变化时才需要重新安装。项目依赖包越多,差距越明显,详细对比见构建缓存与优化。

.dockerignore:不要把不需要的文件发送到构建上下文

本地开发时,文件夹里通常有 node_modules、.git、日志文件等大量文件。为了演示,在文件夹中放一个 30 MB 的 node_modules,再构建一次,留意 load build context 那一行:

$ docker build -t hello-node .

[+] Building 2.0s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.2s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.4s
 => => transferring context: 30.01MB                                                           0.3s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package*.json ./                                                         0.0s
 => CACHED [4/5] RUN npm install --omit=dev                                                    0.0s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         1.3s
 => => exporting layers                                                                        1.0s
 => => exporting manifest sha256:5c28f64104e51f64822405ae7a67a9016f62e9c5a4c8425b5b92c47791af  0.0s
 => => exporting config sha256:21b97fa4b8887f00cdf80b0dbeb6658f8dd678b8c4008f8d1dcb5c191e82ba  0.0s
 => => exporting attestation manifest sha256:bc9366f473fd5bb6fdffec42cdcd8155bd12b28cfc6ac23d  0.0s
 => => exporting manifest list sha256:f36d091892aca26420e44f681f5279c82f8881c32eb6d77978d99ee  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        0.2s

transferring context: 30.01MB:整个 node_modules 都被发送给构建引擎,而且 COPY . . 还会把它复制到镜像中,覆盖镜像里通过 npm install 安装的版本。

创建 .dockerignore,列出不需要发送到构建上下文的文件:

node_modules
.git
.env
*.log

再构建一次:

$ docker build -t hello-node .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 69B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 159B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package*.json ./                                                         0.0s
 => CACHED [4/5] RUN npm install --omit=dev                                                    0.0s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:c7dd5ef36303b2a808ee6b594946ee3c029769363a3e7eecfbc53f1c3795  0.0s
 => => exporting config sha256:b23dccadd37348cdcc5e2ca60934e15f7f7f309f477651ab90b6f3759e2e25  0.0s
 => => exporting attestation manifest sha256:217b64bdf5112df5d8d6131ebb08c0fbdf3b30db091103a9  0.0s
 => => exporting manifest list sha256:4063457524bbb10ef5c91def983dc1385e7aac61245c61fc8a4d568  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        0.0s

构建上下文变成了 159B。.dockerignore 的好处:

  1. 构建更快,尤其是包含大型 node_modules 或 .git 的项目。
  2. 这些文件发生变化时,不会导致 COPY . . 的缓存失效。
  3. 避免将 .env、私钥等机密文件打包进镜像。

主机上的 node_modules 可能包含为 Windows 或 macOS 编译的原生模块,复制到 Linux 容器中会出错。Node.js 项目必须在 .dockerignore 中排除 node_modules。

停止容器

$ time docker stop hello
hello
real	0m 0.69s
user	0m 0.01s
sys	0m 0.00s

0.7 秒就停止了。容器的主进程是 npm,它收到 docker stop 发送的 SIGTERM 后,会将信号转给 node 并正常退出。如果改为直接将 node 作为主进程,就需要在程序中处理 SIGTERM,否则 docker stop 会等待满 10 秒,实际演示详见 Dockerfile 命令大全。

小技巧

  • 选择 alpine 或 slim 版本的基础镜像,可以大幅减小镜像大小(见镜像中的大小对比)。
  • 固定基础镜像的版本,例如 node:24-alpine,不要只写 node。
  • CMD 建议使用 JSON 数组格式(["npm", "start"]),行为最明确。

下一节将完整介绍 Dockerfile 的每个命令。

重点回顾

  • Dockerfile 从 FROM 开始,每个命令都会生成一层。
  • 使用 docker build -t 名称 . 构建,最后的 . 是构建上下文。
  • 先复制依赖包清单再安装依赖包,这样只改代码时就能使用缓存。
  • .dockerignore 用于排除 node_modules、.git、.env 等不需要的文件。

动手练习

练习 1:修改 server.js 的响应文本后重新构建,哪些步骤显示 CACHED?

答案

FROM、WORKDIR、COPY package*.json、RUN npm install 都是 CACHED,只有 COPY . . 之后的步骤会重新执行,因为只有代码发生了变化。

练习 2:将 Dockerfile 中的 COPY package*.json ./ 和 RUN npm install 移到 COPY . . 之后,再修改一次 server.js 并重新构建,会有什么不同?

答案

RUN npm install 也会重新执行,构建会变慢。因为 COPY . . 的内容变了,之后的每一层都要重建,详见构建缓存。

常见问题

Dockerfile 必须叫 Dockerfile 吗?

默认是。其他名称用 docker build -f 文件名 . 指定,参数见 [docker build](/zh/docker/docker-build/)。

我的电脑需要安装 Node.js 吗?

不需要。Node.js 在镜像中,构建和运行都在容器中进行。

不想自己写 Dockerfile,有工具可以生成吗?

Docker Desktop 内置 docker init,回答几个问题就能生成 Dockerfile 和 compose.yaml,实际演示见 [docker init](/zh/docker/docker-init/)。

更新于 2026年10月14日