要说今年最让我没脾气的场景不是服务挂了而是朋友那边部署新环境拉一个基础镜像花了大半天。屏幕上转圈转了十分钟进度条一动不动问他网络说都正常。后来我说要不自己搭个私有镜像仓库先把常用镜像推上去以后不管是新环境部署还是内网分发都从私服拉。他半信半疑直到我们把第一批镜像传到内网仓库之后再开一台全新机器几十秒就完成“拉基础镜像→启动容器”这一步他才明白这件事该早做。这篇就是把我从零搭建 dockerhub 私服本质上就是自建镜像仓库的过程、踩过的坑、以及后面用了一年总结出来的排错经验全写出来。覆盖面包括最基础的 Registry 方案、HTTPS 证书配置、账号认证、常见报错排查链路以及往 Harbor 迁移时的体验差异。不管你是第一次接触私有仓库还是已经搭过但被证书、磁盘、权限问题折磨过应该都能找到对应的解法。1. 先想清楚再动手私服到底解决了什么问题1.1 不是每一个环境都能“秒连官方源”先说背景。很多团队初期不搭私服觉得直接从公共镜像源拉就行省事。但实际跑起来问题集中在几个地方新机器初始化环境时基础镜像反复从远端下载耗时很长尤其是代理机、离线机房、安全隔离区这类网络受限的环境。构建机需要同时拉很多镜像公共源偶尔限速导致流水线排队时间不可控。团队内部发布的应用镜像散落在各台构建机上缺少统一集中管理的地方。安全审计要求镜像来源可追溯公共源上随便拉一个仓库名都不知道背后是谁维护的。这种情况下私有仓库就不是“备胎”了而是整个镜像供应链的核心节点。所有镜像先推送到私服再由私服统一对外提供分发无需每个人都直接访问外部镜像源。网络开销缩小速度提升安全性和可控性也上了一个台阶。1.2 自建仓库的常见方案分层我把当前常见的“dockerhub 私服”方案归纳成三个梯度方便你根据团队规模选型方案复杂度适合场景典型产品轻量级 Registry低个人、小团队、临时测试Docker Registry v2完整企业级仓库中高多团队、需要镜像回收和项目权限Harbor商业存储适配不稳定已有对象存储、需要跨机房分发各种云原生制品库如果只是一台服务器、两三个人用其实 Docker Registry 镜像就够了五秒钟就能起一个能用的仓库。如果涉及权限细分、镜像保留策略、漏洞扫描那就直接上 Harbor没必要自己写管理端。1.3 别指望自建仓库能完全替代“缓存加速”有一个很重要的事实要先说明自建私服不等于镜像加速器。它解决的是“镜像分发可控”的问题但私有服务器访问远端源比如公共 Docker Hub的速度依然取决于服务器所在网络。如果你墙内服务器访问外部源龟速那么私服拉远端镜像也一样龟速。所以我会用比较务实的组合拳解决搭建私有仓库 配置国内可用的镜像加速器或者在网络较好的环境里先同步好镜像。这篇文章的核心还是私服本身但涉及拉外部镜像的步骤会夹带这个思路。需要强调整个过程都是通过正规配置代理镜像源、DNS、镜像加速等方式实现尽量不依赖脆弱链路。2. 三十分钟跑通一个能用的私有仓库2.1 硬件和操作系统准备我搭私服用的是一台四核八线程、16GB 内存的机器硬盘 1TB 的纯 SSD。说实话这个配置对初期团队来说已经偏剩如果只是给十来个人用2 核 4G 加 500GB 空间都能跑得很顺。镜像仓库本质是一个存储 索引服务限制它上限的主要是磁盘 IO 和网络带宽CPU 要求并不高。存储路径上我会单独留出一个数据分区挂载给 Registry 容器避免系统盘写满影响整机。我的目录规划是这样/data/registry-data # 镜像数据存储目录 /data/certs # HTTPS 证书目录 /data/auth # 认证文件目录有人说直接放 /opt 下也行但对于镜像这个“越积越肥”的东西独立分区是很有必要的后面垃圾回收、磁盘扩容都方便。2.2 启动一个最简 Registry 容器先不聊复杂配置用最小命令跑一个 Registrydocker run -d --name registry \ -p 5000:5000 \ -v /data/registry-data:/var/lib/registry \ --restartalways \ registry:2.8.3这里官方镜像建议不省略 tag避免以后升级行为变化影响线上仓库。此时你已经有一个能用的仓库了注册中心默认没有认证也没有 HTTPS。如果你的环境只是内网临时验证那可以直接用。但千万别把这种裸裸的 5000 端口直接暴露到公网Registry 本身没有内置的认证机制别人知道地址就能往里推镜像。注意官方 Registry 老版本默认REGISTRY_STORAGE_DELETE_ENABLEDfalse意味着你调用 Delete API 删除镜像不会被物理清理。如果你想在测试环境开启删除能力需要给容器加环境变量-e REGISTRY_STORAGE_DELETE_ENABLEDtrue否则一开始删不掉还会误以为是自己操作问题。2.3 给镜像打上能进仓库的 tag仓库起来后想把某台机器上的镜像推进去第一个容易出错的点是 tag 格式。私有仓库的镜像全名必须是仓库地址/镜像名:标签的形式。比如本机有一个web-server:latest镜像需要先打 tagdocker tag web-server:latest localhost:5000/web-server:v1.0.0 docker push localhost:5000/web-server:v1.0.0这里localhost:5000在 SSH 登录的服务器上执行没问题但在开发机推送时应该换成服务器实际可达的 IP 或域名例如192.168.10.20:5000。如果你直接docker push localhost:5000/web-server:latest在另一台开发机上想要拉这个镜像肯定找不到因为开发机上的 localhost 是它自己。这属于 tag 书写意识的问题排查时要先确认。2.4 为什么我推荐固定镜像标签而不是 latest初期图省事大家都喜欢推latest结果没过多久就出问题明明构建机重新 build 并推送了线上拉到的却是旧的。这是因为 Docker 默认的拉取策略本地已有 latest 就不会主动远程拉新。这个问题会让人非常困惑也不好排查。我后来强制规定所有进入私服的镜像必须打语义化版本标签例如app-api:1.2.3然后部署脚本里也使用精确版本。线上需要更新时改变部署文件里的版本号再滚动更新。这样至少保证“每次都拿到和标签匹配的内容”不会被缓存坑。3. HTTPS 证书这一步才是私服能不能落地的分水岭3.1 为什么裸 HTTP 仓库没法舒服地跨机器用默认情况下Docker 客户端只信任 HTTPS 仓库看到 HTTP 仓库时会直接拒绝除非你在每台客户端的/etc/docker/daemon.json上的insecure-registries里填写这个仓库地址。这看似是个小配置实际体验却很糟糕每台需要用这个仓库的机器都要改 daemon.json改完还要重启 Docker 守护进程。一旦仓库换了域名或 IP就要去每台机器上同步修改列表。有安全规范的环境里明文 HTTP 推送镜像本身就过不了合规。所以为了后续不挨个机器改配置我优先解决证书问题。3.2 自签证书的完整生成步骤如果你有正式域名且可以免费申请正规 HTTPS 证书那最省事。但很多团队自建仓库只是用内网域名或 IP没有公网证书。此时可以用自签证书。我用 OpenSSL 生成 CA 和服务器证书命令如下mkdir -p /data/certs cd /data/certs # 生成 CA 私钥 openssl genrsa -out ca.key 2048 # 生成 CA 自签证书 openssl req -x509 -new -nodes -key ca.key -days 3650 \ -subj /CNDocker Private CA -out ca.crt # 生成服务器私钥 openssl genrsa -out server.key 2048 # 创建签名请求 openssl req -new -key server.key \ -subj /CNhub.internal.example.com \ -out server.csr接下来要写一个扩展文件把后面的域名和 IP 都填进去否则浏览器或某些客户端校验会不通过cat ext.cnf EOF subjectAltName DNS:hub.internal.example.com, IP:192.168.10.20 extendedKeyUsage serverAuth EOF openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -days 825 -out server.crt -extfile ext.cnf这里有三个细节容易被忽略扩展字段里一定要包含所有会用到的 IP 和域名缺一个客户端访问时就报证书验证失败。证书有效期不要太短自签场景下的证书是 DevOps 手工维护的忘了续期会导致所有新机器拉取失败。825 天是常见上限不少客户端的兼容性上限也在 825 天左右。不要把CN和 SAN 地址填成两个不同的东西否则某些严格客户端会直接报错。3.3 把证书挂进 Registry 容器生成证书后用挂载方式把它带进 Registrydocker run -d --name registry \ -p 443:5000 \ -v /data/registry-data:/var/lib/registry \ -v /data/certs:/certs \ -v /data/auth:/auth \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/server.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/server.key \ -e REGISTRY_STORAGE_DELETE_ENABLEDtrue \ --restartalways \ registry:2.8.3我把容器内的 5000 端口映射到宿主机的 443 端口这样访问地址就是https://hub.internal.example.com省得每次都带:5000。这个映射对统一发布地址挺重要的对用户来说镜像仓库的 URL 越简单越好。3.4 客户端信任 CA 的操作方式要让每台客户端信任这个自签证书不是简单加insecure-registries。正规做法是把 CA 证书复制到对应操作系统可信根证书目录# 以常见的 CentOS/RHEL 系为例 sudo cp ca.crt /etc/pki/ca-trust/source/anchors/ sudo update-ca-trustDebian/Ubuntu 则是sudo cp ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates然后确认/etc/docker/daemon.json里不需要写insecure-registries直接重启 Dockersudo systemctl restart docker这样做的好处是全局可信不仅 Docker 能用其他需要访问仓库的工具也能共享信任链。我在实际项目中宁愿多花五分钟部署 CA也不愿意直接在 daemon.json 里塞 insecure 列表。后者虽然快但后续扩展到 K8s 节点时每个节点都要维护一份名单极易漏配。4. 从裸奔到带锁给私有仓库加账号认证4.1 用 htpasswd 生成本地认证文件Registry 本身不提供用户注册但支持基于 HTTP Basic Auth 的认证。我在/data/auth目录下用 htpasswd 生成密码文件mkdir -p /data/auth htpasswd -Bc /data/auth/htpasswd opsadmin会交互式要求输入密码。如果要追加用户去掉-c参数htpasswd -B /data/auth/htpasswd developer01-B表示强制使用 bcrypt 加密算法Docker 官方文档强烈推荐这种方式不要用旧版 md5。生成后检查文件格式确认存在一个用户一行符合预期。4.2 让 Registry 开启认证并配合 Nginx 反代Registry 容器里挂载/data/auth目录并添加两个环境变量docker run -d --name registry \ -p 127.0.0.1:5000:5000 \ -v /data/registry-data:/var/lib/registry \ -v /data/certs:/certs \ -v /data/auth:/auth \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/server.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/server.key \ -e REGISTRY_AUTHhtpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALMRegistry Realm \ -e REGISTRY_AUTH_HTPASSWD_PATH/auth/htpasswd \ -e REGISTRY_STORAGE_DELETE_ENABLEDtrue \ --restartalways \ registry:2.8.3为了更方便做 TLS 终止和其他安全配置我的服务器上还用了 Nginx 反代把对外的 443 流量转发到内网 Registry 的 5000。Nginx 配置关键部分如下server { listen 443 ssl; server_name hub.internal.example.com; ssl_certificate /data/certs/server.crt; ssl_certificate_key /data/certs/server.key; client_max_body_size 2g; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }切断 5000 端口对外暴露后外部只能走带 TLS 的 443。client_max_body_size必须设置够大否则推镜像时 Nginx 会返回 413 Request Entity Too Large这个坑很典型。4.3 docker login 常见的 401/403 排查思路开启认证后正常拉取镜像前需要先登录docker login hub.internal.example.com可能遇到的问题我按出现频率排一下输入正确的用户名密码却报 401检查 htpasswd 文件是否放在容器内正确路径注意容器重启后挂载目录是否会替换掉原路径。推镜像时报 403先看目标仓库是否存在。Registry 默认开放匿名读但匿名写会被拒。确保已经登录且用户名有权限。登录后马上又失效检查客户端和服务端的系统时间是否一致证书时间对不上也会触发异常安全校验。认证体系建议小步走先验证本机curl -u user:pass https://hub.internal.example.com/v2/能通再走 docker login。这样可以把“网络链路问题”和“Registry 认证问题”切成两段去判断。5. 用了一年我整理出的常见问题排查链路5.1 拉镜像超时不一定是网络问题先看一个现象执行docker pull hub.internal.example.com/app:1.0卡了很久然后报dial tcp: lookup hub.internal.example.com: no such host。这是 DNS 没解析到。但还有一种非常隐蔽的超时DNS 解析正常TCP 也能连上数据却一直拉不动。这时我会先在能正常访问仓库的机器上执行curl -v https://hub.internal.example.com/v2/如果返回正常的{}或认证提示说明仓库本身没问题。再看客户端这边很多情况是公司内网防火墙拦截了对仓库上大文件的下载。排查手法也很简单拉一个小镜像测试是否通畅再拉一个 200MB 以上的镜像测试如果小的能通、大的卡死大概率是流量策略问题而不是 Registry 配置问题。5.2 push 报错 405 Method Not Allowed可以往删缓存方向查有一次同事 push 镜像进度条很快走完突然报 405。这个问题容易被误判成认证问题或权限问题但我观察下来最常出现在 Nginx 反代已经去掉头部、转发目标变成“只读模式”的时候。Registry 支持通过配置管理只读状态常见配置项如auth: htpasswd: realm: basic-realm path: /auth/htpasswd storage: delete: enabled: true如果 team 用配置管理工具统一下发某个配置把delete.enabled隐藏或覆盖了推送时就会出现 405。排错顺序建议是确认配置文件里 deletion 没有 false。确认是否有镜像仓库页面开启了“禁止推送”策略。确认 Nginx 反代层没有把PUT请求意外改写。5.3 磁盘满了Registry 打印不停流转镜像仓库最经典问题就是磁盘爆掉。我见过最严重的一次/data 分区 100%Docker 守护进程和 Registry 日志疯狂刷no space left on device。这时别说推镜像连重启 Registry 都可能失败。解决思路分三个步骤第一步先看大文件。有时候是容器日志不是仓库数据du -sh /var/lib/docker/containers/*/第二步如果确实是/data/registry-data占满可以启用 Registry 的垃圾回收docker exec registry registry garbage-collect --delete-untaggedtrue /etc/docker/registry/config.yml这命令会把不再被任何清单引用的层和镜像删除。要特别留意的是执行垃圾回收会让仓库进入只读状态期间写入操作会失败。最好安排在业务低谷期执行。第三步ar 如果回收后空间仍然吃紧说明 tag 保留策略没做好。需要结合 Harbor 或外部脚本做镜像数量上限清理不能靠纯手工。5.4 一个容易被忽略的事inode 满了有一次分区df -h显示只用了 60%但是拉取镜像失败系统报No space left on device。我用df -i一看inode 已经耗尽。因为仓库里存了海量小标签和碎片文件元数据节点先撑爆了。这种情况光删镜像没用需要把垃圾层清掉甚至重新格式化仓库数据目录。我后来会在监控上同时加磁盘空间和 inode 使用率两项指标避免再次被偷袭。5.5 并发拉取镜像打满带宽后的连锁反应团队从几十人扩张到一百多人的时候内网同时有大量构建任务私服带宽被打满所有拉镜像操作都变慢。这个问题的本质是部署网络拓扑缺乏分流层。我在私服前面加了简单的带宽缓存机制常用镜像在构建机本地留存构建时优先复用本地缓存同时限制每个客户端的并发拉取数避免单台机器一次性拉十几个镜像占完带宽。Docker 客户端可以通过--max-concurrent-downloads参数调节并发配合调度系统能明显缓解带宽压力。6. 如果 Registry 不够用Harbor 是更舒服的升级方向6.1 到了多团队协作阶段Registry 的痛点会越来越明显单用 Registry 时所有用户共用一个账号日志不区分人镜像删除后的垃圾清理也要手工操作没有 Web 界面想看看仓库里有几个项目只能靠 curl API 或装额外工具。这时候上 Harbor 是比较顺理成章的选择。Harbor 本质上是在 Registry 之上封装了认证、项目隔离、WebUI、垃圾回收、漏洞扫描等能力。部署好之后开发者面前多了一个可视化管理入口不再只有漆黑的命令行.6.2 安装 Harbor 的几个关键步骤与坑Harbor 的安装包自带 docker-compose 编排流程上比较清晰下载 release 包 - 修改 harbor.yml - 执行 prepare - 启动。安装前建议用一台干净的机器跑因为它会直接占用部分 Docker 资源和已有 Registry 混在一起容易造成端口冲突和存储互相干扰。我踩过的坑主要有三个harbor.yml 里的 hostname 必须是客户端实际访问的域名不能随手填一个localhost否则 WebUI 登录没问题但 Docker 客户端 push/pull 会解析错地址。默认配置是 HTTP 模式需要把https部分的证书路径改成自己的证书并关闭 HTTP 入口否则对外发布时端口和协议都很混乱。安装目录尽量独立不要放在系统 root 分区下Harbor 自带的数据库和镜像数据体积增长很快。6.3 Harbor 的镜像垃圾回收比 Registry 优雅很多Harbor 在界面上可以直接设置垃圾回收任务还可以设置策略只保留最近 X 个版本、自动清理无 tag 镜像等。相比 Registry 手工跑命令少了误操作风险。但要注意Harbor 底层依然是 Registry 的 storage 格式所以底层磁盘监控不能少界面上回收垃圾不会 100% 释放空间它只是清理元数据和未被引用的层。最终系统层面还是要看分区实际占用。6.4 从 Registry 迁移到 Harbor 的平替思路如果你已经有一段很长的历史镜像在 Registry 里想迁到 Harbor有两个可行办法直接把/data/registry-data的存储目录整体迁移到 Harbor 使用的数据卷注意目录结构兼容性。用工具逐个仓库 pull 下来再 push 到 Harbor慢但可控。我建议小团队直接选第二种顺便借此机会梳理一遍镜像清单把没必要保留的旧版本清掉迁移完的仓库更健康。如果你是几百个仓库的大规模场景再考虑存储层直接挂载迁移。7. 最后补几句维护私服的习惯这类服务低调到几乎没存在感就是最好的状态但越低调越容易被遗忘维护。下面几条是经历了故障之后才养成的习惯。第一给私服磁盘挂上监控同时看空间使用率和 inode 使用率两条指标缺一不可。很多人只看空间踩过 inode 爆掉的坑才反应过来。第二定期执行垃圾回收。我都习惯把它放到每周六凌晨跑期间跳过发布窗口避免写入冲突。每次跑之前先做一次网络和存储状态的快照。第三维护一份镜像白名单。像基础镜像、中间件镜像、线上应用镜像按不同的前缀放到不同项目空间下避免所有人乱推最后仓库变成一个版本垃圾桶。第四别把/data/registry-data放在临时目录或换机器时才想起来迁数据。它在镜像仓库体系里和代码仓库一样重要丢失后虽然有 tag 列表但历史元数据恢复起来很痛苦。如果只想抓到几条自己适用的经验那我就说一句证书的坑必须一次性做对镜像标签不要全用 latest磁盘监控要提前配好。这三个地方往往是私有仓库从“能用了”走向“很好用”的分水岭。