Working with OCI / Docker¶
CodeScoring.Save implements OCI Distribution Spec under the /v2/ prefix and supports Docker, Helm OCI charts, and other OCI artifacts such as oras.
Proxy Repository¶
curl -X POST https://save.example.com/api/v1/repos \
-H "Content-Type: application/json" \
-u "<username>:<password>" \
-d '{
"project": "common",
"name": "dockerhub-proxy",
"format": "docker",
"repository_type": "proxy",
"remote_url": "https://registry-1.docker.io",
"cache_ttl": 86400
}'
For single-name images without slashes, standard Docker Hub normalization library/<image> is applied.
Hosted Repository¶
curl -X POST https://save.example.com/api/v1/repos \
-H "Content-Type: application/json" \
-u "<username>:<password>" \
-d '{
"project": "common",
"name": "docker-hosted",
"format": "docker",
"repository_type": "hosted"
}'
URL Scheme¶
The base scheme is path-based, as required by Docker / OCI:
Nexus-compatible flat URL routing (flat-alias) is also supported. For compatible repositories, the <project>/<repository> prefix is replaced with a short alias or omitted entirely.
Client Configuration¶
Docker / Podman¶
# Login
docker login save.example.com
# Pull through proxy
docker pull save.example.com/common/dockerhub-proxy/library/nginx:latest
# Push to hosted
docker tag myapp:latest save.example.com/common/docker-hosted/myapp:1.0.0
docker push save.example.com/common/docker-hosted/myapp:1.0.0
Anonymous read and docker login
Endpoint /v2/ always returns 401 with a WWW-Authenticate: Bearer realm=... challenge header, even if the repository allows anonymous read. This is required so the Docker client correctly performs the 401 -> retry -> 200 cycle and then sends Basic Auth if needed. This is an OCI Distribution Spec requirement.
Robot accounts in CI
For CI/CD, use a robot account: docker login -u 'sa$<robot-name>' -p '<api-key>' save.example.com. The same approach works for helm registry login and oras login. For details, see Authentication.
Helm¶
CodeScoring.Save accepts Helm charts as regular OCI artifacts. Push/pull is performed with standard helm commands:
# Login (Helm 3.8+)
helm registry login save.example.com -u <username>
# Push
helm package ./mychart
helm push mychart-0.1.0.tgz oci://save.example.com/common/docker-hosted
# Pull
helm pull oci://save.example.com/common/docker-hosted/mychart --version 0.1.0
Plain HTTP for test stands
For test stands without TLS, use --plain-http (Helm 3.13+):
oras¶
oras works with any OCI-compatible artifact: configs, policies, SBOMs, binaries, and others.
# Push an arbitrary file as an OCI artifact
oras push save.example.com/common/docker-hosted/configs/app:v1 \
-u <username> -p <password> \
./config.yaml:application/vnd.example.config
# Pull
oras pull save.example.com/common/docker-hosted/configs/app:v1 \
-u <username> -p <password>
containerd / nerdctl¶
nerdctl login save.example.com
nerdctl pull save.example.com/common/dockerhub-proxy/library/alpine:latest
Flat URL Routing¶
Save supports Nexus-compatible flat namespace for migration from Nexus / Artifactory: a repository can be configured with a prefix alias so that it responds to requests without <project>/<repository> in the path.
Longest-prefix matching is used:
docker pull save.example.com/common/external/golang:1.24.1goes to the repository withflat_alias = common/external/.docker pull save.example.com/johnny-depp:v1goes to a catch-all hosted repository if it is configured.
When creating a repository with a flat alias, use flat_alias_prefix and/or is_catch_all. Prefixes are validated for uniqueness.
Repository URL Migration¶
Use case: migrating a Docker registry from Nexus / Artifactory to CodeScoring.Save.
| Source | URL before migration | URL after migration (with project) | URL after migration (flat-alias) |
|---|---|---|---|
| Nexus | nexus.host.ru:5000/library/nginx:latest |
save.example.com/common/dockerhub-proxy/library/nginx:latest |
save.example.com/library/nginx:latest |
| Artifactory | jfrog.host.ru/docker-remote/library/nginx |
save.example.com/common/dockerhub-proxy/library/nginx |
save.example.com/library/nginx |
| Docker Hub | docker.io/library/postgres:16 |
save.example.com/common/dockerhub-proxy/library/postgres:16 |
save.example.com/library/postgres:16 |
The flat-alias option does not require client-side changes: existing scripts with a hardcoded nexus.host.ru/library/nginx path start working after the host is replaced.
Troubleshooting¶
Checking /v2/¶
curl -i https://save.example.com/v2/
# Expected: HTTP/1.1 401 Unauthorized with Www-Authenticate: Bearer realm=...
Checking the Token Endpoint¶
curl -u "<username>:<password>" \
"https://save.example.com/v2/token?service=save.example.com&scope=repository:common/dockerhub-proxy/library/nginx:pull"
The response is JSON with the token field containing a short-lived Docker v2 JWT.
Listing Tags¶
curl -u "<username>:<password>" \
https://save.example.com/v2/<project>/<repository>/<image>/tags/list
Service Status¶
Repository Audit¶
curl -u "<username>:<password>" \
"https://save.example.com/api/v1/admin/audit?resource_type=repository&q=dockerhub-proxy&limit=50"