Dockerfile 是什么
Dockerfile 是一个纯文本文件,包含一系列有序指令,Docker 按照这些指令逐步构建镜像。每一条指令都可能创建一个新的镜像层(除元数据指令外)。
# 构建镜像
docker build -t myapp:1.0 .
# -t 指定镜像名:标签,. 指定构建上下文目录
所有指令详解
FROM — 基础镜像
每个 Dockerfile 必须以 FROM 开头(除了 ARG 可以在 FROM 之前),指定构建的起点。
FROM node:18-alpine # Alpine Linux 变体,最小化
FROM ubuntu:22.04 # Ubuntu 完整版
FROM scratch # 空镜像,用于静态二进制文件
# 多阶段构建中为每个阶段命名
FROM node:18-alpine AS builder
WORKDIR — 工作目录
设置后续指令(RUN、COPY、CMD 等)的工作目录。如目录不存在会自动创建,等价于 mkdir -p /app && cd /app。
WORKDIR /app
# 推荐使用绝对路径,避免使用 RUN cd /some/path
COPY vs ADD
两者都能复制文件,但推荐优先使用 COPY,因为它行为简单可预期。
# COPY:简单地从构建上下文复制文件/目录到镜像
COPY package*.json ./ # 复制 package.json 和 package-lock.json
COPY src/ ./src/ # 复制整个 src 目录
COPY . . # 复制整个构建上下文(配合 .dockerignore)
# ADD:额外支持 URL 下载和 tar 自动解压
ADD https://example.com/file.tar.gz /tmp/ # 下载并解压
ADD app.tar.gz /app/ # 自动解压 tar 文件
| 指令 | 功能 | 推荐场景 |
|---|---|---|
COPY | 复制本地文件/目录到镜像 | 几乎所有场景 |
ADD | 复制 + URL 下载 + tar 解压 | 需要自动解压 tar 时 |
RUN — 执行命令
在镜像构建时执行命令,每条 RUN 创建一个新层。有两种语法形式:
# Shell 形式(通过 /bin/sh -c 执行,支持 Shell 特性)
RUN apt-get update && apt-get install -y curl
# Exec 形式(直接执行,不经过 Shell,推荐用于避免 Shell 注入)
RUN ["apt-get", "install", "-y", "curl"]
# 最佳实践:多条命令用 && 合并,减少层数,用 \ 换行提高可读性
RUN apt-get update \
&& apt-get install -y \
curl \
git \
vim \
&& rm -rf /var/lib/apt/lists/* # 清理 apt 缓存,减小镜像体积
不要拆分 apt-get update 和 install — 如果分两层写,update 的结果会被缓存,install 时可能拿到过期的包列表,导致构建失败。始终将 update 和 install 放在同一个 RUN 命令中。
ENV — 环境变量
设置的环境变量在容器运行时可见,也可在 Dockerfile 后续指令中引用。
ENV NODE_ENV=production
ENV PORT=3000
ENV APP_HOME=/app
# 在后续指令中引用
WORKDIR $APP_HOME
# 运行时可用 -e 覆盖
# docker run -e NODE_ENV=development ...
ARG — 构建时参数
仅在构建阶段可见的参数,运行时不可见(不同于 ENV)。
# 声明 ARG(可有默认值)
ARG APP_VERSION=1.0.0
ARG BUILD_DATE
# 使用 ARG
LABEL version=$APP_VERSION
# 构建时传入
# docker build --build-arg APP_VERSION=2.0.0 .
不要用 ARG 传递密钥 — ARG 的值会出现在 docker history 中,不够安全。密钥应使用 BuildKit Secret 特性(第8章介绍)。
EXPOSE — 声明端口
仅作为文档说明,告诉使用者这个容器监听哪些端口。不会实际开放端口,需要用 -p 参数实际映射。
EXPOSE 3000
EXPOSE 80/tcp
EXPOSE 53/udp
CMD vs ENTRYPOINT
这是最容易混淆的两个指令。理解它们的关系是关键:
# CMD:容器的默认命令,可被 docker run 末尾的命令覆盖
CMD ["node", "server.js"]
# ENTRYPOINT:容器的入口点,不易被覆盖(需要 --entrypoint 参数)
ENTRYPOINT ["node"]
# 组合使用:ENTRYPOINT 是可执行文件,CMD 是默认参数
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# 典型组合:固定入口,灵活默认参数
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["node", "server.js"]
| 场景 | 用法 |
|---|---|
| 容器就是该程序本身(如工具容器) | 用 ENTRYPOINT,CMD 做默认参数 |
| 容器可以运行不同命令 | 只用 CMD |
| 需要启动脚本做初始化 | ENTRYPOINT 指向 shell 脚本,CMD 是程序启动命令 |
VOLUME — 数据卷挂载点
# 声明容器内的目录为挂载点(数据会持久化)
VOLUME /var/lib/mysql
VOLUME ["/data", "/logs"]
USER — 切换用户
安全最佳实践:不要以 root 运行容器内的进程。
# 创建专用用户组和用户
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
# 切换到该用户(之后的 RUN/CMD/ENTRYPOINT 都以此用户执行)
USER appuser
CMD ["node", "server.js"]
HEALTHCHECK — 健康检查
HEALTHCHECK \
--interval=30s \ # 每30秒检查一次
--timeout=10s \ # 超时10秒视为失败
--start-period=40s \ # 启动后40秒内失败不计入
--retries=3 \ # 连续3次失败才标记为 unhealthy
CMD curl -f http://localhost:3000/health || exit 1
LABEL — 元数据标签
LABEL maintainer="team@example.com"
LABEL version="1.0" \
description="My App Image" \
org.opencontainers.image.source="https://github.com/org/repo"
.dockerignore 文件
类似 .gitignore,指定构建时不要包含在上下文(Context)中的文件和目录,减少构建时间和镜像大小。
# .dockerignore 示例
node_modules/
.git/
.gitignore
*.md
.env
.env.*
coverage/
.nyc_output/
dist/
build/
*.log
.DS_Store
__pycache__/
*.pyc
.pytest_cache/
venv/
*.egg-info/
为什么重要 — 如果 node_modules 不在 .dockerignore 中,执行 COPY . . 时会把本机的 node_modules(可能有数万个文件)全部发送给 Docker daemon,极大拖慢构建速度。
分层缓存原理
Docker 构建时会缓存每一层。只要某层的指令和输入文件不变,就直接使用缓存。但某层失效会导致之后所有层全部重新构建。
# ❌ 错误顺序:改动代码会导致所有依赖重新安装
FROM node:18-alpine
WORKDIR /app
COPY . . # 任何代码改动都使此层失效
RUN npm install # 每次都要重新安装,很慢
# ✅ 正确顺序:利用缓存,只有 package.json 变了才重新安装
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # 只复制依赖描述文件(很少变动)
RUN npm ci # 仅在 package.json 变化时重新安装
COPY . . # 再复制代码(即使代码变了,依赖层缓存有效)
RUN npm run build
缓存失效的触发条件:
- 指令文本本身改变(如
RUN apt-get install curl→ 改为RUN apt-get install curl vim) - COPY/ADD 的源文件内容改变
- 使用
--no-cache参数强制忽略缓存 - 依赖的父层缓存失效
完整示例:Node.js + React 应用
# Dockerfile for a React frontend
# === 构建阶段 ===
FROM node:18-alpine AS builder
WORKDIR /app
# 先复制依赖描述文件,利用缓存
COPY package*.json ./
# 安装依赖(npm ci 更严格,适合 CI 环境)
RUN npm ci --only=production=false
# 复制源代码
COPY . .
# 构建生产版本
RUN npm run build
# === 运行阶段 ===
FROM nginx:1.25-alpine
# 复制构建产物到 nginx 静态文件目录
COPY --from=builder /app/build /usr/share/nginx/html
# 复制自定义 nginx 配置(处理 SPA 路由)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s \
CMD wget -q -O /dev/null http://localhost || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
最佳实践清单
- 使用具体版本标签而非
latest - 选择 Alpine 等最小基础镜像
- 合并 RUN 命令,减少层数
- 将不常变化的层(依赖)放在前面
- 使用
.dockerignore排除不需要的文件 - 使用非 root 用户运行应用
- 使用多阶段构建分离构建和运行环境