镜像与 Dockerfile
上一章使用了别人制作的 Nginx 镜像。本篇将制作一个属于自己的静态网站镜像。完成后,你将能够:
- 理解镜像名、标签、层和构建上下文;
- 编写基础 Dockerfile;
- 使用
.dockerignore排除无关文件; - 构建、运行、检查和删除镜像;
- 理解缓存和多阶段构建的用途。
1. 镜像从哪里来
镜像通常来自两种途径:
- 使用
docker pull从 Docker Hub 或私有仓库下载; - 使用
docker build根据 Dockerfile 自己构建。
镜像引用的一般格式是:
# 完整格式:仓库地址/命名空间/镜像名:标签
registry.example.com/team/myapp:1.0.0
# Docker Hub 官方镜像可以省略仓库地址和命名空间
nginx:1.27-alpine
nginx 是镜像名,1.27-alpine 是标签。省略标签时 Docker 默认使用 latest,但 latest 只是普通标签,并不保证内容固定或一定最新。教程和生产部署都建议显式写版本。
2. 管理已有镜像
# 从 Docker Hub 拉取固定标签的 Nginx 镜像
docker pull nginx:1.27-alpine
# 列出本地镜像;注意 REPOSITORY、TAG、IMAGE ID 和 SIZE
docker image ls
# 查看镜像的启动命令、环境变量、架构和层等元数据
docker image inspect nginx:1.27-alpine
# 查看镜像每一层对应的构建命令和大致大小
docker history nginx:1.27-alpine
同一个镜像可以有多个标签。标签类似指向镜像内容的易读名称,不会复制一份镜像数据。
3. 动手制作第一个镜像
3.1 创建实验目录
# 在用户主目录创建独立实验目录,避免混入其他项目文件
mkdir -p ~/docker-beginner/site
cd ~/docker-beginner/site
# 确认后续命令所在目录
pwd
在目录中创建 index.html:
<!-- 这是要放进镜像的静态首页 -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>我的第一个 Docker 镜像</title>
</head>
<body>
<h1>Docker 构建成功</h1>
<p>这个页面来自自定义镜像。</p>
</body>
</html>
3.2 编写 Dockerfile
在同一目录创建一个文件名严格为 Dockerfile 的文件:
# 使用体积较小的官方 Nginx 镜像作为基础镜像
FROM nginx:1.27-alpine
# 删除基础镜像自带的欢迎页
RUN rm -f /usr/share/nginx/html/index.html
# 把当前构建目录中的首页复制进镜像
COPY index.html /usr/share/nginx/html/index.html
# 记录应用预期监听 80 端口;此指令不会自动发布端口
EXPOSE 80
Dockerfile 指令的作用:
| 指令 | 作用 |
|---|---|
FROM | 指定基础镜像;通常是第一条有效指令 |
RUN | 在构建过程中执行命令,并把结果保存到镜像层 |
COPY | 从构建上下文复制文件到镜像 |
WORKDIR | 设置后续指令的工作目录 |
ENV | 设置镜像中的环境变量 |
EXPOSE | 说明容器预计监听的端口,不负责端口映射 |
CMD | 设置容器默认启动命令,可被运行参数覆盖 |
ENTRYPOINT | 设置容器入口程序,适合固定可执行文件 |
3.3 添加 .dockerignore
docker build 会把指定目录作为构建上下文发送给构建器。不要把 Git 历史、密钥和本地依赖一并发送。在同一目录创建 .dockerignore:
# 版本控制元数据不参与镜像构建
.git
# 环境变量和私钥绝不能复制进镜像
.env
*.pem
# 常见的本地依赖和构建输出
node_modules
dist
# 编辑器与系统临时文件
.DS_Store
.idea
.vscode
3.4 构建镜像
# -t 设置镜像名和标签;最后的点表示当前目录是构建上下文
docker build --tag beginner-site:1.0 .
# 确认新镜像出现在本地列表中
docker image ls beginner-site
构建日志中的 FROM、RUN 和 COPY 基本对应 Dockerfile 中的步骤。最后应该看到镜像被命名为 beginner-site:1.0。
docker build -t beginner-site:1.0 . 最后的 . 表示“把当前目录作为构建上下文”。COPY index.html ... 只能复制上下文内且未被 .dockerignore 排除的文件。
3.5 运行并验证
# 基于自制镜像启动容器,并映射到本机 8080 端口
docker run --detach \
--name beginner-site \
--publish 8080:80 \
beginner-site:1.0
# 请求页面;响应中应出现“Docker 构建成功”
curl http://127.0.0.1:8080
# 查看容器使用的镜像和端口映射
docker ps --filter name=beginner-site
也可以在浏览器打开 http://localhost:8080。完成后清理容器:
# -f 会先停止再删除这个实验容器;不会删除镜像
docker rm --force beginner-site
4. 修改页面并理解缓存
修改 index.html 后构建新版本:
# 使用新标签构建第二版;未变化的步骤可能显示 CACHED
docker build --tag beginner-site:1.1 .
# 同时列出两个标签,比较 IMAGE ID
docker image ls beginner-site
Dockerfile 中每条会改变文件系统的指令通常形成一层。某一步及其输入没有变化时,Docker 可以复用缓存。某一步发生变化后,它以及后续依赖层通常需要重新构建。
缓存优化的基本原则是:先复制变化少的文件,后复制变化频繁的文件。例如 Node.js 项目应先复制依赖清单并安装依赖,再复制源码:
# 使用固定主版本的 Node.js 基础镜像
FROM node:22-alpine
# 后续 COPY 和 RUN 都在 /app 下执行
WORKDIR /app
# 依赖清单通常比业务源码变化少,因此先单独复制
COPY package.json package-lock.json ./
# npm ci 根据锁文件进行可复现安装
RUN npm ci
# 最后复制经常变化的业务源码
COPY . .
# 使用 JSON 数组形式,让 Node 正确接收停止信号
CMD ["node", "server.js"]
如果先执行 COPY . .,任何源码变化都会使后面的依赖安装缓存失效。
5. 多阶段构建
编译型项目需要构建工具,但运行阶段通常不需要。多阶段构建可以在第一阶段编译,在第二阶段只复制运行产物。下面是 Node.js 项目的结构示例:
# syntax=docker/dockerfile:1
# 第一阶段负责安装完整依赖并构建应用
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 第二阶段只包含生产运行所需文件
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
# 只安装生产依赖,减少最终镜像内容
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# 从 build 阶段复制编译结果,不复制源码和构建工具
COPY /app/dist ./dist
# 官方 Node 镜像内置低权限 node 用户
USER node
EXPOSE 3000
# exec 形式可以让应用正确接收 SIGTERM
CMD ["node", "dist/server.js"]
这段示例需要项目本身存在 package.json、锁文件、构建脚本和 dist/server.js,不能直接用于前面的纯 HTML 实验。重点是理解“构建环境”和“运行环境”可以分开。
6. 标签、仓库与 digest
为镜像增加仓库标签后才能推送:
# 为已有镜像增加完整仓库标签;请替换仓库和命名空间
docker tag beginner-site:1.0 registry.example.com/team/beginner-site:1.0
# 登录私有仓库;推荐通过标准输入传递密码,避免出现在命令历史
printf '%s' "$REGISTRY_PASSWORD" | \
docker login registry.example.com --username "$REGISTRY_USER" --password-stdin
# 推送带完整仓库地址的标签
docker push registry.example.com/team/beginner-site:1.0
标签可以被重新指向其他内容;digest 是镜像内容的加密摘要,更适合要求严格复现的部署:
# 查看拉取后记录的内容摘要
docker image inspect nginx:1.27-alpine \
--format '{{json .RepoDigests}}'
# 生产环境可按真实 digest 拉取,尖括号内容需要替换
docker pull nginx@sha256:<真实的镜像摘要>
7. 不要把密钥写进镜像
ARG、ENV、COPY 和构建命令历史都可能泄露凭据。需要私有依赖时,使用 BuildKit secret:
# secret 只在这一条 RUN 指令期间挂载,不写入最终镜像层
RUN \
npm ci
# 将本机 .npmrc 作为临时 secret 提供给上面的构建步骤
docker build \
--secret id=npmrc,src="$HOME/.npmrc" \
--tag private-app:1.0 .
8. 清理实验镜像
# 只删除明确指定的两个实验标签;仍被容器使用时会拒绝删除
docker image rm beginner-site:1.0 beginner-site:1.1
# 查看删除后的镜像列表和空间占用
docker image ls
docker system df
不要为了省事直接执行 docker system prune -a。新手阶段应逐个确认对象,理解容器和镜像之间的引用关系。
9. 本篇练习
- 在首页增加一段文字,构建
beginner-site:1.2并运行验证; - 把 Dockerfile 中的
EXPOSE 80删除,再构建运行,验证-p 8080:80是否仍然有效; - 在目录中创建
.env,确认.dockerignore已排除它; - 用
docker history beginner-site:1.2对照 Dockerfile 的每一步。
下一篇将深入容器根文件系统,解释镜像层、Docker init 层、容器可写层和 OverlayFS 联合视图。