一个轻量、只读的 Docker/OCI 镜像仓库代理。支持按顺序回退上游、在本地中转 Bearer Token、使用 SQLite 缓存不可变的 Digest Manifest,并在服务端跟随 Blob CDN 重定向。Docker 客户端只需访问本代理,不会直接连接认证服务或 Blob CDN。
- 支持 Docker Registry HTTP API V2 的拉取请求(
GET、HEAD) - 多个上游按配置顺序查询和回退
- 在代理内部完成 Token 获取和重定向跟随
- 只缓存由内容摘要标识的不可变 Manifest;不缓存 Tag 对应关系和 Blob
- SQLite 是唯一 Manifest 缓存,支持可选持久化、固定 TTL 淘汰和硬字节上限
- 支持 Manifest 摘要及其子资源的上游亲和性
- 限制并发请求数、Manifest 缓冲和空闲流时长
- 默认阻止访问私有地址和特殊用途地址,降低 SSRF 风险
- 拒绝推送、删除及其他写入操作
- 默认零配置代理 Docker Hub
发布镜像以非 root 用户运行,支持 Linux amd64 和 arm64:
docker run -d --name docker-proxy-lite \
--restart unless-stopped \
-p 127.0.0.1:5000:5000 \
-e PUBLIC_URL=https://docker.example.com \
ghcr.io/ai-trash/docker-proxy-lite:latest请在 5000 端口前配置 HTTPS。最小 Caddy 配置如下:
docker.example.com {
reverse_proxy 127.0.0.1:5000
}创建 compose.yaml:
services:
docker-proxy-lite:
image: ghcr.io/ai-trash/docker-proxy-lite:latest
container_name: docker-proxy-lite
restart: unless-stopped
environment:
PUBLIC_URL: https://docker.example.com
UPSTREAMS: https://registry-1.docker.io
MANIFEST_CACHE_TTL: 744h
MANIFEST_CACHE_DB: /var/cache/docker-proxy-lite/manifests.db
MANIFEST_CACHE_MAX_BYTES: 1GiB
MAX_MANIFEST_BYTES: 8MiB
MAX_CONCURRENT_REQUESTS: "64"
ports:
- "127.0.0.1:5000:5000"
volumes:
- ./manifest-cache:/var/cache/docker-proxy-liteLinux 上先创建目录并将其交给镜像使用的非 root 用户,然后将示例域名替换为实际域名并启动:
mkdir -p manifest-cache
sudo chown 65532:65532 manifest-cache
sudo chmod 700 manifest-cache
docker compose up -d其他平台也需要用等效方式确保 ./manifest-cache 可由容器内的非 root 用户写入;不要为了绕过权限问题而让代理以 root 身份运行。
仓库中也提供了可直接修改的 compose.yaml。
代理 Docker Hub 时,在每台 Docker 主机的 /etc/docker/daemon.json 中配置镜像加速地址:
{
"registry-mirrors": ["https://docker.example.com"]
}修改后重启 Docker:
sudo systemctl restart docker| 环境变量 | 默认值 | 说明 |
|---|---|---|
LISTEN_ADDR |
:5000 |
HTTP 服务监听地址。通常由 Caddy、Nginx 等反向代理提供外部 HTTPS。 |
UPSTREAMS |
https://registry-1.docker.io |
以英文逗号分隔的上游仓库绝对 URL,按顺序尝试。仅支持 HTTP/HTTPS,不能包含凭据、查询参数或片段。 |
PUBLIC_URL |
根据请求推导 | 客户端访问代理时使用的外部 URL,用于生成本地 Token Realm。显式配置时必须使用 HTTPS;仅建议在本地直连开发时使用自动推导的 HTTP 地址。 |
TRUST_PROXY_HEADERS |
false |
是否信任 Forwarded、X-Forwarded-* 等代理头。仅在服务位于可信反向代理之后时启用。 |
MANIFEST_CACHE_TTL |
744h |
Digest Manifest 的空闲有效期。仅当命中时剩余 TTL 不超过 25% 才低频续期;冷数据过期后不会被读取,并在启动和后续写入事务中清理。 |
MANIFEST_CACHE_DB |
空(内存 SQLite) | SQLite 数据库文件路径。为空时使用进程私有的内存数据库;设置后缓存可跨重启复用。应使用本地文件系统,不要放在 NFS 等网络文件系统上。 |
MANIFEST_CACHE_MAX_BYTES |
1GiB |
SQLite 主数据库的硬页容量上限,最小 1MiB。WAL 另有自动检查点和受限日志保留。达到上限时新缓存写入失败开放,但已完成大小和摘要校验的上游响应仍返回客户端。 |
MAX_MANIFEST_BYTES |
8MiB |
共享缓存 Digest Manifest 的完整校验上限。已解析为 Digest 的安全请求若超限或摘要不匹配会返回 502;校验缓冲名额耗尽时返回 503 和 Retry-After: 1。主动绕过共享缓存的私有或不安全请求仍直接流式传输。 |
ALLOW_PRIVATE_REGISTRIES |
false |
是否允许连接私有、回环、链路本地及其他内部地址。仅当所有上游、认证地址和重定向目标均可信时启用。 |
MAX_CONCURRENT_REQUESTS |
64 |
最大并发代理请求数,包含仍在传输的响应流。达到上限时新请求返回 503 和 Retry-After: 1;健康检查不占用名额。 |
上游发生网络传输错误,或返回 404、429、500、502、503、504 时,代理会尝试下一个上游。401、403 及其他状态均为终止响应,不会继续回退。回退机制主要用于在上游触发限流时继续从后续镜像源拉取;若某个仓库必须先认证才能判断镜像是否存在,则初始 401 阶段无法回退。
精确的 Manifest 摘要,以及 Manifest 中声明的子 Manifest、配置和 Blob 摘要,会优先访问提供该内容的上游。Tag 不会写入缓存或亲和性状态。由代理签发记录的 Token 会固定到其认证上游。
共享缓存只保存成功、通过摘要校验的 Digest Manifest GET 响应。安全的 Tag GET 会先向上游发送实时 HEAD,读取 Docker-Content-Digest,再按该 Digest 查询或填充 SQLite;Tag 与 Digest 的对应关系不会保存,因此 Tag 变化会在下一次请求立即生效。Tag HEAD、带查询参数、Range、条件请求、私有或未知认证信息,以及包含非安全头部的请求会绕过共享缓存;重复的 Authorization 头会被拒绝。
SQLite 只持久化 Manifest 重放所需的 Content-Type、Content-Length、ETag、Last-Modified、Vary 和 Docker-Content-Digest。其中摘要和长度由已验证正文重新生成;认证挑战、Cookie、限额/重试信息以及自定义 X-* 响应头不会写入缓存。
SQLite 是 Manifest 缓存的唯一权威实现,不存在第二套应用内 LRU。缓存键包含规范化上游 URL、仓库、Digest、Accept 和安全的 Vary 请求值。命中仅在剩余 TTL 不超过 25% 时通过条件更新续期,避免每次读取都写 WAL;多个实例并发续期时使用旧过期时间进行条件更新。续期写入失败只记录警告,当前已经完整校验的命中仍会返回;其他运行时读取错误会失败开放并重新访问上游。
MANIFEST_CACHE_DB 为空时使用进程私有的内存 SQLite;设置文件路径后启用 WAL、5 秒 busy timeout、256 页自动检查点、受限日志保留和 synchronous=NORMAL。数据库打开、容量配置或建表失败会阻止服务启动;运行时读取错误会记录警告并直接访问上游,写入失败不会影响已经验证成功的响应。仅 Digest Manifest 会进入 SQLite,Blob、Tag 对应关系、Token、认证挑战、Token 跟踪、上游亲和性、请求合并和请求限流状态都不会入库。
数据库应位于本地文件系统,并由容器中的非 root 用户写入。TTL 是唯一的行淘汰策略;MANIFEST_CACHE_MAX_BYTES 只限制 SQLite 页增长,不会按 LRU 或条目数主动删除仍有效的行。过期行在启动和成功写入事务中删除,其页面可被 SQLite 复用。缓存写满时响应继续正常返回,但该次内容不会入库。
旧变量 MANIFEST_CACHE_DIR、MANIFEST_CACHE_MAX_ENTRIES、MANIFEST_CACHE_TAG_TTL 和 MANIFEST_CACHE_DIGEST_TTL 只要非空就会导致启动失败,并在错误中给出迁移目标。请分别改用 MANIFEST_CACHE_DB、MANIFEST_CACHE_MAX_BYTES 和 MANIFEST_CACHE_TTL;Tag 不会缓存。
Blob 每次都从上游流式传输,代理会在服务端跟随 Registry 到 CDN 的重定向,不会缓存 Blob。上游读取和客户端写入使用两分钟的可刷新空闲超时:只要数据持续传输,大文件拉取不会受到总时长限制;停滞的连接会被释放。
默认情况下,代理会在 DNS 解析和实际连接阶段阻止私有地址、回环地址、链路本地地址及其他特殊用途地址。内置传输不会读取环境代理设置,以免绕过目标地址检查。
共享缓存安全的 Digest Manifest 必须先完整读入有界缓冲并验证摘要,才会写入响应。已知或未知长度超出 MAX_MANIFEST_BYTES、读取失败或摘要不匹配均返回 502,不会转发任何未验证正文;缓冲名额耗尽返回 503。Blob 和主动绕过共享缓存的请求不走这条完整缓冲路径。
本项目不提供安全的按客户端上游隔离。不同客户端需要不同上游策略时,请部署独立实例或使用不同域名。
curl http://127.0.0.1:5000/-/healthz成功时返回 200 OK。
go test ./... -count=1
go test -race ./... -count=1
go vet ./...
ko build --push=false --oci-layout-path=../docker-proxy-lite-oci ./cmd/docker-proxy-lite推送严格符合 vMAJOR.MINOR.PATCH 或 SemVer 预发布格式的 Git 标签后,GitHub Actions 会将镜像发布到 ghcr.io/ai-trash/docker-proxy-lite,包括以下标签:
- 完整 Git 标签
- 去掉前导
v的版本号 - 提交摘要标签
- 稳定版本额外更新
latest
发布任务只会在测试、Race 检测和静态检查通过后运行,并要求被标记的提交属于 origin/main 历史。建议在 GitHub 仓库中配置 Tag Ruleset,仅允许可信维护者创建发布标签;也可以为发布任务配置需要审核的受保护 Environment。
GHCR 首次发布时可能将软件包设为私有。在依赖匿名拉取前,请到软件包设置中将可见性改为公开。