Skip to content

Container registry

neosource hosts container images alongside your code, on the same accounts and the same permissions. An image lives at:

neocr.io/{owner}/{name}:{tag}

There is nothing to enable and no separate registry account: the credential is an ordinary access token, and docker is the client.

The registry answers on two hosts, and they are the same registry — same accounts, same images, same digests:

host use it for
neocr.io everything new. This is the short name we advertise.
cr.neosource.dev anything already pointing at it. Served permanently.

Two, not one, because an image reference embeds its host. neocr.io/acme/api:v3 is not a name you look up in a registry setting — it is baked into other people’s Dockerfiles, docker-compose.ymls, Kubernetes manifests and lockfiles, and it stays there long after anyone remembers where it came from. A registry host that stops answering breaks builds we cannot see and cannot fix. So cr.neosource.dev is not deprecated and is not on a timer.

Two, and not three: every additional host is another string that must keep resolving forever.

The password is an access token — never your account password:

Terminal window
echo "$NEOSOURCE_TOKEN" | docker login neocr.io -u your-handle --password-stdin

The username is not checked. The token identifies you; the username is only what your credential helper files the entry under. Use your handle so the ~/.docker/config.json entry is readable by a human.

Behind that one command, docker runs the standard OCI token exchange: it probes /v2/, reads the Bearer challenge, and swaps your token for a short-lived JWT scoped to exactly the repository and actions it is about to use. You do not have to do anything with that — but it is why a leaked build log containing a registry bearer token exposes one repository for five minutes rather than your whole account.

Terminal window
docker build -t neocr.io/acme/api:v3 .
docker push neocr.io/acme/api:v3
Terminal window
docker pull neocr.io/acme/api:v3

There is no separate “create a registry” step, but push to the name of a repository that already exists. An image name that corresponds to no repository cannot accept an upload yet; the push fails on the first layer with 400 BLOB_UPLOAD_INVALID.

A push you are not allowed to make fails with 401 Unauthorized on the first layer instead, and without a message naming the problem — the token exchange hands out no scope for a name it will not let you write, so a misspelled owner, a repository you cannot reach, and a token missing repo:registry:write all look identical from the client. Check the owner and name first, the token’s scopes second.

To see what is published, ask the registry for the tag list:

Terminal window
curl -H "authorization: Bearer $NEOSOURCE_TOKEN" \
https://neocr.io/v2/acme/api/tags/list
{ "name": "acme/api", "tags": ["v3"] }

The registry is API-only today: there is no image browser in the web UI, and repository catalog listing (/v2/_catalog) is not implemented. tags/list on a namespace you can read is how you see what is there.

Create the token at /settings/developer for a personal one, or under Organization settings → Tokens for a workspace one owned by a service account. Both scope pickers have a Container registry group.

the job scopes
pull images (a deploy, a CI base image) repo:registry:read
build and publish images repo:registry:read and repo:registry:write
also remove old tags add repo:registry:delete

Restrict the token to the repositories it needs while you are there — a resource-locked token is refused every namespace outside its list.

A docker push asks the registry whether each layer is already stored before uploading it, and that check authorizes as a pull. A token holding only repo:registry:write starts an upload session fine and then gets a 401 on the first layer probe. Grant both.

Why publish does not include delete, or git

Section titled “Why publish does not include delete, or git”

This is the part worth understanding, because it is the reason the registry scopes exist separately at all:

  • repo:registry:write cannot delete. A publish credential that can also unpublish turns a leaked token into a way to remove releases people are already pulling. Deletion is its own scope, and write does not imply it.
  • repo:registry:* carries no git access. A token holding only the registry scopes cannot clone the repository, cannot push commits, and cannot read issues or settings. A build machine that publishes images does not need your source history, and now it does not get it.

The implication runs one way only. repo:write and repo:admin do carry the registry actions, so a broad token still publishes exactly as it always did — which is precisely why the narrow one is worth minting.

The workspace-scoped pair, workspace:registry:read / workspace:registry:write, is the same trade one level up: every image the workspace owns, still no git, and still no delete. Reach for it when one credential has to publish several of a workspace’s images rather than one repository’s — and note that only a workspace Admin or Owner holds them, so granting them to a member-role service account grants nothing at all.

Each image namespace carries its own visibility, and that is what decides anonymous pull:

  • publicdocker pull works with no credential at all, the same way git clone does on a public repository.
  • anything else — a token on every request.

A namespace created by a push starts out with the visibility of the repository of the same name, or private when there is no such repository. From then on it is a property of the namespace, not of the repository: a private repository can hold a public image, and a public one can hold a private image. Changing it is not exposed in the web UI yet.

Push and delete always need a token, whatever the visibility.

Store the token as a workspace CI secret and log in from a step. Use a workspace token rather than a personal one — it belongs to a service account instead of to whoever happens to have set the pipeline up:

- name: Log in to the registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login neocr.io -u ci --password-stdin

For every push after the image name exists, the token needs repo:registry:read + repo:registry:write and nothing else. If that pipeline also has to git push, that is a second, separate credential — keeping them apart is the whole point.