Skip to content

私有仓库:搭 registry 与清磁盘 ​

内网推拉镜像、避免依赖 Docker Hub、控制镜像来源。官方 registry:2 够用,但默认不开删除,磁盘只会涨。

场景 ​

  • 内网服务器不能访问外网镜像仓库。
  • 想把构建产物存自己机器。
  • 需要给镜像打上审计留痕。

一、最小可用的 registry ​

bash
mkdir -p /data/registry

docker run -d --name registry \
  --restart unless-stopped \
  -p 5000:5000 \
  -v /data/registry:/var/lib/registry \
  registry:2
bash
curl -s http://127.0.0.1:5000/v2/_catalog
text
{"repositories":[]}

返回 {"repositories":[]} 就说明服务正常。

二、推拉镜像 ​

bash
docker tag shop-app:1.0.0 registry.internal:5000/shop/app:1.0.0
docker push registry.internal:5000/shop/app:1.0.0
text
The push refers to repository [registry.internal:5000/shop/app]
5f70bf18a086: Pushed
8e2c4d6a0b1f: Pushed
c1a2b3d4e5f6: Pushed
1.0.0: digest: sha256:8f2c1e9a7b3d5c8e0a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f4a6b8c0d2e4f size: 1573

拉取:

bash
docker pull registry.internal:5000/shop/app:1.0.0

HTTP 仓库必须配置 insecure ​

如果 registry 没有 HTTPS:

text
Error response from daemon: Get "https://registry.internal:5000/v2/":
http: server gave HTTP response to HTTPS client

在每一台要推拉的机器上加 daemon.json:

json
{
  "insecure-registries": ["registry.internal:5000"]
}
bash
systemctl restart docker

insecure-registries 必须写对主机名

Docker 的匹配是按 host:port 精确匹配的。写 registry.internal(不带端口)和 10.0.0.50:5000 是两条不同的规则。用哪个地址推,就写哪个。

三、查看内容 ​

bash
# 仓库列表
curl -s http://registry.internal:5000/v2/_catalog

# 某仓库的 tag
curl -s http://registry.internal:5000/v2/shop/app/tags/list

# manifest(看层和大小)
curl -s -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
  http://registry.internal:5000/v2/shop/app/manifests/1.0.0 | python3 -m json.tool | head -20
text
{"repositories":["shop/app","infra/nginx","infra/redis"]}
{"name":"shop/app","tags":["1.0.0","1.0.1","latest"]}

manifest 里的 digest 就是镜像的唯一标识。生产部署建议用 digest,不用 tag——tag 会被覆盖,digest 不会。

bash
docker pull registry.internal:5000/shop/app@sha256:8f2c1e9a...

四、开启删除并清理磁盘 ​

默认 registry 不允许删除,DELETE 请求返回 UNSUPPORTED。要开:

yaml
# /etc/registry/config.yml
version: 0.1
storage:
  filesystem:
    rootdirectory: /var/lib/registry
  delete:
    enabled: true
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
  draintimeout: 60s
bash
docker rm -f registry

docker run -d --name registry --restart unless-stopped \
  -p 5000:5000 \
  -v /data/registry:/var/lib/registry \
  -v /etc/registry/config.yml:/etc/docker/registry/config.yml \
  registry:2

删除某个 tag(要带 digest):

bash
DIGEST=$(curl -s -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
  http://registry.internal:5000/v2/shop/app/manifests/1.0.0 \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["config"]["digest"])')

curl -s -X DELETE http://registry.internal:5000/v2/shop/app/manifests/$DIGEST -w '%{http_code}\n'
text
202

删除 manifest 不等于释放磁盘

registry 是内容寻址存储,删 manifest 只是删了索引,blob 还在磁盘上。必须再跑垃圾回收:

bash
docker exec registry /bin/registry garbage-collect \
  --delete-untagged /etc/docker/registry/config.yml

输出:

text
app: marking manifest sha256:8f2c...
app: deleting manifest sha256:8f2c...
app: marked blob sha256:5f70bf18a086...
app: marking blob sha256:5f70bf18a086...
6 blobs marked, 4 blobs and 2 manifests eligible for deletion

GC 期间 registry 应当只读或停止,否则可能删到正在上传的 blob。

定期清理脚本 ​

bash
#!/bin/bash
set -euo pipefail
REG=registry.internal:5000
KEEP=10          # 每个仓库保留最近 10 个 tag

docker exec registry /bin/registry garbage-collect --delete-untagged \
  /etc/docker/registry/config.yml

真正的"保留 N 个"需要自己算 tag 的创建时间,官方 registry 不提供。要么脚本按 tag 命名规则(如 1.4.2 语义化)排序删除,要么直接上 Harbor。

五、加 HTTPS 与认证 ​

内网也建议上 HTTPS + 基础认证,尤其是跨网段访问。

1. 生成密码文件 ​

bash
mkdir -p /etc/nginx/{certs,conf}
htpasswd -Bc /etc/nginx/htpasswd admin
text
New password:
Re-type new password:
Adding password for user admin

2. nginx 反向代理 ​

nginx
# /etc/nginx/conf.d/registry.conf
server {
    listen 443 ssl;
    server_name registry.internal;

    ssl_certificate     /etc/nginx/certs/registry.crt;
    ssl_certificate_key /etc/nginx/certs/registry.key;

    client_max_body_size 0;          # 不限上传大小,否则大镜像推不上去
    chunked_transfer_encoding on;

    location / {
        auth_basic "Registry";
        auth_basic_user_file /etc/nginx/htpasswd;

        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-Proto $scheme;
        proxy_request_buffering off;
    }
}

client_max_body_size 0 是必须的

nginx 默认只允许 1MB 请求体。推送镜像时每一层都是一个 PUT,超过 1MB 直接 413 Request Entity Too Large。这是私有仓库最常见的坑。

3. 登录并推送 ​

bash
docker login registry.internal
docker push registry.internal/shop/app:1.0.0
text
Authenticating with existing credentials...
Login Succeeded

The push refers to repository [registry.internal/shop/app]
...

有了 HTTPS + 认证后,insecure-registries 就可以去掉。

六、registry 的定位 ​

需求方案
十来个镜像、几个人用registry:2 够了
多项目、多团队、要 UIHarbor
要漏洞扫描、镜像签名Harbor(集成 Trivy、Cosign)
要跨地域复制Harbor 的 replication
要配额和保留策略Harbor

Harbor 是 registry 的上层封装,底层还是 distribution。中小团队从 Harbor 起步更省事,registry:2 更适合"自己写脚本管"的场景。

七、踩过的坑 ​

现象根因处置
server gave HTTP response to HTTPS client没配 insecure-registries配 daemon.json 并重启
413 Request Entity Too Largenginx 默认 1MBclient_max_body_size 0
push 到一半断网络或磁盘满查 df -h /data
DELETE 返回 UNSUPPORTED没开 storage.delete.enabled改配置重启
删了 manifest 磁盘没降只删索引,blob 还在跑 garbage-collect
GC 后镜像拉不下来GC 时 registry 在写GC 期间停写
换机器后镜像不见了数据没挂卷-v /data/registry:/var/lib/registry
unauthorized没 docker login 或密码文件错重新登录,检查 htpasswd
tag 覆盖后旧镜像还在旧 manifest 变 untaggedGC 加 --delete-untagged