很多个人站长在用 Docker 部署网站之后,都会遇到一个很实际的问题:自己构建的镜像怎么保存和分发?如果服务器重装或者要迁移到新机器,难道每次都要重新拉代码、重新 build 一遍?更麻烦的是,有些镜像里包含了数据库连接串、密钥文件,直接推到 Docker Hub 公网仓库是绝对不行的。这时候,自建一个私有镜像仓库(Docker Registry)就是最合理的解法。本文用一台 2GB 内存的入门 VPS,从零把私有 Registry 跑起来,并讲清楚认证、HTTPS、垃圾回收和日常运维的完整流程。
一、为什么个人站长需要一个私有 Registry
先把需求讲清楚,避免为了技术而技术。私有 Registry 解决的是下面这几类真实痛点:
- 镜像持久化:本地 docker build 出来的镜像只存在于当前服务器的 /var/lib/docker 里,一旦服务器被重装或者 docker system prune 手滑执行,镜像就没了。推到 Registry 等于给镜像做了一份可靠的异地副本。
- 多机部署:一台应用服务器、一台备份服务器、一台测试机,三台机器要跑同一个镜像。有 Registry 之后,只需要 build 一次、push 一次,其余机器 docker pull 就行。
- 避免泄露敏感信息:自建镜像里往往包含 .env、证书、内部 API 地址。推到公网仓库哪怕是 private repo,也有被误设为 public 或者账号被盗的风险。私有 Registry 只监听自己的域名,风险面小得多。
- 节省带宽与时间:从 Docker Hub 拉取在国内网络环境下经常超时或者龟速,自建 Registry 走内网或者就近节点,速度快一个数量级。
二、部署前的准备工作
Registry 官方镜像 registry:2 本身很轻,但它默认只提供 HTTP,且没有任何认证。生产环境必须补上两件事:HTTPS 和 basic auth。所以准备清单如下:
- 一台 VPS,建议 1 核 2GB 及以上,磁盘根据镜像数量决定(一般 20GB 起步)
- 一个子域名,例如
registry.example.com,解析到这台机器 - Docker 与 Docker Compose 已安装
- Nginx 已安装(用于反向代理 + SSL 卸载)
- 一份 SSL 证书,可以用 Let's Encrypt 免费申请
这里采用「Nginx 做 TLS 终止 + 反向代理,Registry 跑在容器内监听 5000 端口」的架构。这样做的好处是证书管理、限流、访问日志都可以复用你已有的 Nginx 经验,Registry 容器本身保持最简配置。
三、生成 basic auth 认证文件
Registry 支持 htpasswd 格式的认证文件。先生成密码文件:
mkdir -p /data/registry/auth docker run --rm \ --entrypoint htpasswd \ httpd:2 -Bbn registry_admin 你的强密码 \ > /data/registry/auth/htpasswd
几个要点:-B 表示使用 bcrypt 加密,比早期 MD5 安全得多;-b 允许在命令行直接传密码(注意这会留在 shell history 里,生产环境建议交互式输入);生成的 htpasswd 文件要确认权限为 600,属主是启动容器的用户。
如果后面要增加用户,重复执行上面的命令并追加输出即可(用 >> 而非 >)。要删除用户,用文本编辑器删掉对应行。
四、用 Docker Compose 启动 Registry
创建目录与配置文件:
mkdir -p /data/registry/data cd /data/registry
编写 docker-compose.yml:
version: "3"
services:
registry:
image: registry:2
container_name: registry
restart: always
ports:
- "127.0.0.1:5000:5000"
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_STORAGE_DELETE_ENABLED: "true"
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
volumes:
- ./auth:/auth:ro
- ./data:/var/lib/registry注意 ports 绑定的地址是 127.0.0.1:5000 而不是 0.0.0.0:5000。这一点非常重要:Registry 本身不处理 TLS,如果把它暴露在公网 IP 上,攻击者可以直接用明文 HTTP 访问你的镜像仓库,认证凭据也会以明文传输。绑定到本地回环,只允许本机的 Nginx 转发过来,是最稳妥的做法。
启动并检查状态:
docker compose up -d docker compose ps curl -s http://127.0.0.1:5000/v2/ -i
最后一条命令如果返回 401 Unauthorized,说明认证已经生效(这是正常且期望的结果);如果返回 200 且没有认证,说明 htpasswd 配置没读到,需要检查挂载路径和文件权限。
五、配置 Nginx 反向代理与 HTTPS
Registry 对上传大镜像有要求,Nginx 必须放开请求体限制,否则 push 一个 300MB 的镜像会得到 413 Request Entity Too Large。同时在 Docker 的分层上传过程中,会出现没有 Host 头或者 Host 为空的请求,需要给默认 server 加一段兜底配置。
站点的 server 配置:
server {
listen 443 ssl http2;
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;
client_max_body_size 0;
chunked_transfer_encoding on;
location / {
proxy_pass http://127.0.0.1:5000;
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;
proxy_read_timeout 900;
proxy_buffering off;
}
}同时需要一段默认 server 来处理 Host 为空的请求,否则 push 过程中会随机报错:
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
client_max_body_size 0;
chunked_transfer_encoding on;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $http_host;
}
}这里 client_max_body_size 0 表示不限制请求体大小,这在 Registry 场景下是必要配置。proxy_read_timeout 900 是给大镜像推送留足时间,避免传输中途被 Nginx 掐断。
配置完成后 reload:nginx -t && systemctl reload nginx。
六、配置 Docker 客户端信任并推送镜像
在自己的开发机或应用服务器上,先登录:
docker login registry.example.com # 输入用户名 registry_admin 和刚才设置的密码
如果客户端报 x509: certificate signed by unknown authority,说明证书链不完整或者客户端没安装根证书。用 Let's Encrypt 的正式证书不会有这个问题;如果你用的是自签证书,需要在客户端的 /etc/docker/daemon.json 里通过 insecure-registries 声明(仅限内网测试环境使用,公网绝不要这么做)。
给镜像打标签并推送。假设本地已经构建好一个名为 my-blog 的镜像:
docker tag my-blog:latest registry.example.com/my-blog:latest docker tag my-blog:latest registry.example.com/my-blog:2026-09-12 docker push registry.example.com/my-blog:latest docker push registry.example.com/my-blog:2026-09-12
推荐同时打 latest 和日期戳两个标签。latest 方便拉取最新版,日期戳则让你在出问题时能精确回滚到某个历史版本。这个习惯在多机部署环境里价值极高。
在另一台服务器上验证拉取:
docker login registry.example.com docker pull registry.example.com/my-blog:latest docker images | grep my-blog
七、查看仓库内容与管理镜像
Registry 提供了 REST API,可以直接查询仓库列表和标签:
# 列出所有仓库 curl -u registry_admin:密码 -s \ https://registry.example.com/v2/_catalog | python3 -m json.tool # 列出某个仓库的所有标签 curl -u registry_admin:密码 -s \ https://registry.example.com/v2/my-blog/tags/list | python3 -m json.tool
删除某个标签时要注意,Registry 的删除是「按 digest 删除 manifest」,而不是按标签名:
# 1. 先取到该标签对应的 digest curl -u registry_admin:密码 -s -I \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ https://registry.example.com/v2/my-blog/manifests/2026-09-12 | grep -i docker-content-digest # 2. 用 digest 删除 curl -u registry_admin:密码 -X DELETE \ https://registry.example.com/v2/my-blog/manifests/sha256:xxxxxxxx
删除 manifest 只是让它从 API 里消失,磁盘空间并不会立刻释放。真正回收空间需要执行垃圾回收:
docker exec registry bin/registry garbage-collect \ --delete-untagged=true /etc/docker/registry/config.yml
垃圾回收建议放在低峰时段,并且回收前先停止应用容器的写入(回收期间不要 push)。如果镜像仓库很大,这个过程可能持续几分钟到几十分钟,属于正常现象。
八、定时清理与磁盘监控
个人的 Registry 最容易死在磁盘写满上。持续集成平台每天构建多次,如果只打 latest 标签并反复覆盖,旧的 manifest 会变成悬空对象累积在磁盘里。建议做两件事:
- 定期清理策略:用一条 crontab 每月执行一次 garbage-collect,并配合脚本删除超过 90 天的旧日期标签。
- 磁盘告警:用 df 加一个简单脚本,当 /data 分区使用率超过 80% 时发邮件或钉钉告警。写法和普通服务器磁盘监控一致。
# /usr/local/bin/registry-gc.sh #!/bin/bash set -e LOG=/var/log/registry-gc.log echo "=== $(date '+%F %T') start ===" >> $LOG docker exec registry bin/registry garbage-collect \ --delete-untagged=true /etc/docker/registry/config.yml >> $LOG 2>&1 echo "=== $(date '+%F %T') done ===" >> $LOG du -sh /data/registry >> $LOG
加入 crontab:0 4 1 * * /usr/local/bin/registry-gc.sh,即每月 1 日凌晨 4 点执行。
九、备份 Registry 数据
Registry 的所有镜像层和元数据都存在 /data/registry/data 目录下,结构是纯文件系统,所以备份非常直接:用 rsync 同步到异地即可,不需要额外导出工具。这正是自建 Registry 相比云服务的一个优势——数据完全可控、可用最朴素的方式备份。
rsync -avz --delete \ /data/registry/data/ \ backup@backup-host:/backup/registry-data/
建议同时把 auth/htpasswd 和 docker-compose.yml 一起备份,这样恢复时只需要在新机器上:恢复 data 目录 → 恢复 compose 文件 → docker compose up -d → 配好 Nginx,整个过程十分钟内可以完成。
十、常见故障排查
- push 报 413:Nginx 的 client_max_body_size 没放开,检查是否设为 0。
- push 报 405 或者随机失败:缺少 Host 为空的 default server 兜底段,补齐即可。
- pull 报 unauthorized:客户端没 login,或者密码里有特殊字符被 shell 解释。用单引号包裹密码重试。
- 登录成功但 push 报 unknown blob:通常是中间被代理或防火墙截断,检查是否用了 CDN 并且没放行 chunked 上传。
- 磁盘暴涨:参考第八节的垃圾回收,注意回收前必须先删除不再需要的 manifest,否则唯一引用的层不会被清理。
- 容器启动即退出:看
docker logs registry,绝大多数是 htpasswd 文件路径或权限错误。
十一、小结
自建私有镜像仓库并不复杂,核心的配置量比想象中少:一个 compose 文件、一段 Nginx 反代、一份 htpasswd。真正考验站长的是后续的运维细节——磁盘回收、定期备份、标签规范。把这几件事做成脚本和定时任务之后,你的镜像管理就进入了可持续状态:多台服务器共享同一套镜像、部署一次到位、回滚有据可依。
对于个人站长来说,这套方案还有一个隐性收益:你不再依赖任何外部平台。镜像仓库、证书、备份全部握在自己手里,这对长期运营的独立网站而言,和数据库、代码一样重要。值得花一个下午把它一次配好。