跳到主要内容

Dockerfile 编写

Dockerfile 是一个文本文件,包含构建 Docker 镜像的所有指令。本章将详细介绍 Dockerfile 的语法和最佳实践。

Dockerfile 基础

什么是 Dockerfile?

Dockerfile 是一个用于构建镜像的脚本文件,每条指令都会在镜像中创建一个新的层。Docker 读取 Dockerfile 中的指令,按顺序执行构建过程。

基本结构

# 注释
INSTRUCTION arguments
  • 指令不区分大小写,但约定使用大写
  • 指令按顺序执行
  • Dockerfile 必须以 FROM 指令开始

构建镜像

# 基本构建
docker build -t my-image:v1 .

# 指定 Dockerfile 路径
docker build -t my-image:v1 -f Dockerfile.dev .

# 构建时传递参数
docker build -t my-image:v1 --build-arg VERSION=1.0 .

# 不使用缓存构建
docker build -t my-image:v1 --no-cache .

基础指令

FROM - 基础镜像

指定构建镜像的基础镜像,必须是 Dockerfile 的第一条指令:

# 使用最新版本
FROM ubuntu

# 指定版本
FROM ubuntu:22.04

# 使用特定平台
FROM --platform=linux/arm64 ubuntu:22.04

# 多阶段构建命名
FROM node:18 AS builder

LABEL - 元数据标签

添加镜像的元数据信息:

LABEL maintainer="[email protected]"
LABEL version="1.0.0"
LABEL description="这是一个示例镜像"

# 合并多个标签
LABEL maintainer="[email protected]" \
version="1.0.0" \
description="示例镜像"

ARG - 构建参数

定义构建时的变量,只在构建过程中有效:

# 定义构建参数
ARG VERSION=1.0
ARG NODE_VERSION=18

# 使用参数
FROM node:${NODE_VERSION}

# 构建时覆盖
# docker build --build-arg VERSION=2.0 -t my-image .

ENV - 环境变量

设置环境变量,在容器运行时仍然有效:

# 设置单个环境变量
ENV APP_HOME=/app

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

# 使用环境变量
WORKDIR ${APP_HOME}

ENV 与 ARG 的区别

特性ARGENV
作用范围构建阶段构建和运行阶段
可被覆盖构建时 --build-arg运行时 -e
可见性不在镜像中在镜像中

工作目录和用户

WORKDIR - 工作目录

设置工作目录,类似于 cd 命令:

# 设置工作目录
WORKDIR /app

# 如果目录不存在,会自动创建
WORKDIR /app/src

# 可以使用环境变量
ENV APP_HOME=/app
WORKDIR ${APP_HOME}

# 相对路径(相对于上一个 WORKDIR)
WORKDIR subdirectory

USER - 运行用户

指定运行容器时的用户:

# 创建用户
RUN useradd -m -s /bin/bash appuser

# 切换用户
USER appuser

# 切换用户和组
USER appuser:appgroup

# 使用 UID
USER 1000

文件操作

COPY - 复制文件

从构建上下文复制文件到镜像:

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

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

# 复制多个文件
COPY package.json package-lock.json /app/

# 使用通配符
COPY *.json /app/

# 保持文件属性
COPY --chown=appuser:appgroup app/ /app/

# 从其他阶段复制(多阶段构建)
COPY --from=builder /app/dist /app/dist

ADD - 添加文件

功能更强大的文件添加指令:

# 添加本地文件
ADD app.tar.gz /app/

# 添加远程文件(自动下载)
ADD https://example.com/file.txt /app/

# 自动解压 tar 文件
ADD archive.tar.gz /app/

COPY vs ADD

特性COPYADD
复制本地文件支持支持
下载远程文件不支持支持
自动解压不支持支持
推荐使用特定场景
最佳实践

优先使用 COPY,ADD 仅在需要自动解压或下载远程文件时使用。

执行命令

RUN - 运行命令

在构建过程中执行命令:

# shell 格式
RUN apt-get update

# exec 格式(推荐)
RUN ["apt-get", "update"]

# 多行命令使用 && 连接
RUN apt-get update && apt-get install -y \
curl \
vim \
&& rm -rf /var/lib/apt/lists/*

# 使用 heredoc(Dockerfile 1.4+)
RUN <<EOF
apt-get update
apt-get install -y curl vim
rm -rf /var/lib/apt/lists/*
EOF

最佳实践

  1. 合并多个 RUN 命令,减少镜像层数
  2. 在同一层清理缓存文件
  3. 使用 apt-get install -y --no-install-recommends 减少安装包
# 好的做法
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*

# 不好的做法(产生两个层)
RUN apt-get update
RUN apt-get install -y curl

CMD - 容器启动命令

指定容器启动时默认执行的命令:

# exec 格式(推荐)
CMD ["nginx", "-g", "daemon off;"]

# shell 格式
CMD nginx -g "daemon off;"

# 作为 ENTRYPOINT 的默认参数
CMD ["--port", "3000"]

注意

  • 一个 Dockerfile 中只能有一个 CMD
  • 如果运行容器时指定了命令,CMD 会被覆盖
  • exec 格式会被解析为 JSON 数组,必须使用双引号

ENTRYPOINT - 入口点

配置容器为可执行程序:

# exec 格式
ENTRYPOINT ["docker-entrypoint.sh"]

# 结合 CMD 使用
ENTRYPOINT ["node"]
CMD ["app.js"]

# 运行时可以覆盖 CMD
# docker run my-image server.js

ENTRYPOINT vs CMD

特性ENTRYPOINTCMD
是否可被覆盖需要 --entrypoint直接覆盖
用途固定命令默认参数
组合使用主命令默认参数
# 组合使用示例
ENTRYPOINT ["node"]
CMD ["app.js"]

# 运行 node app.js
docker run my-image

# 运行 node server.js
docker run my-image server.js

端口和卷

EXPOSE - 声明端口

声明容器运行时监听的端口:

# 声明单个端口
EXPOSE 80

# 声明多个端口
EXPOSE 80 443

# 声明 UDP 端口
EXPOSE 53/udp

注意

  • EXPOSE 只是声明,不会实际发布端口
  • 实际发布端口需要使用 -p-P 选项
  • 主要用于文档说明和自动端口映射

VOLUME - 声明数据卷

声明容器应该使用的持久化存储:

# 声明单个卷
VOLUME /data

# 声明多个卷
VOLUME ["/data", "/logs"]

使用场景

  • 数据库数据目录
  • 日志目录
  • 用户上传文件目录
# MySQL 示例
VOLUME /var/lib/mysql

# Redis 示例
VOLUME /data

健康检查

HEALTHCHECK - 健康检查

配置容器的健康检查命令:

# 基本语法
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1

# 参数说明
# --interval: 检查间隔(默认 30s)
# --timeout: 超时时间(默认 30s)
# --start-period: 启动等待时间(默认 0s)
# --retries: 重试次数(默认 3)

健康状态

  • starting:启动中
  • healthy:健康
  • unhealthy:不健康
# Web 应用健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1

# 数据库健康检查
HEALTHCHECK --interval=10s --timeout=3s \
CMD pg_isready -U postgres || exit 1

多阶段构建

多阶段构建可以显著减小最终镜像大小:

# 构建阶段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 运行阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

优点

  • 最终镜像不包含构建工具
  • 减小镜像体积
  • 提高安全性
# Go 应用示例
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]

实战示例

Node.js 应用

# 使用官方 Node.js 镜像
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

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

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 生产阶段
FROM node:18-alpine

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

WORKDIR /app

# 复制构建产物
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./

# 切换用户
USER nextjs

# 环境变量
ENV NODE_ENV=production

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

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

Python 应用

FROM python:3.11-slim

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*

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

# 复制应用代码
COPY . .

# 创建非 root 用户
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Java 应用

# 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests

# 运行阶段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

# 非 root 用户
RUN addgroup -S java && adduser -S javauser -G java
USER javauser

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

最佳实践

1. 使用 .dockerignore

创建 .dockerignore 文件排除不需要的文件:

.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.env
*.md
tests/

2. 减少镜像层数

# 不好:多层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# 好:合并为一层
RUN apt-get update && apt-get install -y \
curl \
vim \
&& rm -rf /var/lib/apt/lists/*

3. 合理使用缓存

将不常变化的指令放在前面:

# 先复制依赖文件
COPY package*.json ./
RUN npm install

# 再复制源代码(经常变化)
COPY . .

4. 使用特定版本标签

# 不好:使用 latest
FROM node:latest

# 好:指定版本
FROM node:18.19.0-alpine

5. 最小化镜像大小

# 使用 Alpine 镜像
FROM node:18-alpine

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

# 使用多阶段构建
# ...(见上文示例)

小结

本章我们学习了:

  1. Dockerfile 的基本结构和语法
  2. 常用指令:FROM、RUN、CMD、ENTRYPOINT、COPY、ADD
  3. 环境配置:ENV、ARG、WORKDIR、USER
  4. 端口和卷:EXPOSE、VOLUME
  5. 健康检查:HEALTHCHECK
  6. 多阶段构建优化镜像大小
  7. 实战示例和最佳实践

练习

  1. 为一个简单的 Node.js 应用编写 Dockerfile
  2. 使用多阶段构建优化镜像大小
  3. 添加健康检查指令
  4. 配置非 root 用户运行容器
  5. 对比优化前后的镜像大小差异