Zerlaer's Notebook

欢迎来到我的博客|

Dockerfile 最佳实践与优化技巧

Dockerfile 基础概述

什么是 Dockerfile?

Dockerfile 是一个文本文件,包含了一系列用于构建 Docker 镜像的指令。它定义了镜像的基础环境、依赖项、配置和启动命令等。通过 docker build 命令,我们可以根据 Dockerfile 构建出可重复的、一致的镜像。

Dockerfile 的基本结构

# 基础镜像
FROM ubuntu:20.04

# 维护者信息
LABEL maintainer="[email protected]"

# 环境变量
ENV APP_HOME /app
ENV NODE_ENV production

# 工作目录
WORKDIR $APP_HOME

# 复制文件
COPY package*.json ./

# 执行命令
RUN npm install

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 3000

# 启动命令
CMD ["npm", "start"]

核心指令详解

FROM

指定基础镜像,是 Dockerfile 的第一个指令。

# 使用官方镜像
FROM node:14-alpine

# 使用特定版本
FROM ubuntu:20.04

# 使用多阶段构建的基础镜像
FROM golang:1.16 as builder

RUN

执行命令并创建新的镜像层。

# 执行单个命令
RUN apt-get update && apt-get install -y nginx

# 执行多个命令(使用 && 连接,减少镜像层)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        wget \
        git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

COPY vs ADD

COPY:简单地复制文件和目录。

# 复制单个文件
COPY package.json ./

# 复制目录
COPY src/ ./src/

ADD:除了复制功能外,还支持解压缩文件和从 URL 下载文件。

# 复制并解压缩 tar 文件
ADD app.tar.gz /app/

# 从 URL 下载文件(不推荐,建议使用 RUN curl 或 RUN wget)
ADD https://example.com/file.txt /app/

WORKDIR

设置工作目录,后续的命令会在该目录下执行。

# 设置工作目录
WORKDIR /app

# 相对路径(相对于前一个 WORKDIR)
WORKDIR src

ENV

设置环境变量,可在后续的命令中使用。

# 设置单个环境变量
ENV NODE_ENV production

# 设置多个环境变量
ENV APP_HOME=/app \
    PORT=3000 \
    DEBUG=false

EXPOSE

声明容器运行时暴露的端口。

# 暴露单个端口
EXPOSE 80

# 暴露多个端口
EXPOSE 80 443

CMD vs ENTRYPOINT

CMD:设置容器的默认命令,可被 docker run 命令覆盖。

#  exec 格式(推荐)
CMD ["npm", "start"]

# shell 格式
CMD npm start

ENTRYPOINT:设置容器的入口点,不可被 docker run 命令直接覆盖。

# exec 格式(推荐)
ENTRYPOINT ["npm"]
CMD ["start"]

# 这样运行:docker run my-app test
# 实际执行:npm test

VOLUME

创建挂载点,用于持久化数据。

# 创建挂载点
VOLUME ["/app/data"]

# 或者
VOLUME /app/data

USER

指定后续命令的执行用户。

# 创建用户并切换到该用户
RUN useradd -ms /bin/bash appuser
USER appuser

ARG

定义构建参数,可在 docker build 命令中通过 --build-arg 覆盖。

# 定义构建参数
ARG NODE_VERSION=14

# 使用构建参数
FROM node:${NODE_VERSION}-alpine

构建命令:

docker build --build-arg NODE_VERSION=16 -t my-app .

多阶段构建

多阶段构建是一种通过多个 FROM 指令在单个 Dockerfile 中创建多个构建阶段的技术,用于减小最终镜像的大小。

基本示例:Go 应用

# 第一阶段:构建
FROM golang:1.16 as builder

WORKDIR /app

COPY . .

RUN go build -o app .

# 第二阶段:运行
FROM alpine:3.14

WORKDIR /app

# 从构建阶段复制可执行文件
COPY --from=builder /app/app .

# 安装运行时依赖
RUN apk add --no-cache ca-certificates

# 运行应用
CMD ["./app"]

高级示例:Node.js 应用

# 第一阶段:安装依赖
FROM node:14-alpine as deps

WORKDIR /app

COPY package*.json ./

RUN npm install --production

# 第二阶段:构建应用
FROM node:14-alpine as builder

WORKDIR /app

COPY . .
COPY --from=deps /app/node_modules ./node_modules

RUN npm run build

# 第三阶段:运行
FROM node:14-alpine as runner

WORKDIR /app

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
    adduser -S appuser -u 1001

# 复制构建结果和依赖
COPY --from=builder /app/build ./build
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["npm", "start"]

镜像大小优化技巧

1. 使用轻量级基础镜像

# 不推荐:使用完整的 Ubuntu 镜像
FROM ubuntu:20.04

# 推荐:使用轻量级的 Alpine 镜像
FROM alpine:3.14

# 或者使用特定语言的 Alpine 镜像
FROM node:14-alpine
FROM python:3.9-alpine
FROM golang:1.16-alpine

2. 最小化镜像层

# 不推荐:多个 RUN 指令,创建多个镜像层
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get clean

# 推荐:单个 RUN 指令,使用 && 连接命令
RUN apt-get update && \
    apt-get install -y --no-install-recommends nginx && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

3. 清理不需要的文件

# 清理包管理器缓存
RUN apt-get clean && rm -rf /var/lib/apt/lists/*  # Debian/Ubuntu
RUN yum clean all && rm -rf /var/cache/yum/*      # CentOS/RHEL
RUN apk add --no-cache package && rm -rf /var/cache/apk/*  # Alpine

# 清理 npm 缓存
RUN npm install && npm cache clean --force

# 清理源码和构建文件
RUN go build -o app . && rm -rf /go/src

4. 使用多阶段构建

如前面的示例所示,使用多阶段构建可以只包含运行时需要的文件,而不包含构建工具和依赖项。

5. 合理使用 .dockerignore 文件

创建 .dockerignore 文件,排除不需要复制到镜像中的文件:

# 排除版本控制文件
.git
.gitignore

# 排除依赖目录
node_modules
vendor

# 排除构建输出目录
build
dist

# 排除测试文件
test
__tests__
*.test.js

# 排除环境配置文件
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# 排除文档
README.md
LICENSE

安全性最佳实践

1. 使用官方或经过验证的基础镜像

# 推荐:使用官方镜像
FROM node:14-alpine

# 不推荐:使用未知来源的镜像
FROM someuser/node:14

2. 避免以 root 用户运行应用

# 创建非 root 用户
RUN addgroup -g 1001 -S appuser && \
    adduser -S appuser -u 1001

# 切换到非 root 用户
USER appuser

3. 定期更新基础镜像

# 使用特定版本的基础镜像,并定期更新
FROM node:14.17.0-alpine

# 而不是使用 latest 标签
FROM node:latest

4. 避免在镜像中存储敏感信息

# 不推荐:硬编码敏感信息
ENV API_KEY=secret123

# 推荐:使用环境变量或 Docker secrets
ENV API_KEY=${API_KEY}

5. 扫描镜像中的安全漏洞

使用工具如 Trivy、Clair 等扫描镜像中的安全漏洞:

# 使用 Trivy 扫描镜像
docker pull aquasec/trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image my-app

实战:为常见应用编写 Dockerfile

1. Node.js 应用

# 使用多阶段构建
FROM node:14-alpine as deps

WORKDIR /app

COPY package*.json ./

RUN npm install --production

FROM node:14-alpine as builder

WORKDIR /app

COPY . .
COPY --from=deps /app/node_modules ./node_modules

RUN npm run build

FROM node:14-alpine as runner

WORKDIR /app

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
    adduser -S appuser -u 1001

# 复制构建结果和依赖
COPY --from=builder /app/build ./build
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["npm", "start"]

2. Python 应用

FROM python:3.9-alpine

WORKDIR /app

# 创建非 root 用户
RUN addgroup -g 1001 -S appuser && \
    adduser -S appuser -u 1001

# 安装依赖
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 8000

# 启动应用
CMD ["python", "app.py"]

3. Go 应用

FROM golang:1.16-alpine as builder

WORKDIR /app

COPY . .

RUN go mod download && \
    go build -o app .

FROM alpine:3.14

WORKDIR /app

# 创建非 root 用户
RUN addgroup -g 1001 -S appuser && \
    adduser -S appuser -u 1001

# 安装运行时依赖
RUN apk add --no-cache ca-certificates

# 复制可执行文件
COPY --from=builder /app/app ./

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 8080

# 启动应用
CMD ["./app"]

4. Java 应用

FROM maven:3.8.4-openjdk-11-slim as builder

WORKDIR /app

COPY . .

RUN mvn clean package -DskipTests

FROM adoptopenjdk:11-jre-hotspot

WORKDIR /app

# 创建非 root 用户
RUN groupadd -g 1001 appuser && \
    useradd -u 1001 -g appuser appuser

# 复制构建结果
COPY --from=builder /app/target/app.jar ./

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 8080

# 启动应用
CMD ["java", "-jar", "app.jar"]

常见问题与解决方案

1. 构建速度慢

问题docker build 命令执行缓慢。

解决方案

  • 使用 Docker 构建缓存:合理安排指令顺序,将不常变化的指令放在前面
  • 使用多阶段构建:减少中间层的大小
  • 配置镜像加速器:提高基础镜像的拉取速度
  • 优化依赖安装:使用 --no-cache 或类似选项减少缓存

2. 镜像大小过大

问题:构建的镜像体积过大,占用过多存储空间。

解决方案

  • 使用轻量级基础镜像:如 Alpine
  • 最小化镜像层:合并多个 RUN 指令
  • 清理不需要的文件:删除缓存、源码等
  • 使用多阶段构建:只包含运行时需要的文件

3. 容器启动失败

问题:根据 Dockerfile 构建的镜像运行时失败。

解决方案

  • 检查 CMD/ENTRYPOINT 指令:确保命令正确
  • 检查文件权限:确保应用文件有正确的执行权限
  • 检查依赖项:确保所有运行时依赖都已安装
  • 检查端口映射:确保容器端口与主机端口正确映射

4. 安全漏洞

问题:镜像扫描工具发现安全漏洞。

解决方案

  • 更新基础镜像:使用最新版本的基础镜像
  • 更新依赖项:定期更新应用依赖
  • 移除不必要的软件:减少攻击面
  • 遵循安全最佳实践:如非 root 用户运行

最佳实践总结

1. 基础镜像选择

  • 使用官方或经过验证的基础镜像
  • 使用特定版本的镜像,避免使用 latest 标签
  • 优先选择轻量级基础镜像,如 Alpine

2. 指令使用

  • 合理安排指令顺序,利用 Docker 构建缓存
  • 使用 COPY 而非 ADD(除非需要解压缩或下载功能)
  • 使用 exec 格式的 CMD 和 ENTRYPOINT 指令
  • 每个 RUN 指令只做一件事,并清理不需要的文件

3. 镜像优化

  • 使用多阶段构建减少最终镜像大小
  • 合理使用 .dockerignore 文件排除不需要的文件
  • 最小化镜像层,合并多个 RUN 指令
  • 清理包管理器缓存和构建文件

4. 安全性

  • 避免以 root 用户运行应用
  • 不存储敏感信息在镜像中
  • 定期扫描镜像中的安全漏洞
  • 遵循最小权限原则

5. 可维护性

  • 添加注释说明 Dockerfile 的各个部分
  • 使用环境变量管理配置
  • 为镜像添加合理的标签
  • 保持 Dockerfile 简洁明了

总结

编写高效、安全的 Dockerfile 是容器化应用的基础。通过本文的学习,你应该已经掌握了 Dockerfile 的核心指令、多阶段构建、镜像优化和安全最佳实践等知识,并能够为常见的应用类型编写专业的 Dockerfile。

作为一名运维工程师,掌握 Dockerfile 编写技巧不仅可以提高镜像构建效率,还可以确保应用的安全性和可靠性。在实际工作中,我们需要根据具体的应用场景和需求,灵活运用这些最佳实践,不断优化 Dockerfile,才能构建出高质量的容器镜像。

参考资料