Docker 入门第三阶段Dockerfile 与自定义镜像系列Docker 从零到实战阶段第三阶段Dockerfile预计学习时间1 周前置要求完成第二阶段日常命令 MySQL/Redis 实战更新时间2026 年 7 月写在前面前两阶段你学会了拉取别人的镜像、运行和管理容器。但真实开发中你需要把自己的项目打包成镜像——部署到测试环境、交给同事、推到服务器都靠它。第三阶段的目标从「只会用别人的镜像」到「能把自己的应用打包成镜像」。学完本文你将能够读懂并编写一个完整的 Dockerfile理解镜像分层layer和构建缓存机制使用.dockerignore优化构建区分CMD和ENTRYPOINT的适用场景独立完成一个 Node.js / 静态网站项目的容器化目录1. 本阶段学习地图2. 什么是 Dockerfile3. 核心指令详解4. 构建与运行镜像5. 镜像分层与构建缓存6. .dockerignore 文件7. CMD 与 ENTRYPOINT8. 实战一容器化 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. 什么是 Dockerfile2.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-alpinePython 应用python:3.12-alpineGo 应用golang:1.22-alpine静态网站nginx:1.25-alpine需要完整 Linux 工具ubuntu:22.04alpine是基于 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复制本地文件/目录日常使用 COPYADD复制 自动解压 tar 支持 URL仅在需要自动解压时用官方建议能用 COPY 就用 COPYADD 的额外能力容易带来意外行为。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_ENVproduction ENV PORT3000构建时和运行时都可用后续指令可直接引用$NODE_ENVENV APP_HOME/app WORKDIR $APP_HOME3.6 EXPOSE — 声明端口EXPOSE 3000仅声明容器监听的端口不会自动映射到宿主机实际映射仍需docker run -p 3000:3000主要作用是文档化 与 Compose 配合3.7 CMD — 默认启动命令CMD [npm, start]容器启动时执行的默认命令一个 Dockerfile 只能有一个CMDdocker 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-appdockerrmmy-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 构建时如果某一层的指令和输入没有变化就复用缓存跳过该层及之后的层。第一次构建全部执行慢 修改源码后重建层 14 命中缓存只重新执行层 56快 修改 package.json 后重建层 12 命中缓存层 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 示例在项目根目录创建.dockerignorenode_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 核心区别对比项CMDENTRYPOINT作用默认启动命令固定入口命令docker run传参覆盖CMD追加到 ENTRYPOINT 后面数量限制只能有一个只能有一个典型场景普通应用启动工具类镜像如docker run my-cli --help7.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.jsCMD 被完全替换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 devCMD 被覆盖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.jsconstexpressrequire(express);constappexpress();constPORTprocess.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 编写 DockerfileFROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . EXPOSE 3000 CMD [npm, start]8.3 创建 .dockerignorenode_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-expressdockerrmmy-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 编写 DockerfileFROM 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.confserver { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }DockerfileFROM 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 GBnode:20-alpine~130 MBnginx:1.25~190 MBnginx: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 --frombuilder /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源文件不在构建上下文中检查路径和.dockerignorenpm: not found基础镜像不是 Node 镜像确认FROM node:20-alpineCannot find module express没执行npm install或node_modules被 .dockerignore 排除后没 install确保 Dockerfile 中有RUN npm installport is already allocated宿主机端口被占用换-p端口构建很慢没有.dockerignore传输了大量文件添加.dockerignore改了代码但容器行为没变没有重新 builddocker build后再docker run镜像体积很大用了非 alpine 镜像或 COPY 了 node_modules换 alpine 添加 .dockerignore13. 下一步学什么完成第三阶段后进入第四阶段Docker Compose 多容器编排。你将学习用docker-compose.yml管理多个服务一条命令启动「应用 数据库 缓存」数据卷volume持久化服务间网络通信预习第四阶段核心概念services:web:build:.ports:-3000:3000depends_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-fDockerfile路径-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 从零到实战」系列的第三篇。建议先完成第一、二阶段后再阅读。