综合 单二进制 + SQLite 搭私有镜像仓库:stashhub 兼做 Docker Hub 拉取缓存

2026-09-29 20:01:41

单二进制 + SQLite 搭私有镜像仓库:stashhub 兼做 Docker Hub 拉取缓存

stashhub 是一个自托管镜像仓库,当前版本 v0.1,MIT 协议,定位是 Harbor / Zot 的轻量替代。项目地址:https://www.stash-hub.one/(官网未直接给出仓库地址)。

它的形态是一个 Go 二进制,内嵌 distribution v3,内嵌 React 19 前端,状态全部落在 SQLite 里,docker compose up 就能跑。协议层兼容 OCI Distribution v2,客户端可用 Docker / Podman / Skopeo;存储侧支持 SQLite 与 S3、R2、MinIO、B2、DO Spaces 等 6 种以上后端。部署不需要单独腾一台服务器,一台 VPS、Raspberry Pi 或办公电脑都够,从零到能 push 大约 30 秒,常驻内存约 60MB。

主要能力可以归成几块:

  • 单二进制零配置:一个 Go 二进制,内嵌 distribution v3 与 React 19 前端,SQLite 持久化全部状态。
  • OCI 协议原生:没有代理层、没有协议翻译,docker push/pull/login 直连标准 v2 协议。
  • 拉取代理缓存:可以配成 Docker daemon 的 registry-mirror,支持多上游路由、Manifest TTL、LRU sweeper、tag 白名单淘汰,这些都在管理面板可调。
  • 鉴权、会话、审计:bcrypt + 旋转刷新 JWT;每个用户的 PAT 可以当 docker 密码用;有实时会话列表;每次 push/pull/delete 都有审计行落库。
  • 管理后台:用户、存储、GC、保留策略 dry-run、SMTP 测试、镜像加速策略都在 Web UI,改完即时生效。
  • 存储:默认 filesystem 驱动,改配置即可切到 S3/R2/MinIO/B2/DO Spaces,同一份二进制、同一套 UX。

拉取代理的工作流程大致是:客户端 docker pull alpine:3.19 → stashhub 的 manifest 命中(ttl 6h)→ cache layer 命中 sha256:e07f… → 未命中则从 registry-1.docker.io 流式回源 → 回源数据写入 filesystem/S3/R2 → 返回客户端,下次走本地命中。

Web UI 用 React 19 + Tailwind 4 + Radix UI,中英 i18n,暗色/亮色两套主题。

官方给了一张与 Harbor、Zot 的对比表,标注为 fresh deploy、1 用户、约 100 tags 的实测数据,仅供参考:

能力stashhubHarborZot
单二进制部署是否,多服务是
内置 Web UI是,React 19是极简
拉取代理缓存多上游proxy 项目基础
管理后台用户·GC·保留·SMTP企业级无
审计日志用户 + 仓库是无
部署时间~30s~1h(helm)~2min
内存占用~60MB~1GB+~80MB
气质小团队·自托管企业·银行CLI 优先

需要留意这是 v0.1 早期版本,生产采用前请自行评估。

快速开始

单二进制 + SQLite 默认就能跑。docker compose up 之后浏览器打开 http://localhost:8080,按引导设置第一个 admin。

docker-compose.yml:

services:
stashhub:
image: stashhub/stashhub:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/var/lib/stashhub
environment:
# 必填:外部访问地址(决定 mirror endpoint 的 scheme)
EXTERNAL_URL: https://stash-hub.one
# 必填:JWT secret(≥ 32 字节随机串)
JWT_SECRET: "$(openssl rand -hex 32)"

生产 compose 的启动方式:

$ docker compose -f docker-compose.prod.yml up -d
$ docker compose logs stashhub | grep "bootstrap admin"
level=info bootstrap admin = admin / k3-7gZ-9pX-2vQ
level=info → open http://localhost:8080

第一次启动访问 /,Web UI 引导创建第一个 admin 账号。登录后进入 /admin,所有运行期配置都在这里,改完即时生效,不需要重启。右上角创建第一个仓库,然后用 docker login + docker push 验证链路:

$ docker compose up -d
$ docker login stash-hub.one
Username: alice / Password: ****
Login Succeeded
$ docker push stash-hub.one/alice/api:v1.4.0
The push refers to repository [stash-hub.one/alice/api]
v1.4.0: digest: sha256:9b3a…7c2e size: 1789
$ curl -s $REG/api/v1/repositories/alice/api/tags | jq -r '.[].name'
v1.4.0 / v1.3.2 / latest

要注意 EXTERNAL_URL 会影响到生成的镜像 endpoint、setup-mirror.sh 里的 URL 以及 CORS 行为,必须写最终用户访问的协议 + 域名,例如 https://stash-hub.one。

存储后端

默认是 filesystem,所有 blob/manifest 落到 DATA_DIR 下的子目录,适合单机或测试环境。需要弹性扩容时切到对象存储,同一份二进制,热重载不重启。

S3 / R2 / MinIO / B2 / Spaces 的入口在管理后台 → System Settings → Storage,切到 s3 后填这些字段:

  • region:S3 区域;MinIO/R2 填 auto 或留空。
  • bucket:桶名。
  • access_key / secret_key:secret_key 在 PATCH 时传空字符串表示保留原值。
  • region_endpoint:非 AWS S3 必填,例如 https://.r2.cloudflarestorage.com。
  • force_path_style:MinIO / 旧版 S3 需要 true;R2 / AWS 留 false。
  • root_directory:桶内前缀,多环境共享一个桶时建议设置,例如 prod/。

S3 模式下还有一个本地 L1 缓存:把热 layer 缓存到本机磁盘,命中后跳过远端往返。Storage 页底部的 Local Cache 卡片可以调上限与 LRU 天数,0 表示关闭。

Storage 页还有 Probe 按钮(test before save),不写库、不切 manager,按「假如保存」的配置跑一次 put/stat/get/delete。

镜像加速

stashhub 可以同时作为私有 registry 和 pull-through cache。做法是配一个独立域名给镜像加速用,docker daemon 把它当 mirror,第一次拉穿透回源,之后在局域网内命中。

1)配镜像加速域名

管理后台 → Site Settings → mirror_domain,填一个不同于 registry_hostname 的裸域名,例如 mirror.stash-hub.one。两个域名都解析到同一台 stashhub。

这里为什么要两个域名:镜像加速是把 Docker Hub / GHCR 的内容反向代理到自己域下,如果和私有 registry 共用 host,会出现命名冲突,攻击者可以推送同名仓库覆盖上游内容。结构性隔离能直接杜绝这类攻击。

2)加上游

Admin → Mirror → Upstreams。一个上游 = 一个 path prefix + remote URL + 可选凭据,客户端访问 /v2/[prefix]/[image] 时按 prefix 路由。

  • alias:展示名,如 dockerhub。
  • path_prefix:URL 前缀,匹配 ^[a-z0-9][a-z0-9_-]{0,62}$。
  • remote_url:上游 v2 端点,如 https://registry-1.docker.io。
  • username / password:可选,AES-256-GCM 加密落盘,密钥派生自 JWT_SECRET。
  • is_default_mirror:客户端不写 prefix 时落到哪个上游,最多 1 个。

3)客户端 daemon 配置

每台需要加速的机器上执行:

curl -fsSL https://stash-hub.one/setup-mirror.sh | sudo bash

脚本会自动检测 Docker Desktop / Linux+systemd / Colima / OrbStack,备份原 daemon.json 后把 mirror endpoint 合并进 registry-mirrors。

缓存策略

Admin → Mirror → Policy,四维淘汰字段:

  • lru_days:未访问超过 N 天淘汰。
  • max_cache_bytes:总容量上限。
  • min_hits_to_cache:首次拉不缓存,到 N 次命中才落盘。
  • tag_whitelist_regex:只缓存符合正则的 tag,屏蔽 nightly/dev 之类的噪音。

Manifest TTL(manifest_ttl_hours)参考 Artifactory:到期后 HEAD 上游比对 digest,没变就只刷时间戳,开销接近零。

运行期配置

几乎所有可调项都在 Web 管理面板,改一个字段立即生效。

  • Site Settings:site_name、registry_hostname(私有 registry 域名)、mirror_domain(镜像加速专用域名,必须 ≠ registry_hostname,留空表示不开启)、public_signup_enabled、default_visibility(public/private)。
  • SMTP:配置完成后有 Send Test Email 按钮,向当前 admin 的注册邮箱发测试邮件,收件人锁死为操作者自己,避免账号被攻陷后变成 SMTP 跳板。
  • Retention:全局默认 + 仓库级 override,GC 提供 dry-run。
  • Trusted Proxies / Real IP:站在反向代理后面时配置 CIDR 列表与 header 名,让审计日志记录真实客户端 IP。空列表等于关闭。

管理 API

所有 Web UI 操作都走 /api/v1,可以直接对接 CI 或自动化脚本。鉴权用 session cookie 或 API token。

  • GET /api/v1/config:公开信息,站点名、注册开关、镜像加速 endpoint。
  • GET /api/v1/admin/settings:完整运行期配置,secret 字段以 *_set 布尔位返回。
  • PATCH /api/v1/admin/settings:局部更新;nil 字段不变;Storage 改动触发热重载,失败自动回滚。
  • POST /api/v1/admin/storage/test:用提交的 patch 跑 put/stat/get/delete,不写库、不切 manager。
  • GET /api/v1/admin/upstreams:列出镜像加速上游。
  • POST / PATCH / DELETE:上游 CRUD,完成后自动 reload proxy 集合。
  • POST /api/v1/admin/mirror/sweep:手动触发缓存清理 sweeper。
  • GET /api/v1/admin/mirror/stats:按上游聚合的实时命中率与容量占用。

完整 OpenAPI schema 见仓库 internal/webapi/ 目录。

常见问题

忘了 admin 密码?

在服务端执行:

./stashhub admin reset-password

会生成一次性令牌,走标准重置流程。

docker login 的密码栏填什么?

推荐填 PAT,不要用登录密码。Docker 26+ 默认走 containerd image store,containerd 的凭据路径与 moby legacy 不一致;PAT 是后端校验的明文 token,pull 走 containerd resolver 时认证更稳。PAT 前缀是 sthb_。

怎么生成 PAT?

Web UI 右上角头像 → 设置 → Tokens → 新建。可以选过期时间,不填表示永不过期。token 只在创建那一次完整显示,离开页面后只能看到前几位。当前实现里 PAT 不分 scope,一个 token 等同该账号全部权限,按主密码的强度来托管。

公有仓库需要 login 才能 pull 吗?

不需要。visibility=public 的仓库允许匿名 pull,docker pull host/repo:tag 直接生效。但 push、delete、查看协作者始终强制鉴权。

怎么共享仓库?

改可见性(public)或加协作者:reader 只 pull,writer 可 push + pull,admin 含改设置和删仓库。owner 默认隐含最高权限。

怎么删 tag 或仓库?

Web UI 仓库页的 tag 行操作菜单可以删 tag;仓库设置最底部「危险区」可以整库删除。CI/脚本用:

DELETE /api/v1/repositories/{owner}/{name}/tags/{tag}
DELETE /api/v1/repositories/{owner}/{name}

注意元数据立即消失,但底层 blob 占用要等垃圾回收才释放——手动触发 Admin → Mirror → Sweep,或等定时任务。

registry 必须 HTTPS 吗?

是。Docker daemon 默认按 HTTPS 跟 registry 握手,裸 HTTP 部署在 docker login 或 GitLab CI 的 docker push 时会撞到:

Error response from daemon: Get "https://your.host/v2/": http: server gave HTTP response to HTTPS client

两种修法:

方案 A · HTTPS 反代(推荐长期):在 stashhub 前挂 Caddy/nginx/Traefik 做 TLS 终端,EXTERNAL_URL 改成 https://...。Caddy 一行:

your.host { reverse_proxy stashhub:8080 }

内网自签 CA(mkcert/cfssl)把 CA 装到 /etc/docker/certs.d//ca.crt。

方案 B · insecure-registries(应急,仅内网):每台跑 docker login 的机器(包括 GitLab Runner)改 /etc/docker/daemon.json:

{"insecure-registries": ["your-stashhub-host:port"]}

然后 systemctl restart docker。host 字段不带 http:// / https:// 前缀。Runner 跑 dind 时 flag 要传给 dind service:

services:
- name: docker:dind
command: ["--insecure-registry=your-stashhub-host:port"]

这条路径下 PAT / 登录密码走明文,只在测试或隔离环境用。

反代后 docker push 大镜像报 413?

nginx 默认 client_max_body_size 是 1m,blob 上传几乎一定超。server/location 块改成 client_max_body_size 0;(不限)或至少 4G。Caddy 默认不限;Traefik 看 buffering.maxRequestBodyBytes。

从 filesystem 切到 S3,旧镜像怎么办?

旧数据不会自动迁移。Storage 切换是热重载新 manager,旧目录里的 blob/manifest 仍在磁盘上,但 registry 不再读,等于变孤儿。要保留就先迁后切:

aws s3 sync /registry/ s3:////

保持同名前缀,确认新桶能列到对应路径后再切换。反向同理。Storage 页的 Probe 只测连通性,不验证内容是否搬过去。

mirror 缓存策略默认值合理吗?

默认值对多数团队够用:lru_days=30、max_cache_bytes=50 GiB、min_hits_to_cache=1(首次拉就缓存)、manifest_ttl_hours=2。需要调的场景:

  • 磁盘紧 → 调小 max_cache_bytes 与 lru_days。
  • 上游 tag 频繁变动(:latest、:edge)→ 把 manifest_ttl_hours 缩短到 1 或更短。
  • 不想被 nightly/dev 噪音塞满 → 用 tag_whitelist_regex 只放行关心的 tag。
  • 只想缓存真正热的镜像 → 把 min_hits_to_cache 提到 2~3,首次拉穿透不落盘。

为什么 docker pull 仍走外网?

检查三件事:

  1. docker info | grep -A1 'Registry Mirrors' 是否包含你的 mirror endpoint;
  2. mirror_domain 的 DNS 是否解析到 stashhub;
  3. EXTERNAL_URL 的 scheme 与边缘 TLS 终端是否一致——错配会导致 docker daemon 静默回退到 docker.io。

© stashhub · MIT

复制全文 生成海报 Docker 镜像仓库 OCI 运维 自托管

推荐文章

程序员茄子在线接单