Docker 教程

自定义 Docker 镜像

掌握如何手工打造专属的 Docker 镜像,是每一位与 Docker 打交道的技术人员的必修课。这不仅能让你精准定义应用程序所需的环境和依赖,还能确保你的应用无论在开发电脑上、测试服务器里,还是最终的生产环境中,都能表现出绝对的一致性和可移植性。

了解如何编写高效的 Dockerfile、如何给镜像“瘦身”以及如何管理构建层,是实现高效容器化的关键所在。

1. 深入剖析 Dockerfile 基础

Dockerfile 就是一个纯文本的“菜谱”,里面记录了你在命令行上手动组装镜像时需要执行的所有命令。当你下达构建镜像的指令时,Docker 就会照着这本“菜谱”,按顺序一行一行地执行。每执行完一条修改了文件系统的指令,镜像就会增加一个新的“层 (Layer)”。深入理解 Dockerfile 的语法和结构,是创造高效、可重复构建的镜像的基石。

1.1 Dockerfile 核心语法回顾

Dockerfile 由一行行的指令组成。虽然指令不区分大小写,但行业惯例是把指令全部大写,以便让人一眼就能把它们和后面的参数区分开。你可以用 # 号来添加注释。

让我们重温一下那些最常用的“常客”:

  • FROM: 指定你这栋“楼”要建在哪个“地基”上(基础镜像)。这通常是 Docker Hub 上的另一个镜像。它必须是 Dockerfile 中除注释外的第一条有效指令。
  • RUN: 在构建镜像的过程中,在容器内部执行命令。通常用来装软件、改配置等。
  • CMD: 规定容器启动后默认要干什么。一个 Dockerfile 只能有一个 CMD。如果写了多个,只有最后一个管用。你在 docker run 时是可以轻易覆盖这个默认命令的。
  • ENTRYPOINT: 让容器像一个普通的可执行程序那样运行。它和 CMD 类似,但更难被覆盖,通常用来定义容器的主进程。
  • COPY: 把你电脑上的文件或文件夹原封不动地复制到镜像里面。
  • ADD: 算是 COPY 的增强版,它能自动解压压缩包,还能直接从网络链接下载文件。
  • WORKDIR: 设定工作目录,接下来的指令都会在这个目录下执行。
  • EXPOSE: 声明容器运行时打算监听哪些端口(主要是为了文档说明,并不会真的帮你把端口暴露到外网)。
  • ENV: 在容器内设置环境变量。
  • ARG: 定义一些变量,让你在执行 docker build 命令时,可以通过 --build-arg 参数把值传进去。
  • VOLUME: 创建一个数据卷挂载点,用于保存需要持久化的数据。
  • USER: 指定接下来执行命令时使用哪个用户(UID 或用户名)。
  • LABEL: 以键值对的形式给镜像贴上各种标签(比如版本号、作者信息)。
  • STOPSIGNAL: 设置让容器优雅退出的系统信号。
  • HEALTHCHECK: 告诉 Docker 怎么检查容器是不是“病”了(是否还在正常工作)。
  • SHELL: 允许你更换 RUN 指令默认使用的 Shell 环境。

1.2 Dockerfile 的黄金结构法则

一个排版良好、结构清晰的 Dockerfile,不仅自己看着舒服,日后维护起来也更轻松。推荐采用以下结构:

  1. 基础镜像 (FROM): 选择一个尽量小巧且合适的起点。比如用 Alpine Linux 能极大减小体积。
  2. 元数据 (LABEL): 加上维护者是谁、是什么版本、干嘛用的。
  3. 环境变量 (ENV): 提前配置好应用需要的环境变量。
  4. 安装依赖 (RUN): 安装应用运行需要的软件。尽量把相关的 RUN 命令用 && 连起来,减少镜像层数
  5. 拷贝代码 (COPY/ADD): 把你的应用代码放进镜像。
  6. 工作目录 (WORKDIR): 设置好代码运行的目录。
  7. 暴露端口 (EXPOSE): 声明应用监听的端口。
  8. 启动命令 (CMD/ENTRYPOINT): 告诉容器启动时具体执行哪个程序。

1.3 指令应用实例展示

来看看这些指令在实际中是怎么写的:

FROM

# 站在官方 Python 3.9 (轻量版) 的肩膀上
FROM python:3.9-slim-buster

RUN

# 安装依赖包。加上 --no-cache-dir 可以避免保存没用的缓存,让镜像更苗条
RUN pip install --no-cache-dir -r requirements.txt

COPYWORKDIR

# 切换到 /app 目录
WORKDIR /app
# 把当前目录下的所有东西都复制到镜像的 /app 里面
COPY . /app

CMDENV

# 设置环境变量
ENV APP_HOME /app
ENV DEBUG=True

# 容器启动时运行 app.py
CMD ["python", "app.py"]

EXPOSELABEL

LABEL maintainer="your_email@example.com"
LABEL version="1.0"
LABEL description="一个简单的 Python Web 应用"

# 声明应用监听 8000 端口
EXPOSE 8000

1.4 完整示例:组装一个 Python 应用镜像

把上面的碎片拼起来,就是一个完整的、可以直接运行 Python 应用的 Dockerfile:

# 1. 选好地基
FROM python:3.9-slim-buster

# 2. 定好工作目录
WORKDIR /app

# 3. 先拷贝依赖配置文件(利用缓存机制)
COPY requirements.txt .

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

# 5. 拷贝剩下的全部代码
COPY . .

# 6. 声明端口
EXPOSE 8000

# 7. 设置个环境变量玩玩
ENV NAME Docker

# 8. 设定启动命令
CMD ["python", "app.py"]

2. 把代码变成镜像:执行构建

写好了 Dockerfile,怎么把它变成真正的镜像呢?这就需要用到 docker build 命令了。

在包含 Dockerfile 的目录下打开终端,输入:

docker build -t my-python-app .
  • -t my-python-app: 给这个刚出炉的镜像起个名字(打上标签 Tag),方便以后找它。
  • .: 这个点非常重要!它代表构建上下文 (Build Context),也就是告诉 Docker:“嘿,Dockerfile 和我要打包进去的文件都在当前这个目录里,你就在这儿找吧。”

构建完成后,你就可以让它跑起来了:

docker run -p 8000:8000 my-python-app

这条命令把主机的 8000 端口和容器的 8000 端口连了起来,并启动了我们刚做的镜像。

3. 进阶玩法:打造专家级 Dockerfile

掌握了基础,我们再来看看如何通过高级技巧让你的镜像更小、构建更快。

3.1 终极瘦身秘籍:多阶段构建 (Multi-Stage Builds)

多阶段构建允许你在一个 Dockerfile 里使用多个 FROM 语句。每一个 FROM 都代表一个全新的“阶段”。最妙的是,你可以把前一个阶段生成的好东西(比如编译好的程序)直接“偷”到后一个阶段来用,而那些为了编译安装的笨重工具,全都可以扔掉!

这就像你找了个大厨房(编译环境)做大餐,做好之后,只把精美的菜肴端到干净小巧的餐厅(运行环境)里,厨房里的锅碗瓢盆全都不带过去。

来看一个 Go 语言程序的例子:

# --- 阶段一:在这个阶段我们叫它 "builder" (负责编译) ---
FROM golang:1.17-alpine AS builder
WORKDIR /app
# 拷贝代码并下载依赖
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 编译出一个名为 myapp 的可执行文件
RUN go build -o myapp

# --- 阶段二:这是最终要运行的镜像 (只要极简环境) ---
FROM alpine:latest
WORKDIR /app
# 【核心操作】把上一个阶段 (builder) 编译好的 myapp 复制过来!
COPY --from=builder /app/myapp .
EXPOSE 8080
# 运行编译好的程序
CMD ["./myapp"]

最终生成的镜像,只包含了基础的 Alpine 系统和那个单独的 myapp 程序,体积小得惊人!

3.2 极致加速:榨干 Docker 缓存的价值

我们在上一章提到过,Docker 构建时会一层层地缓存。如果某一层没变,它就直接拿来用,这能省下大把时间。

想要缓存命中率高,记住这三个原则:

  1. 频繁变动的放后面:比如应用代码(COPY . .)经常改,一定要放在 Dockerfile 的最后面。像安装系统软件(RUN apt-get update)这种几个月不改一次的,放最前面。
  2. 指令要专一稳定:哪怕你改了 RUN 命令里的一个空格,这一层和它后面的所有缓存都会失效。
  3. 别拷没用的东西:如果拷贝进镜像的文件有变动,缓存也会失效。

3.3 保持干净:学会使用 .dockerignore

构建镜像时,Docker 会把你指定的目录(也就是那个 .)下的所有东西都发给 Docker 引擎。如果你的目录下有巨大的日志文件或者几十兆的 .git 文件夹,这会极大地拖慢构建速度,并让镜像变得臃肿。

这就是 .dockerignore 文件出场的时候了。它的用法和 .gitignore 一模一样,专门用来告诉 Docker:“这些文件你别管,别打包进去。”

在 Dockerfile 同级目录下建一个 .dockerignore 文件,写上:

node_modules
.git
.DS_Store
*.log

这样,这四类文件或文件夹就会被 Docker 彻底无视。