Docker 入门第三阶段:Dockerfile 与自定义镜像
系列:Docker 从零到实战
阶段:第三阶段(Dockerfile)
预计学习时间:1 周
前置要求:完成第二阶段(日常命令 + MySQL/Redis 实战)
更新时间:2026 年 7 月
写在前面
前两阶段你学会了拉取别人的镜像、运行和管理容器。但真实开发中,你需要把自己的项目打包成镜像——部署到测试环境、交给同事、推到服务器,都靠它。
第三阶段的目标:
从「只会用别人的镜像」到「能把自己的应用打包成镜像」。
学完本文,你将能够:
- 读懂并编写一个完整的 Dockerfile
- 理解镜像分层(layer)和构建缓存机制
- 使用
.dockerignore优化构建 - 区分
CMD和ENTRYPOINT的适用场景 - 独立完成一个 Node.js / 静态网站项目的容器化
目录
- 1. 本阶段学习地图
- 2. 什么是 Dockerfile
- 3. 核心指令详解
- 4. 构建与运行镜像
- 5. 镜像分层与构建缓存
- 6. .dockerignore 文件
- 7. CMD 与 ENTRYPOINT
- 8. 实战一:容器化 Node.js 项目
- 9. 实战二:Nginx 托管静态网站
- 10. 镜像优化入门
- 11. 动手练习
- 12. 阶段小结与自测
- 13. 下一步学什么
1. 本阶段学习地图
第三阶段:Dockerfile │ ├── 基础 │ ├── Dockerfile 是什么 │ ├── docker build / docker run │ └── 构建上下文(build context) │ ├── 核心指令 │ ├── FROM / WORKDIR │ ├── COPY / ADD │ ├── RUN / CMD / ENTRYPOINT │ ├── EXPOSE / ENV / ARG │ └── LABEL │ ├── 原理 │ ├── 镜像分层(layer) │ ├── 构建缓存 │ └── .dockerignore │ └── 实战 ├── Node.js 应用容器化 ├── Nginx 静态网站 └── 镜像体积优化入门2. 什么是 Dockerfile
2.1 定义
Dockerfile 是一个纯文本文件,里面按顺序写着一组指令,告诉 Docker 如何一步步构建镜像。
类比理解:
| 概念 | 类比 |
|---|---|
| Dockerfile | 菜谱 |
| 每条指令(FROM、RUN、COPY…) | 菜谱里的每一步 |
docker build | 按菜谱做菜 |
| 最终镜像 | 做好的菜 |
2.2 为什么需要 Dockerfile
第二阶段你用的是别人做好的镜像(nginx、mysql、redis)。自己的项目没有现成镜像,需要:
- 选一个基础镜像(如
node:20-alpine) - 把项目代码复制进去
- 安装依赖
- 声明启动命令
Dockerfile 就是把这四步写成可重复执行的脚本。任何人拿到 Dockerfile 都能构建出相同的环境。
2.3 文件位置和命名
- 文件名固定为
Dockerfile(首字母大写,无后缀) - 通常放在项目根目录
- 构建时通过
-f可指定其他路径,初学阶段放在根目录即可
my-project/ ├── Dockerfile ← 在这里 ├── package.json ├── src/ └── ...3. 核心指令详解
3.1 FROM — 指定基础镜像
每个 Dockerfile必须以FROM开头,声明基于哪个镜像构建。
FROM node:20-alpine# 多阶段构建时可以有多个 FROM(进阶内容,第十节会提到) FROM node:20-alpine AS builder FROM nginx:1.25-alpine选择基础镜像的建议:
| 场景 | 推荐基础镜像 |
|---|---|
| Node.js 应用 | node:20-alpine |
| Python 应用 | python:3.12-alpine |
| Go 应用 | golang:1.22-alpine |
| 静态网站 | nginx:1.25-alpine |
| 需要完整 Linux 工具 | ubuntu:22.04 |
alpine是基于 Alpine Linux 的精简版,镜像体积小,生产环境首选。
3.2 WORKDIR — 设置工作目录
WORKDIR /app- 后续所有指令(
COPY、RUN、CMD)都在这个目录下执行 - 如果目录不存在,会自动创建
- 类似
cd /app,但比RUN cd /app更可靠
# ✅ 推荐 WORKDIR /app COPY . . # ❌ 不推荐 RUN cd /app COPY . .3.3 COPY — 复制文件到镜像
COPY package.json ./ COPY src/ ./src/ COPY . .格式:COPY <源路径> <目标路径>
- 源路径相对于构建上下文(通常是项目根目录)
- 目标路径相对于
WORKDIR
# 只复制 package.json 和 package-lock.json COPY package*.json ./ # 复制整个 src 目录 COPY src/ ./src/COPY vs ADD
| 指令 | 能力 | 建议 |
|---|---|---|
COPY | 复制本地文件/目录 | 日常使用 COPY |
ADD | 复制 + 自动解压 tar + 支持 URL | 仅在需要自动解压时用 |
官方建议:能用 COPY 就用 COPY,ADD 的额外能力容易带来意外行为。
3.4 RUN — 构建时执行的命令
RUN npm install RUN apt-get update && apt-get install -y curl- 在构建镜像时执行,结果写入镜像层
- 每条
RUN产生一个新层 - 多个命令建议合并成一条,减少层数:
# ✅ 推荐:合并成一层 RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* # ❌ 不推荐:三层 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/*3.5 ENV — 设置环境变量
ENV NODE_ENV=production ENV PORT=3000- 构建时和运行时都可用
- 后续指令可直接引用:
$NODE_ENV
ENV APP_HOME=/app WORKDIR $APP_HOME3.6 EXPOSE — 声明端口
EXPOSE 3000- 仅声明容器监听的端口,不会自动映射到宿主机
- 实际映射仍需
docker run -p 3000:3000 - 主要作用是文档化 + 与 Compose 配合
3.7 CMD — 默认启动命令
CMD ["npm", "start"]- 容器启动时执行的默认命令
- 一个 Dockerfile 只能有一个
CMD docker run后面跟的命令会覆盖CMD
两种写法:
# exec 格式(推荐) CMD ["npm", "start"] # shell 格式 CMD npm start3.8 ENTRYPOINT — 固定入口命令
ENTRYPOINT ["npm", "start"]- 容器启动时一定会执行的命令
docker run后面的参数会追加到 ENTRYPOINT 后面,而不是覆盖
CMD和ENTRYPOINT的详细对比见 第七节。
3.9 指令速查表
| 指令 | 执行时机 | 作用 |
|---|---|---|
FROM | 构建时 | 指定基础镜像 |
WORKDIR | 构建时 | 设置工作目录 |
COPY | 构建时 | 复制文件到镜像 |
RUN | 构建时 | 执行命令(安装依赖等) |
ENV | 构建 + 运行 | 设置环境变量 |
ARG | 构建时 | 构建参数(可传入) |
EXPOSE | 文档 | 声明端口 |
CMD | 运行时 | 默认启动命令(可覆盖) |
ENTRYPOINT | 运行时 | 固定入口命令(参数追加) |
4. 构建与运行镜像
4.1 构建上下文
执行docker build时,最后一个参数是构建上下文路径:
dockerbuild-tmy-app:1.0.# └── 当前目录为构建上下文Docker 会把该目录下的文件打包发给守护进程。COPY的源路径都相对于这个目录。
# 指定其他目录为上下文dockerbuild-tmy-app:1.0 ./backend# 指定其他 Dockerfiledockerbuild-fdocker/Dockerfile.prod-tmy-app:1.0.4.2 构建镜像
dockerbuild-tmy-app:1.0.| 参数 | 含义 |
|---|---|
build | 构建镜像 |
-t my-app:1.0 | 给镜像打标签(名称:版本) |
. | 构建上下文为当前目录 |
构建过程输出:
[+] Building 15.2s (10/10) FINISHED => [internal] load build definition from Dockerfile => [1/5] FROM node:20-alpine => [2/5] WORKDIR /app => [3/5] COPY package*.json ./ => [4/5] RUN npm install => [5/5] COPY . . => exporting to image => => naming to docker.io/library/my-app:1.04.3 运行自定义镜像
dockerrun-d-p3000:3000--namemy-app my-app:1.0和运行官方镜像一样,只是镜像名换成了你自己构建的。
4.4 查看镜像
dockerimages|grepmy-app4.5 重新构建
修改代码或 Dockerfile 后需要重新构建:
dockerbuild-tmy-app:1.1.dockerstop my-app&&dockerrmmy-appdockerrun-d-p3000:3000--namemy-app my-app:1.15. 镜像分层与构建缓存
5.1 什么是镜像层
Docker 镜像由一系列只读层(layer)堆叠而成。Dockerfile 中每条会修改文件系统的指令都会产生一个新层:
FROM node:20-alpine # 层 1:基础镜像 WORKDIR /app # 层 2:创建工作目录 COPY package*.json ./ # 层 3:复制依赖文件 RUN npm install # 层 4:安装依赖 COPY . . # 层 5:复制源码 CMD ["npm", "start"] # 层 6:元数据(不产生文件层)查看层结构:
dockerhistorymy-app:1.05.2 构建缓存
Docker 构建时,如果某一层的指令和输入没有变化,就复用缓存,跳过该层及之后的层。
第一次构建:全部执行(慢) 修改源码后重建:层 1~4 命中缓存,只重新执行层 5~6(快) 修改 package.json 后重建:层 1~2 命中缓存,层 3 起全部重建(较慢)5.3 利用缓存加速构建
关键原则:把变化少的步骤放前面,变化多的放后面。
# ✅ 推荐:依赖文件先复制、先安装 COPY package*.json ./ RUN npm install # 只有 package.json 变了才重新 install COPY . . # 源码变了只重建这一层 # ❌ 不推荐:源码和依赖一起复制 COPY . . RUN npm install # 任何文件变化都重新 install这就是为什么几乎所有 Node.js Dockerfile 都先COPY package*.json再RUN npm install。
5.4 强制不使用缓存
dockerbuild --no-cache-tmy-app:1.0.调试 Dockerfile 或确保完全重新构建时使用。
6. .dockerignore 文件
6.1 作用
.dockerignore告诉 Docker构建时忽略哪些文件,类似.gitignore。
6.2 为什么需要
没有.dockerignore时,COPY . .会把以下内容也打进镜像:
node_modules/(几百 MB,构建时会重新 install).git/(版本历史,无用且巨大).env(可能含密钥,安全风险)dist/、build/(构建产物,可能过时)
后果:
| 问题 | 影响 |
|---|---|
| 构建变慢 | 传输大量无用文件 |
| 镜像变大 | 携带 node_modules、.git 等 |
| 安全风险 | .env 密钥被打进镜像 |
| 缓存失效 | 无关文件变化导致缓存层重建 |
6.3 示例
在项目根目录创建.dockerignore:
node_modules dist .git .env .env.* *.md .vscode .idea Dockerfile docker-compose.yml6.4 验证
# 构建时观察传输大小dockerbuild-tmy-app:1.0.# 第一行会显示:transferring context: XXkB# 有 .dockerignore 后应该明显变小7. CMD 与 ENTRYPOINT
这是初学者最容易混淆的两个指令。
7.1 核心区别
| 对比项 | CMD | ENTRYPOINT |
|---|---|---|
| 作用 | 默认启动命令 | 固定入口命令 |
docker run传参 | 覆盖CMD | 追加到 ENTRYPOINT 后面 |
| 数量限制 | 只能有一个 | 只能有一个 |
| 典型场景 | 普通应用启动 | 工具类镜像(如docker run my-cli --help) |
7.2 CMD 示例
FROM node:20-alpine WORKDIR /app COPY . . CMD ["npm", "start"]# 使用默认 CMDdockerrun my-app:1.0# 实际执行:npm start# 覆盖 CMDdockerrun my-app:1.0nodeserver.js# 实际执行:node server.js(CMD 被完全替换)7.3 ENTRYPOINT 示例
FROM node:20-alpine WORKDIR /app COPY . . ENTRYPOINT ["npm", "start"]# 使用 ENTRYPOINTdockerrun my-app:1.0# 实际执行:npm start# 追加参数dockerrun my-app:1.0 ----port4000# 实际执行:npm start -- --port 40007.4 组合使用(推荐模式)
ENTRYPOINT ["npm"] CMD ["start"]dockerrun my-app:1.0# 执行:npm startdockerrun my-app:1.0 run dev# 执行:npm run dev(CMD 被覆盖,ENTRYPOINT 保留)7.5 初学阶段建议
直接用
CMD就够了。大多数 Web 应用、API 服务用CMD ["npm", "start"]或CMD ["node", "server.js"]即可。ENTRYPOINT 在工具类镜像和高级场景中更常见。
8. 实战一:容器化 Node.js 项目
以一个最简单的 Express 应用为例(你可以换成自己的项目)。
8.1 项目结构
my-express-app/ ├── package.json ├── server.js └── Dockerfile ← 新建package.json:
{"name":"my-express-app","version":"1.0.0","scripts":{"start":"node server.js"},"dependencies":{"express":"^4.18.0"}}server.js:
constexpress=require('express');constapp=express();constPORT=process.env.PORT||3000;app.get('/',(req,res)=>{res.json({message:'Hello from Docker!'});});app.listen(PORT,()=>{console.log(`Server running on port${PORT}`);});8.2 编写 Dockerfile
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . EXPOSE 3000 CMD ["npm", "start"]8.3 创建 .dockerignore
node_modules .git .env *.md8.4 构建并运行
# 构建dockerbuild-tmy-express:1.0.# 运行dockerrun-d-p3000:3000--namemy-express my-express:1.0# 验证curlhttp://localhost:3000# 预期:{"message":"Hello from Docker!"}8.5 修改代码后重新部署
# 1. 修改 server.js(比如改返回消息)# 2. 重新构建dockerbuild-tmy-express:1.1.# 3. 替换旧容器dockerstop my-express&&dockerrmmy-expressdockerrun-d-p3000:3000--namemy-express my-express:1.1# 4. 验证curlhttp://localhost:30009. 实战二:Nginx 托管静态网站
前端项目(Vue / React / Nuxt 构建产物)通常只需要 Nginx 提供静态文件服务。
9.1 项目结构
my-static-site/ ├── index.html ├── css/ │ └── style.css ├── js/ │ └── app.js └── Dockerfile9.2 编写 Dockerfile
FROM nginx:1.25-alpine COPY . /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]就四行。Nginx 官方镜像已经配置好了,/usr/share/nginx/html是默认网站根目录。
9.3 构建并运行
dockerbuild-tmy-site:1.0.dockerrun-d-p8080:80--namemy-site my-site:1.0浏览器访问http://localhost:8080即可看到页面。
9.4 自定义 Nginx 配置(了解即可)
如果项目需要 SPA 路由(如 Vue Router history 模式),需要自定义配置:
my-static-site/ ├── dist/ ├── nginx.conf ← 自定义配置 └── Dockerfilenginx.conf:
server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }Dockerfile:
FROM nginx:1.25-alpine COPY dist/ /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]10. 镜像优化入门
10.1 选择精简基础镜像
| 镜像 | 大小(约) |
|---|---|
node:20 | ~1 GB |
node:20-alpine | ~130 MB |
nginx:1.25 | ~190 MB |
nginx:1.25-alpine | ~40 MB |
# ✅ 生产环境优先 alpine FROM node:20-alpine # ❌ 除非需要完整 Debian 工具链 FROM node:2010.2 减少层数
# ✅ 合并 RUN 命令 RUN npm install --production && \ npm cache clean --force # ❌ 分开写产生多余层 RUN npm install --production RUN npm cache clean --force10.3 多阶段构建(了解概念)
把「构建」和「运行」分开,最终镜像只包含运行所需的文件:
# ── 阶段 1:构建 ── FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # ── 阶段 2:运行 ── FROM nginx:1.25-alpine COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]最终镜像不包含node_modules、源码和构建工具,体积大幅减小。多阶段构建在 Nuxt / Vue / React 项目中非常常用,第四阶段 Compose 实战时会进一步用到。
10.4 对比镜像大小
dockerimages|grepmy-# REPOSITORY TAG SIZE# my-express 1.0 130MB (alpine)# my-site 1.0 40MB (nginx alpine)11. 动手练习
练习 1:从零编写 Dockerfile
目标:不复制本文示例,凭记忆写一个 Node.js 应用的 Dockerfile
要求:
- 基于
node:20-alpine - 先复制
package*.json,再npm install - 再复制源码
- 暴露 3000 端口
- 启动命令
npm start
完成后构建并运行,用curl验证。
练习 2:感受构建缓存
目标:理解缓存如何加速构建
# 1. 首次构建,记录耗时timedockerbuild-tcache-test:1.0.# 2. 不修改任何文件,再次构建timedockerbuild-tcache-test:1.0.# 预期:几乎瞬间完成,输出 CACHED# 3. 修改 server.js 中的一个字符串,再次构建timedockerbuild-tcache-test:1.1.# 预期:比首次快,但比第 2 步慢(层 5 重建)# 4. 修改 package.json,再次构建timedockerbuild-tcache-test:1.2.# 预期:层 3 起全部重建,较慢练习 3:.dockerignore 对比
目标:直观感受.dockerignore的作用
# 1. 删除 .dockerignore,构建并观察dockerbuild-tno-ignore:1.0.# 注意 transferring context 的大小# 2. 创建 .dockerignore(排除 node_modules、.git 等),再次构建dockerbuild-twith-ignore:1.0.# 对比 transferring context 的大小练习 4:查看镜像层
目标:理解每层指令如何对应镜像层
dockerhistorymy-express:1.0观察输出,对照 Dockerfile 每条指令,理解层的大小和创建时间。
12. 阶段小结与自测
12.1 知识脑图
Dockerfile 第三阶段 ├── 构建流程 │ ├── Dockerfile(菜谱) │ ├── docker build -t <名>:<标签> .(做菜) │ └── docker run(上菜) ├── 核心指令 │ ├── FROM(基础镜像) │ ├── WORKDIR(工作目录) │ ├── COPY(复制文件) │ ├── RUN(构建时执行) │ ├── ENV(环境变量) │ ├── EXPOSE(声明端口) │ └── CMD / ENTRYPOINT(启动命令) ├── 优化 │ ├── 构建缓存(依赖文件先 COPY) │ ├── .dockerignore(排除无用文件) │ ├── alpine 基础镜像(减小体积) │ └── 多阶段构建(构建与运行分离) └── 实战 ├── Node.js 应用 └── Nginx 静态网站12.2 自测清单
- 能独立编写一个 Node.js 项目的 Dockerfile?
- 能解释为什么先
COPY package*.json再RUN npm install? - 知道
.dockerignore的作用,能写出基本的忽略规则? - 能区分
CMD和ENTRYPOINT? - 能使用
docker build构建镜像并用docker run运行? - 能使用
docker history查看镜像层? - 修改代码后知道需要重新
docker build? - 知道
alpine镜像为什么更小?
12.3 常见错误速查
| 报错 | 原因 | 解决 |
|---|---|---|
COPY failed: file not found | 源文件不在构建上下文中 | 检查路径和.dockerignore |
npm: not found | 基础镜像不是 Node 镜像 | 确认FROM node:20-alpine |
Cannot find module 'express' | 没执行npm install或node_modules被 .dockerignore 排除后没 install | 确保 Dockerfile 中有RUN npm install |
port is already allocated | 宿主机端口被占用 | 换-p端口 |
| 构建很慢 | 没有.dockerignore,传输了大量文件 | 添加.dockerignore |
| 改了代码但容器行为没变 | 没有重新 build | docker build后再docker run |
| 镜像体积很大 | 用了非 alpine 镜像或 COPY 了 node_modules | 换 alpine + 添加 .dockerignore |
13. 下一步学什么
完成第三阶段后,进入第四阶段:Docker Compose 多容器编排。
你将学习:
- 用
docker-compose.yml管理多个服务 - 一条命令启动「应用 + 数据库 + 缓存」
- 数据卷(volume)持久化
- 服务间网络通信
预习:第四阶段核心概念
services:web:build:.ports:-"3000:3000"depends_on:-dbdb:image:postgres:16environment:POSTGRES_PASSWORD:secretvolumes:-pgdata:/var/lib/postgresql/datavolumes:pgdata:dockercompose up-d# 一条命令启动所有服务dockercompose down# 一条命令停止并清理附录:第三阶段命令与模板速查卡
常用命令
# 构建镜像dockerbuild-t<名>:<标签>.dockerbuild --no-cache-t<名>:<标签>.dockerbuild-f<Dockerfile路径>-t<名>:<标签>.# 运行自定义镜像dockerrun-d-p<宿主>:<容器>--name<名><镜像>:<标签># 查看镜像层dockerhistory<镜像>:<标签># 删除镜像dockerrmi<镜像>:<标签>Node.js 应用 Dockerfile 模板
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . EXPOSE 3000 CMD ["npm", "start"]静态网站 Dockerfile 模板
FROM nginx:1.25-alpine COPY . /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"].dockerignore 模板
node_modules dist .git .env .env.* *.md .vscode .idea学习建议:第三阶段最关键的是「亲手写一个 Dockerfile 并构建成功」。选你最熟悉的一个项目(Nuxt、Express、Python Flask 都行),把它容器化。构建成功、浏览器能访问的那一刻,你对 Docker 的理解会跃升一个台阶。
第四阶段见!
本文是「Docker 从零到实战」系列的第三篇。建议先完成第一、二阶段后再阅读。