中文 English

Docker 镜像不求人:从 0 到 1 在自己家里搭一个 13GB 的私有仓库

发布时间: 2026-06-26
Docker Registry Private Registry htpasswd 反向代理 Pull-Through Self-Hosted Infra

先说结论

我家那台小服务器上跑着一个 registry:2 镜像仓库,已经攒了 13 GB 缓存、95 个仓库。它在局域网里给我所有的 NAS、PVE、Mac、Windows 提供 Docker Hub 镜像加速,冷启动一个 alpine 不到 1 秒,完全不走公网 Docker Hub

这篇文章不讲"先装 Docker 然后跑个容器"这种一句废话教程。我会把真实在生产环境跑的部署脚本、登录鉴权流程、反向代理最容易踩的 4 个坑、htpasswd 密码文件怎么管理、缓存怎么排错,一次性摊开。

看完你应该能做的:① 在 10 分钟内起一个带登录的 registry;② 解释清楚 WWW-Authenticate 头为啥不能丢;③ 给老板、给家人讲明白这个仓库到底在干什么。

封面:从 Docker Hub 到自家仓库的迁移示意,左公网、右自建、中间一道防火墙

图 1:左边那个蓝色大盒子是 Docker Hub 官方,中间那个青色的小盒子就是我自己家里搭的 registry:2,右边紫色是局域网里所有要拉镜像的机器。整篇文章就是讲中间那个小盒子是怎么搭起来的。


一、问题背景:为什么我非要在家里搞一个私有仓库?

把时间倒回 2023 年。那时候我家里有几台机器:NAS、PVE 集群、Mac mini、偶尔一台 Windows 笔记本。每台机器都需要拉一堆 docker 镜像:nginxredisjellyfinportainerhomeassistant 等等。

一开始大家各自直接 docker pull nginx:latest,各自跑各自的 NAT 出公网,各自受 Docker Hub 那 100 pulls / 6h 的限流:

我当时想到的解决方案就一个:在局域网里搞一个统一的"镜像仓库",所有机器都从它拉,这个仓库再去公网拉一次,然后缓存下来。第二次任何机器再拉,直接走内网。

这个东西其实就是 Docker 官方提供的 registry:2 镜像,一个 24 MB 的 Go 程序,跑起来就是一个 HTTP 服务,完全够用。

仓库架构:registry 镜像把客户端的请求拦下来,缓存到本地磁盘,再去 upstream 拉

图 2:docker pull 的真实流程:客户端 → GET /v2/ → GET manifest → 按层 GET blob → 解压到本地。私有仓库就是把这些 GET 请求拦在自己机器上,优先吐本地缓存,缓存没有才去 upstream 拉。

听起来很简单对吧?事实上也确实不复杂,只是坑比较多。下面把我这几年踩过的坑和我现在的最终方案一起讲清楚。


二、问题表现:你以为装好了,实际"半哑"

我一开始直接 docker run -d -p 5000:5000 registry:2,跑起来,docker pull localhost:5000/alpine,嗯能用。

但真要把家里的 NAS 和 PVE 都接进来,马上就冒出 4 个"半哑"问题:

  1. docker push 报错 401,因为我没配鉴权。任何能访问 5000 端口的人都能 push,完全没设防。
  2. NAS 那边 docker pull my-server:5000/xxx 一直 connection refused,因为我把端口绑在 127.0.0.1,内网别的主机根本到不了。
  3. 重启服务器之后,缓存全没了,因为我没把 /var/lib/registry 挂到宿主机目录。
  4. docker login 死活提示 unauthorized,但服务端日志说 Www-Authenticate: Basic realm="...",看上去又是发了的——这是反代 / TLS / 域名解析三个里某一个出的问题,日志看不出谁

这 4 个坑每个都能让你卡半天。下面我按"先让 docker login 跑通、再让它持久化、最后做反向代理"的顺序讲,这样每一步你都只面对一个变量。

小白能听懂的类比:你想自己在家搞一个"小区便利店",卖水卖零食。

  • 货架就是磁盘(放商品);
  • 仓库就是 registry 容器(进货出货);
  • 大门口保安就是 htpasswd(没卡不让进);
  • 街道入口的反向代理就是小区大门口的传达室(验通行证、把包裹转交给你)。

任何一个环节漏掉,要么东西丢了,要么客人进不来。


三、问题根因:你看到的 401,可能不是密码错

我直接抛结论:docker login 在 80% 的"明明密码对、为啥还 401"的场景里,问题都不在密码,而在 HTTP 头

Docker daemon 用的是 OCI Distribution Spec v2 的认证流程,大概是这样:

  1. 客户端先发一个不带 token 的 GET /v2/ 给仓库;
  2. 仓库回 401 Unauthorized,并在响应头里写 WWW-Authenticate: Basic realm="...";
  3. 客户端看到这个头,就重新发一次请求,这次带上 Authorization: Basic base64(user:pass);
  4. 仓库验证账号,通过就吐真正的 manifest。

最关键的第 2 步:那个 WWW-Authenticate 头必须原封不动回到客户端。如果你前面套了一层 Nginx / Caddy / Traefik 反向代理,这层代理默认会把 WWW-Authenticate吞掉(因为它是 WWW-* 系列),结果客户端根本不知道这是个需要鉴权的服务,直接报"unauthorized"。

另一个常见原因:你给反代配了 HTTPS,但 docker daemon 端没信任这个证书(docker daemon 只信任系统 CA 和 /etc/docker/certs.d/<host>/ca.crt),于是 TLS 握手阶段就先挂了,根本走不到鉴权那一步。

还有一个隐藏原因:docker daemon 只允许通过 https://localhost 拉镜像。也就是说,如果你仓库跑在内网,但客户端配的是 http://192.168.x.x:5000,docker daemon 会直接拒绝,根本不发请求。

小学生版解释: 你去公司前台领访客卡。前台说:“请出示工牌”。这就是 WWW-Authenticate 头。 如果前台忘了说这句话,你掏出工牌也没用,因为你不知道要掏。 如果公司大门是塑料门、没有真锁(就是 HTTP 而不是 HTTPS),保安根本不让你进。


四、解决问题:从 1 行 docker run 开始,10 分钟跑通一个完整仓库

下面是我现在生产环境在用的部署脚本,经过半年稳定运行。一共 4 个步骤,每一步都加上了"为什么要这么写"的注释。

步骤 1:准备一个长期保存的目录

# 在宿主机上建目录,缓存和配置都在这里,重启/迁移都不会丢
mkdir -p /docker/registry-dockerhub   # 配置文件
mkdir -p /docker/registry-cache/dockerhub  # blob 缓存(13 GB 都是这里)
mkdir -p /docker/registry/auth        # htpasswd 密码文件

为什么要把缓存放到宿主机目录? Docker 容器默认是 ephemeral(临时)的:容器一删,里面的数据全没了。 把 /var/lib/registry 这个容器目录挂到宿主机的 /docker/registry-cache/dockerhub,就相当于给容器装了一块"外挂硬盘",容器怎么折腾,硬盘上的数据都不会丢。

步骤 2:生成 htpasswd 密码文件

# 一次性生成,只跑一次就行
docker run --rm \
  --entrypoint htpasswd \
  httpd:2.4 -Bbn admin 'MyS3cretPass!' \
  > /docker/registry/auth/htpasswd

跑完之后你的密码文件长这样:

admin:$2y$05$kxqfBjMmhK6eOZ8m9eRkgeW7FZmkjj8OQ8iCpzJZ1Vb5lpWWGbH1e

生成 htpasswd 密码文件的真实终端截图

图 3:htpasswd 用 bcrypt 把密码加密,磁盘上存的是 $2y$05$... 这种哈希,反推不出原始密码。htpasswd -Bbn 三个参数分别是 bcrypt 算法、从命令行读密码、追加到文件末尾。

千万不要用 -m (md5)! -m 是历史悠久的 md5 加密,非常容易被彩虹表撞。Docker daemon 实际上能接受 md5、sha1、bcrypt,但 md5 的弱点不在兼容性,而在你已经 2026 年了,还在用 1995 年的加密,说服力不够。

步骤 3:写一份 config.yml(可选,默认配置就能用)

cat > /docker/registry-dockerhub/config.yml <<'YAML'
version: 0.1
log:
  fields:
    service: registry-dockerhub-cache
storage:
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
proxy:
  remoteurl: https://hub1.nat.tf
YAML

这就是我那台私有仓库的真实 config.yml

图 4:proxy.remoteurl 这一行是关键 —— 它告诉 registry:“当本地缓存没有某个 blob 的时候,去这个 URL 拉。“不写这一行,registry 就只接受 push,不会主动去 upstream 拉新镜像。

proxy.remoteurl 是什么? 类比:它就是便利店的"进货渠道”。客人要一瓶水,货架上没有,你就去 https://hub1.nat.tf 那个批发商那里进货,放进货架,下次客人再来就有货了。 注意:https://hub1.nat.tf 在我这里工作得很好(详见我之前写的 从 5m14s 到 0.6s 那篇),在你那里可能需要换成别的源,实测一次再说。

步骤 4:起容器

docker run -d \
  --name registry-dockerhub \
  --restart=always \
  -p 8082:5000 \
  -v /docker/registry-dockerhub/config.yml:/etc/docker/registry/config.yml \
  -v /docker/registry-cache/dockerhub:/var/lib/registry \
  -v /docker/registry/auth:/auth \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  registry:2

docker run 命令逐行解释

图 5:把这行命令复制粘贴就能起一个能用的 registry。-e REGISTRY_AUTH_* 这三个环境变量是 htpasswd 鉴权的全部开关,少一个都不行。

跑完之后验证一下:

$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES               IMAGE          PORTS
registry-dockerhub  registry:2     0.0.0.0:8082->5000/tcp
registry-ghcr       registry:2     0.0.0.0:8083->5000/tcp

$ curl -sS http://localhost:8082/v2/
{}  # 注意这里是 401 + WWW-Authenticate,不是 200

为什么 curl /v2/ 返回 401 而不是 200? 这是 Docker 协议故意设计的"握手”:返回 401 + WWW-Authenticate 头,告诉客户端"我是 v2 仓库,你要鉴权才能访问"。 大部分监控脚本会用 curl /v2/ | grep -q '{}' 判断仓库存活,但配了鉴权之后这条命令永远返回 401,监控会误报挂掉。 正确的健康检查是:curl -i https://<host>/v2/ | head -1,看 HTTP/1.1 401 就 OK。


五、客户端怎么用:从一台机器到整个集群

单台客户端(一次性操作):

# 1. 登录(在内网里用 IP 也可以,docker daemon 接受内网 HTTP)
docker login 192.168.x.x:8082
# Username: admin
# Password: ********
# Login Succeeded

# 2. 打 tag + push
docker tag hello-world:latest 192.168.x.x:8082/test/hello-world:v1
docker push 192.168.x.x:8082/test/hello-world:v1

# 3. 另一台机器 pull
docker pull 192.168.x.x:8082/test/hello-world:v1

docker login + push + pull 的真实流程截图

图 6:先 docker login 拿到 token,然后 docker push 把镜像推上去。另一台机器直接 docker pull,不再走公网 Docker Hub。整条流程都是局域网内网,延迟 < 1ms。

全集群配置(一次配置,所有机器生效):

在每台 docker daemon 的 /etc/docker/daemon.json 里加:

{
  "insecure-registries": ["192.168.x.x:8082", "192.168.x.x:8083"]
}

为什么不直接用 https://? 家里内网,自签证书 + 让所有客户端信任 CA 这套流程很麻烦,等于为了一个家庭内部工具要搭一个 mini PKI。 Docker 官方提供 insecure-registries 这个开关,就是给你这种"内网非生产"的场景用的。生产环境请上 https + 反代 + CA 证书。

整集群生效之后,所有 docker pull library/alpine:3.19 会自动重定向到 192.168.x.x:8082/library/alpine:3.19,完全无感知。

类比: daemon.json 里的 insecure-registries 就像小区门口的"业主白名单"。 业主不需要每次回家都刷门禁卡,只要门卫认得这张脸,直接放行。 docker login 是给门卫加新面孔,docker pull 是日常进出。


六、缓存到底是怎么回事:13GB 看着多,实际去重后才占 13GB

跑了一段时间之后,你可以看一下磁盘:

$ du -sh /docker/registry-cache/*
13G   /docker/registry-cache/dockerhub
3.0G  /docker/registry-cache/ghcr

13GB 是真实的吗?。但同样的 13GB 物理容量,可能承载了上百个镜像仓库

我们看一下 blob 在磁盘上怎么存的:

$ find /docker/registry-cache/dockerhub/docker/registry/v2/blobs/sha256/ \
    -type f -printf '%s %p\n' | sort -rn | head -5
934308001 .../sha256/de/de4a0c57...cfff/data   # alpine 根文件系统 ~890 MB
753000000 .../sha256/13/13d4f7c2...a51d/data
540000000 .../sha256/f5/f5fc7c45.../data
534000000 .../sha256/89/894f7c54.../data
422000000 .../sha256/9c/9c8a7d83.../data

缓存目录的真实布局 + 大小统计

图 7:blob 的存储路径就是它自己的 sha256 前 2 位 + 完整 hash。这意味着:两个不同的镜像,如果它们共享同一个 base 层(比如都基于 library/alpine),那个 layer 在磁盘上只会存一份

“按 sha256 寻址” 用图书馆来打比方: 学校图书馆有 100 万本书。如果按"位置 1、位置 2、位置 3…“编号,完全一样的两本《五年高考三年模拟》会放在不同的位置,占两份地方。 但如果按"这本书第一句话的 SHA256"编号,所有完全相同的书永远只能放一个位置,自动去重。 Docker registry 就是这个原理:任何一份内容在磁盘上只存 1 份,无论多少个镜像引用它。

下面是我这台 registry 里 library/alpine 镜像缓存的 tag 数:

$ curl -sS http://localhost:8082/v2/library/alpine/tags/list | jq '.tags | length'
219

仓库里 alpine 镜像缓存了 219 个 tag

图 8:光是 library/alpine 一个镜像,我这台仓库就缓存了 219 个 tag,从 2.6latest 都有。它们共享大量 base 层,所以实际占盘远小于"219 个独立 alpine 镜像"的大小。

整个仓库里有 95 个独立仓库名,从 library/alpinezyplayer/zyplayer-doc,但物理上只占 13 GB:

v2/_catalog 返回的真实 JSON,95 个仓库

图 9:/v2/_catalog 列出当前仓库里所有镜像名。这是我家那台 registry 的真实输出,可以看到从 library/alpineportainer/portainer-cevaultwarden/server,全部在本地缓存里。


七、反向代理:最容易踩坑的 4 个配置点

如果你想让这个仓库对外可访问(家人从外面拉镜像、CI 触发拉取),就需要在前面套一个反向代理。这是 401 错误最密集的地方

以 Caddy 为例,一份能跑的最小配置:

registry.lab.local {
    reverse_proxy localhost:8082 {
        # 关键 1: 必须透传 WWW-Authenticate 头
        header_up -WWW-Authenticate
        header_up +WWW-Authenticate

        # 关键 2: 大文件不要缓冲,流式转发
        flush_interval -1
    }
}

以 Nginx 为例,同样的事:

server {
    listen 443 ssl;
    server_name registry.lab.local;

    ssl_certificate /etc/ssl/lab.crt;
    ssl_certificate_key /etc/ssl/lab.key;

    location / {
        proxy_pass http://127.0.0.1:8082;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # ⚠️ 必须透传这俩头
        proxy_pass_request_headers on;
        proxy_buffering off;
        proxy_request_buffering off;
        client_max_body_size 0;
    }
}

最容易出错的 4 个点:

  1. proxy_buffering off 没写。Nginx 默认会把上游响应缓冲到磁盘再发给客户端,导致 docker push 大镜像超时。一定要 off
  2. client_max_body_size 0 没写。默认 1 MB,你 push 一个 500 MB 镜像直接报 413。
  3. WWW-Authenticate 头被吞。Nginx 默认对 WWW-* 系列会过滤掉一部分,要用 proxy_pass_request_headers on 显式放行。
  4. HTTPS 证书链不完整。Let’s Encrypt 申请的证书有时候是 chain 不全的,要让 docker daemon 信任你 cert chain,最稳的是把 chain.pem + cert.pem 拼成一个文件。

这 4 个坑,任一命中,你的现象都是"docker login 报 unauthorized,但服务端日志看着又没问题”。 排错顺序:① curl -i 看 WWW-Authenticate;② 看 Nginx error log;③ 用 openssl s_client 测 TLS 握手。


八、3 种部署形态:你属于哪一种?

3 种部署形态对比:家庭 / 小团队 / 公司

图 10:同一个 registry:2 镜像,在不同规模下要拼的"周边"不一样。我家用的是最左边那一档;创业团队用中间;公司用最右。

维度 家庭 / 实验室 小团队 公司
客户端数 1 ~ 5 10 ~ 50 几百 ~ 几千
反代 不要 Caddy / Nginx + 自签 CA Nginx / Envoy + 商业 CA
鉴权 htpasswd 单用户 htpasswd + 读写分组 LDAP / OIDC / OAuth
存储 本机 filesystem 本机 filesystem + 备份 S3 / OSS / MinIO
监控 du -sh 看一下 Prometheus exporter Grafana + 告警 + SLA
高可用 不需要,坏了就重起 单点 + 备份 多副本 + 跨地域
SLA 没有 没有 99.9%

我家用的方案就是最左边那一档:单容器 + filesystem + htpasswd + crontab 备份,跑了一年没出问题。没必要为了"家庭内网"这个场景过度设计


九、实战排错:docker login 一直 unauthorized

这是我见过的最常见的问题,也是网上最容易被错误回答的问题。下面是我的标准排错流程。

# Step 1: 看服务端是不是真的在监听
$ ss -tlnp | grep 8082
LISTEN 0  4096  0.0.0.0:8082  0.0.0.0:*  users:(("docker-proxy",pid=226477,fd=8))

# Step 2: 不带认证 curl 一下,看 401 + WWW-Authenticate
$ curl -i http://localhost:8082/v2/
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
Www-Authenticate: Basic realm="Registry Realm"
X-Content-Type-Options: nosniff
Content-Length: 79

{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}

# Step 3: 带认证再 curl 一次,应该回 200
$ curl -i -u admin:'MyS3cretPass!' http://localhost:8082/v2/
HTTP/1.1 200 OK
Docker-Distribution-Api-Version: registry/2.0
X-Content-Type-Options: nosniff
Content-Length: 2

{}

# Step 4: 客户端 docker login
$ rm ~/.docker/config.json    # 先把旧凭据清掉,排除缓存干扰
$ docker login -u admin -p 'MyS3cretPass!' 192.168.x.x:8082
Login Succeeded

完整的 docker login 401 排错流程截图

图 11:从 curl -i 看 401 头开始,到 htpasswd -Bv 验证密码,到 rm ~/.docker/config.json 清缓存,标准的"客户端不认服务端"的排错 3 步。

4 个常见错误信息对照表:

错误信息 真正的原因
no such host 域名解析失败 / 写错 IP
connection refused 服务端没起来 / 端口错
tls: failed to verify certificate HTTPS 但证书不被信任
unauthorized: authentication required 服务端起来了,但 WWW-Authenticate 头丢了 / 密码错

十、备份和清理:放了一年磁盘会不会爆?

会,默认 168 小时(7 天)清理一次没访问的 blob。但实测下来 13 GB 一年没爆过磁盘,因为:

  1. 内部去重让"内容增长"和"磁盘增长"严重脱钩;
  2. 大部分常用镜像(nginx、redis、alpine、postgres)的 layer 已经被拉过无数次,后续新版本只增加 diff,实际增量很小;
  3. 默认 scheduler 每 24 小时跑一次 GC,把 7 天没访问的 blob 删掉。

如果你想自己控制保留策略,加一行:

storage:
  filesystem:
    rootdirectory: /var/lib/registry
  delete:
    enabled: true
  cache:
    blobdescriptor: inmemory

然后用 registry 自带的 GC API:

# 标记 7 天没访问的 blob 为待删
$ curl -X POST -u admin:'MyS3cretPass!' \
    http://localhost:8082/v2/_catalog  # 先 warm up

$ curl -X POST -u admin:'MyS3cretPass!' \
    "http://localhost:8082/v2/_catalog?n=1000"

# 看 GC 状态
$ curl -u admin:'MyS3cretPass!' \
    http://localhost:8082/debug/health | jq .

备份就更简单:整个 /docker/registry-cache 目录直接 tar 就行。

# 每周日凌晨 3 点打包
0 3 * * 0 tar czf /backup/registry-$(date +\%F).tgz /docker/registry-cache

类比: GC = 超市每周下架过期 7 天的酸奶。 备份 = 给超市拍一张"今日货架照",出事了能照着摆回去。 你不用每天管它,但别等真丢了才想起来没备份


十一、Q&A

Q:用 registry:2 跟用 Harbor 有什么区别?

A:Harbor 是"registry + UI + 镜像扫描 + 复制策略 + LDAP"的一站式平台,适合公司。家用用 Harbor 杀鸡用牛刀,光资源占用就要 4 GB 内存起步,而且 Harbor 自己也是用 registry:2 做底层。我家用 24 MB 的 registry:2 完全够,简单是复杂的解药

Q:我不想用 htpasswd,能不能用公司 LDAP?

A:可以,registry:2 支持 token-based auth,写一个小 HTTP 服务,接收 registry 发过来的 ?service=...&scope=...,查 LDAP 后回 token。Harbor / Distribution 官方都给了 sample。但最简单的方式仍然是 htpasswd,多用户管理麻烦的话就用不同的 htpasswd 文件。

Q:docker login 一直 unauthorized,但 curl -u admin:pass 没问题?

A:99% 是反代吞了 WWW-Authenticate。在你反代的 vhost 里强制加:

proxy_pass_request_headers on;   # Nginx
header_up +WWW-Authenticate       # Caddy

然后 curl -i https://your.host/v2/,确认响应里 WWW-Authenticate: Basic realm="..." 这一行。

Q:缓存会不会让镜像不"新"?比如我 upstream 拉到 alpine:3.19 了,3.20 发布了我怎么办?

A:不会。registry:2 在每次 pull 时,都会先发一个 HEAD /v2/<name>/manifests/<tag> 给 upstream,问它"这个 tag 现在指向哪个 digest?"。如果 upstream 返回的 digest 不等于本地缓存的 digest,registry 会主动去 upstream 拉新的 manifest + 新的 blob。整个过程对客户端完全无感,客户端只会看到"我又拉到最新版了"。

Q:如何把现有的本地镜像批量推到这个私有仓库?

A:写一个小脚本:

#!/bin/bash
DEST="192.168.x.x:8082"
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | while read img; do
    new=$(echo "$img" | sed "s|^|$DEST/|")
    docker tag "$img" "$new"
    docker push "$new" >/dev/null 2>&1 && echo "pushed: $new"
done

强烈不推荐把所有镜像都推到私有仓库 — 私有仓库主要是"缓存 + 内部业务镜像",公开镜像还是直接拉上游更省事。

Q:insecure-registries 和 HTTPS,我到底选哪个?

A:内网用 http + insecure-registries,生产用 httpsinsecure-registries 这个开关就是给你内网用的,合规风险为 0(docker daemon 自己把 http 卡在非 loopback 地址会拒,所以默认就安全)。如果你要给外面用,上 Caddy 自动 Let’s Encrypt,一分钟搞定 HTTPS。

Q:registry 容器本身需不需要 docker restart policy?

A:必须加 --restart=always。registry 是个无状态服务,挂了直接重启不会有任何数据损失(数据都在挂载的宿主机目录里)。我家里这台跑了一年多,容器重启过 5、6 次,缓存 0 丢失。


十二、参考资料

  1. Docker Hub usage and rate limits — Docker 官方关于 100 pulls / 6h / IP 限流的说明
  2. Distribution: Configuring a registryproxy.remoteurlhtpasswdstorage 字段的官方文档
  3. Distribution: Registry as a pull through cache — 官方 mirror recipe,包括 daemon.json 怎么配
  4. Docker daemon: insecure-registries — 内网 HTTP 仓库的开关
  5. Distribution: Token Authentication Specification — 为什么 WWW-Authenticate 头如此重要
  6. Caddy reverse_proxy: header_up directives — Caddy 透传认证头的官方说明
  7. htpasswd man page (Apache HTTP Server)htpasswd -Bbn 参数的含义
  8. 我之前写的:从 5m14s 到 0.6s 那篇 — registry 镜像源怎么选、怎么排错、cron fallback 怎么做

最后:私有仓库不是什么高深的东西,它本质上就是一个 24 MB 的 Go HTTP 服务 + 一块外挂硬盘。把它想成"小区便利店"而不是"沃尔玛",你会发现整个事情就只是:装个货架、放点存货、门口站个保安

—— 完 ——