ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Docker镜像管理实战:从拉取、构建瘦身到私有仓库部署

Docker镜像管理实战:从拉取、构建瘦身到私有仓库部署 镜像管理这事儿看着简单实际上是个细水长流的活儿。我自己从刚开始只会docker pull和docker run到后来折腾镜像瘦身、私有仓库、批量清理踩了不少坑也攒了一些还算好用的经验。这篇文章就围绕镜像管理这一件事把从拉取、存储、构建、清理到私有化部署的完整链路都梳理一遍尽量把关键参数和背后的原理都讲透。1. 镜像管理到底在管什么很多人觉得镜像管理无非就是拉镜像、跑容器真到出问题的时候才发现镜像层面的混乱会直接拖垮整个开发流程。我见过最典型的场景一台服务器上堆了几十个None标签的悬空镜像磁盘占了大几十 GB谁都不敢删因为没人知道哪个被哪个容器依赖着。还有团队里每个人从 Docker Hub 拉镜像的版本都不一样生产环境一说回滚发现镜像 tag 根本没对应上某次提交。所以镜像管理的前提是先搞清楚它管的是哪几件事。镜像内容管理镜像由多层只读层叠加而成如何构建、如何复用缓存、如何给镜像瘦身。镜像生命周期管理拉取、标记、推送、删除、导出、导入、清理每一条命令都有副作用不能乱用。镜像仓库管理本地的 Docker Hub、自建的私有仓库、镜像加速源怎么配、怎么选、怎么限流直接决定拉取速度和可用性。镜像与容器的关系管理镜像文件、容器可写层、数据卷是三种不同的存储很多人删镜像删不掉就是因为没分清这三者的关系。这个认知框架建立起来之后再去看那些花哨的 Docker 命令其实就两类一类是操作镜像本身的一类是操作容器和存储的。搞混了就会出现“我删了镜像磁盘怎么没释放”这种经典疑问。下面我把每个环节拆开讲把我自己的操作习惯和遇到过的坑都写进去。2. 镜像拉取与镜像源配置基础但最影响体验2.1 Docker 镜像加速器的配置逻辑国内用 Docker 最难受的就是拉镜像慢尤其是拉一些比较大的基础镜像比如mysql:8.0、gitlab/gitlab-ce有时候能卡到怀疑人生。Docker 默认从 Docker Hub 拉取国内网络环境的可用性大家都懂所以第一步就是把镜像加速器配置好。在 Docker Engine 的配置文件里增加 registry-mirrors常见路径是/etc/docker/daemon.jsonLinux或 Docker Desktop 的设置界面Windows/Mac。这里有个细节很多人以为加速器配好后所有镜像都会走加速实际上它只对 Docker Hub 官方镜像生效而且只对“没有显式指定 registry 前缀”的镜像生效。如果你拉的是quay.io/prometheus/node-exporter这类第三方仓库的镜像加速器是管不着的。配置好之后建议重启 Docker 服务然后用docker info查看 Registry Mirrors 一栏是否生效。我测试过多个加速地址实测下来不同的加速源在高峰期差异很明显所以更稳妥的做法是配两个以上Docker 会按顺序尝试。2.2 拉取镜像时值得留意的几个参数docker pull是最常用的命令但有一些参数很容易被忽略。--platform这个参数非常实用。我在 MacApple Silicon上做开发经常需要拉取不同架构的镜像比如在 arm64 的机器上拉 x86 的镜像做兼容测试。如果不指定Docker 默认拉取当前平台对应的架构加上--platform linux/amd64才能拿到目标架构版本。-q参数可以静默拉取只输出镜像 ID适合在脚本里判断镜像是否存在。还有一个容易踩坑的点docker pull ubuntu:latest和docker pull ubuntu结果是相同的但latest标签是浮动标签它会随着上游更新而指向新的镜像 ID。如果生产环境依赖latest拉取镜像等于把版本控制权交给了上游。我自己的习惯是除非是本地试验环境否则一律使用明确的版本 tag甚至用reposha256:...这种 digest 方式锁定镜像彻底杜绝“昨天还能跑今天拉下来就挂了”的情况。2.3 Windows 和 Mac 上 Docker Desktop 的镜像存储位置镜像管理过程中磁盘空间是个绕不开的话题。Docker Desktop 默认把镜像存在虚拟磁盘里Windows 是 WSL2 vhdx 文件Mac 是 Docker.raw位置在用户目录下随着镜像越拉越多这个文件会越涨越大。关键问题是删除镜像后这个文件不会自动缩小所以会看到“明明删了几十个镜像磁盘占用却没变”的现象。解决方案有两个一是用 Docker Desktop 的磁盘清理功能在 Troubleshoot - Clean / Purge data 里处理二是手动执行docker system prune之后再通过diskutilMac或Optimize-VHDWindows Hyper-V压缩虚拟磁盘。我在 Windows 上试过把 Docker 虚拟磁盘迁移到其他盘主要是修改 WSL 的.wslconfig配置把 vhdx 文件路径指过去操作步骤不复杂但注意一定要先退出 Docker Desktop再迁移文件否则会损坏磁盘。3. 镜像构建与瘦身从一开始就控制体积3.1 编写 Dockerfile 的常见误区镜像体积的膨胀绝大多数是从 Dockerfile 的阶段就埋下的。很多初学者的第一版 Dockerfile 长这样FROM ubuntu:20.04 RUN apt update RUN apt install -y build-essential python3 python3-pip RUN pip install flask COPY . /app CMD [python3, /app/app.py]每一行RUN都会产生一个新的镜像层这三个 RUN 就把临时下载的.deb包、pip 缓存全部留在了镜像层里镜像体积白白多了好几百兆。优化的方式是合并 RUN 指令并在同一条指令内清理缓存FROM ubuntu:20.04 RUN apt update \ apt install -y --no-install-recommends build-essential python3 python3-pip \ pip install --no-cache-dir flask \ rm -rf /var/lib/apt/lists/* COPY . /app CMD [python3, /app/app.py]装包时加上--no-install-recommendspip 加上--no-cache-dir apt 的 lists 缓存清理掉这几步能砍掉不少体积。这里有个判断标准镜像层是只读的每一层都会永久保存所以任何“只在该步骤临时需要后面不再使用”的文件都应该在同一条 RUN 里创建并删除。3.2 多阶段构建的实际应用多阶段构建是镜像瘦身最有效的手段之一尤其适合编译型语言的项目。思路很简单第一阶段用完整的编译环境编译出产物第二阶段用精简的运行环境只拷贝产物过去。以一个 Go 项目为例# 阶段一编译 FROM golang:1.20 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED0 GOOSlinux go build -o app . # 阶段二运行 FROM alpine:latest WORKDIR /root/ COPY --frombuilder /app/app . CMD [./app]这种写法的好处是最终镜像只包含 alpine 基础层和编译产物golang 编译器、源码文件都不会进入最终镜像体积可以从 800MB 降到 20MB 左右。Python、Node.js 等解释型语言也可以借鉴这个思路先用完整镜像安装依赖并构建前端资源再把node_modules或dist目录拷贝到精简运行时镜像配合.dockerignore排除无关文件。3.3 基础镜像选择的取舍基础镜像的选择直接决定了整个镜像的体积底座。ubuntu:22.04大概 70MBdebian:bookworm-slim大概 40MBalpine:3.18只有 7MB 左右。但不要盲目追求小体积alpine 用的是 musl libc和 glibc 存在兼容性差异有些预编译的二进制包、Python 的manylinuxwheel 在其中可能无法运行。如果项目依赖比较复杂优先选择debian-slim系列体积比标准版少很多但保持了 glibc 兼容性。我的建议是能跑通的情况下选最精简的跑不通再换但换之前要清楚原因换基础镜像不只是一个体积问题还涉及动态链接库、DNS 解析行为、时区数据等多方面的差异。4. 镜像查看、标记与清理日常运维高频操作4.1 常用查看命令的深层含义docker images列出的信息包括 REPOSITORY、TAG、IMAGE ID、CREATED、SIZE。很多人只看前三列实际上 SIZE 列也有讲究它显示的是镜像压缩后的逻辑大小而不是在磁盘上实际占用的空间。同一个基础镜像被多个镜像共享时磁盘占用是去重的但docker images列出的每一行都会重复计算体积所以看到所有镜像总大小加起来很大不代表磁盘真的用了这么多。docker system df是查看真实磁盘占用最直接的工具它会分别列出 Images、Containers、Local Volumes、Build Cache 的占用情况并标明 RECLAIMABLE可回收的大小。这个命令应该成为你每周例行检查的固定动作。docker history image可以看到镜像每一层的创建命令和大小对排查“哪一层把镜像搞大了”非常有用。我曾经排查过一个镜像体积异常的问题docker history显示某一步COPY . /app产生了 400MB 的层最后发现是因为.dockerignore没写把node_modules和.git一起打进去了。这类问题不看 history 根本定位不到。4.2 tag 的正确使用方式tag 是镜像的“引用名”同一个镜像 ID 可以有多个 tag它们指向同一份镜像层数据。这种情况下删除某个 tag不会真正删除镜像文件只有所有 tag 都被删除、并且没有容器引用该镜像 ID 时镜像数据才会真正释放。团队协作时tag 命名规范非常重要。我比较推荐项目名-环境-版本或者项目名-版本-构建号这种结构例如user-service:prod-1.4.2。永远不要在生产环境使用latest也尽量不要手动覆盖同一个 tag 推送新镜像否则在分布式环境下容易出现“有的节点拉到新镜像有的节点还是旧镜像”的中间态。如果确实需要更新建议用新的版本号 tag 推送然后更新部署配置。4.3 清理镜像的几个命令和它们分别会做什么docker rmi image删除镜像docker image prune清理悬空镜像即没有 tag 且没有被容器引用的镜像docker system prune清理所有未使用的对象包括停止的容器、悬空镜像、未使用的网络和构建缓存。这里要特别注意docker system prune -a会把所有没有被运行中的容器使用的镜像全部删掉即使它们有 tag。如果你只是停止了一个容器但没删除它prune -a不会删掉它引用的镜像但如果你删除了容器对应镜像也就成了可清理对象。个人心得在开发机上我一般用docker system prune不带-a只清悬空镜像和停止容器在 CI 构建机上才会定期使用docker system prune -af因为构建机上只关心下次构建能否正常拉取基础镜像本地缓存删了重新拉就行。5. 私有镜像仓库从registry到harbor的落地经验5.1 什么时候需要私有镜像仓库团队规模小、镜像也不多的时候直接拉镜像就行自建仓库反而增加维护成本。但一旦出现下面几种情况就该考虑私有仓库了代码托管在内网镜像本身包含敏感业务代码或内部配置。服务器无法访问外网或者从外网拉镜像的链路不稳定。需要做镜像的合规审批、漏洞扫描、版本管理。多台服务器要部署同一个版本与其每台都去公网拉不如推送到内网私有仓库再让服务器从内网拉取速度更快、也更可控。5.2 Docker Registry 快速部署registry是 Docker 官方提供的轻量私有仓库镜像部署非常简单docker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry-data:/var/lib/registry \ --restartalways \ registry:2推送镜像的时候需要先把镜像打上私有仓库地址的 tagdocker tag myapp:1.0 192.168.1.100:5000/myapp:1.0 docker push 192.168.1.100:5000/myapp:1.0如果私有仓库地址是 HTTP 协议Docker 客户端默认会拒绝推送因为 Docker 要求 registry 必须使用 HTTPS需要在每台客户端机器的/etc/docker/daemon.json中添加{ insecure-registries: [192.168.1.100:5000] }设置完成后重启 Docker 才能生效。这个细节坑过不少人我看过很多“推送失败”的问题排查最后都是因为忘了加insecure-registries。5.3 Harbor 部署与使用要点registry够用但缺少 Web 界面、权限管理、镜像同步这些能力。harbor是一个比较成熟的企业级镜像仓库方案它自带 Web UI、RBAC 权限控制、镜像复制、漏洞扫描还支持配额管理。Harbor 一般通过 docker-compose 部署。解压安装包后编辑harbor.yml需要重点关注这几个配置hostname必须是客户端能访问到的域名或 IP不能写localhost。harbor_admin_password管理员初始密码部署后要马上改。data_volume镜像数据存储路径。线上环境建议放在独立的数据盘而不是系统盘否则镜像多了会把根分区撑爆。http.port默认 80。database和redis的密码生产环境建议改掉默认值。Harbor 部署完以后客户端的配置和 registry 一样如果走 HTTP也要在daemon.json里配置insecure-registries。Harbor 创建项目的时候可以设置项目级别的公开/私有权限私有项目需要先在页面或通过docker login进行认证然后把 tag 打成harbor地址/项目名/镜像名:版本再推送。5.4 私有仓库的镜像同步策略如果线上有多套环境比如测试环境、预发环境、生产环境通常会构建一次镜像然后推送到测试仓库经审批后同步到生产仓库。Harbor 内置了基于规则的镜像复制功能可以把指定项目下的镜像根据 tag 过滤规则推送到目标仓库。这个机制比“手动拉取再推送”更可靠因为它有状态记录同步失败的镜像能直观看到比较适合有合规要求的发布流程。对于 K8s 环境建议使用imagePullPolicy: IfNotPresent加私有仓库拉取密钥的方式配合每次发版更新 tag这样每次部署都能正确拉取新镜像又不会因为集群内每一台节点都去公网拉取而浪费时间。6. 镜像导出、导入与离线分发有些服务器是内网环境不能访问任何外部仓库这时候就需要镜像的离线分发能力。docker save和docker load是这一场景下的核心命令。导出镜像到 tar 文件docker save -o myapp-1.0.tar myapp:1.0可以一次性保存多个镜像docker save -o images.tar nginx:1.25 mysql:8.0 myapp:1.0在目标机器上导入docker load -i images.tar这里有个细节docker save会完整保留镜像的所有层数据包括镜像历史适合跨机器完整迁移而docker export是导出容器的文件系统不包含镜像层信息和历史记录导入后是一个“扁平的”文件系统镜像主要用于做快照不能用于保留镜像的 tag、层结构等信息。如果你想把一个正在运行的容器作为基础镜像分发用docker commit而不是docker export。大文件的分发建议先压缩再传输。docker save出来的 tar 文件往往很大用gzip压缩可以显著减小比如docker save myapp:1.0 | gzip myapp-1.0.tar.gz导入时gunzip -c myapp-1.0.tar.gz | docker load。我传一个 1.2GB 的镜像 tar 到内网服务器压缩后只有 400MB 左右传输时间缩短了三分之二。7. 镜像相关的常见问题与排查技巧7.1 镜像删除不掉提示“image is being used by container”这个比较常见意思是镜像已经被某个容器即使是停止状态的容器引用了。解决方案是先找到引用该镜像的容器docker ps -a | grep image docker rm container_id然后再删除镜像。如果你确定这个容器不再需要可以用docker rm -f强制删除。如果是大量残留容器占着镜像直接用docker container prune清理所有停止的容器再执行镜像删除。7.2 磁盘空间不足但删了镜像磁盘没变化这种情况一般有三个原因一是构建缓存占用的空间二是容器可写层占用三是数据卷占用。docker system df能一眼看出是哪个部分占用的。构建缓存用docker builder prune清理数据卷要单独处理docker volume prune会删除所有没有被容器引用的数据卷使用前要谨慎一旦删除无法恢复。7.3docker pull卡住或超时大部分原因是网络问题。先看加速器是否生效其次可以尝试换一个加速源或者使用具体版本的 tag 而不是latest因为latest的 manifest 列表有时候反而会因为网络问题拉不下来。如果在内网环境优先检查是否配置了代理、防火墙是否放行 443 端口。曾经遇到过一个比较隐蔽的问题公司出口代理对 Docker Hub 的 API 请求做了拦截但镜像层下载地址是 CDN 域名不在拦截名单里导致 manifest 拉不到、镜像层下载却在正常进行表现就是一直卡在 pulling fs layer 那里排查了很久。7.4 镜像 build 时COPY报错找不到文件多半是.dockerignore把文件排除了或者COPY的源路径写错了。Docker 的COPY相对路径是相对于 Dockerfile 所在目录也就是构建上下文不是相对于宿主机当前路径。如果文件确实存在于 Dockerfile 旁边但仍然报错检查一下构建命令里指定的上下文目录是否正确比如docker build -f docker/Dockerfile .和docker build -f docker/Dockerfile docker/的COPY上下文基准是完全不同的。7.5 常见问题速查表问题现象可能原因排查命令解决方式镜像删除失败容器仍在引用docker ps -a删除相关容器后再rmi磁盘占用未释放虚拟磁盘未压缩docker system dfprune 后在 Docker Desktop 中压缩磁盘pull 超时/卡住镜像源网络受限docker info查看加速源更换加速器、配置代理build 时 COPY 失败上下文目录不对查看构建命令调整-f与上下文路径镜像体积异常大缓存文件被打入镜像docker history合并 RUN、清理缓存、写 .dockerignore私仓推送失败registry 未走 HTTPSdocker push报错信息配置insecure-registries8. 镜像管理的几个实际操作心得最后分享一些个人实践中的习惯。第一我会在一台长期使用的开发机上每周跑一次docker system df看一眼占用然后执行docker system prune清理悬空镜像和停止容器构建缓存看情况处理。这个习惯帮我避免过多次“突然没磁盘”的窘境。第二团队内部我推行了镜像命名规范项目名/服务名:版本号-环境比如order-service/order-api:2.3.1-prod。并且约定生产环境必须使用明确版本号不允许出现latest。发布脚本里对 tag 做校验不符合规范直接拒绝构建。第三所有生产环境的 Dockerfile 统一使用多阶段构建基础镜像固定 tag不允许用latest。Dockerfile 变更必须走代码评审因为构建方式的变更直接影响产物体积和构建时间。第四私有仓库的备份非常重要。Harbor 的数据库和镜像存储目录要定期备份。镜像数据一旦丢失重新构建所需要的基础环境和历史版本很难完整还原。我见过只备份数据库不备份存储目录最后磁盘损坏导致镜像全部丢失的案例重建成本非常高昂。镜像管理这件事说到底是把“用什么镜像、从哪里来、怎么构建、怎么存、怎么删”这套流程建立起来。它不需要多高深的技术但需要从第一天就开始规范操作。等到镜像多到失控再回头补课代价会大很多。根据我自己的实操经验现阶段最值得花时间的几个方向是彻底吃透镜像层的存储机制、把 Dockerfile 的构建优化做到位、把私有仓库的权限和同步规则配置清楚。这三件事做好日常工作中 80% 的镜像管理问题都能规避掉。
返回列表