03 - Dockerfile 编写与镜像构建
Dockerfile 是什么?
Dockerfile 是一个文本文件,包含一系列指令,Docker 根据这些指令自动构建镜像。 可以理解为"镜像的菜谱"——告诉 Docker 一步步如何做出一个镜像。
Dockerfile 指令速查
| 指令 | 用途 | 示例 |
|---|---|---|
FROM | 指定基础镜像(必须是第一条指令) | FROM python:3.11-slim |
WORKDIR | 设置工作目录 | WORKDIR /app |
COPY | 复制文件到镜像 | COPY . /app |
ADD | 复制文件(支持 URL 和自动解压) | ADD app.tar.gz /app |
RUN | 构建时执行命令 | RUN pip install -r requirements.txt |
CMD | 容器启动时的默认命令 | CMD ["python", "app.py"] |
ENTRYPOINT | 容器启动时的入口命令 | ENTRYPOINT ["python"] |
ENV | 设置环境变量 | ENV APP_ENV=production |
EXPOSE | 声明容器监听的端口(文档用途) | EXPOSE 8080 |
VOLUME | 声明数据卷挂载点 | VOLUME ["/data"] |
ARG | 构建时的参数 | ARG VERSION=1.0 |
LABEL | 元数据标签 | LABEL maintainer="darren" |
USER | 指定运行用户 | USER appuser |
HEALTHCHECK | 健康检查 | HEALTHCHECK CMD curl -f http://localhost/ |
CMD vs ENTRYPOINT
这是初学者最容易混淆的两个指令:
CMD —— 默认命令,可被覆盖
dockerfile
FROM ubuntu
CMD ["echo", "Hello World"]bash
docker run myimage # 输出: Hello World
docker run myimage echo Hi # 输出: Hi(CMD 被覆盖)ENTRYPOINT —— 入口命令,不易被覆盖
dockerfile
FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello World"]bash
docker run myimage # 输出: Hello World(ENTRYPOINT + CMD)
docker run myimage Hi # 输出: Hi(ENTRYPOINT + 覆盖的 CMD)最佳实践
- ENTRYPOINT: 定义容器的"身份"(它是什么)
- CMD: 定义默认参数(可以被用户覆盖)
dockerfile
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run myimage → python app.py
# docker run myimage test.py → python test.py构建上下文与 .dockerignore
构建上下文
执行 docker build 时,Docker 会将指定目录的所有文件发送给 Docker Daemon:
bash
docker build -t myapp .
# ^ 这个点就是构建上下文(当前目录).dockerignore
和 .gitignore 类似,告诉 Docker 哪些文件不需要发送到构建上下文:
# .dockerignore
.git
node_modules
__pycache__
*.pyc
.env
.venv
README.md
docker-compose*.yml为什么重要:如果项目有大量不相关的文件(比如 node_modules),不使用 .dockerignore 会导致构建上下文传输缓慢。
镜像分层原理(深入)
Dockerfile 中的每条指令都会创建一个新的镜像层:
dockerfile
FROM python:3.11-slim # Layer 1: 基础镜像(约 120MB)
WORKDIR /app # Layer 2: 设置工作目录(几乎 0)
COPY requirements.txt . # Layer 3: 复制依赖文件(几 KB)
RUN pip install -r requirements.txt # Layer 4: 安装依赖(可能几十 MB)
COPY . . # Layer 5: 复制应用代码
CMD ["python", "app.py"] # Layer 6: 元数据(0 size)缓存机制
Docker 构建时会利用缓存加速:
- 如果某一层的指令和上下文没有变化,直接使用缓存
- 一旦某一层缓存失效,它之后的所有层都会重新构建
所以:把变化频率低的指令放前面,变化频率高的放后面。
dockerfile
# ✗ 不好:每次代码改变都要重新安装依赖
COPY . .
RUN pip install -r requirements.txt
# ✓ 好:只有 requirements.txt 变了才重新安装依赖
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .实战示例
示例 1:Python Flask 应用
dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]示例 2:Node.js 应用
dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]示例 3:Go 应用(多阶段构建预告)
dockerfile
# 构建阶段
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# 运行阶段
FROM alpine:3.19
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]多阶段构建的详细讲解在进阶章节。
构建命令
bash
# 基本构建
docker build -t myapp .
# 指定 tag
docker build -t myapp:v1.0 .
# 指定 Dockerfile 路径
docker build -f Dockerfile.prod -t myapp:prod .
# 不使用缓存构建
docker build --no-cache -t myapp .
# 传递构建参数
docker build --build-arg VERSION=2.0 -t myapp .
# 查看构建的镜像
docker images myappDockerfile 最佳实践
1. 使用精简基础镜像
dockerfile
# ✗ 完整镜像,约 900MB
FROM python:3.11
# ✓ slim 镜像,约 120MB
FROM python:3.11-slim
# ✓✓ alpine 镜像,约 50MB(但可能有兼容性问题)
FROM python:3.11-alpine2. 合并 RUN 指令减少层数
dockerfile
# ✗ 三层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✓ 一层
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*3. 使用非 root 用户
dockerfile
RUN adduser --disabled-password --gecos '' appuser
USER appuser4. 添加 HEALTHCHECK
dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 15. 利用 .dockerignore
始终创建 .dockerignore 文件排除不必要的文件。
实操练习 Lab 03:构建你的第一个镜像
详见 Lab 03 - 构建你的第一个 Docker 镜像。
