Docker私有镜像仓库选型与部署:Nexus与Harbor实战对比 前两个月帮某个内部团队做容器化改造代码和业务系统都没出大问题反倒是镜像分发这个环节折腾得够呛公共镜像仓库拉取经常超时开发机上同一张基础镜像重复下载了好几次团队内部打出来的镜像名五花八门Tag 越来越随意。折腾一圈之后我把 Nexus 和 Harbor 这两条主流私有镜像仓库路线都完整跑了一遍该踩的坑基本都踩了。这篇文章就把选型思路、部署步骤、常见排错一次性讲清楚适合准备在公司内网搭建 Docker 私有镜像仓库的运维、开发或者架构同学参考。1. 为什么需要自建Docker镜像仓库1.1 三个现实痛点直接催生私有仓库需求先说第一个痛点网络与效率。开发环境里的机器如果每次都去公共镜像仓库拉取镜像高峰期经常遇到超时或者连接被重置。更麻烦的是同一个镜像在十台机器上可能被重复拉取十遍既浪费时间又浪费带宽。把这个过程改成走内网私有仓库之后第一次拉取从上游同步到本地后续全部走内网速度稳定带宽成本也大幅下降。第二个痛点是版本管理与规范。没有统一仓库时团队里很可能出现这样的情况A 同学打包用的是nginx:latestB 同学打包用的是nginx:1.25.1C 同学的生产环境还在用nginx:1.24。等到出问题排查时根本不知道线上跑的到底是什么版本。私有仓库可以从流程上强制要求镜像命名规范比如带上项目前缀、环境标识和明确版本号至少能保证每一份镜像的来源和版本都可追溯。第三个痛点是安全与审计。很多公司内部网络与外部网络隔离应用打包、发布、回滚都得在大内网完成没有私有仓库根本转不动。就算不隔离研发同学随手往公共仓库推送包含内部代码的镜像也是一个很大的安全隐患。自建仓库之后权限细分、操作审计、漏洞扫描这些能力才有落地的载体。1.2 镜像仓库到底在解决什么问题搞清楚镜像仓库的核心职责后面的选型思路才会清晰。它本质上就是一个运行在服务器上的注册中心按照 Docker Registry HTTP API V2 协议对外提供镜像的存储、分发、查询能力。一个合格的私有镜像仓库至少包含四层能力。第一是存储层镜像的每一层 blob 和 manifest 元数据需要持久化到磁盘或者对象存储。第二是分发层客户端通过docker pull、docker push调用仓库的 API仓库需要支持并发、鉴权和流量控制。第三是代理缓存层私有仓库可以代理上游公共仓库的镜像拉取过的镜像缓存在本地后续拉取不再消耗外网带宽。第四是管理运维层比如权限管理、镜像删除、空间回收、日志审计、备份恢复。如果只是个人学习或者三五个人的小团队直接跑一个原生 Registry 容器也能做到前三层。但到了需要多人协作、环境交付、安全审计的场景就需要 Nexus 或者 Harbor 这样的企业级方案了。2. 方案选型Nexus与Harbor到底怎么选2.1 Nexus的定位与适用场景Nexus 本身是一个通用制品仓库管理器Docker 镜像只是它支持的众多制品格式之一。同一个 Nexus 实例里面既可以管理 Maven 的 jar 包也可以管理 npm 包还能管理 Docker 镜像、Helm Chart、Raw 文件等等。对于那种研发团队已经在用 Nexus 管理其他制品的公司来说直接在现有实例上开启 Docker 仓库功能是成本最低的一条路。我用下来的感受是Nexus 的部署非常轻官方提供了容器镜像一条命令就能启动。管理界面虽然比 Harbor 粗糙不少但胜在功能足够、稳定、权限模型清晰。它适合两类团队一类是已经有 Nexus 使用经验的团队另一类是希望用一个工具同时管理多种制品不想为每种制品分别搭一套系统的团队。2.2 Harbor的定位与适用场景Harbor 走的是完全不同的路线。它由 CNCF 孵化是专门为容器镜像场景设计的企业级仓库。Harbor 底层基于开源的 Docker Distribution 做存储和分发在其上叠加了 Web 管理界面、基于项目的权限体系、镜像复制、漏洞扫描、镜像签名、审计日志等一系列能力。从实际使用体验来说Harbor 的 Web UI 比 Nexus 更适合管理镜像每个项目可以单独设置公开或者私有用户权限可以细粒度到项目的推拉权限操作日志也完整记录。镜像复制功能特别实用比如从一个开发环境 Harbor 复制到生产环境 Harbor或者从公共仓库拉取镜像后摆放到内网 Harbor都是可视化的界面操作。2.3 一张表看清选型方向对比维度NexusHarbor核心定位通用制品仓库容器镜像专用仓库部署复杂度单容器简单多组件依赖 docker-composeDocker 仓库类型hosted / proxy / group项目级仓库支持代理缓存权限与审计基础角色权限项目级 RBAC、操作审计完整镜像复制较弱强大支持 push / pull 模式漏洞扫描不支持内置支持多数据库资源消耗单进程内存占用偏高多容器整体占用可控适用场景已有 Nexus、多制品需求纯容器场景、对安全审计要求高选型没有绝对的对错。我的个人建议是如果团队已经有 Nexus 并且使用熟练完全可以先基于 Nexus 开启 Docker 仓库省去再维护一套系统的成本如果是从零开始搭建私有仓库又希望镜像的权限管理、复制容灾、漏洞扫描做得更专业直接上 Harbor 更省心。3. Nexus搭建Docker镜像仓库实操3.1 部署前的准备目录、端口与账号Nexus 虽然只是一个容器但部署前有几个关键点要提前规划清楚。第一是存储目录Nexus 的所有配置、内置数据库、blob 存储都在/nexus-data目录下宿主机上要挂一个独立数据盘一般放到/srv/nexus-data或者/data/nexus-data。这个目录会随镜像仓库数据增长越来越大必须单独规划磁盘空间。第二是端口规划。Nexus 默认管理端口是 8081但如果要开 Docker 仓库需要额外规划 Docker 专用端口。因为 Nexus 是在同一个 JVM 进程里监听多个端口来提供不同仓库的访问我这里规划了 8082 作为 Docker HTTP 端口8083 作为 Docker HTTPS 端口。第三是账号问题。Nexus 容器内部默认以 UID 200 的用户运行如果挂载目录的属主不是 200容器启动时会因为目录权限问题直接失败。所以最稳妥的做法是先创建目录再把属主改成 200。3.2 容器化部署与初始化配置下面是完整部署命令我用的 Nexus 版本是 3.66.0目前稳定可用mkdir -p /srv/nexus-data chown -R 200:200 /srv/nexus-data docker run -d --name nexus3 \ --restartalways \ -p 8081:8081 \ -p 8082:8082 \ -p 8083:8083 \ -v /srv/nexus-data:/nexus-data \ sonatype/nexus3:3.66.0提示如果宿主机是 CentOS 7 且使用较旧内核建议检查防火墙是否放行 8081、8082、8083 端口否则外部客户端访问不到。容器启动之后Nexus 初始化需要一点时间。默认管理员账号是admin初始密码不在界面上而是写在数据目录的admin.password文件里。可以这样查看cat /srv/nexus-data/admin.password首次登录系统会强制要求修改密码。我强烈建议这一步不要跳过而且密码不要用admin123这种因为一旦暴露匿名用户甚至可能直接推送镜像覆盖你的仓库。如果要调 JVM 内存参数可以在 docker run 时通过环境变量传入。Nexus 官方镜像支持INSTALL4J_ADD_VM_PARAMS例如docker run -d --name nexus3 \ -e INSTALL4J_ADD_VM_PARAMS-Xms2g -Xmx4g -XX:MaxDirectMemorySize2g \ ...其余参数同上实测下来Nexus 默认 JVM 参数在 4G 内存的机器上会吃得很满有条件就上 8G 内存或者显式限制堆大小避免它和宿主机其他进程抢内存。3.3 创建Docker仓库hosted、proxy、groupNexus 里 Docker 仓库有三种类型理解它们的区别非常关键Docker hosted本地私有仓库用于存放团队自己的镜像可读写。Docker proxy代理外部公共仓库的镜像只读缓存例如代理 Docker Hub。Docker group组合多个仓库把 hosted 和 proxy 聚合成一个统一入口客户端只需要配置一个地址。登录管理界面后进入 Settings → Repository → Repositories → Create repository选择docker (hosted)。配置时主要关注两个地方一个是 HTTP 端口填 8082另一个是勾选是否开启 Docker v1 API。目前主流版本 Docker 客户端都基于 V2 APIv1 可以不用开。创建docker (proxy)时Remote Storage 地址填 Docker Hub 的官方地址也就是https://registry-1.docker.io。如果公司网络访问 Docker Hub 不稳定可以在 Nexus 的 proxy 仓库里配置 Docker 官方镜像加速器作为上游地址实测下来拉取成功率和速度都会好很多。注意这里说的上游地址必须是符合 Registry V2 协议的地址不能随便填一个网页 URL。最后创建docker (group)把刚创建的 hosted 和 proxy 加进去。group 仓库的端口可以另开一个比如 8084。这样客户端只需要配置 group 端口推送镜像时 Nexus 会自动写入 hosted 仓库拉取镜像时 group 会先查 hosted 再查 proxy 缓存逻辑非常清晰。3.4 客户端连接HTTP 与 insecure-registries 配置Nexus 创建好仓库之后客户端还不能直接访问因为 Docker 客户端默认要求仓库走 HTTPS。我搭建时用的内网环境没有证书最简单可行的方案是把仓库地址加入 Docker daemon 的 insecure-registries 列表。编辑客户端的/etc/docker/daemon.json加上如下内容{ insecure-registries: [ 192.168.1.10:8082, 192.168.1.10:8084 ] }修改之后必须重启 Docker 服务才生效systemctl restart docker这里有个常见认知误区很多人以为localhost:8082可以直接访问实际上 Docker 客户端在解析仓库地址时完全按你填的地址去校验 TLS。如果你走 HTTP无论本地还是远程都必须把地址塞进 insecure-registries。如果希望走 HTTPS 并且让客户端信任证书更规范的做法是使用自建 CA将 CA 证书放到客户端的/etc/docker/certs.d/仓库地址/ca.crt目录。例如访问192.168.1.10:8083就需要把证书放到/etc/docker/certs.d/192.168.1.10:8083/ca.crt。这样 daemon.json 里不需要配置 insecure-registries连接过程依然是加密的。3.5 推送与拉取验证在客户端完成登录并推送镜像docker login 192.168.1.10:8082 docker tag nginx:1.25 192.168.1.10:8082/team-a/nginx:1.25 docker push 192.168.1.10:8082/team-a/nginx:1.25成功推送几个镜像后建议在另一台干净的机器上拉取验证docker pull 192.168.1.10:8082/team-a/nginx:1.25提示Nexus 的匿名访问默认是开启的也就是说不登录也能直接 pull 甚至 push。生产环境建议在安全设置里关闭匿名访问或者至少在仓库配置里限制匿名用户的权限否则等于仓库完全裸奔。4. Harbor搭建Docker镜像仓库实操4.1 Harbor组件构成与工作原理Harbor 的架构比 Nexus 复杂不少部署时会拉起多个容器协作。核心组件包括harbor-core主服务负责 API 接口、用户认证、项目权限、访问控制。harbor-registry基于 Docker Distribution 的镜像存储与分发服务。harbor-dbPostgreSQL 数据库存储用户、项目、镜像元数据等关系型数据。harbor-redis缓存和任务队列支持。harbor-portalWeb 前端页面。harbor-jobservice处理异步任务包括镜像复制、垃圾回收、漏洞扫描。nginx统一入口的反向代理路由所有访问请求。harbor-log集中日志收集。从使用者角度理解Harbor 的 Web 界面是项目维度的。每个项目相当于一个命名空间镜像名通常写成项目名/镜像名:Tag。权限控制精确到项目级别外部用户只能操作被授权的项目这一点比 Nexus 的仓库权限模型直观很多。4.2 证书生成与 harbor.yml 配置Harbor 的部署方式比较固定下载离线安装包改配置执行安装脚本。我建议所有生产环境都走 HTTPS所以第一步先生成自签名证书。首先生成 CA 根证书mkdir -p /root/harbor-certs cd /root/harbor-certs openssl req -newkey rsa:4096 -nodes -keyout ca.key \ -x509 -days 3650 -out ca.crt -subj /CNHarbor-CA然后生成 Harbor 服务器证书这里假设访问域名是registry.internal.example实际规划时替换成你自己的域名openssl req -newkey rsa:4096 -nodes -keyout registry.key \ -out registry.csr -subj /CNregistry.internal.example cat ext.cnf EOF subjectAltName DNS:registry.internal.example,DNS:localhost,IP:192.168.1.10 EOF openssl x509 -req -in registry.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out registry.crt -days 3650 -extfile ext.cnf这里有一处容易出现坑的地方很多人在扩展配置里只写了域名如果客户端恰好用 IP 访问仓库证书校验会报 SAN 不匹配。所以 ext.cnf 里把需要用到的 IP 也加进去最省事。解压离线安装包并复制配置模板wget https://github.com/goharbor/harbor/releases/download/v2.10.1/harbor-offline-installer-v2.10.1.tgz tar xzvf harbor-offline-installer-v2.10.1.tgz cd harbor cp harbor.yml.tmpl harbor.ymlharbor.yml 里关键配置如下hostname: registry.internal.example http: port: 80 https: port: 443 certificate: /root/harbor-certs/registry.crt private_key: /root/harbor-certs/registry.key harbor_admin_password: AdminChangeMe123 database: password: rootChangeMe123 data_volume: /data/harbor注意hostname 必须与证书中的 CN/SAN 对应否则客户端即使信任了证书还是会因为域名不匹配报错。4.3 离线安装与服务启动配置改好后执行安装./prepare ./install.shinstall.sh会自动检查 docker 和 docker-compose 环境拉取依赖镜像然后启动全部 Harbor 组件。启动完成后可以用docker-compose ps查看容器状态正常情况下所有服务都应该是 Up 状态。提示如果机器上用的是docker compose插件而不是旧版docker-compose安装脚本一般会自动识别但如果遇到版本报错检查一下 docker-compose 是否已经正确安装。启动成功后浏览器访问https://registry.internal.example用 admin 账号和 harbor.yml 里配置的密码登录就能看到 Harbor 的管理界面。4.4 项目、用户与 CLI/机器人账号Harbor 的项目权限模型建议这样用先创建一个library项目作为公共基础镜像空间再按团队创建独立私有项目例如team-a、team-b。在项目设置里可以选择“公开”或者“私有”。公开项目允许任意用户拉取镜像适合放 nginx、mysql、redis 这类公共底座镜像私有项目只允许项目成员访问适合放业务镜像。具体操作路径是项目 → 新建项目 → 填写项目名称选择访问级别。创建完成后在项目 → 成员里添加用户并分配角色角色有项目管理员、维护者、开发者、访客等分别对应不同权限。如果是给 CI/CD 系统使用我不建议直接用 admin 账号而应该创建机器人账号。Harbor 里每个项目都可以创建机器人账号并为它指定推拉权限比在 CI 配置里保存明文密码安全得多。客户端推送流程和 Nexus 类似docker login registry.internal.example docker tag nginx:1.25 registry.internal.example/library/nginx:1.25 docker push registry.internal.example/library/nginx:1.25如果客户端还不信任自建 CA同样需要先把ca.crt放到客户端的/etc/docker/certs.d/registry.internal.example/ca.crt目录。4.5 镜像复制与垃圾回收Harbor 的镜像复制功能是我非常推荐优先测试的。它可以实现从一个 Harbor 实例复制镜像到另一个 Harbor 实例也可以从 Harbor 主动拉取公共仓库或者其他 Registry 的镜像。配置路径是系统管理 → 复制管理 → 新增规则。创建规则时需要先配置目标仓库的地址、用户名、密码然后选择复制模式push 模式是源 Harbor 主动推送pull 模式是目标 Harbor 主动拉取。实际项目里我比较常用的做法是开发环境 Harbor 上配置一套 push 规则把library项目里的基础镜像同步到生产 Harbor生产 Harbor 上再配置一套 pull 规则定时从开发环境拉取业务镜像实现灾备和多活。垃圾回收功能用于释放删除镜像后占用的磁盘空间。路径是系统管理 → 垃圾清理 → 立即 GC。这里必须注意删除镜像标签只是把元数据删了存储里的 blob 层并不会立即释放必须等 GC 运行。但 GC 执行期间镜像存储的并发访问可能影响推拉性能所以我会把 GC 安排在业务低峰期手动触发或者配置定时任务定期执行。5. 镜像仓库运维中的常见问题与排查5.1 登录推送时报HTTP / x509 错误这是私有仓库搭建后最常遇到的问题报错通常分两种。第一种是http: server gave HTTP response to HTTPS client。原因是客户端默认走 HTTPS而仓库实际提供的是 HTTP 服务。解决办法就是把仓库地址加入/etc/docker/daemon.json的insecure-registries重启 Docker 后再重新登录。第二种是x509: certificate signed by unknown authority或者x509: certificate is valid for ..., not ...。前者说明客户端不信任你的 CA 证书需要把 CA 证书放到/etc/docker/certs.d/域名/ca.crt后者说明证书的域名或者 IP 与访问地址不匹配检查签发证书时填的 CN 和 SAN。排查时还有一个很实用的技巧别急着在客户端反复试先在服务器上用curl -v https://仓库地址/v2/看看证书和响应是什么如果 curl 都报错客户端大概率也会报错。5.2 删除镜像后磁盘空间不释放很多同学删除镜像后看一眼磁盘发现空间一点没变心里就开始慌。这其实是镜像存储机制决定的。不管是 Nexus 还是 Harbor底层都按内容寻址存储镜像 tag 指向 manifestmanifest 再指向多个 blob。删除 tag 只是把 manifest 标记为删除blob 还需要专门的回收任务去清理。Harbor 的操作路径在上面已经写过删除 tag 后进入系统管理 → 垃圾清理 → 立即 GC。Nexus 则需要在管理界面创建compact blob store任务对 blob 存储执行压缩回收。建议不要在磁盘占用 90% 以上时才去清理那时候 GC 可能因为空间不足跑不动。常态做法是设置监控磁盘使用率超过 70% 就触发提醒然后手动或者定时执行 GC。5.3 并发推拉导致502与超时公司里镜像仓库一旦被 CI 大量使用并发推拉就上来了这时候容易出现502 Bad Gateway或者timeout。先从这几个方向排查。第一查磁盘。df -h看一下仓库数据卷所在磁盘是不是满了如果满了镜像写入会失败nginx 层就会返回 502。第二查 Harbor 的 nginx 日志和 core 日志定位具体是哪个上游服务挂了。第三查 PostgreSQL 和 Redis 的连接数默认配置在高并发下可能出现连接耗尽。Nexus 这边主要查 JVM 内存和线程数内存不足会表现为大量请求堆积然后超时。实测下来我遇到过多次 502 都是因为磁盘写满不是服务真的挂了。所以排查 502 的第一步永远是去看磁盘和日志而不是盲目重启。5.4 备份与迁移的实用思路镜像仓库的数据备份不能只拷贝数据库。Harbor 的元数据在 PostgreSQL 里但真正的镜像 blob 在data_volume目录下两者要一起备份才完整。我一般用两种方式组合平时用 crontab 定时导出数据库同时定期对data_volume做快照或者 rsync 到备份服务器每次大版本升级前直接把整个 Harbor 目录停服拷贝一份确保万无一失。Nexus 更简单整个/nexus-data目录就是全部数据用docker cp或者 rsync 冷备即可。注意在备份前最好让 Nexus 处于低负载状态避免文件在写入过程中被拷贝导致数据不一致。6. 写在最后的一点个人体会把我个人踩坑的经验总结成一句话镜像仓库的价值往往不在部署成功那一刻而在后续半年的稳定运行里。Nexus 轻量、适合和现有制品体系融合Harbor 功能全、适合认真做镜像治理两者没有谁绝对更好。真正让我觉得重要的是把命名规范、权限最小化、备份策略、空间监控这些运维习惯养成而不是等磁盘被写满、仓库被匿名用户改了之后再补救。另外再分享一个小技巧无论选哪种方案建议先在内网用 HTTP 跑通全流程再叠加 HTTPS 和证书校验。这样排错时可以分清楚是网络问题、协议问题还是证书问题不会混在一起无从下手。后面如果有什么新版本的特性或者更顺手的部署方式我会继续在这个系列里补充更新。