
搞 Docker 这些年我越来越觉得私有仓库是迟早要自己搭一遍的东西。之前一直在公共镜像仓库上拉官方镜像速度不稳定不说团队内部构建出来的镜像想统一管理总不能一个个 tar 包传来传去。后来在几台内网服务器上搭了 Docker 私有仓库Docker Registry整个镜像流转效率明显不一样了。这篇就把我用 Docker 快速部署私有仓库的完整过程写出来从最基础的 registry 容器到加证书、加账号认证再到上线后常见的坑一次讲清楚。这篇内容适合想在内部环境搭建镜像仓库的运维、后端开发也适合刚接触 Docker 没多久、想把镜像统一管起来的同学。你会看到一套“先用起来再逐步完善”的搭建路径而不是一上来就抛一堆重组件。1. 部署之前先想清楚你到底需要什么样的私有仓库1.1 轻量 Registry 和完整制品库怎么选Docker 官方的私有仓库镜像叫registry它实际上是一个很纯粹的镜像存储与分发服务只提供一套 HTTP API也就是常说的 Registry V2 协议没有网页界面也没有复杂权限体系。它的特点是轻、稳、部署快一个容器起来就是仓库。与之对应的是 Harbor它在 Registry 外面包了一层完整的企业级能力Web 管理界面、多项目隔离、RBAC 权限、漏洞扫描、镜像复制、Quota 配额等等。部署起来要依赖 PostgreSQL、Redis组件更多适合需要多人协作、安全审计比较严格的公司内部使用。我的建议是先按规模和诉求来定。个人学习、小团队内部用、边缘节点同步镜像直接上registry:2就行。如果你在的是一个二三十人的研发团队项目多、角色杂、要给人开不同权限那这个时候再上 Harbor 也完全不迟。没必要一上来就把系统搞得很重因为私有仓库的复杂度不是来自镜像本身而是来自“谁有权限推拉、镜像放哪、怎么审计”这些管理问题。这里我给一个对比方便你决策维度Docker RegistryHarbor部署复杂度一个容器搞定需要 Postgres、Redis 等多个组件Web 界面无有完整管理后台权限控制依赖 htpasswd 或外部认证项目级 RBAC镜像删除/GC调用 API 手动触发 GC界面操作 自动回收机制适合场景个人/小团队/边缘节点中大型研发团队1.2 私有仓库到底解决了什么问题公共镜像仓库能解决“从网上拉镜像”的问题但解决不了内部镜像怎么统一管理的问题。我举几个实际场景你就明白了第一内网下载速度快。服务器在自己机房或者云上内网客户端从私有仓库拉镜像走的是内网带宽几十 GB 的基础镜像秒级到分钟级拉完比从公网一个个层拖下来体验好太多。第二镜像可追踪、可复用。团队里构建好的应用镜像统一推到私有仓库后测试、生产环境按 tag 拉取版本清晰不会出现“你这镜像哪来的”“这包是不是最新的”这种问题。第三离线环境能交付。很多内网生产环境完全隔离外网这时候私有仓库就是一个“镜像中转站”。在有网的机器拉好镜像推送到内网私有仓库生产机器再从私有仓库拉整个交付链路就通了。私有仓库经常被比作“镜像的 GitHub”。这个比喻很贴切代码要托管到统一的 Git 仓库镜像也应该有统一的存储和版本管理。唯一的区别是GitHub 是公网服务镜像仓库你可以用 Docker 在自己服务器上 5 分钟搭一个。1.3 整体架构长什么样先把最小架构说清楚。你只需要一台装好 Docker 的服务器在主机上创建三个目录数据目录比如/opt/registry/data用来存放镜像层数据。证书目录比如/opt/registry/certs存放 HTTPS 证书和私钥。认证目录比如/opt/registry/auth存放账号密码文件。然后启动一个registry:2容器把宿主机的 5000 端口映射到容器内的 5000 端口把上面三个目录分别挂载进去再通过环境变量告诉 Registry 证书和认证文件的位置。为什么把目录挂载出来因为容器本身是脆弱的你随时可能删掉重建。如果镜像数据直接写在容器可写层里容器一删仓库里的所有镜像也跟着没了。把数据放在宿主机目录容器怎么重建都没关系。为什么端口用 5000这是 Docker Registry 的默认端口官方文档、各种客户端工具都默认认这个端口不用额外记忆。生产环境我强烈建议加 HTTPS。不加 HTTPS 的话所有流量都是明文镜像内容在网络上裸奔这在内网还好说一旦跨网络传输就是安全隐患。而且加了 HTTPS 之后客户端配置反而更简单不用给 Docker daemon 设insecure-registries只要信任你的自签名证书就行。2. 最简可用版5 分钟把 Registry 跑起来2.1 拉镜像、起容器、先验证端口通不通先在宿主机上拉取官方镜像docker pull registry:2然后直接启动一个最简容器。这一步先不配证书和认证只为了让环境先通起来docker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ --restartalways \ registry:2启动之后用下面的命令验证curl http://localhost:5000/v2/_catalog正常情况下会返回一个空 JSON类似{}表示服务正常。如果你在公网上或者跨机器访问记得先确认宿主机防火墙放行 5000 端口云服务器还要在安全组里加规则。这一步经常被忽略导致本机 curl 正常别的机器就是连不上。如果你的 Docker 权限有问题比如执行 docker 命令时报Got permission denied while trying to connect to the Docker daemon socket那是因为当前用户不在 docker 用户组里。执行下面的命令把用户加进去sudo usermod -aG docker $USER然后退出终端重新登录再执行 docker 命令就正常了。这个问题在刚接触 Docker 的机器上非常常见顺手记一下。2.2 数据卷、证书目录和配置项说明最简版跑通了但还差两步才敢长期用一是把证书目录和认证目录挂进去二是把关键配置项通过环境变量注入容器。Registry 容器内的配置文件路径是/etc/docker/registry/config.yml。你不需要去改这个文件Registry 支持用环境变量覆盖配置命名规则是把配置文件里的层级结构用下划线连起来。比如配置里的http: tls: certificate: /certs/domain.crt key: /certs/domain.key对应的环境变量是REGISTRY_HTTP_TLS_CERTIFICATE和REGISTRY_HTTP_TLS_KEY。这种“配置文件分段 环境变量覆盖”的设计很常见好处是容器的镜像本身不用改所有个性化配置都在启动命令里。这也是为什么我一直推荐用docker run或docker compose来管理 Registry而不是把配置写死在镜像里。挂载目录时有个细节容器内 Registry 的数据目录是/var/lib/registry这个路径千万别记错。有些同学挂载成/var/lib/docker或者随便挂一个目录结果启动没报错但是容器重建之后镜像全没了就是这个路径搞错了。容器启动参数我建议固定加--restartalways。Registry 作为基础服务机器的 Docker 服务重启了、或者宿主机重启了容器要能自动拉起来。不加这个参数哪天机器重启你的仓库服务就一直挂着客户端那边只会收到连接失败的报错。2.3 本地推镜像走上 HTTPS 或明文信任现在仓库服务已经起来了但你直接用docker push推镜像大概率会失败。原因很简单Registry 只提供 HTTP 明文服务而 Docker 客户端默认用 HTTPS 访问仓库地址。两者不匹配Docker 会直接拒绝连接。解决办法有两个给 Registry 配上 HTTPS 证书或者在客户端 Docker daemon 里把仓库地址加入insecure-registries。二选一但我的建议很明确自己生成一套自签名证书配 HTTPS。原因后面会细说。先生成证书。我在服务器上通常会创建一个certs目录然后用 openssl 生成自签名证书命令如下mkdir -p /opt/registry/certs cd /opt/registry/certs openssl req -newkey rsa:2048 -nodes \ -keyout domain.key \ -x509 -days 365 \ -out domain.crt \ -subj /CN192.168.1.100 \ -addext subjectAltNameDNS:localhost,IP:127.0.0.1,IP:192.168.1.100其中的192.168.1.100换成你实际的服务器 IP。如果你有域名也可以用域名来访问那-subj和subjectAltName里的值改成域名就行。自签名证书的有效期默认我写的一年实际生产环境你可以根据自己的证书管理制度调整天数。证书生成之后重新创建 Registry 容器把证书挂载进去并用环境变量指定docker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/certs:/certs \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/domain.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/domain.key \ --restartalways \ registry:2接下来的问题就是客户端怎么信任这张自签名证书。Linux 上的 Docker 守护进程dockerd会读取/etc/docker/certs.d/仓库地址/下的证书。以192.168.1.100:5000这个仓库地址为例需要这样做mkdir -p /etc/docker/certs.d/192.168.1.100:5000 cp domain.crt /etc/docker/certs.d/192.168.1.100:5000/ca.crt复制完证书后重启 Docker 服务sudo systemctl restart dockerWindows 和 macOS 上如果你用的是 Docker Desktop则要把domain.crt导入到系统的受信任根证书颁发机构里然后重启 Docker Desktop。这个过程在 Windows 上相对繁琐我实际测试时发现 Linux 客户端的信任方式最直接。如果你就是想快速验证可以走insecure-registries的明方式。Linux 下编辑/etc/docker/daemon.json{ insecure-registries: [192.168.1.100:5000] }然后重启 Docker 服务。这种方式确实快但推送和拉取都是明文流量而且 Docker Desktop 下改这个配置还要重启 Docker Desktop 才能生效所以除非是纯内网临时测试否则我建议你用 HTTPS。配置完之后把一个本地镜像打个 tag 再推送docker tag nginx:alpine 192.168.1.100:5000/nginx:alpine docker push 192.168.1.100:5000/nginx:alpine这里有一个新手最容易踩的坑镜像 tag 必须带上仓库地址前缀。如果你只执行docker push nginx:alpineDocker 会默认把它推送到 Docker Hub 上去而不是你的私有仓库。这个误操作轻则浪费流量重则把你的镜像内容传到公网非常危险。所以“先 tag 再 push”这个习惯一定要养成。3. 从能跑到能用认证、查询与统一入口3.1 账号密码认证与 Bearer Token 流程很多人私有仓库起来之后发现一个问题任何能访问到这个仓库地址的人都能随意推拉镜像。内网环境里这看起来问题不大但一旦扩大使用范围这几乎等于把服务器裸奔。所以第二个必须做的事就是加认证。Registry 的认证方式很简单用 htpasswd 文件存储账号密码。先生成密码文件mkdir -p /opt/registry/auth docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin YourPassword /opt/registry/auth/htpasswd这里我用的httpd:2镜像自带的 htpasswd 工具你也可以直接用系统安装的htpasswd命令。-B表示使用 bcrypt 加密-b表示直接在命令行传入密码-n表示输出到标准输出而不是写文件。一定要用-B因为 Registry 对 bcrypt 格式支持最稳定有些旧版的 MD5 格式会登录失败。然后重新创建容器加认证相关的环境变量docker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/certs:/certs \ -v /opt/registry/auth:/auth \ -e REGISTRY_AUTHhtpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALMRegistry Realm \ -e REGISTRY_AUTH_HTPASSWD_PATH/auth/htpasswd \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/domain.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/domain.key \ --restartalways \ registry:2之后客户端第一次访问私有仓库时需要先登录docker login 192.168.1.100:5000输入用户名密码Docker 会把这个仓库地址的认证信息保存在本地后续 push/pull 就会自动携带凭证。聊一下这个认证流程它能帮你理解很多奇奇怪怪的报错。当 Docker 客户端向 Registry 发起一个请求时如果 Registry 开启了认证它会先返回 401并带一个WWW-Authenticate头指向认证服务地址。客户端收到 401 后用本地保存的账号密码去换一个 Bearer Token再用这个 Token 去访问实际资源。所以你会发现如果你不在docker login里输入账号直接 push 就会报 401 Unauthorized登录过一次之后后续操作都很顺畅就是这个 Token 在起作用。3.2 没有页面怎么管理镜像API 就是你的界面Registry 默认没有管理页面很多人一开始会不习惯。但实际上它暴露的 HTTP API 足够完成绝大多数管理操作配合 curl 和 jq 就能快速查询、删镜像。先看仓库里有哪些镜像curl -u admin:YourPassword https://192.168.1.100:5000/v2/_catalog返回结果是一个 JSON里面是仓库名称列表。再查某个镜像有哪些 tagcurl -u admin:YourPassword https://192.168.1.100:5000/v2/nginx/tags/list如果你在脚本里用建议装一下 jqalias regcurl -s -u admin:YourPassword https://192.168.1.100:5000 reg/v2/_catalog | jq reg/v2/nginx/tags/list | jq删除镜像则是先拿到 manifest 的 digest再调用 DELETE 接口。以删除nginx:latest为例# 获取 digest curl -I -u admin:YourPassword -H Accept: application/vnd.docker.distribution.manifest.v2json https://192.168.1.100:5000/v2/nginx/manifests/latest # 拿到 Docker-Content-Digest 的值然后删除 curl -u admin:YourPassword -X DELETE https://192.168.1.100:5000/v2/nginx/manifests/digest注意这里的Accept头必须要带否则 Registry 返回的 digest 可能不是 manifest 的 digest删的时候会 404。关键点Registry 的 DELETE 接口默认是关闭的。不开启时你执行 DELETE 会收到 405 或者 404。要开启删除能力需要在启动容器时加一个环境变量-e REGISTRY_STORAGE_DELETE_ENABLEDtrue3.3 想让团队用起来更舒服加统一入口Registry 容器监听的是 5000 端口地址看起来很技术。如果团队内部想把仓库统一放在一个规范域名下比如registry.example.com并且用标准的 443 端口可以通过 nginx 做一个前置转发层把外部请求转发到 Registry 容器。这个其实很好理解你可以在一台机器上把 443 端口的流量转给 5000 端口对外暴露的地址就变成了https://registry.example.com而不是https://192.168.1.100:5000。最终客户端的访问体验和用 Docker Hub 几乎一样不需要记端口号。nginx 的核心配置大致是这样server { listen 443 ssl; server_name registry.example.com; ssl_certificate /etc/nginx/conf.d/domain.crt; ssl_certificate_key /etc/nginx/conf.d/domain.key; client_max_body_size 0; 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; } }有两个细节需要注意。第一client_max_body_size必须设成 0 或者足够大否则大镜像上传时会被 nginx 拦截报 413 Request Entity Too Large。第二如果 nginx 这边已经做了 TLS 终止Registry 容器自己就不用再挂证书了否则会出现双重加密。反过来如果你让 Registry 自己处理 HTTPSnginx 那边就用 http 转发到 5000。两种方式都能跑通只要别混着来。如果只是团队内部用没有统一域名需求那直接把 IP:5000 发给同事也行。这个前置入口不是必须的但它能让仓库地址更规范、更容易记忆。4. 上线之后最常踩的坑问题排查与避坑清单4.1 高频报错速查表把常见错误整理成一张速查表你遇到问题直接对照着看报错信息可能原因处理方式x509: certificate signed by unknown authority客户端不信任自签名证书把 CA 证书放到/etc/docker/certs.d/仓库地址/http: server gave HTTP response to HTTPS client仓库是 HTTP客户端默认走 HTTPS配置 insecure-registries 或改用 HTTPSGet ... connection refused容器没启动或防火墙挡了 5000 端口检查容器状态放行防火墙和安全组规则401 Unauthorized未登录或认证配置没生效执行 docker login检查 htpasswd 路径denied: requested access to the resource is denied镜像 tag 未加仓库前缀或没有权限补全 tag 地址检查仓库名称413 Request Entity Too Largenginx 转发层限制了 body 大小设置 client_max_body_size 0磁盘空间不足镜像层堆积删除接口未启用或没做 GC开启 DELETE执行垃圾回收表格里的前两个错误是最常见的每天都在不同机器上反复出现。两者其实是一个问题的两面仓库端用了 HTTPS 你就得让客户端信任证书仓库端用了 HTTP 你就得让客户端放弃 HTTPS。搞清楚自己仓库到底是哪种模式排查就快多了。4.2 那些不报错但是会让你崩溃的细节比报错更难受的是“看起来一切正常但实际没做对”。第一个坑tag 忘记加仓库前缀。前面说过了但值得再强调一次。docker push nginx:alpine和docker push 192.168.1.100:5000/nginx:alpine的目标天差地别。前者在没有任何配置的情况下会尝试推到 Docker Hub后者才推到你自己的仓库。很多人在这一步把内部镜像泄露到公网一定要警惕。第二个坑删了镜像磁盘空间没释放。Registry 的 DELETE API 只是把镜像的元数据标记为删除底层存储在/var/lib/registry里的 blob 文件不会立即消失。必须手动执行垃圾回收。命令是docker exec registry bin/registry garbage-collect /etc/docker/registry/config.ymlgarbage-collect是 Registry 自带的子命令它会扫描当前存储目录把所有没有被任何 manifest 引用的 blob 删掉。这是真正的空间释放步骤不做的话磁盘只会越用越多。第三个坑跨机器推不上去先查防火墙再查证书。很多人在本机 push 成功换一台机器就失败考虑的大部分问题都在证书信任上其实更常见的原因是防火墙没放行 5000 端口。排查顺序我一般是这样# 在客户端机器上先测端口通不通 telnet 192.168.1.100 5000 # 通了之后再试登录 docker login 192.168.1.100:5000如果端口都不通后面所有证书、认证的排查都是在浪费时间。先解决连通性再看协议和证书这是最基本的排查思路。4.3 磁盘清理和镜像归档的完整脚本生产环境跑了一段时间后你会看到/var/lib/registry的体积越来越大。我之前维护过一个仓库数据目录居然涨到了上百 GB其中一个原因就是开发同事每天都在推送新的latesttag旧镜像的 blob 一直留在磁盘上。我自己常用的清理思路是先通过 API 把每个仓库的旧 digest 删掉再执行一次 GC。下面这个脚本用一个稍旧的 tag 名清理示例实际使用时建议结合你自己的 tag 保留策略#!/bin/bash REGISTRY192.168.1.100:5000 AUTHadmin:YourPassword # 所有仓库列表 repos$(curl -s -u $AUTH https://$REGISTRY/v2/_catalog | jq -r .repositories[]) if [ -z $repos ]; then echo No repositories found. exit 0 fi for repo in $repos; do echo cleanup repo: $repo # 获取 tags tags$(curl -s -u $AUTH https://$REGISTRY/v2/$repo/tags/list | jq -r .tags[] 2/dev/null) if [ -z $tags ]; then continue fi # 这里示例只保留 latest其它 tag 全删 for tag in $tags; do if [ $tag latest ]; then continue fi digest$(curl -s -I -u $AUTH -H Accept: application/vnd.docker.distribution.manifest.v2json \ https://$REGISTRY/v2/$repo/manifests/$tag \ | grep -i ^Docker-Content-Digest: | awk {print $2} | tr -d \r) if [ -n $digest ]; then curl -s -u $AUTH -X DELETE https://$REGISTRY/v2/$repo/manifests/$digest /dev/null echo deleted tag: $tag fi done done echo start garbage collection docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml -m echo done脚本里的-m参数用来在 GC 时打印内存回收信息方便看效果。这个脚本只是一个清理雏形实际生产环境你要根据自己的 tag 策略改保留逻辑千万别真的把所有非 latest 的 tag 全删掉。另外提醒一下生产环境做 GC 要慎重。GC 过程中 Registry 服务会短暂不可用或变慢因为它需要扫描整个存储目录而且这个操作会加锁。不要在业务高峰时段跑能放到凌晨就放到凌晨。5. 从私有仓库到团队基础能力5.1 离线环境搬镜像save/load 组合拳私有仓库最常见的价值体现就是离线环境。我遇到过很多次这样的场景服务器完全访问不了外网但应用必须跑起来依赖的 Docker 镜像也在公网仓库里。这时候私有仓库就有了用武之地。首先在一台有外网的机器上把需要的镜像拉下来docker pull nginx:alpine然后打包成 tar 文件docker save nginx:alpine -o nginx-alpine.tar把 tar 文件通过任何你能用的方式U 盘、内置文件服务器、刻盘传到目标机器上然后加载docker load -i nginx-alpine.tar加载完成后给镜像打上私有仓库地址前缀推送到内网仓库docker tag nginx:alpine 192.168.1.100:5000/nginx:alpine docker push 192.168.1.100:5000/nginx:alpine这样内网其他机器就能直接从私有仓库拉取这个镜像再也不用一台台机器去 load tar 包。说实话我第一次在内网服务器上处理几十个镜像时这个方法帮我节省的时间按小时算都不夸张。5.2 和 CI/CD 配合登录凭证不要写进代码私有仓库稳定运行之后第二个自然而然的用途就是接 CI/CD。不管是 GitLab CI、Jenkins 还是 GitHub Actions构建完镜像之后推送到内网私有仓库部署阶段再从仓库拉取整个流程才算闭环。这里我想特别强调一个安全问题docker login的账号密码不要明文写在 CI 配置文件里更不要提交到代码仓库。很多团队搭建私有仓库后第一版 CI 配置里直接写docker login 192.168.1.100:5000 -u admin -p YourPassword这样做一旦代码仓库权限泄露仓库账号密码也跟着泄露。合理的做法是使用 CI 平台提供的 Secret / 凭据管理功能把账号密码配成受保护的变量流水线运行时由平台注入。这是一个非常容易被忽略但极其重要的细节。另外CI 环境中每次构建前做好docker logout也是一个好习惯避免凭证被缓存在构建机上给后续使用同一台构建机的任务留下安全隐患。我个人的习惯是私有仓库这类基础组件前期多花半小时把证书、认证、清理策略全部配好后期能省无数个不眠之夜。明文跑通只是第一步但绝不是终点。如果你正打算在自己团队内搭私有仓库我建议就从我上面这套方案开始先用registry:2快速跑起来再逐步加上 HTTPS、认证、GC 脚本、CI 集成。等你把这些都理顺了你会明显感觉到镜像流转这件事变得非常顺畅大家再也不会为了一个镜像文件互相传包了。