第2章 Docker镜像管理:镜像仓库概念、常用镜像命令、镜像分层原理、构建自定义镜像

聊到Docker,镜像管理绝对是绕不开的核心话题。我刚开始接触容器化的时候,总觉得镜像就是个黑盒子——拉下来就能跑,至于里面是什么结构、怎么构建的,完全没深究。直到有一次线上环境因为镜像层数太多导致部署慢得离谱,我才意识到:不懂镜像管理,迟早要踩坑。

这一章,咱们就把镜像管理这块彻底讲透。从仓库概念到常用命令,从分层原理到自定义构建,一步步来。

2.1 镜像仓库:你的镜像该放哪儿?

镜像仓库,说白了就是存镜像的地方。你本地构建好的镜像,总不能一直留在本机吧?要部署到别的机器,或者团队协作共享,就得有个集中存放的地方。

镜像仓库分两种:

  • 公共仓库:最典型的就是Docker Hub。全球最大的镜像市场,什么镜像都有。我个人习惯把基础镜像都从Docker Hub拉,比如ubuntu、nginx、python这些官方镜像,质量有保障。
  • 私有仓库:企业内部用的。可以用Docker Registry自己搭,也可以用Harbor、Nexus这些企业级方案。我在项目中遇到过,公司安全要求所有镜像必须走内部仓库,不能直接连外网。这时候私有仓库就是刚需。
小提示: 拉取公共镜像时,建议指定具体版本号,别用latest标签。我见过太多因为latest指向的版本变了,导致环境不一致的惨案。

镜像的命名规则也很简单:[仓库地址/]镜像名[:标签]。比如 nginx:1.21,或者 registry.example.com/myapp:v1.0。标签就是版本号,方便你管理不同版本的镜像。

2.2 常用镜像命令:每天都要用的那些

镜像相关的命令,我列几个最常用的。你想想看,平时工作里是不是天天跟它们打交道?

命令 作用 示例
docker pull 拉取镜像 docker pull nginx:1.21
docker images 查看本地镜像列表 docker images
docker rmi 删除镜像 docker rmi nginx:1.21
docker tag 给镜像打标签 docker tag nginx:1.21 myrepo/nginx:v1
docker push 推送镜像到仓库 docker push myrepo/nginx:v1
docker inspect 查看镜像详细信息 docker inspect nginx:1.21

嗯,这里要注意一点:docker rmi 删除的是镜像,不是容器。如果你有容器正在使用这个镜像,得先删容器才能删镜像。我曾经因为没注意这个顺序,在那折腾了半天。

还有一个命令我经常用——docker image prune。它能清理掉那些没用的、悬空的镜像层。时间久了,本地会堆积一堆无用的镜像,占磁盘空间。定期跑一下这个命令,能省不少空间。

2.3 镜像分层原理:为什么Docker镜像这么轻?

这个问题,我当年面试的时候被问过。说白了,Docker镜像之所以轻量,核心就在于分层。

每个Docker镜像由多个只读层叠加而成。每一层代表一个指令的变更。比如你写个Dockerfile:

FROM ubuntu:20.04          # 层1:基础系统
RUN apt-get update         # 层2:更新包列表
RUN apt-get install -y nginx  # 层3:安装nginx
COPY index.html /var/www/  # 层4:复制文件
CMD ["nginx", "-g", "daemon off;"]  # 层5:启动命令

每一行指令,都会生成一个新层。这些层是只读的,而且可以复用。什么意思呢?比如你有两个镜像,都基于ubuntu:20.04,那它们会共享同一个基础层。你拉取第二个镜像时,基础层已经存在了,只需要下载差异层就行。

核心要点: 分层复用是Docker镜像设计的精髓。它让镜像传输、存储都变得高效。你想想看,如果每个镜像都是独立的完整文件系统,那得占多少空间?

我遇到过这样一个场景:团队里多个微服务都基于同一个Python基础镜像。因为分层机制,每个服务只需要拉取自己独有的业务代码层,基础层大家共用。部署速度提升了不少。

还有一个细节——写时复制。容器运行时,会在镜像层之上加一个可写层。你对容器的任何修改,都写在这个可写层里。容器删除后,可写层也就没了。所以容器是无状态的,数据要持久化得用卷挂载。

2.4 构建自定义镜像:从零开始造一个

光会拉现成的镜像不够,工作中经常需要自己构建。构建自定义镜像,最标准的方式就是写Dockerfile。

我拿一个简单的Python Web应用举例:

# 1. 选择基础镜像
FROM python:3.9-slim

# 2. 设置工作目录
WORKDIR /app

# 3. 复制依赖文件
COPY requirements.txt .

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

# 5. 复制应用代码
COPY . .

# 6. 暴露端口
EXPOSE 5000

# 7. 启动命令
CMD ["python", "app.py"]

然后执行构建命令:

docker build -t my-python-app:v1.0 .

这里有几个坑,我踩过,得提醒你:

避坑指南:
  • 层数别太多:每一条RUN、COPY都会生成新层。层数太多,镜像会变得臃肿。我建议把多个RUN命令用 && 合并,比如 RUN apt-get update && apt-get install -y nginx
  • 注意构建上下文docker build 最后的 . 表示构建上下文。Docker会把整个上下文目录打包发送给守护进程。如果你目录里有大文件或敏感文件,记得用 .dockerignore 排除掉。
  • 基础镜像选对版本:slim版本比full版本小很多,适合生产。alpine版本更小,但有些库可能不兼容。我一般选slim,平衡体积和兼容性。

构建完成后,用 docker images 就能看到你刚造的镜像了。再配合 docker tagdocker push,就能把它推到仓库里,供团队其他人使用。

说到构建,还有一个技巧——多阶段构建。比如你编译一个Go程序,编译环境需要完整的Go工具链,但运行环境只需要编译好的二进制文件。多阶段构建可以让你在同一个Dockerfile里,先用一个大镜像编译,再把产物复制到一个小镜像里。最终镜像只包含运行所需的东西,体积能小很多。

# 第一阶段:编译
FROM golang:1.18 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 第二阶段:运行
FROM alpine:3.15
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]

你看,最终镜像只有几MB,而不是几百MB。这个技巧我在微服务部署中经常用,效果很好。

好了,镜像管理这块就聊到这儿。下一章咱们进入容器运行时的内容,到时候会讲到怎么用镜像启动容器、管理容器生命周期。记得把镜像命令多练练,熟能生巧。