Dockerfile 详解

Dockerfile 是 Docker 的灵魂——一个文本文件,描述"怎么从一个基础镜像,一步步构建出你自己的应用镜像"。掌握了它,你就能把任何项目打包成"扔到任何机器都能跑"的镜像。本章把全部常用指令讲透,并解释让构建又快又小的"分层缓存"原理。

一个完整模板

先看一个典型 Node.js 项目的 Dockerfile,基本结构一网打尽:

# 一个典型 Node.js 项目的 Dockerfile,逐行注释

# 基础镜像:从哪个现成镜像开始构建(必须是第一条非注释指令)
FROM node:20-alpine

# 设置工作目录(容器内的路径,不存在会自动创建)
WORKDIR /app

# 只复制依赖清单(利用缓存层,见后文)
COPY package*.json ./

# 安装生产依赖(--omit=dev 不装 devDependencies)
RUN npm ci --omit=dev

# 复制源码(放后面,因为源码常变,会让后续缓存失效)
COPY . .

# 构建产物(如果是 TypeScript / 前端项目)
RUN npm run build

# 声明容器监听的端口(仅文档作用,运行时还要 -p 映射)
EXPOSE 3000

# 设环境变量(运行时仍可被 docker run -e 覆盖)
ENV NODE_ENV=production

# 容器启动时默认执行的命令
CMD ["node", "dist/main.js"]

这个文件名叫 Dockerfile(无扩展名,首字母大写),放在项目根目录。配套一个 .dockerignore(见后文)。写好后 docker build 一下就生成镜像。

构建、运行、调试

# 构建、运行、调试 Dockerfile

# 构建镜像(注意最后的点表示"上下文目录")
docker build -t myapp:1.0 .
#         镜像名:标签  上下文(决定了 COPY 能复制什么)

# 指定 Dockerfile 路径(默认找 ./Dockerfile)
docker build -f Dockerfile.prod -t myapp:1.0 .

# 不使用缓存(排查缓存问题用)
docker build --no-cache -t myapp:1.0 .

# 构建参数(ARG)
docker build --build-arg NODE_VERSION=18 -t myapp:1.0 .

# 多平台构建(Apple Silicon 上构建 amd64 镜像)
docker build --platform linux/amd64 -t myapp:1.0 .

# 跑起来验证
docker run -d --name myapp -p 3000:3000 myapp:1.0

上下文(命令最后的那个点)是新手最容易忽略的概念。它告诉 Docker "构建时允许访问哪个目录的文件"——COPY . . 复制的就来自这个范围。把 Dockerfile 放项目根、上下文设为 . 是最常见做法。注意上下文越大,构建越慢(全部内容会打包发给守护进程),所以一定要配 .dockerignore

FROM:基础镜像

FROM 必须是 Dockerfile 的第一条非注释指令。选择基础镜像是镜像大小和安全的关键决策:

原则:越精简越好,但要保证依赖能装上。多阶段构建(见最佳实践章)能在编译时用完整镜像、运行时用 alpine,两全其美。

RUN:执行命令

RUN 在构建时执行命令,产出新的一层。它是 Dockerfile 里最常用、也最容易写出"胖镜像"的指令:

正确写法示例:把装包、清理一行搞定。

# RUN 合并写法:多条命令用 && 串联,减少层数 + 清理缓存

# ❌ 错误写法:三条 RUN,产生三层,缓存也留着
RUN apt-get update
RUN apt-get install -y curl git
RUN rm -rf /var/lib/apt/lists/*

# ✅ 正确写法:一条 RUN 完成,只有一层,缓存即装即清
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl git \
    && rm -rf /var/lib/apt/lists/*

# alpine 用 apk,清理用 --no-cache 或 rm /var/cache/apk/*
RUN apk add --no-cache curl git

# 写法要点:
#   1. 用 && 连接,前一条失败后一条不执行(避免半成品层)
#   2. 用 \ 换行让长命令更可读(Dockerfile 行续符)
#   3. --no-install-recommends / --no-cache 减少多余文件

COPY vs ADD:复制文件

# COPY vs ADD

# COPY:复制本地文件到镜像(简单可靠,推荐)
COPY package.json /app/
COPY package*.json /app/
COPY src/ /app/src/                # 复制整个目录
COPY . .                           # 复制上下文所有文件(配合 .dockerignore)

# ADD:COPY 的超集,多了三个能力
ADD https://example.com/app.tar.gz /tmp/   # 自动下载 URL
ADD app.tar.gz /app/              # 自动解压本地 tar.gz / zip
ADD app.tar.gz /app/              # 但不会自动解压从 URL 下载的压缩包

# 最佳实践:永远优先用 COPY
# 只有需要自动解压本地 tar 包时才用 ADD
# 想下载远程文件?用 RUN curl + COPY 更可控

# USER:以非 root 用户运行(安全)
RUN addgroup -S app && adduser -S app -G app
USER app

永远优先用 COPY。它语义清晰、行为可预测。ADD 的"自动下载 URL"和"自动解压"功能看似方便,但容易让构建依赖网络状态、产物不可控。需要远程文件就 RUN curl -o ...

CMD vs ENTRYPOINT:启动命令

这是 Dockerfile 里最容易混淆的一对。理解关键是"谁会被 docker run 后面的参数覆盖":

# CMD vs ENTRYPOINT(新手最容易混淆)

# CMD:容器默认命令,可被 docker run 后面的参数覆盖
FROM alpine
CMD ["echo", "hello"]
# docker run myimage           → 输出 hello
# docker run myimage echo hi   → 输出 hi(CMD 被覆盖!)

# ENTRYPOINT:容器入口,不会被覆盖,docker run 后的参数会作为它的参数
FROM alpine
ENTRYPOINT ["echo"]
# docker run myimage           → 报错(echo 没参数)
# docker run myimage hello     → 输出 hello(hello 作为 echo 的参数)

# 经典组合:ENTRYPOINT 定主程序,CMD 给默认参数
FROM alpine
ENTRYPOINT ["ping"]
CMD ["localhost"]
# docker run myimage             → ping localhost
# docker run myimage 8.8.8.8     → ping 8.8.8.8(CMD 被覆盖,主程序不变)

# shell 形式 vs exec 形式
CMD echo hi              # shell 形式,会通过 /bin/sh -c 执行
CMD ["echo", "hi"]       # exec 形式,推荐(能收到 SIGTERM 信号)

记住经典组合:ENTRYPOINT 定主程序,CMD 给默认参数。这样既能开箱即用,又能灵活传参。还要注意 exec 形式优于 shell 形式——shell 形式会让命令成为 /bin/sh 的子进程,收不到 SIGTERM 信号,stop 时只能被 SIGKILL,无法优雅退出。

WORKDIR、ENV、EXPOSE、USER

# 环境变量与构建参数

# ENV:运行时环境变量(容器内进程能读到)
ENV NODE_ENV=production
ENV LOG_LEVEL=info
ENV PATH="/app/node_modules/.bin:$PATH"

# ARG:构建时变量(只在构建期间可见,运行时不存在)
ARG NODE_VERSION=20
FROM node:$NODE_VERSION-alpine
ARG BUILD_NUMBER
RUN echo "Build $BUILD_NUMBER" > /app/version.txt

# 多个 ENV 写一起(更省层)
ENV NODE_ENV=production \
    LOG_LEVEL=info \
    PORT=3000

# 运行时覆盖 ENV
docker run -e NODE_ENV=staging myapp
# 其他常用指令

# WORKDIR:设置后续指令的工作目录(等于 cd,推荐用绝对路径)
WORKDIR /app
RUN pwd                            # 输出 /app

# EXPOSE:声明端口(仅文档/提示作用,不真正映射)
EXPOSE 3000
EXPOSE 80/tcp 443/tcp
# 真正对外访问还要 docker run -p 3000:3000

# VOLUME:声明匿名卷挂载点(运行时若不 -v,Docker 自动分配)
VOLUME /app/data
VOLUME ["/app/data", "/app/logs"]

# HEALTHCHECK:让 Docker 自动检测容器健康
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
# 不健康会显示在 docker ps 的 STATUS 里:Up 5 minutes (unhealthy)

几个要点:

.dockerignore:必配文件

# .dockerignore:排除无关文件(类似 .gitignore)

# 放在构建上下文目录,与 Dockerfile 同级
# 内容示例:
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose*.yml
.env
.env.local
dist
build
*.md
.vscode
.idea

# 好处:
# 1. 加速构建(少传无关文件给守护进程)
# 2. 避免意外把密钥、缓存打进镜像
# 3. 让 COPY . . 的语义更可控

# 没有这个文件,COPY . . 会把整个目录(包括 node_modules!)都打进镜像

没配 .dockerignore 就 COPY . . 是新手常犯的大错——node_modules.git、构建产物全被打进镜像,既慢又臃肿,还可能把 .env 这种含密钥的文件泄露进镜像。每个项目都该有个 .dockerignore。

构建缓存原理(让构建飞起来)

Docker 构建镜像时,会从上到下逐条执行 Dockerfile,每条指令产出一层。关键机制是:

所以指令顺序直接决定构建速度。不变的放前面,常变的放后面。最典型的反例:

这条规则理解了,你写的 Dockerfile 就比 80% 的人专业。下一个层次是多阶段构建——见最佳实践章。

← 上一篇 Docker 容器

下一篇 Docker 数据卷

✈️💬