去年我们容器化项目刚上线那会儿,运维同事半夜给我打电话:一批新节点同时从公共仓库拉同一个基础镜像,拉了一个多小时还没完,有一半节点直接报连接超时。那晚之后,我意识到“企业内自搭建容器镜像服务”这件事,不只是省流量的优化项,而是上线稳定性的底线。
这篇文章就围绕这个话题展开,聊聊我在企业内自建 Docker 镜像服务的完整过程:什么时候需要自建、仓库方案怎么选、Docker 环境怎么一次装对、Harbor 怎么落地,以及上线之后一定会遇到的那些坑。无论你是刚接手容器化的小团队运维,还是准备做私有化交付的开发,内容都能直接照着抄。
1. 企业在什么情况下需要自建容器镜像服务:先对需求做减法
我见过不少团队上来就部署私有仓库,理由是“大家都说有需要”,结果跑了一年,仓库里就那么几个镜像,纯属给自己加维护负担。自建镜像服务之前,先算清楚账。
1.1 公共镜像仓库的三大痛点
第一个痛点是拉取速度。我们那次事故就是典型,几十台机器同时从公共仓库拉镜像,公共仓库不会给企业内网任何优先级,高峰期连接超时、下载中断是常态。即便单次拉取还算正常,一旦遇到扩容、发布集中时段,慢就是原罪。
第二个痛点是供应链安全。团队里总会有人图省事,从第三方镜像博客、论坛下载镜像来用,或者直接拉一个作者不明的最新标签。这些镜像里的底层依赖、环境变量、入口脚本,经过扫描没有?出过供应链投毒事件吗?企业环境里,镜像来源不可控等于把生产系统的大门敞开。
第三个痛点是私密性与追溯。业务镜像里隐含了内网IP、配置、代码片段,推到公共仓库即使在私有项目下,心里也不踏实。而且上线出问题要追责时,公共仓库给不了你细粒度的审计日志。
总结成一句话:镜像拉不动、镜像信不过、镜像追不到,这三条里占两条,自建镜像服务就有必要了。
1.2 自建前先确认这四件事
第一,镜像数量和团队规模。如果团队三五个后端,只有两三个业务镜像,那不一定需要 Harbor 这种重量级方案,后面选型部分我再展开。
第二,权限模型。公司对发布权限有要求吗?比如开发能推、测试只能拉、运维能删,这直接决定了仓库软件需要具备多强的 RBAC 能力。
第三,网络环境。机房能访问公共仓库吗?离线内网环境如何同步基础镜像?这个不搞清楚,仓库搭起来也是空壳。
第四,存储与备份。镜像仓库吃磁盘,尤其要存大量业务镜像和历史版本时,磁盘扩容、备份策略要在选型前想好。
确认完这四件事,你对自己的需求画像就清晰了:到底是要一个“能放镜像的网盘”,还是要一套“镜像交付平台”。
2. 镜像仓库选型对了吗:Docker Registry、Harbor与轻量级方案
选型这件事,我见过太多人拍脑袋。有人因为 Docker Registry 是官方出品就选了,结果上生产后没权限控制、没 UI,哭着搭跳板机来管理;也有人小团队就上 Harbor,天天为升级维护头疼。方案只有合不合适,没有哪个绝对更好。
2.1 三种主流方案的横向对比
我这里把最常见的三种方案放在一张表里来对比:Docker Registry、Harbor、Nexus Repository Manager。
| 对比维度 | Docker Registry | Harbor | Nexus Repository Manager |
|---|---|---|---|
| 安装复杂度 | 低,两条命令 | 中,需 Docker Compose 部署及 prepare 流程 | 中,Java 环境,需 JVM 调优 |
| Web 界面 | 无,仅 API | 有,功能完整,中文友好 | 有,但界面偏传统 |
| RBAC 权限 | 无,只支持 token,无项目概念 | 有,项目级角色:管理员/开发者/访客 | 有仓库级权限,配置偏复杂 |
| 镜像复制 | 可用第三方工具 | 支持多目标复制、定时复制 | 支持代理与复制,但偏向制品库 |
| 漏洞扫描 | 无 | 内置 Trivy | 依赖插件,需要额外配置 |
| 维护成本 | 低 | 中,组件多如 PostgreSQL、Redis、Trivy | 中高,JVM 和依赖问题常伴 |
| 适合场景 | 小团队、临时环境 | 企业级镜像交付平台 | 多类型制品集中管理 |
值得注意的是,Nexus 在企业里更多是当通用制品库用的,Maven、npm、Docker 通吃。如果公司已经有了 Nexus 且维护得不错,不必非要再叠一套 Harbor,直接在 Nexus 里启用 Docker 仓库也能用。但如果你要的是“容器镜像服务”这个独立能力,Harbor 更聚焦,权限模型也更贴近容器团队的使用习惯。
2.2 为什么我推荐Harbor作为企业级起点
我前后帮三个团队搭过镜像仓库,最终都落在 Harbor 上。原因很现实:Harbor 是开源项目里把“权限、追溯、复制”这三件事开箱即用的方案。
按项目建空间这个功能尤其关键。开发团队各自一个项目,A 团队推送镜像时默认只有本团队和指定测试组能拉取;新人入职只需要被加到对应项目里,不需要东奔西走改配置文件。另外 Harbor 的审计日志能查到“谁在什么时间删除了哪个 tag”,这对线上追责太重要了。
Harbor 的多实例复制也值得单独说一句。我们后来做了主备两个节点,主节点挂了,备节点还能继续提供拉取服务,客户端侧配置多个 registry 地址即可。整体上,安全性、可控性、功能完整度,Harbor 对企业场景的覆盖是最均衡的。
2.3 一次性理清部署规模与资源预估
小规模团队,比如五六个开发、一两个项目,我建议 4 核 8G 起步,存储按 500G 预估。这个配置能支撑 Harbor 自带的 PostgreSQL、Redis 和 Trivy,虽然内存会稍微紧张,但日常完全够用。
如果团队超过二十人、镜像数量上千,或者要跑自动化流水线频繁推拉,建议 8 核 16G 起步,存储 1T 以上,并把数据盘单独挂载。镜像仓库最怕的就是日志和镜像抢同一块磁盘空间,系统盘被写满后,Docker 的所有操作都会异常。
注意:Harbor 的安装包分在线和离线两种,企业内网建议直接下载离线安装包。离线包大概 1GB 左右,安装时不需要再去拉取依赖容器镜像,省很多事。软件的获取,正常情况下从官方渠道下载即可。
3. 先把Docker底座扶稳:绕开Docker安装中的高频坑
镜像服务搭建的前提是 Docker 本身要稳定。我排查过的多数“镜像服务连不上”“仓库部署失败”问题,最后都倒在了最基础的 Docker 安装和环境配置上。这里集中讲三个高频坑。
3.1 报错virtualization support not detected,如何定位到具体原因
Docker Desktop failed to start because virtualisation support wasn't detected这个报错,Windows 上装 Docker Desktop 的人十有八九见过。问题本质是 Docker Desktop 需要硬件虚拟化支持,但系统层面没有把开关打开。
排查按这个顺序来:先重启进入 BIOS/UEFI,找到 Intel VT-x 或 AMD-V 相关选项,确认开启。然后回到 Windows,按下 Win+R 输入optionalfeatures,在“启用或关闭 Windows 功能”中勾选“虚拟机平台”和“适用于 Linux 的 Windows 子系统”,重启电脑。
我踩过的一个隐性坑是“快速启动”导致系统没真正重启,BIOS 已经改了,但 Windows 的虚拟化平台没初始化,一直报同一个错。解决办法很笨但有效:设置里关掉“启用快速启动”,彻底关机再开机。如果是在虚拟机里跑 Docker Desktop,还需要宿主机开启嵌套虚拟化,否则虚拟机里的 VT-x 是等不到的。
3.2 Linux主机上Docker服务启动失败的排查三板斧
Linux 服务器上 Docker 服务起不来,最常见的报错是failed to start docker application container engine.。别急着重新安装,按三板斧来。
第一板斧:看日志。执行journalctl -u docker -n 100,不要只看 systemctl status 的五行摘要。绝大多数情况下,真实原因就在日志末尾几行。
第二板斧:检查 daemon.json 格式。这是我最常发现的问题:多打一个逗号、少了一个引号、把注释写进 JSON,Docker 直接起不来。改完配置务必用dockerd --validate或者技术手段校验 JSON 格式,再重启服务。
第三板斧:查网桥冲突。内网机器经常出现 docker0 网桥和已有网段冲突,导致 Docker 启动时创建网络失败。解决方式是在/etc/docker/daemon.json中显式指定"bip": "10.88.0.0/24",避开冲突网段。
3.3 别再sudo一切了:Docker权限问题的正确解法
permission denied while trying to connect to the docker api at unix:///var/run/docker.sock这条报错,出现率极高。原因是当前用户不在 docker 用户组里,对 docker.sock 没有访问权限。
常用解法是sudo usermod -aG docker 用户名,然后重新登录当前会话,报错就消失了。但我必须提醒一句:加入 docker 组的用户等同于拥有 root 权限,因为 Docker 本身可以挂载宿主目录、执行特权容器,生产环境要谨慎,别给所有开发人员都开 docker 组。
更稳妥的做法是使用 sudo 统一授权,或者在 CI 脚本里使用专门的 CI 账号并严格限制权限。我在真实环境里见过有人为了让某应用能操作 Docker,直接chmod 777 /var/run/docker.sock,这跟把 root 密码贴门口没什么区别,千万不要这么干。
4. 完整落地:用Harbor搭建私有镜像仓库并跑通上传与拉取
这一章进入实操。我会按我自己搭 Harbor 的完整流程来写,从环境准备、证书生成到启动验证,最后跑通上传和拉取。
4.1 部署前的准备工作与常见误区
准备工作有两件:一是确保 Docker 和 Docker Compose 插件可用,二是给 Harbor 规划一个固定内网域名,比如registry.example.local。域名要提前定,后续所有机器上的镜像地址都会用到它,后期改域名是件伤筋动骨的事。
很多教程让你直接用 HTTP 跑 Harbor,图省事,但我不建议。镜像在传输过程中是明文,在内网环境里也许尚可,但一旦接入 CI/CD、多机房同步,HTTP 的隐患会放大。正确做法是生成自签 HTTPS 证书或直接使用企业内部 CA 签发证书。
注意一个常见误区:Harbor 的install.sh会自动检查端口占用。如果 80 或 443 端口已被 Nginx 或其他应用占用,安装会失败。提前把端口规划好,Harbor 官方默认会占用 80 做 HTTP 重定向。
4.2 生成HTTPS证书并完成Harbor安装
我用自签证书流程演示,企业有 CA 的替换成 CA 签发即可。先生成私钥和自签名证书:
openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -sha256 -days 3650 \ -subj "/CN=registry.example.local" \ -key ca.key \ -out ca.crt openssl genrsa -out registry.key 2048 openssl req -new \ -subj "/CN=registry.example.local" \ -key registry.key \ -out registry.csr openssl x509 -req -in registry.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -days 3650 -sha256 \ -extfile <(printf "subjectAltName=DNS:registry.example.local") \ -out registry.crt然后解压 Harbor 离线包,编辑harbor.yml:
hostname: registry.example.local http: port: 80 https: port: 443 certificate: /data/cert/registry.crt private_key: /data/cert/registry.key harbor_admin_password: Harbor12345 database: password: root123 data_volume: path: /data/harbor建议把证书放到/data/cert下,数据盘独立挂载到/data。执行./prepare生成配置文件,然后./install.sh。安装过程中会自动拉起 PostgreSQL、Redis、Trivy 等组件。启动后用docker ps检查容器状态,有Exited状态的容器就看docker logs定位问题。
4.3 终端登录、上传镜像与客户端信任配置
Harbor 起来之后,终端做一次完整的镜像推拉演练。先登录:
docker login registry.example.local -u admin -p Harbor12345给本地镜像打上完整仓库地址标签,并推送:
docker tag nginx:1.25 registry.example.local/library/nginx:1.25 docker push registry.example.local/library/nginx:1.25这里有个关键点:nginx:1.25 这个 tag 里没有指定仓库地址,推送前必须补齐registry.example.local/library/前缀,否则 Docker 会尝试推到默认的公共仓库,然后报权限错误。
客户端机器拉取镜像前,需要信任刚才生成的 CA 证书。没有配置企业 CA 的环境,可以临时在 Docker 配置里加一条insecure-registries:
{ "insecure-registries": ["registry.example.local"] }改完重启 Docker。但注意,insecure-registries仅适合快速联调,生产环境一定走正式 CA 信任链。
5. 接入业务:微服务项目怎么通过Compose与私有仓库做交付
镜像仓库搭好,要真正解决业务问题才算完。这一章讲的是把私有镜像仓库接入日常发布流程,让开发、测试环境用一条命令就能拉起整套服务。
5.1 一个真实场景:MySQL、Redis、业务镜像都从私有仓库拉
我之前接触过一个典型的业务交付场景:一套系统依赖 MySQL 8.0、Redis 主从、两个业务微服务。上线前把相关镜像全部整理进 Harbor,测试环境通过 Docker Compose 一键拉起。
MySQL 和 Redis 这类中间件镜像,从官方源拉取后重新打标签推入私有仓库。比如:
docker pull mysql:8.0 docker tag mysql:8.0 registry.example.local/common/mysql:8.0 docker push registry.example.local/common/mysql:8.0Redis 主从也一样处理。两个业务微服务则由构建流水线在 CI 阶段直接打包推送到 Harbor 的各自项目下。整体好处是:测试环境不依赖外网,拉取速度稳定,版本可控。
5.2 docker-compose.yml里的image地址与版本策略
在 compose 文件里,镜像地址必须写完整,这是团队新人在很长时间内最容易犯的错。下面是示范片段:
services: mysql: image: registry.example.local/common/mysql:8.0 container_name: app-mysql environment: MYSQL_ROOT_PASSWORD: secret ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql networks: - app-net redis-master: image: registry.example.local/common/redis:7.0 container_name: app-redis-master command: redis-server --appendonly yes networks: - app-net business-api: image: registry.example.local/business/api:1.2.3 depends_on: - mysql - redis networks: - app-net注意两个细节。第一,标签要写1.2.3这样的固定版本,不要用latest,否则生产环境无法复现同一份代码。第二,容器间通信时不要用 localhost,使用 compose 里的服务名,比如业务API里连接 Redis 的主机地址写redis-master,而不是127.0.0.1。
5.3 镜像下载慢的彻底解法:内网缓存与mirror配置
私有仓库解决了内网分发问题,但首次把一个新镜像从上游拉到内网,该慢还是慢。这里两步走。
第一步,在 Harbor 里配置代理缓存项目。Harbor 支持把上游镜像仓库作为远端仓库,创建一个代理项目后,团队从内网拉公共镜像,实际上是 Harbor 在后台拉取并缓存下来。第一次拉取慢,之后内网分发就是秒级。
第二步,客户端配置registry-mirror。在每台 Docker 主机的/etc/docker/daemon.json中加入:
{ "registry-mirrors": ["https://registry.example.local"] }重启 Docker 后,拉取镜像时 Docker 会优先请求内网 mirror。注意,我更多建议直接用 Harbor 代理项目来解决“拉取上游镜像”的诉求,因为registry-mirror不能覆盖所有场景,比如带认证的镜像仓库就无法优雅地走普通 mirror。
经验之谈:不要一次性让几十台节点同时拉同一个新镜像,它们会同时触发 Harbor 回源,直接把内网带宽打满。我先在一台机器上拉好镜像、推送到 Harbor,再通知其他节点从 Harbor 拉,这种“预热”操作比任何调参都好使。
6. 排障实录:镜像仓库运维一年的典型问题速查
最后这些内容,都是我这一年来在镜像仓库日常运维中实际遇到的问题,写成一份能快速定位的速查表,遇到类似报错直接对号入座。
6.1 镜像拉取慢、超时与回源冲突
镜像拉取慢,第一步不是调参会参数,而是先判断慢在哪一段。分别在内网访问 Harbor 域名看响应速度,再在 Harbor 所在服务器上测试到上游仓库的连通性。如果是 Harbor 回源慢,优先排查代理项目配置和目标仓库地址是否正确。
docker image pull超时还有一个容易被忽略的原因:标签写错。镜像名不存在时,Docker 会反复重试,看起来像超时,实际是 404。使用已经推送到 Harbor 的完整地址,不要随意编 tag。
并发回源冲突我上面提过。如果公司有多个项目同时用同一个新镜像,建议由运维统一在 Harbor 里先拉一次,避免节点各自回源。想要主动调整下载带宽,还可以在 daemon.json 里设置max-concurrent-downloads适当限制并发数。
6.2 Docker容器之间网络不通的排查思路
docker网络不通几乎是新团队的标配问题。业务容器和数据库容器都跑在同一台宿主机,业务容器里连127.0.0.1访问 MySQL,肯定失败,因为 MySQL 不在业务容器自己的网络命名空间里。
正确做法是把容器放在同一自定义网络里:
docker network create app-net docker network connect app-net app-mysql docker network connect app-net business-api或者直接把 compose 文件定义了app-net,所有服务挂上去。容器内通过服务名通信,Docker 内置 DNS 负责解析。还有种情况是宿主机开了防火墙,阻断了容器跨主机通信,尤其是多节点 Docker Swarm 或跨宿主机场景,要在防火墙上放行容器网络对应的端口。
排查时我一般先执行docker network inspect 网络名,看有哪些容器在网络上,再进容器里用ping或telnet验证连通性,一步步缩小范围。
6.3 磁盘占满、权限报错与Harbor垃圾回收
镜像仓库和 Docker 主机最大的隐形杀手就是磁盘。我见过雪崩式的故障:开发环境跑了几十套 compose 项目,镜像越堆越多,最终磁盘 100%,Docker 服务直接卡死。先看占用情况:
docker system df这个命令会列出镜像、容器、卷、构建缓存各自的占用。日常清理用docker image prune -a删除悬空镜像,docker system prune -af则更激进,连停止的容器、未使用的网络都会一并清掉。注意 prune 有风险,确认没有需要的容器再执行。
Harbor 数据卷也需要定期管理。Harbor 删除镜像后,实际存储层不会立即释放空间,要在 Harbor 管理界面执行“垃圾回收”。第一次跑 GC 时会发现释放出大量空间,这是正常现象。建议把 GC 作为月度任务固定下来。
权限报错除了前面说的 docker.sock 问题,还可能是 Docker 服务运行用户和 socket 文件权限被改动导致。如果排查后报错依旧,重启 Docker 服务前,先确认/var/run/docker.sock的属主和 660 权限没有被篡改。
我在实际维护过程中最大的体会是:搭建镜像仓库只是起点,真正的日常功夫在于把流程定下来,让每个团队的镜像都有统一的命名规范、固定版本、专人审批。自建容器镜像服务没什么高深技术,但它能逼着你把“谁来推送、谁能拉取、坏了怎么恢复”这些最现实的问题想明白,而想明白这些问题,往往比仓库存放了多少镜像更重要。
最后分享一个小技巧:我后来给 Harbor 加了一条定时任务,每天凌晨执行数据库备份和镜像仓库 GC 前的安全快照。运行半年基本不太需要操心,偶尔出问题也能从备份快速恢复。如果你刚开始落地企业内自建镜像服务,先从这套节奏开始,稳定住再迭代。