自建 Docker 私有镜像仓库详细教程:官方 Registry 从安装、认证到生产加固全方案
Docker 用久了都会遇到同一个问题:镜像越攒越多,docker pull 越来越慢,内网机器根本拉不到 Docker Hub,自研的基础镜像也没地方统一存放。这篇文章从零开始,用官方 registry:2 镜像搭一套自己的 Docker 镜像仓库,从一行命令跑起来,到认证、HTTPS、Nginx 反代、docker-compose 一键部署,再到日常运维、垃圾回收和生产加固,全部讲透。按本文操作,你可以在一台装好 Docker 的 Linux 服务器上,半小时内得到一个能直接上生产的私有仓库。
一、为什么需要私有镜像仓库
Docker Hub 是默认的公共仓库,但它解决不了下面这些场景:
- 内网 / 离线环境:生产网段不能访问外网,镜像必须在内部流转,离线安装 Docker 那篇教程里
docker save / load只能应付一次性分发,长期维护还得靠仓库。 - 拉取速度:跨国拉镜像慢且不稳定,内网仓库基本是局域网线速,CI 构建时间能省下一大截。
- 自研镜像统一管理:团队自己的基础镜像、业务镜像需要版本化存放,而不是散落在各台机器的
/root/*.tar里。 - 合规与可控:镜像不出内网,不依赖第三方服务的可用性,出了问题自己能修。
- CI/CD 集成:Jenkins、GitLab CI、Argo CD 这类流水线都需要一个
docker push / pull的落点。
官方 Registry 本身就是一个镜像,部署成本极低;它不带 UI、不带权限管理之外的花哨功能,但这正是它的优点:简单、稳定、占用资源少。本文以它为主线,需要更重的功能时再考虑 Harbor。
二、选型对比:Registry、Harbor、Nexus、Docker Hub
先把几个常见方案放在一起对比,帮你判断官方 Registry 是否够用:
| 方案 | 定位 | 部署成本 | 权限管理 | Web UI | 镜像扫描 | 适合场景 |
|---|---|---|---|---|---|---|
| 官方 Registry | 纯镜像仓库 | 极低,一个容器 | 基础认证(htpasswd / token) | 无(可另装 UI) | 无 | 个人、小组、内网分发 |
| Harbor | 企业级仓库 | 中等,多个组件 | RBAC、项目隔离、OIDC | 有 | 有(Trivy) | 公司级、多团队、合规要求高 |
| Nexus Repository | 通用制品库 | 中等 | RBAC、LDAP | 有 | 有(需商业版) | 同时管 Maven、npm、Docker 等 |
| Docker Hub | 公有托管 | 零 | 组织、团队 | 有 | 有(Docker Scout) | 公开镜像、个人项目 |
结论很直接:只是想在内网有个地方存镜像、团队规模不大,官方 Registry 完全够用;如果要项目隔离、细粒度权限、漏洞扫描、镜像复制,再上 Harbor。Harbor 底层用的也是官方 Registry,本文讲的配置和运维知识(存储结构、删除与垃圾回收、API)到 Harbor 上同样适用。
版本说明:官方镜像是 registry:2,属于 2.8.x 线,当前最新是 2.8.3。2025 年发布的 v3.0.0(镜像 distribution/registry:3)主打 OCI 规范和性能提升,但生态和文档仍以 2.x 为主,生产环境建议先用 2.8.3。本文全部以 registry:2.8.3 为准,快速体验时用 registry:2 也可以。
三、环境准备与规划
开始之前,先确认这几项:
- Docker:已安装并能正常运行(任意较新版本即可;离线环境参考本站《Linux 离线安装 Docker 25.0.4 详细教程》)。
- 端口:Registry 默认监听容器内 5000 端口,本文映射到宿主机 5000;如果走 Nginx 反代,对外只暴露 443。
- 磁盘:镜像数据全存在宿主机目录,空间按镜像总量 + 30% 余量规划,
overlay2所在的磁盘不要和数据目录抢空间。 - 域名(可选):生产环境建议用域名 + HTTPS,如
registry.example.com;纯内网测试用 IP 也可以。
本文用到的目录规划如下,全程统一,不用到处找:
| 宿主机路径 | 用途 | 容器内挂载点 |
|---|---|---|
/opt/registry/data | 镜像数据(blobs + 元数据) | /var/lib/registry |
/opt/registry/config.yml | Registry 配置文件 | /etc/docker/registry/config.yml |
/opt/registry/auth/htpasswd | 认证账号文件 | /auth/htpasswd |
/opt/registry/certs/ | TLS 证书与私钥 | /certs |
# 先把目录建好,后面所有步骤都用这套路径
mkdir -p /opt/registry/{data,auth,certs}
docker --version
# Docker version 25.0.4, build 1a576c9 类似输出即表示 Docker 正常
四、最简安装:5 分钟跑起来
先不加任何认证和证书,用最简方式把仓库跑起来,验证通路。理解这一步,后面所有加固都是在它上面叠加配置。
# 拉取官方镜像(生产建议 pin 具体版本 registry:2.8.3)
docker pull registry:2.8.3
# 启动:映射 5000 端口,数据挂到宿主机目录,容器随 Docker 自启
docker run -d \
-p 5000:5000 \
--restart=always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
registry:2.8.3
# 查看状态
docker ps --filter name=registry
docker logs registry --tail 20
验证仓库是否正常工作。Registry 的根接口 GET /v2/ 正常时返回空 JSON 和 200:
# 正常返回 {},表示仓库已就绪
curl -s http://localhost:5000/v2/
# {}
# 仓库还是空的,镜像列表为空数组
curl -s http://localhost:5000/v2/_catalog
# {"repositories":[]}
再走一遍完整的推拉流程,用 hello-world 做测试:
# 1. 拉一个测试镜像
docker pull hello-world:latest
# 2. 打上仓库地址的 tag:格式是 仓库地址/镜像名:标签
docker tag hello-world:latest localhost:5000/hello-world:latest
# 3. 推到私有仓库
docker push localhost:5000/hello-world:latest
# 4. 删掉本地镜像,验证能从仓库拉回来
docker rmi localhost:5000/hello-world:latest hello-world:latest
docker pull localhost:5000/hello-world:latest
docker run --rm localhost:5000/hello-world:latest
# 5. 再查一次仓库,已经有内容了
curl -s http://localhost:5000/v2/_catalog
# {"repositories":["hello-world"]}
到这里,一个能用的私有仓库已经跑起来了。但它现在是完全开放的:任何人能 push、能 pull、没有加密。下面几节逐项把它加固到生产可用。
五、数据持久化与目录结构
第四节的 -v /opt/registry/data:/var/lib/registry 就是持久化的关键:容器删了重建,镜像数据还在宿主机目录里。看一下存进去之后目录长什么样:
find /opt/registry/data -maxdepth 4 -type d | head -20
# /opt/registry/data
# /opt/registry/data/docker
# /opt/registry/data/docker/registry
# /opt/registry/data/docker/registry/v2
# /opt/registry/data/docker/registry/v2/blobs
# /opt/registry/data/docker/registry/v2/repositories
# /opt/registry/data/docker/registry/v2/repositories/hello-world
# /opt/registry/data/docker/registry/v2/repositories/hello-world/_layers
# /opt/registry/data/docker/registry/v2/repositories/hello-world/_manifests
# /opt/registry/data/docker/registry/v2/repositories/hello-world/_uploads
这个结构要理解透,后面删镜像、备份、排障都绕不开它:
blobs/sha256/<前两位>/<摘要>/data:镜像层和配置文件按内容摘要存放,是真正占磁盘的部分;同一个层被多个镜像引用时只存一份。repositories/<镜像名>/_manifests/tags/<标签>/current/link:标签指向的 manifest 摘要。repositories/<镜像名>/_layers:该镜像引用了哪些 blob 的索引。
两点注意:不要直接删 blobs 下的文件,删镜像必须走第十二节的 API + 垃圾回收;备份整个 /opt/registry/data 就是备份整个仓库,恢复时原样拷回去即可。
六、config.yml 完整配置详解
Registry 的行为由配置文件控制,容器内默认路径是 /etc/docker/registry/config.yml。官方镜像启动时如果不指定配置文件,就用内置的默认配置。下面是一份生产可用的完整配置,先给全貌,再逐项解释:
version: 0.1
log:
level: info
formatter: text
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
maintenance:
uploadpurging:
enabled: true
age: 168h
interval: 24h
dryrun: false
http:
addr: :5000
# 生产启用 HTTPS 时取消下面三行注释,并挂载证书目录
# tls:
# certificate: /certs/registry.crt
# key: /certs/registry.key
headers:
X-Content-Type-Options: [nosniff]
# 生产启用认证时取消下面四行注释,并挂载 auth 目录
# auth:
# htpasswd:
# realm: basic-realm
# path: /auth/htpasswd
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
catalog:
maxentries: 2000
关键项说明:
| 配置项 | 作用 | 备注 |
|---|---|---|
log.level | 日志级别:debug / info / warning / error | 排障时临时改 debug,平时 info |
storage.filesystem.rootdirectory | 镜像数据根目录 | 对应挂载的 /var/lib/registry |
storage.delete.enabled | 是否允许 API 删除镜像 | 默认 false;想删镜像必须开,见第十二节 |
storage.maintenance.uploadpurging | 自动清理未完成的上传残留 | age 168h 表示 7 天前的未完成上传会被清掉 |
storage.cache.blobdescriptor | blob 元数据缓存 | inmemory 即可,省磁盘 IO |
http.tls | 启用 HTTPS | 证书路径见第八节 |
http.auth.htpasswd | 基础认证 | 账号文件路径见第七节 |
health.storagedriver | 存储后端健康检查 | 配合 /debug/health 用 |
catalog.maxentries | _catalog 接口单次返回上限 | 2.8.2 起默认值 1000,仓库大时调大 |
配置文件也可以用环境变量代替,格式是把配置路径大写、点换成下划线再加 REGISTRY_ 前缀。例如 storage.delete.enabled 对应 REGISTRY_STORAGE_DELETE_ENABLED=true。简单场景用环境变量更省事,复杂场景用配置文件更清晰。本文后面以配置文件为主。
把上面的配置保存为 /opt/registry/config.yml,然后用它启动(注意只读挂载配置文件,避免容器内误改):
docker stop registry && docker rm registry
docker run -d \
-p 5000:5000 \
--restart=always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml:ro \
registry:2.8.3
# 改了 config.yml 后必须重启容器才生效
docker restart registry
docker logs registry --tail 10
# level=info msg="listening on [::]:5000"
七、启用认证:htpasswd 基本认证
没有认证的仓库等于把内网镜像敞开给所有人,生产环境必须上认证。官方 Registry 自带的认证方式是 htpasswd(HTTP Basic Auth),账号密码存在一个文件里,由 Nginx 的 htpasswd 工具生成。注意:Registry 镜像本身不带 htpasswd 命令,官方文档的做法是用 httpd:2 镜像来生成。
# 生成账号文件:-B 表示 bcrypt 加密(Registry 只认 bcrypt),-b 从命令行传密码,-n 不写文件直接输出
# 第一个用户会创建文件,后续用户用追加模式(去掉 > 改 >>,且不能加 -c)
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin 'Admin@2026' > /opt/registry/auth/htpasswd
# 再加一个只有拉取权限概念上分不开的普通用户(Registry 的 htpasswd 不分读写权限,见文末说明)
docker run --rm --entrypoint htpasswd httpd:2 -Bbn dev 'Dev@2026' >> /opt/registry/auth/htpasswd
# 检查文件内容(密码是 bcrypt 哈希,看不到明文)
cat /opt/registry/auth/htpasswd
# admin:$2y$05$...
# dev:$2y$05$...
# 宿主机上装了 apache2-utils 也可以直接生成,不用拉 httpd 镜像
# Debian/Ubuntu: apt install -y apache2-utils
# RHEL/Rocky: dnf install -y httpd-tools
# htpasswd -Bbn admin 'Admin@2026' > /opt/registry/auth/htpasswd
然后在 config.yml 里取消第六节预留的 auth 四行注释,把 auth 目录挂进容器并重启:
docker stop registry && docker rm registry
docker run -d \
-p 5000:5000 \
--restart=always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml:ro \
-v /opt/registry/auth:/auth:ro \
registry:2.8.3
# 未登录时访问会被拒绝
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/v2/
# 401
curl -s -u admin:'Admin@2026' http://localhost:5000/v2/
# {}
# 客户端登录(密码会保存在 ~/.docker/config.json)
docker login localhost:5000 -u admin -p 'Admin@2026'
# Login Succeeded
# 登录后再推拉就都正常了
docker push localhost:5000/hello-world:latest
改密码就是重新生成对应行的哈希并重启容器(Registry 启动时读一次账号文件,运行中改文件不生效)。另外要认清 htpasswd 的局限:它只有"能进 / 不能进",不分"只读 / 读写"——任何一个合法账号既能 pull 也能 push、也能删镜像。需要按项目隔离读写权限时,就该换 Harbor 的 RBAC 了。
八、启用 HTTPS:三种证书方案
Docker 客户端默认只信任 HTTPS 的仓库(localhost 除外),生产环境必须上 TLS。三种证书方案按推荐顺序排列:
方案 A:Let's Encrypt 正式证书(有公网域名时首选)
# 用 certbot 申请(域名已解析到本机,80 端口可达)
dnf install -y certbot # 或 apt install -y certbot
certbot certonly --standalone -d registry.example.com
# 证书在 /etc/letsencrypt/live/registry.example.com/,拷一份给 Registry 用
cp /etc/letsencrypt/live/registry.example.com/fullchain.pem /opt/registry/certs/registry.crt
cp /etc/letsencrypt/live/registry.example.com/privkey.pem /opt/registry/certs/registry.key
chmod 600 /opt/registry/certs/registry.key
# 续期后要重新拷并重启容器,可以写个 cron 钩子自动做
方案 B:自签证书(纯内网最常用)
cd /opt/registry/certs
# 生成私钥 + 自签证书(注意 SAN 里要写域名和 IP,否则新版客户端会拒)
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout registry.key -x509 -days 3650 -out registry.crt \
-subj "/CN=registry.example.com" \
-addext "subjectAltName=DNS:registry.example.com,DNS:reg,IP:192.168.1.10"
chmod 600 registry.key
openssl x509 -in registry.crt -noout -text | grep -A1 "Subject Alternative Name"
方案 C:内网 CA 签发(有统一 CA 时最规范)
用公司内网 CA 签发服务器证书,流程和方案 B 类似,只是把 CSR 交给 CA 签。好处是所有内网机器只要信任一次根 CA,所有服务通用,不用每台机器单独信任每个自签证书。
证书就绪后,在 config.yml 里取消 http.tls 两行注释,把证书目录挂进容器并重启:
docker stop registry && docker rm registry
docker run -d \
-p 5000:5000 \
--restart=always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml:ro \
-v /opt/registry/auth:/auth:ro \
-v /opt/registry/certs:/certs:ro \
registry:2.8.3
# HTTPS 已生效,HTTP 会直接拒绝
curl -sk https://localhost:5000/v2/
# {}
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/v2/
# 400
自签证书和内网 CA 有一个额外步骤:每台要拉镜像的客户端机器,都要信任这张证书,否则会报 x509: certificate signed by unknown authority:
# 把 registry.crt 分发到客户端,放到 Docker 专用的证书目录
# 目录名必须是 仓库域名:端口
mkdir -p /etc/docker/certs.d/registry.example.com:5000
cp registry.crt /etc/docker/certs.d/registry.example.com:5000/ca.crt
# 同时也加到系统信任库(curl 这类工具也要用)
cp registry.crt /usr/local/share/ca-certificates/registry.crt
update-ca-certificates # RHEL/Rocky 用 update-ca-trust extract
systemctl restart docker
docker login registry.example.com:5000 -u admin -p 'Admin@2026'
# Login Succeeded
九、内网 HTTP 仓库:insecure-registries
如果仓库只在内网用、不想折腾证书,可以让 Registry 继续跑 HTTP,代价是每台客户端都要显式声明"这个仓库是不安全的"。这是开发测试环境的常见做法,生产内网如果已有完善的网络隔离也可以接受,但对外暴露的仓库绝对不能这么干。
# 在每一台客户端上改 daemon.json(没有就新建)
cat /etc/docker/daemon.json
# {
# "insecure-registries": ["192.168.1.10:5000"]
# }
# 改完重启 Docker 守护进程
systemctl restart docker
# 验证:不加这个配置时 push 会报
# Get "https://192.168.1.10:5000/v2/": http: server gave HTTP response to HTTPS client
docker login 192.168.1.10:5000 -u admin -p 'Admin@2026'
docker push 192.168.1.10:5000/hello-world:latest
两个细节:localhost(127.0.0.1)是例外,Docker 默认就允许用 HTTP 访问本机仓库,不用配 insecure-registries;另外 insecure-registries 里写的是仓库地址(IP:端口或域名:端口),不要写成 http:// 开头的 URL。
十、Nginx 反向代理:域名 + TLS 终止
生产环境更推荐的架构是:Registry 本身只监听本机 5000 端口跑 HTTP,由 Nginx 对外提供 HTTPS。这样证书、域名、访问日志、限流都集中在 Nginx 一层,Registry 容器不用直接碰证书,续期也只需要 reload Nginx。
# 让 Registry 只监听本机,不对外暴露 5000
docker stop registry && docker rm registry
docker run -d \
-p 127.0.0.1:5000:5000 \
--restart=always \
--name registry \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml:ro \
-v /opt/registry/auth:/auth:ro \
registry:2.8.3
Nginx 配置(假设证书已按第八节方案 A 申请,域名 registry.example.com):
# /etc/nginx/conf.d/registry.conf
upstream docker-registry {
server 127.0.0.1:5000;
}
server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# 关键:镜像层可能几个 GB,必须放开请求体大小限制,否则大镜像 push 到一半会被 413 拒绝
client_max_body_size 0;
# 大层推送耗时很长,把超时放宽;同时关掉请求缓冲,让数据流式透传
proxy_read_timeout 900s;
proxy_request_buffering off;
chunked_transfer_encoding on;
location / {
proxy_pass http://docker-registry;
proxy_set_header Host $http_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;
}
}
# HTTP 强制跳转 HTTPS
server {
listen 80;
server_name registry.example.com;
return 301 https://$host$request_uri;
}
nginx -t && systemctl reload nginx
# 走域名验证,带认证访问
curl -s -u admin:'Admin@2026' https://registry.example.com/v2/_catalog
# {"repositories":["hello-world"]}
docker login registry.example.com -u admin -p 'Admin@2026'
# 注意:Nginx 监听 443 时,仓库地址就是域名本身,不用加 :5000
docker tag hello-world:latest registry.example.com/hello-world:latest
docker push registry.example.com/hello-world:latest
这里最容易踩的坑就是 client_max_body_size:Nginx 默认只允许 1MB 请求体,推镜像时一定会报 413 Request Entity Too Large,设为 0 表示不限制。第二个坑是 Host 头必须原样转发($http_host),Registry 的认证流程依赖它。
十一、docker-compose 一键部署
把前面所有配置收敛到一个 compose 文件里,换机器部署时一条命令搞定。再顺手加一个轻量 Web UI(joxit/docker-registry-ui),没有它的话,查仓库内容只能调 API。
# /opt/registry/docker-compose.yml
services:
registry:
image: registry:2.8.3
container_name: registry
restart: always
ports:
- "127.0.0.1:5000:5000"
volumes:
- /opt/registry/data:/var/lib/registry
- /opt/registry/config.yml:/etc/docker/registry/config.yml:ro
- /opt/registry/auth:/auth:ro
# delete.enabled 已在 config.yml 里开启;环境变量和配置文件二选一即可,
# 环境变量写法见第六节(REGISTRY_STORAGE_DELETE_ENABLED=true)
ui:
image: joxit/docker-registry-ui:latest
container_name: registry-ui
restart: always
ports:
- "127.0.0.1:8080:80"
environment:
- REGISTRY_URL=http://registry:5000
- REGISTRY_TITLE=My Private Registry
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
- SINGLE_REGISTRY=true
depends_on:
- registry
cd /opt/registry
docker compose up -d
docker compose ps
# NAME IMAGE STATUS
# registry registry:2.8.3 Up
# registry-ui joxit/docker-registry-ui:latest Up
# UI 走 Nginx 同样反代一个子路径或子域名即可,内网直接访问也行
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/
# 200
注意 compose 文件里我把 Registry 的 5000 和 UI 的 8080 都绑在 127.0.0.1 上,对外只通过 Nginx 暴露,这种"服务只监听本机、统一走反代"的习惯能省掉很多误暴露端口的安全事故。UI 的删除功能需要 Registry 开 delete.enabled,且 UI 本身不做权限校验,生产环境不要把它直接暴露到公网,至少套一层 Nginx 基础认证。
十二、日常运维:查询、删除、垃圾回收与备份
查看镜像:Registry V2 API
REG=https://registry.example.com
AUTH='admin:Admin@2026'
# 列出所有仓库(2.8.2 起单次最多返回 1000 条,超了用 ?n=100&last=xxx 分页)
curl -s -u "$AUTH" $REG/v2/_catalog
# {"repositories":["hello-world","myapp/backend"]}
# 列出某个镜像的所有标签
curl -s -u "$AUTH" $REG/v2/myapp/backend/tags/list
# {"name":"myapp/backend","tags":["v1.0","v1.1","latest"]}
# 查看某个标签的 manifest 摘要(删镜像要用这个摘要,见下节)
curl -s -I -u "$AUTH" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
$REG/v2/myapp/backend/manifests/v1.0 | grep -i docker-content-digest
# Docker-Content-Digest: sha256:abc123...
删除镜像:先删 manifest,再垃圾回收
Registry 的删除分两步,这是新手最容易误解的地方:调删除 API 只删掉标签和 manifest 的引用,磁盘空间要靠垃圾回收(garbage-collect)才真正释放。前提是 config.yml 里 storage.delete.enabled: true(第六节已开)。
REG=https://registry.example.com
AUTH='admin:Admin@2026'
DIGEST=sha256:abc123... # 上一步查到的 Docker-Content-Digest
# 1. 按摘要删除 manifest(按标签删是不行的,必须用摘要)
curl -s -u "$AUTH" -X DELETE $REG/v2/myapp/backend/manifests/$DIGEST
# 成功返回 202 Accepted
# 2. 停掉 Registry,用一次性容器跑垃圾回收(必须停服或开只读模式,否则 GC 会拒绝)
docker stop registry
docker run --rm \
-v /opt/registry/data:/var/lib/registry \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml:ro \
registry:2.8.3 garbage-collect /etc/docker/registry/config.yml
docker start registry
# GC 输出里能看到回收了多少 blob
# myapp/backend: marking manifest sha256:abc123...
# myapp/backend: marking blob sha256:...
# 4 blobs marked, 2 blobs eligible for deletion
# ...
不想停服的话,可以把 storage.maintenance.readonly.enabled 设为 true 后重启(只读模式下 push 会被拒绝、pull 正常),跑完 GC 再改回 false。2.8+ 的 GC 还支持 --delete-untagged 参数,会连没有标签引用的 manifest 一起清理,适合定期清理悬空镜像。
备份与恢复
# 备份:停服后把整个数据目录打 tar 包(config.yml、auth、certs 也一起备走)
docker stop registry
tar -czf /backup/registry-$(date +%F).tar.gz -C /opt/registry data config.yml
docker start registry
# 恢复:解压回原位置即可,版本一致的前提下无需任何转换
tar -xzf /backup/registry-2026-11-11.tar.gz -C /opt/registry
docker restart registry
如果数据目录在 S3 这类对象存储上,备份就是对象存储自己的快照/跨区复制,不用再 tar。定期 GC 也建议写进 cron,例如每周日凌晨停服跑一次,脚本就是上面那几行。
十三、常见问题排查表
按报错现象查,先看现象再对原因,基本覆盖了实际部署中 90% 的问题:
| 报错现象 | 原因 | 解决办法 |
|---|---|---|
http: server gave HTTP response to HTTPS client | 仓库是 HTTP,客户端默认按 HTTPS 访问 | 配 HTTPS(第八节),或客户端加 insecure-registries(第九节) |
x509: certificate signed by unknown authority | 自签/内网 CA 证书客户端不信任 | 证书拷到 /etc/docker/certs.d/域名:端口/ca.crt 并重启 Docker |
unauthorized: authentication required | 没登录或账号密码错 | docker login 重新登录;检查 htpasswd 文件与 config.yml 的 auth 路径 |
denied: requested access to the resource is denied | 登录了但仓库拒绝(常为认证配置未生效) | 确认容器重启过、auth 目录已挂载、htpasswd 是 bcrypt(-B)生成 |
blob unknown to registry | push 的层在仓库里不存在 | 一般是断点续传残留或层损坏,重推一次;仍不行则检查存储后端磁盘空间 |
manifest unknown | 标签或摘要不存在 | 先查 tags/list 确认标签名;删镜像时必须用摘要不是标签 |
413 Request Entity Too Large | Nginx 默认请求体 1MB 上限 | client_max_body_size 0; 后 reload Nginx |
| push 大镜像中途断开 / 超时 | 反代超时太短或磁盘满了 | proxy_read_timeout 900s;检查 df -h 磁盘余量 |
_catalog 只返回 1000 条 | 2.8.2 起默认上限 1000 | catalog.maxentries 调大,或用 ?n=&last= 分页拉取 |
| GC 报错或空间没释放 | Registry 没停服、delete 未开启 | 确认 delete.enabled: true;GC 前停服或开只读模式;删 manifest 必须按摘要 |
排障时最有用的两个命令:docker logs registry --tail 100 -f 看服务端日志(把 log.level 临时改成 debug 能看到每次请求的细节),客户端加 --debug(如 docker --debug push ...)看完整的 HTTP 交互。
十四、生产加固清单
对照这张清单过一遍,再对外提供服务:
- 认证必开:htpasswd 只是底线,密码用 bcrypt 且定期轮换;账号文件权限 600,容器内只读挂载。
- 全站 HTTPS:对外只暴露 443,Registry 只监听 127.0.0.1:5000,证书用 Let's Encrypt 自动续期。
- 网络隔离:仓库所在服务器只开放 443(和 SSH),5000/8080 不对外暴露;有条件放内网 VLAN 或安全组后面。
- 存储后端:镜像量上 TB 后,把
storage从 filesystem 换成 S3 / MinIO / 阿里云 OSS,配置里对应改s3驱动段,数据目录不再是单点。 - 资源限制:compose 里给 Registry 加
deploy.resources.limits(如内存 2G、CPU 2 核),防止某个大推送把宿主机拖垮。 - 只读与删除策略:
delete.enabled只在需要清理时开,平时可关;定期 GC 用 cron 在低峰期执行。 - 日志与监控:日志接集中式日志系统;
/debug/health接 Prometheus 或 uptime 监控,存储后端异常能第一时间告警。 - 备份:数据目录每日备份(或对象存储快照),恢复流程至少演练过一次。
- 版本 pin 住:不要用
registry:2这种浮动标签跑生产,用registry:2.8.3,升级先在测试环境验证。 - 权限边界:记住 htpasswd 不分读写权限,多团队、要审计、要扫描时直接上 Harbor,不要在 Registry 上硬凑。
十五、总结与常用命令速查
全文的路线其实就一条:先跑起来,再逐项加固。最简安装验证通路 → 配置文件接管行为 → htpasswd 上认证 → HTTPS(或 Nginx 反代)上加密 → compose 收敛部署 → API + 垃圾回收做运维 → 对照加固清单收尾。官方 Registry 的设计哲学就是"只做仓库这一件事",理解了存储结构和 V2 API,排障和扩展都不难;哪天业务长大了要 RBAC、扫描、复制,平滑迁移到 Harbor 即可,数据目录和镜像格式都是通用的。
# ---- 速查:最常用命令 ----
# 启动 / 停止 / 重启
docker compose -f /opt/registry/docker-compose.yml up -d
docker compose -f /opt/registry/docker-compose.yml down
docker restart registry
# 状态与日志
docker ps --filter name=registry
docker logs registry --tail 50 -f
# 健康检查与镜像列表
curl -s https://registry.example.com/v2/
curl -s -u admin:'Admin@2026' https://registry.example.com/v2/_catalog
# 推拉镜像
docker tag myapp:v1.0 registry.example.com/myapp:v1.0
docker push registry.example.com/myapp:v1.0
docker pull registry.example.com/myapp:v1.0
# 删除镜像(查摘要 -> DELETE -> 垃圾回收)
DIGEST=$(curl -s -I -u admin:'Admin@2026' \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry.example.com/v2/myapp/manifests/v1.0 \
| grep -i docker-content-digest | awk '{print $2}' | tr -d '\r')
curl -s -u admin:'Admin@2026' -X DELETE \
https://registry.example.com/v2/myapp/manifests/$DIGEST
docker stop registry
docker run --rm -v /opt/registry/data:/var/lib/registry \
-v /opt/registry/config.yml:/etc/docker/registry/config.yml:ro \
registry:2.8.3 garbage-collect /etc/docker/registry/config.yml
docker start registry
相关阅读:本站《Linux 离线安装 Docker 25.0.4 详细教程》和《Linux 安装 Docker Compose v2 详细教程》分别是本文的前置条件(Docker 本体与 compose 工具),三篇合起来就是一套完整的内网容器基础设施搭建手册。






