Dockerfile是镜像的"配方"

前几章我们用现成的镜像,今天学习如何编写Dockerfile,创建自己的镜像。

什么是Dockerfile?

Dockerfile是一个文本文件,包含一系列指令,用于构建Docker镜像。

类比

  • Dockerfile = 烹饪食谱
  • 镜像 = 做好的菜
  • 容器 = 端上桌的菜

Dockerfile基础语法

基本结构

# 注释以#开头
FROM ubuntu:20.04
RUN apt-get update
CMD ["echo", "Hello, Docker!"]

指令执行顺序

Docker会按照从上到下的顺序执行指令,每执行一条指令就会创建一个新的镜像层

常用指令详解

1. FROM - 基础镜像

# 基本用法
FROM python:3.11
FROM ubuntu:20.04

# 多阶段构建的第一阶段
FROM python:3.11 AS builder

选择基础镜像的原则:

  1. 官方镜像优先pythonnodenginx
  2. 版本明确python:3.11.7python:3.11 更明确
  3. 体积要小:使用alpine或slim版本
  4. 安全可靠:优先使用经过安全审计的镜像

常用基础镜像:

  • alpine:最轻量(5MB)
  • slim:比完整版小很多
  • latest:最新版(生产环境不推荐)

2. LABEL - 镜像元数据

LABEL maintainer="yourname@example.com"
LABEL version="1.0"
LABEL description="My awesome application"

查看镜像标签:

docker inspect myapp | grep Labels

3. RUN - 执行命令

# Shell形式(默认)
RUN apt-get update && apt-get install -y curl

# Exec形式(推荐)
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "curl"]

# 组合多个命令(减少层数)
RUN apt-get update && \
    apt-get install -y \
    curl \
    wget \
    vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

优化建议:

  • 合并RUN指令(减少层数)
  • 清理缓存(减小镜像体积)
  • 使用 --no-cache-dir(Python)
  • 使用 && 连接命令

4. COPY - 复制文件

# 复制文件
COPY requirements.txt /app/

# 复制目录
COPY . /app/

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

COPY vs ADD的区别:

  • COPY:只能复制本地文件
  • ADD:可以复制本地文件、URL、自动解压tar文件
  • 推荐优先使用COPY,更明确和可控

5. ADD - 高级复制

# 复制本地文件
ADD app.py /app/

# 下载文件
ADD https://example.com/file.tar.gz /app/

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

何时使用ADD:

  • 需要下载远程文件
  • 需要自动解压压缩包
  • 否则使用COPY

6. WORKDIR - 工作目录

# 设置工作目录
WORKDIR /app

# 相当于
RUN cd /app

优势:

  • 后续指令的路径都相对于工作目录
  • 自动创建不存在的目录
  • 更清晰和易读
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

7. ENV - 环境变量

# 设置单个环境变量
ENV PYTHONUNBUFFERED=1

# 设置多个
ENV TZ=Asia/Shanghai \
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8

# 构建时使用
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y tzdata

环境变量的用途:

  • 配置应用参数
  • 控制依赖安装行为
  • 传递运行时配置

8. EXPOSE - 暴露端口

# 暴露端口
EXPOSE 80
EXPOSE 443
EXPOSE 8000

# 一次暴露多个
EXPOSE 80 443 8000

注意:

  • EXPOSE 只是声明,不会真正开放端口
  • 真正开放端口需要 docker run -p
  • 作用:文档说明、Docker自动映射(-P)

9. CMD - 启动命令

# Exec形式(推荐)
CMD ["python", "app.py"]
CMD ["nginx", "-g", "daemon off;"]

# Shell形式
CMD python app.py

# 作为ENTRYPOINT的参数
CMD ["--config", "nginx.conf"]

CMD vs ENTRYPOINT:

  • CMD:可以被 docker run 的参数覆盖
  • ENTRYPOINT:不能被覆盖,只能通过 --entrypoint 覆盖

10. ENTRYPOINT - 入口点

# Exec形式
ENTRYPOINT ["python", "app.py"]

# Shell形式
ENTRYPOINT python app.py

ENTRYPOINT + CMD组合:

ENTRYPOINT ["python"]
CMD ["app.py"]

运行时:

docker run myimage                    # 执行: python app.py
docker run myimage test.py            # 执行: python test.py

11. VOLUME - 数据卷

# 声明数据卷
VOLUME /data
VOLUME ["/data", "/logs"]

# 使用匿名卷
VOLUME /var/lib/mysql

作用:

  • 声明容器需要持久化的数据目录
  • 提示用户挂载数据卷
  • 防止容器删除时数据丢失

12. USER - 运行用户

# 切换用户
USER nginx
USER 1000:1000

# 创建用户并切换
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

安全最佳实践:

  • 不要以root用户运行容器
  • 创建专用用户运行应用

13. ARG - 构建参数

# 定义构建参数
ARG VERSION=1.0
ARG BUILD_DATE

# 使用构建参数
LABEL version=${VERSION}
LABEL build_date=${BUILD_DATE}

构建时传递参数:

docker build --build-arg VERSION=2.0 --build-arg BUILD_DATE=$(date) -t myapp .

ARG vs ENV:

  • ARG:构建时使用,不会保留到镜像
  • ENV:运行时使用,保留到镜像

多阶段构建

为什么需要多阶段构建?

问题:构建镜像包含很多构建工具(gcc、make等),最终镜像体积很大。

解决:多阶段构建,分离构建环境和运行环境。

基本语法

# 构建阶段
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# 运行阶段
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

实战示例:Go应用

# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app

# 运行阶段
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]

优势:

  • 构建阶段:包含所有构建工具(大)
  • 运行阶段:只包含可执行文件(小)
  • 最终镜像体积从1GB减少到10MB!

实战:构建Python Web应用

项目结构

my-flask-app/
├── app.py
├── requirements.txt
└── Dockerfile

app.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello():
    return jsonify({
        "message": "Hello, Docker!",
        "version": "1.0"
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

requirements.txt

flask==2.3.0
gunicorn==21.2.0

Dockerfile

# 多阶段构建
# 构建阶段
FROM python:3.11 AS builder

WORKDIR /app

# 只复制依赖文件(利用缓存)
COPY requirements.txt .

# 安装依赖到用户目录
RUN pip install --user --no-cache-dir -r requirements.txt

# 运行阶段
FROM python:3.11-slim

WORKDIR /app

# 复制依赖
COPY --from=builder /root/.local /root/.local

# 复制应用代码
COPY . .

# 设置环境变量
ENV PATH=/root/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Shanghai

# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]

构建和运行

# 构建镜像
docker build -t my-flask-app:latest .

# 运行容器
docker run -d -p 8000:8000 --name myflask my-flask-app

# 测试
curl http://localhost:8000
# 输出:{"message":"Hello, Docker!","version":"1.0"}

# 查看日志
docker logs -f myflask

Dockerfile最佳实践

1. 利用构建缓存

# 不好的做法(经常变化的文件放前面)
COPY . .
RUN pip install -r requirements.txt

# 好的做法(不常变化的文件放前面)
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

原理:如果Dockerfile的指令和文件没有变化,Docker会复用缓存。

2. 最小化层数

# 不好(多层)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# 好(一层)
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean

3. 使用.dockerignore

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

.git
.gitignore
README.md
__pycache__
*.pyc
*.pyo
*.pyd
venv
.venv
.env
tests/
docs/

作用

  • 减少构建上下文大小
  • 提高构建速度
  • 避免敏感信息进入镜像

4. 优化层顺序

不常变化的放前面,常变化的放后面:

# 不常变化的
FROM python:3.11-slim
WORKDIR /app

# 基本不变化
COPY requirements.txt .
RUN pip install -r requirements.txt

# 经常变化
COPY . .

5. 使用多阶段构建

减小最终镜像体积:

FROM python:3.11 AS builder
# ... 安装依赖 ...

FROM python:3.11-slim
COPY --from=builder ...

6. 不要在镜像中存储密钥

# 不好(密钥进入镜像历史)
COPY .env .

# 好(运行时注入)
COPY .env.example .
# 运行时用 -e 参数或 --env-file

7. 使用特定的镜像标签

# 不好(可能更新到不兼容的版本)
FROM python:3

# 好(明确版本)
FROM python:3.11.7

8. 清理临时文件

# 清理apt缓存
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 清理pip缓存
RUN pip install --no-cache-dir -r requirements.txt

本章小结

  • Dockerfile指令:FROM、RUN、COPY、ADD、WORKDIR、CMD等
  • 最佳实践:利用缓存、最小化层数、多阶段构建
  • 安全考虑:不存储密钥、使用特定版本、不以root运行
  • 性能优化:使用.dockerignore、优化层顺序、清理缓存

现在已经会写Dockerfile了,下一章我们来学习数据持久化!

继续学下去,马上就能做实用项目了!