自定义 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,不仅自己看着舒服,日后维护起来也更轻松。推荐采用以下结构:
- 基础镜像 (
FROM): 选择一个尽量小巧且合适的起点。比如用 Alpine Linux 能极大减小体积。 - 元数据 (
LABEL): 加上维护者是谁、是什么版本、干嘛用的。 - 环境变量 (
ENV): 提前配置好应用需要的环境变量。 - 安装依赖 (
RUN): 安装应用运行需要的软件。尽量把相关的RUN命令用&&连起来,减少镜像层数。 - 拷贝代码 (
COPY/ADD): 把你的应用代码放进镜像。 - 工作目录 (
WORKDIR): 设置好代码运行的目录。 - 暴露端口 (
EXPOSE): 声明应用监听的端口。 - 启动命令 (
CMD/ENTRYPOINT): 告诉容器启动时具体执行哪个程序。
1.3 指令应用实例展示
来看看这些指令在实际中是怎么写的:
FROM
# 站在官方 Python 3.9 (轻量版) 的肩膀上
FROM python:3.9-slim-busterRUN
# 安装依赖包。加上 --no-cache-dir 可以避免保存没用的缓存,让镜像更苗条
RUN pip install --no-cache-dir -r requirements.txtCOPY 和 WORKDIR
# 切换到 /app 目录
WORKDIR /app
# 把当前目录下的所有东西都复制到镜像的 /app 里面
COPY . /appCMD 和 ENV
# 设置环境变量
ENV APP_HOME /app
ENV DEBUG=True
# 容器启动时运行 app.py
CMD ["python", "app.py"]EXPOSE 和 LABEL
LABEL maintainer="your_email@example.com"
LABEL version="1.0"
LABEL description="一个简单的 Python Web 应用"
# 声明应用监听 8000 端口
EXPOSE 80001.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 构建时会一层层地缓存。如果某一层没变,它就直接拿来用,这能省下大把时间。
想要缓存命中率高,记住这三个原则:
- 频繁变动的放后面:比如应用代码(
COPY . .)经常改,一定要放在 Dockerfile 的最后面。像安装系统软件(RUN apt-get update)这种几个月不改一次的,放最前面。 - 指令要专一稳定:哪怕你改了 RUN 命令里的一个空格,这一层和它后面的所有缓存都会失效。
- 别拷没用的东西:如果拷贝进镜像的文件有变动,缓存也会失效。
3.3 保持干净:学会使用 .dockerignore
构建镜像时,Docker 会把你指定的目录(也就是那个 .)下的所有东西都发给 Docker 引擎。如果你的目录下有巨大的日志文件或者几十兆的 .git 文件夹,这会极大地拖慢构建速度,并让镜像变得臃肿。
这就是 .dockerignore 文件出场的时候了。它的用法和 .gitignore 一模一样,专门用来告诉 Docker:“这些文件你别管,别打包进去。”
在 Dockerfile 同级目录下建一个 .dockerignore 文件,写上:
node_modules
.git
.DS_Store
*.log这样,这四类文件或文件夹就会被 Docker 彻底无视。