Files
cs-note/gyh/docker/Dockerfile编写指南.md
2026-05-24 11:42:38 +08:00

11 KiB
Raw Permalink Blame History

tags, create time
tags create time
docker
devops
containerization
2026-04-28 14:30

Dockerfile 编写指南

一份从"能跑"到"跑得好"的 Dockerfile 教程。通过教学式讲解、流程图和实战示例,帮你写出体积小、构建快、启动安全的镜像。

概述

Dockerfile 是镜像构建的蓝图,每一行指令都会在镜像中生成一层。写好 Dockerfile 的核心目标:镜像更小、构建更快、启动更安全。

[!tip] 初学者思考题 为什么 RUN apt-get install nginx && rm -rf /var/lib/apt/lists/* 不能拆成两个 RUN?拆开后镜像体积会不会变小?(答案见正文 2.2 节)

正文

一、Dockerfile 基本结构

一张图理解 Dockerfile 的构建流程:

flowchart LR
    A["FROM 基础镜像"] --> B["WORKDIR 设工作目录"]
    B --> C["COPY 依赖文件 + RUN 安装"]
    C --> D["COPY 源代码"]
    D --> E["RUN 构建产物"]
    E --> F["EXPOSE 声明端口"]
    F --> G["CMD 启动命令"]
    style A fill:#e1f5fe
    style G fill:#c8e6c9

[!note] 构建流程 构建过程是自上而下逐行执行的,每一步都基于上一步的结果生成新层。层序排列直接影响缓存命中率和镜像体积。

一个标准的 Node.js 项目 Dockerfile:

# 1. 基础镜像
FROM node:20-slim AS builder

# 2. 维护者信息(可选)
LABEL maintainer="yourname@example.com"

# 3. 设置工作目录
WORKDIR /app

# 4. 先复制依赖文件,利用 Docker 缓存
COPY package*.json ./
RUN npm ci --production

# 5. 再复制源代码
COPY . .

# 6. 构建
RUN npm run build

# 7. 暴露端口
EXPOSE 3000

# 8. 启动命令
CMD ["node", "dist/index.js"]

二、常用指令详解

2.1 FROM — 基础镜像

[!question] 为什么镜像大小差距这么大? 完整 OS + 工具链 vs 精简版 = 1GB vs 10MB,这就是 FROM 选择的影响。

选镜像原则:有 Alpine 选 Alpine,有 Slim 选 Slim,没有再选完整版。

FROM node:20-alpine          # 推荐:最小体积 ~150MB
FROM node:20-slim            # 推荐:平衡方案 ~200MB
FROM node:20                 # 不推荐:完整 OS,镜像臃肿 ~1GB
FROM python:3.12-slim-bookworm
FROM golang:1.22-alpine
FROM ubuntu:24.04            # 一般作为多阶段构建的第一步
镜像类型 大小 适用场景
alpine 5-10MB 生产环境、追求极致体积
slim 100-200MB 大多数生产场景,兼容性好
完整版 1GB+ 开发环境、需要完整工具链

2.2 RUN — 执行命令

[!warning] 关键概念:Docker 层不可删 RUN 产生的层会永久留在镜像中,即使后续 rm 了也没用。所以更新、安装、清理必须在同一个 RUN 里。

# ❌ 不推荐:每行一个 RUN,产生多层,rm 无效
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*

# ✅ 推荐:合并为一行,用 && 连接,用 \ 换行
RUN apt-get update && \
    apt-get install -y nginx && \
    rm -rf /var/lib/apt/lists/*

2.3 COPY vs ADD

[!abstract] 简单原则 99% 的情况用 COPY。 ADD 的额外功能(自动解压、远程下载)行为不可预测。

# COPY — 简单复制,优先使用
COPY package.json ./
COPY . .

# ADD — 仅在两种场景有用
ADD https://example.com/file.tar.gz /tmp/  # 下载远程文件(但容易出错)
ADD file.tar.gz /app/                        # 自动解压(COPY + RUN tar 更清晰)

2.4 WORKDIR — 工作目录

WORKDIR /app                        # 设置工作目录
COPY package.json ./                # 相对于 /app
RUN npm install
COPY . .

# WORKDIR 会被后续指令继承,且会自动创建
WORKDIR /subdir                     # 现在是 /app/subdir
RUN pwd                             # 输出 /app/subdir

2.5 EXPOSE — 声明端口

[!info] EXPOSE 不自动映射端口 EXPOSE 只是文档性质的声明,实际端口映射需要在 docker run -p 或 docker-compose 中指定。

EXPOSE 3000
EXPOSE 80 443

2.6 ENV — 环境变量

ENV NODE_ENV=production
ENV APP_PORT=3000

# 容器启动时可以覆盖:docker run -e DB_HOST=192.168.1.100 my-app

2.7 CMD vs ENTRYPOINT

指令 特点 可被 docker run 覆盖?
CMD 默认命令,最灵活 ✅ 完全覆盖
ENTRYPOINT 入口点,固定执行 ⚠️ 需用 --entrypoint 强制覆盖
# CMD — 大多数情况用这个就够了
CMD ["node", "dist/index.js"]

# ENTRYPOINT + CMD — 入口固定,参数可配
ENTRYPOINT ["node"]
CMD ["dist/index.js"]

三、多阶段构建(核心优化手段)

[!abstract] 一句话理解多阶段构建 先用大镜像构建 → 再把产物复制到最小镜像中运行。 构建工具不留到最终镜像。

构建流程图:

flowchart TB
    subgraph Phase1["第一阶段:构建(大镜像)"]
        A["node:20-alpine"] --> B["安装全套依赖 + devDependencies"]
        B --> C["TypeScript / Maven / Go 编译"]
        C --> D["生成 dist/ / target/*.jar"]
    end

    subgraph Phase2["第二阶段:运行(最小镜像)"]
        E["node:20-alpine / JRE Alpine / Alpine"] --> F["只装 production 依赖"]
        F --> G["COPY --from=builder 复制产物"]
        G --> H["启动应用"]
    end

    Phase1 -->|COPY --from=builder| Phase2

    style Phase1 fill:#fff3e0
    style Phase2 fill:#e8f5e9

3.1 前端多阶段构建(Node + TypeScript)

# ========== 第一阶段:构建 ==========
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci                     # ① 先装依赖,利用缓存
COPY . .                       # ② 再复制源码,代码改了才重建
RUN npm run build              # ③ 编译

# ========== 第二阶段:运行 ==========
FROM node:20-alpine AS production
RUN apk add --no-cache tini    # tini 处理僵尸进程(推荐)
WORKDIR /app
COPY package*.json ./
RUN npm ci --production        # 只装 production 依赖
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
ENV NODE_ENV=production
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]

3.2 Java 多阶段构建

# 第一阶段:Maven 构建
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests

# 第二阶段:运行(JRE 镜像,无 Maven)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

3.3 Go 多阶段构建

# 第一阶段:编译
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server

# 第二阶段:运行(纯 Alpine,仅几 MB)
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /app/server
EXPOSE 8080
CMD ["/app/server"]

四、.dockerignore 文件

[!danger] 和 .gitignore 一样重要! 不写 .dockerignore 会把不必要的文件塞进镜像,增大体积、暴露密钥。

# 依赖
node_modules
npm-debug.log

# Git
.git
.gitignore

# 构建产物(别把 build 拷进去,让容器自己 build)
dist
build
*.local

# 编辑器和文档
.vscode
.idea
*.swp
README.md
LICENSE
tests

# Docker 自身文件
docker-compose.yml
Dockerfile
.dockerignore

# 环境变量(千万不要拷进去!)
.env
.env.local
.env.*.local

五、最佳实践清单

✅ 应该做

实践 说明
使用具体版本号 FROM node:20.11-alpine,不要用 latest
用 alpine 或 slim 减小镜像体积
合并 RUN 指令 减少镜像层数
先复制 package.json 再 npm install 利用 Docker 层缓存
用 .dockerignore 排除不必要的文件
用多阶段构建 分离构建和运行环境
非 root 用户运行 安全考虑
用 CMD ["exec格式"] 避免 shell 包装问题

❌ 不应该做

实践 问题
用 latest 标签 构建结果不可复现
在 Dockerfile 中存密码/密钥 暴露敏感信息
用 ADD 替代 COPY 不可预测行为
安装不必要的应用 镜像臃肿
用 RUN apt-get install && apt-get remove 删除的文件仍在镜像层中
以 root 用户运行应用 安全风险

六、安全加固

6.1 使用非 root 用户

FROM node:20-alpine
WORKDIR /app

# 利用 --chown 设置文件所有者,避免切换用户后权限问题
COPY --chown=node:node package*.json ./
RUN npm ci
COPY --chown=node:node . .

# 切换到非 root 用户
USER node

EXPOSE 3000
CMD ["node", "dist/index.js"]

6.2 不要安装 build 依赖到生产镜像

# ❌ 错误:devDependencies 也被装进镜像
RUN npm install

# ✅ 正确:多阶段构建
FROM node:20-alpine AS builder
RUN npm install            # 包含 devDependencies
RUN npm run build

FROM node:20-alpine
RUN npm ci --production    # 只装 production 依赖
COPY --from=builder /app/dist ./dist

6.3 密钥管理原则

# ❌ 绝对不要硬编码密码
RUN echo "password=123456" > /app/config.env

# ❌ 不要在构建时传递密钥(会留在镜像层中)
# docker build --build-arg DB_PASSWORD=secret .

# ✅ 运行时传入环境变量
# docker run -e DB_PASSWORD=secret my-app

# ✅ 生产环境用 Kubernetes Secrets / AWS Secrets Manager

6.4 扫描镜像漏洞

# 使用 trivy 扫描镜像
trivy image my-app:v1.0

# 只输出高危及以上
trivy image --severity HIGH,CRITICAL my-app:v1.0

# 直接扫描 Dockerfile(不用构建镜像)
trivy config Dockerfile

七、构建缓存优化

[!tip] 缓存命中规则 Docker 从上到下逐层检查缓存。任何一行变更,后续所有层全部失效重构建。 所以要把经常变的内容放下面。

# ❌ 错误:代码改动 → npm install 也重新执行
COPY . .
RUN npm install

# ✅ 正确:先复制不变的依赖文件
COPY package*.json ./
RUN npm install
COPY . .

构建缓存策略流程图:

flowchart LR
    A["package.json 未变"] -->|"缓存命中"| B["跳过 npm install"]
    C["package.json 变更"] -->|"缓存失效"| D["重新 npm install"]
    E["源码变更"] --> B
    B --> F["最终镜像"]
    D --> F

    style A fill:#c8e6c9
    style C fill:#fff3e0
    style E fill:#fff3e0

八、常见问题排查

8.1 镜像太大

# 分析镜像各层大小
docker history my-app:latest

# 解决:alpine 基础镜像 + 多阶段构建 + .dockerignore

8.2 容器启动后立即退出

# 查看日志
docker logs <容器名>

# 常见原因:
# 1. CMD/ENTRYPOINT 写错了
# 2. 主进程不是 PID 1(用 tini 或 dumb-init)
# 3. 应用启动失败(检查日志)

8.3 端口无法访问

# 1. 检查 docker run -p 是否映射
docker run -p 3000:3000 ...

# 2. 容器内应用监听 0.0.0.0(不是 localhost)
# 3. 检查防火墙

关联笔记