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 的区别:
| 特性 | ARG | ENV |
|---|---|---|
| 作用范围 | 构建阶段 | 构建和运行阶段 |
| 可被覆盖 | 构建时 --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:
| 特性 | COPY | ADD |
|---|---|---|
| 复制本地文件 | 支持 | 支持 |
| 下载远程文件 | 不支持 | 支持 |
| 自动解压 | 不支持 | 支持 |
| 推荐使用 | 是 | 特定场景 |
最佳实践
优先使用 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
最佳实践:
- 合并多个 RUN 命令,减少镜像层数
- 在同一层清理缓存文件
- 使用
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:
| 特性 | ENTRYPOINT | CMD |
|---|---|---|
| 是否可被覆盖 | 需要 --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
# 使用多阶段构建
# ...(见上文示例)
小结
本章我们学习了:
- Dockerfile 的基本结构和语法
- 常用指令:FROM、RUN、CMD、ENTRYPOINT、COPY、ADD
- 环境配置:ENV、ARG、WORKDIR、USER
- 端口和卷:EXPOSE、VOLUME
- 健康检查:HEALTHCHECK
- 多阶段构建优化镜像大小
- 实战示例和最佳实践
练习
- 为一个简单的 Node.js 应用编写 Dockerfile
- 使用多阶段构建优化镜像大小
- 添加健康检查指令
- 配置非 root 用户运行容器
- 对比优化前后的镜像大小差异