Dockerfile 详解
Dockerfile 是制作镜像的"图纸",用声明式语法描述镜像的构建过程。
Dockerfile 基础
构建上下文
docker build -t my_app:1.0 .
# │
# └── 构建上下文(build context)—— 这个目录下的所有文件会被发送到 Docker Daemon⚡ 性能提示:构建上下文越大,
docker build越慢。务必使用.dockerignore排除不需要的文件(node_modules、.git、日志等)。
.dockerignore
# .dockerignore
node_modules
.git
*.log
.env
dist
.cache指令全集
FROM — 指定基础镜像
FROM nginx:1.25
# 多阶段构建中可以有多个 FROM
FROM golang:1.21 AS builder
# 从上一阶段拷贝
FROM alpine:3.18
COPY --from=builder /app/binary /app/binaryWORKDIR — 设置工作目录
WORKDIR /app
# 后面的 RUN / COPY / CMD 都在 /app 下执行
# 如果目录不存在会自动创建💡 最佳实践:使用
WORKDIR而非RUN cd /app。前者是声明式的,且对后续所有指令生效。
COPY vs ADD
# COPY:从构建上下文复制文件(推荐)
COPY ./src /app/src
COPY --chown=1000:1000 ./config /app/config # 同时指定所有者
# ADD:除了 COPY 的功能外,还能自动解压 tar 和支持 URL
ADD archive.tar.gz /app/ # 自动解压
ADD https://example.com/file /app/ # 从 URL 下载(不推荐——缺乏缓存控制)| 指令 | 用途 | 推荐场景 |
|---|---|---|
COPY |
复制本地文件 | 所有场景(首选) |
ADD |
复制 + 自动解压 | 仅在需要自动解压 tar 时使用 |
🚨 陷阱:
ADD从 URL 下载的文件不会经过校验,且会破坏构建缓存。如果需要下载文件,建议用RUN curl/wget。
RUN — 执行构建时命令
# Shell 形式(默认 /bin/sh -c)
RUN apt-get update && apt-get install -y curl
# Exec 形式
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "curl"]💡 最佳实践:
- 将多个命令合并为一个
RUN,用&&连接,减少镜像层数。- 在同一
RUN中清理包管理器缓存。- 把变化最频繁的步骤放在最后。
# ✅ 好的写法
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/*ENV — 设置环境变量
ENV APP_HOME=/app \
APP_PORT=8080 \
NODE_ENV=production
# 在后续 RUN 中可以使用
RUN echo $APP_HOME
ENV在构建时和运行时都存在。如果只需构建时变量,使用ARG。
ARG — 构建参数
# 声明构建参数
ARG VERSION=1.0
# 在构建时传入
# docker build --build-arg VERSION=2.0 -t app:2.0 .
FROM node:18
ARG NODE_ENV=production
RUN echo "Building for $NODE_ENV"🔬 深入原理:ARG 只在构建阶段存在,不会保留在最终镜像中。跨阶段使用时,需要在每个需要它的 FROM 块中重新声明。
ARG 和 ENV 的作用范围和生命周期完全不同——ARG 是构建时的临时变量,ENV 是持久化到镜像的环境变量。
另外,从安全角度考虑,不要在 ARG 中传递密钥等敏感信息,因为 ARG 的值会出现在 docker history 中。
| 指令 | 生命周期 | 可见性 |
|---|---|---|
ENV |
构建时 + 运行时 | docker inspect 可见 |
ARG |
仅构建时 | docker history 可见 |
🚨 陷阱:
ARG和ENV都会出现在docker history中,不要在 ARG 中传递密码、token 等敏感信息。敏感信息应使用 Docker BuildKit 的--secret功能。
CMD — 容器启动默认命令
# Exec 形式(推荐)
CMD ["nginx", "-g", "daemon off;"]
# Shell 形式(会包裹在 /bin/sh -c 中执行)
CMD nginx -g 'daemon off;'
# 作为 ENTRYPOINT 的默认参数
CMD ["--help"]🚨 陷阱:一个 Dockerfile 中只有最后一个
CMD生效。docker run image command可以覆盖CMD。
ENTRYPOINT — 容器入口点
# Exec 形式(推荐)
ENTRYPOINT ["python3", "main.py"]
# Shell 形式
ENTRYPOINT python3 main.pyCMD vs ENTRYPOINT
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
docker run 传参是否覆盖 |
完全覆盖 | 参数追加到后面 |
| 推荐用途 | 定义默认行为(可被覆盖) | 定义容器的主要功能(不易被覆盖) |
| 组合使用 | CMD 作为 ENTRYPOINT 的默认参数 | ENTRYPOINT 定义程序,CMD 定义默认参数 |
# 组合使用示例
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
# docker run my_image → docker-entrypoint.sh nginx -g 'daemon off;'
# docker run my_image postgres → docker-entrypoint.sh postgresEXPOSE — 声明端口
EXPOSE 8080
EXPOSE 8080/udp
EXPOSE 80/tcp
# 多个端口
EXPOSE 80 443 8080
EXPOSE仅用于文档说明,并不会实际发布端口。实际端口映射仍需docker run -p或 Compose 的ports。
VOLUME — 声明挂载点
VOLUME /var/lib/mysql
VOLUME ["/var/log", "/var/lib/mysql"]🚨 陷阱:
VOLUME在 Dockerfile 中声明后,该目录的后续修改(通过 RUN)会被丢弃。VOLUME 应在 Dockerfile 末尾声明,或通过运行时-v指定。
USER — 切换用户
# 先创建用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 切换到非 root 用户
USER appuser
# 也可以使用 UID:GID
USER 1000:1000💡 安全最佳实践:容器不应以 root 身份运行。在 Dockerfile 中创建专用用户并
USER切换。
HEALTHCHECK — 健康检查
# 检查方式
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 检查 MySQL
HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1
# 禁用基础镜像的健康检查
HEALTHCHECK NONE| 参数 | 默认值 | 说明 |
|---|---|---|
--interval |
30s | 检查间隔 |
--timeout |
30s | 单次检查超时 |
--start-period |
0s | 容器启动后等待时间 |
--retries |
3 | 失败多少次后标记为 unhealthy |
SHELL — 指定 Shell
# Windows 容器
SHELL ["powershell", "-Command"]
# 默认(Linux)
SHELL ["/bin/sh", "-c"]
# 其他 Shell
SHELL ["/bin/bash", "-c"]LABEL — 元数据
LABEL maintainer="user@example.com"
LABEL version="1.0"
LABEL description="My application"
# 一行多标签(减少层数)
LABEL org.opencontainers.image.authors="user@example.com" \
org.opencontainers.image.version="1.0"多阶段构建(Multi-stage Build)
🔬 核心价值:将编译环境和运行环境分离,最终镜像只包含运行时需要的文件,大幅缩小镜像体积。
# ======= 第一阶段:编译 =======
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# ======= 第二阶段:运行 =======
FROM alpine:3.18
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/server /app/server
COPY --from=builder /app/config.yaml /app/config.yaml
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app/server"]对比:
| 方式 | 镜像大小 |
|---|---|
| 不拆分阶段 | ~800 MB(含 Go 编译器) |
| 多阶段构建 | ~15 MB(仅 alpine + 二进制) |
BuildKit 高级功能
挂载缓存(Build Mounts)
# 缓存 Go 模块下载
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# 缓存 apt 包
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y gcc
# 挂载密钥(不留在镜像中)
RUN --mount=type=secret,id=mysecret \
cat /run/secrets/mysecret# 启用 BuildKit
DOCKER_BUILDKIT=1 docker build -t my_app .
# 使用密钥文件
docker build --secret id=mysecret,src=./secret.txt -t my_app .并行构建和外部缓存
# 指定外部缓存源
docker build --cache-from=my_app:latest -t my_app:1.0 .
# 缓存写入外部(如 registry)
docker build --cache-to=type=registry,ref=myregistry.com/cache:latest .典型 Dockerfile 模板
Go 应用
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/server /server
USER 1000
EXPOSE 8080
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["/server"]Node.js 应用
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM node:18-alpine
RUN apk add --no-cache tini
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
USER node
EXPOSE 3000
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/main.js"]Python 应用
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim
COPY --from=builder /root/.local /root/.local
WORKDIR /app
COPY . .
ENV PATH=/root/.local/bin:$PATH
USER 1000
ENTRYPOINT ["python", "main.py"]常见陷阱
| 陷阱 | 说明 | 正确做法 |
|---|---|---|
latest 标签 |
不保证是最新,且在不同时间构建结果不同 | 显式指定版本号 |
| 构建上下文过大 | 整个目录发给 daemon | 添加 .dockerignore |
| 层数过多 | 每个 RUN/COPY/ADD 创建一层 | 合并 RUN 命令 |
| 使用 root 运行 | 安全风险 | 创建专用用户并 USER 切换 |
CMD 写了多个 |
只有最后一个生效 | 只写一个 CMD |
ADD 从 URL 下载 |
无缓存、无校验 | 用 RUN curl 替代 |
用 ARG 传密钥 |
docker history 可见 |
用 BuildKit --secret |
| 容器前台进程退出 | PID 1 退出容器即停止 | 确保 CMD 在前台运行 |
apt-get update 单独一行 |
缓存过时 | 与 install 合并 |