外观
私有仓库:搭 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:2bash
curl -s http://127.0.0.1:5000/v2/_catalogtext
{"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.0text
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.0HTTP 仓库必须配置 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 dockerinsecure-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 -20text
{"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: 60sbash
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 deletionGC 期间 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 admintext
New password:
Re-type new password:
Adding password for user admin2. 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.0text
Authenticating with existing credentials...
Login Succeeded
The push refers to repository [registry.internal/shop/app]
...有了 HTTPS + 认证后,insecure-registries 就可以去掉。
六、registry 的定位
| 需求 | 方案 |
|---|---|
| 十来个镜像、几个人用 | registry:2 够了 |
| 多项目、多团队、要 UI | Harbor |
| 要漏洞扫描、镜像签名 | 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 Large | nginx 默认 1MB | client_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 变 untagged | GC 加 --delete-untagged |