裸 distribution 太素、Harbor 太重:Dockery 把 registry 收进一个容器
Dockery 是自托管 Docker Registry:Distribution v3.1.0 + React UI + 账户/权限 + 单镜像。一个容器同时跑 registry、API、Web UI,共用同一个 nginx 端口对外,面向小团队和个人开发者。
- 项目地址:https://github.com/bizjs/Dockery
- 镜像:
ghcr.io/bizjs/dockery - 设计文档:
docs/dockery-design.md - 部署参考:
docs/deployment.md
背景
裸跑 distribution/distribution 太原始:没有 UI、没有账户,htpasswd 所有人共用一把钥匙。Harbor 又太重,官方 compose 十来个容器,五个人小组的配置成本大于收益。Dockery 走中间地带:一个镜像、一个端口,不依赖 Postgres/Redis。
| | 裸 distribution | Harbor | Dockery |
| Web 管理界面 | 无 | 有 | 有 |
| 账户/角色 | htpasswd(平权) | LDAP/OIDC/SSO | 三档角色 + glob 仓库模式 |
| 需要起的容器数 | 1 | 10+ | 1 |
| 外部依赖 | 无 | Postgres+Redis | 无 |
| 备份 | 一个目录 | 多库多卷 | 一个 /data 目录 |
| 多架构镜像 | 自己打 | 官方提供 | amd64 + arm64 官方 |
| 从零到可用 | 装完还得自研 UI | 半天起 | 几分钟 |
特性:
- 推送/拉取/浏览 OCI + Docker v2 镜像;
- CLI 与 Web UI 共用账户,三档角色 admin/write/view + per-user glob 仓库模式;
- Ed25519 短命 registry JWT(默认 5 分钟,registry 通过 JWKS 验签);
- 可动态启用 Tag 防覆盖(现有 tag 不得移动到不同 digest,默认关闭);
- React 19 UI(登录、角色守卫、用户&权限管理、改密);
- 一个镜像、一个端口、SQLite + 文件系统 blob;备份只需
/data。
v0.1 不做:镜像扫描、cosign 签名、复制、多租户、HA、代理缓存。
快速开始
# Docker Desktop → 设置 → Docker Engine: "insecure-registries": ["localhost:5001"]
make dev DOCKERY_ADMIN_PASSWORD='change-me' # 本地构建镜像并启动整套
open http://localhost:5001 # 用 admin / change-me 登录
DOCKERY_ADMIN_PASSWORD 只在 /data 首次初始化时生效。TLS 请自挂反向代理。
推送镜像:
docker login localhost:5001
docker tag hello-world localhost:5001/demo/hello:1
docker push localhost:5001/demo/hello:1
部署
生产直接拉预构建镜像 ghcr.io/bizjs/dockery,不要从源码构建。生产必须钉具体版本号,别用滚动的 :latest。TLS 自挂反向代理(nginx/Caddy/Traefik);未启用 TLS 时,docker 客户端要把 localhost:5001(或实际地址)加进 insecure-registries。
方式 A,docker run:
docker run -d \
--name dockery \
--restart unless-stopped \
-p 5001:5000 \
-v /srv/dockery:/data \
-e DOCKERY_ADMIN_PASSWORD='change-me-on-first-boot' \
-e REGISTRY_AUTH_TOKEN_REALM='https://registry.example.com/token' \
ghcr.io/bizjs/dockery:0.1.0
方式 B,docker compose:用仓库里的 docker-compose.ghcr.yml,先 export DOCKERY_ADMIN_PASSWORD / REGISTRY_AUTH_TOKEN_REALM / DOCKERY_IMAGE,然后
docker compose -f docker-compose.ghcr.yml pull
docker compose -f docker-compose.ghcr.yml up -d
用户与权限
Web UI 管理员菜单 Manage users 可以创建用户、改角色、改密、启停、删除;permissions 抽屉给 write/view 用户加/改/撤销仓库模式。view 角色看不到删除按钮。
CLI 备用(无需启动 HTTP 服务):
docker exec -it dockery dockery-api -conf /etc/dockery user list
docker exec -it dockery dockery-api -conf /etc/dockery user create alice write
docker exec -it dockery dockery-api -conf /etc/dockery user grant alice 'alice/*,shared/app'
docker exec -it dockery dockery-api -conf /etc/dockery user passwd alice
docker exec -it dockery dockery-api -conf /etc/dockery user revoke 42 # permission id
docker exec -it dockery dockery-api -conf /etc/dockery user delete alice
系统会拒绝删除或降级最后一个 admin。
Tag 防覆盖
管理员可在 Settings → Registry → Tag overwrite protection 动态切换,无需重启。默认关闭,升级后继续允许覆盖,避免打断已有 CI;空库用默认值且不写配置行。开启后首次创建 tag 和重复推送同一 digest 仍成功,把现有 tag 移到不同 digest 则返回 Registry 原生 409 DENIED。需要滚动更新 latest 等 tag 时,可把精确 tag 名加入覆盖例外列表;不支持通配符。
环境变量
必须:
DOCKERY_ADMIN_PASSWORD—— 首次启动的 admin 密码。/data为空时必填,之后忽略。空 DB + 未设 → api 故意 fatal(不随机生成密码,避免日志泄漏)。
常用(生产):
REGISTRY_AUTH_TOKEN_REALM,默认http://localhost:5001/token—— distribution 在WWW-Authenticate里告诉 docker CLI 去哪拿 JWT。必须是 docker CLI 实际能访问到的 URL,填错 →docker push401。DOCKERY_ADMIN_USERNAME,默认admin。DOCKERY_IMAGE(仅 compose),默认ghcr.io/bizjs/dockery:latest,用于固定镜像 tag。
其他(高级):
REGISTRY_STORAGE_*原样透传给 distribution,可切 S3/OSS/Azure;REGISTRY__都会被 distribution 消费。DOCKERY_OTEL_ENDPOINTOTLP/HTTP 遥测端点,默认不设、遥测关闭——distribution v3 默认往localhost:4318刷 traces,不开收集器就持续打 connection refused,Dockerfile 固定OTEL_SDK_DISABLED=true。- 其余项(token TTL、issuer、session cookie)在
docker/rootfs/etc/dockery/config.yaml。
/data 存储与备份
/data/
├── registry/ 镜像 blob(默认 filesystem driver)
├── db/dockery.db SQLite(users / repo_permissions / audit_log)
└── config/
├── jwt-private.pem Ed25519 私钥(0600),单一真源
└── jwt-jwks.json 每次启动由私钥派生
备份 = /data 整包。丢 jwt-private.pem → 签出去的 token 全废;丢 dockery.db → 用户表重置。用 REGISTRY_STORAGE_* 切 S3/OSS/Azure 只会搬 registry/,db/ 和 config/ 仍需挂 /data。生产推荐宿主目录 bind-mount。
架构
外部 :5001(host → :5000 container)→ nginx → / 静态(web-ui)、/token、/api/*(dockery-api :3001)、/v2/*(distribution :5001)。dockery-api 在 manifest PUT 时介入;其它 /v2/* 直达 registry。registry 用 JWKS 验签 jwt-jwks.json。容器内三个进程由 supervisord 编排。
设计取舍与踩坑
协议用官方 distribution,不是自写兼容实现;账户模块 Go + 纯 Go SQLite 驱动(modernc.org/sqlite,零 CGO),单一静态二进制;ORM 用 ent;JWT 用 golang-jwt/jwt/v5 + crypto/ed25519 手签;密码 bcrypt;Registry 二进制从官方镜像 COPY 进最终镜像;基础镜像 alpine:3.20;进程管控 supervisord(替代原计划 s6-overlay)。
两个关键坑:
- distribution v3.1.0 的
rootcertbundle只识别 CERTIFICATE PEM,会静默丢弃 PUBLIC KEY block,所以用 RFC 7517 JWKS 而非 rootcertbundle; - UI session 用不透明 ID + 内存 Store,进程重启后全员重登。
适用场景
3~30 人内部/项目组 registry;CI/CD 产物仓,镜像不出内网;家庭实验室、单人开发者自托管;边缘节点、ARM 小机的镜像缓存。需要扫描/签名/HA/跨机复制请上 Harbor。
本地开发与发布
本地开发:make dev;前端热重载 cd apps/web-ui && pnpm install && pnpm dev(:5173,/api /token /v2 代理到 :5001);只改 Go 也可 cd apps/api && make run。
发布:打 v* tag → GitHub Actions 构建推送 ghcr.io//: 与 :latest(linux/amd64 + linux/arm64)。