2. Docker镜像管理:镜像的拉取与推送、Dockerfile基础指令、构建自定义镜像、镜像分层原理与优化
聊到Docker,镜像就是绕不开的核心。说白了,镜像就是容器的「模板」,你运行容器之前,得先把镜像准备好。这一章我带你从拉取镜像开始,一步步深入到构建自定义镜像,最后再聊聊镜像分层的底层逻辑。嗯,这些内容都是我日常工作中反复接触的东西,希望能帮你建立起一个扎实的认知。
2.1 镜像的拉取与推送
先从最基础的操作说起。拉取镜像,用的是 docker pull 命令。比如你想拉一个 MySQL 8.0 的镜像:
docker pull mysql:8.0
这条命令会从 Docker Hub(默认的公共仓库)把镜像下载到本地。我个人习惯在拉取时明确指定版本标签,而不是用 latest。为什么?因为 latest 是个动态标签,你这次拉的和下次拉的可能是不同版本,这在生产环境里是个大坑。我曾经遇到过因为 latest 自动更新导致数据库版本不兼容的问题,从那以后我就养成了指定具体版本的习惯。
推送镜像到仓库,需要先登录:
docker login
docker push yourusername/yourimage:tag
这里要注意,推送前你的镜像名必须包含仓库地址或用户名前缀。比如你构建了一个镜像叫 myapp:1.0,直接推是推不上去的,得先打个标签:
docker tag myapp:1.0 yourusername/myapp:1.0
docker push yourusername/myapp:1.0
docker pull registry.example.com/myapp:1.0 这样的方式拉取,既安全又可控。
2.2 Dockerfile 基础指令
构建自定义镜像,离不开 Dockerfile。它就像一份「菜谱」,告诉 Docker 怎么一步步组装出你想要的镜像。我重点讲三个最常用的指令:FROM、RUN、CMD。
2.2.1 FROM:指定基础镜像
几乎每个 Dockerfile 都以 FROM 开头。它指定了你的镜像基于哪个基础镜像构建。比如:
FROM ubuntu:22.04
或者更轻量的:
FROM alpine:3.18
我个人偏好用 Alpine 作为基础镜像,因为它体积小,只有几兆。但要注意,Alpine 用的是 musl libc,有些软件可能不兼容。你想想看,如果项目依赖了某些需要 glibc 的库,用 Alpine 就会出问题。这时候我会选择 Ubuntu 或 Debian 的 slim 版本,体积也不算大。
2.2.2 RUN:执行构建命令
RUN 用于在构建过程中执行命令。每一条 RUN 都会创建一个新的镜像层。比如安装 Nginx:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
这里有个细节:我习惯把 apt-get update 和 apt-get install 写在同一行,用 && 连接。为什么?因为如果分开写,Docker 会缓存第一层的结果,下次构建时如果缓存命中,就不会重新执行 apt-get update,导致安装的可能是过时的包。嗯,这个坑我踩过不止一次。
2.2.3 CMD:定义容器启动命令
CMD 指定容器启动时要执行的命令。它和 RUN 的区别在于:RUN 是构建时执行,CMD 是运行时执行。比如:
CMD ["nginx", "-g", "daemon off;"]
注意,CMD 推荐使用 JSON 数组格式,而不是 shell 格式。因为 shell 格式会启动一个 shell 进程,而 JSON 格式直接执行,更高效也更安全。
CMD 指令。如果写了多条,只有最后一条生效。如果你需要同时运行多个进程,建议用 ENTRYPOINT 配合脚本实现。
2.3 构建自定义镜像
有了 Dockerfile,就可以用 docker build 构建镜像了。假设你的 Dockerfile 在当前目录下:
docker build -t myapp:1.0 .
这里的 -t 指定镜像名和标签,最后的 . 是构建上下文路径。构建上下文指的是 Docker 在构建过程中能访问到的文件目录。我见过有人把整个项目根目录作为上下文,结果里面塞了几个 G 的日志文件,构建慢得要命。所以,建议在项目根目录下创建一个 .dockerignore 文件,排除不必要的文件:
node_modules
*.log
.git
.DS_Store
来看一个完整的例子,构建一个简单的 Python Web 应用:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
这个 Dockerfile 做了几件事:基于 Python 3.11 slim 镜像,设置工作目录,先复制依赖文件并安装(利用缓存),再复制源码,最后指定启动命令。你发现没有?我把 COPY requirements.txt 和 COPY . 分开了。这样做的目的是:如果只改了源码,没改依赖,Docker 会直接使用缓存的依赖安装层,构建速度会快很多。
2.4 镜像分层原理与优化
镜像分层是 Docker 的核心设计之一。每个镜像由多个只读层叠加而成,每一层对应 Dockerfile 中的一条指令。当你拉取或构建镜像时,Docker 会复用已有的层,只下载或构建变化的部分。
举个例子,假设你有两个镜像:
- 镜像 A:基于 Ubuntu,安装了 Python 3.11
- 镜像 B:基于 Ubuntu,安装了 Python 3.11 和 Flask
镜像 B 会复用镜像 A 的 Ubuntu 层和 Python 安装层,只新增一个 Flask 安装层。这就是为什么多个镜像共享基础层时,磁盘占用会大大减少。
但分层也有代价。每多一层,镜像体积就会增加,而且层数过多会影响容器启动性能。我总结了几条优化经验:
- 合并 RUN 指令:把多个
RUN合并成一个,减少层数。比如:
RUN apt-get update && \
apt-get install -y package1 package2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
- 清理临时文件:安装完软件后,及时清理缓存和临时文件,避免它们成为镜像的一部分。
- 选择合适的基础镜像:能用 Alpine 就别用 Ubuntu,能用 slim 就别用 full。
- 利用构建缓存:把不常变动的指令放在前面,频繁变动的放在后面。
COPY . . 放在了 RUN mvn package 之前。结果每次改一行代码,整个 Maven 构建都要重跑,构建一次要 5 分钟。后来我把 COPY pom.xml 提前,利用缓存,构建时间降到了 30 秒。记住:把「变化慢」的指令放前面,「变化快」的放后面。
最后,用 docker history 命令可以查看镜像的分层信息:
docker history myapp:1.0
你会看到每一层的大小和创建指令。这有助于你分析哪些层占用了空间,从而有针对性地优化。
好了,这一章的内容就到这里。镜像管理是 Docker 的基础,但也是容易出问题的地方。你只要掌握了拉取、推送、构建和分层优化,后续的容器化部署就会顺畅很多。下一章我们聊聊容器网络,到时候见。