Skip to content
Dockerfile 详解

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/binary

WORKDIR — 设置工作目录

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 块中重新声明。 ARGENV 的作用范围和生命周期完全不同——ARG 是构建时的临时变量,ENV 是持久化到镜像的环境变量。

另外,从安全角度考虑,不要在 ARG 中传递密钥等敏感信息,因为 ARG 的值会出现在 docker history 中。

指令 生命周期 可见性
ENV 构建时 + 运行时 docker inspect 可见
ARG 仅构建时 docker history 可见

🚨 陷阱ARGENV 都会出现在 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.py

CMD 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 postgres

EXPOSE — 声明端口

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 合并