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.
Two hostnames, and neither goes away
Section titled “Two hostnames, and neither goes away”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.
Logging in
Section titled “Logging in”The password is an access token — never your account password:
echo "$NEOSOURCE_TOKEN" | docker login neocr.io -u your-handle --password-stdinThe 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.
Push and pull
Section titled “Push and pull”docker build -t neocr.io/acme/api:v3 .docker push neocr.io/acme/api:v3docker pull neocr.io/acme/api:v3There 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:
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.
Which scope to mint
Section titled “Which scope to mint”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.
Why publish needs read as well
Section titled “Why publish needs read as well”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:writecannot 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.
Visibility
Section titled “Visibility”Each image namespace carries its own visibility, and that is what decides anonymous pull:
- public —
docker pullworks with no credential at all, the same waygit clonedoes 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-stdinFor 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.
See also
Section titled “See also”- Authentication and tokens — how scopes, resource locks, expiry, and rotation work.
- The job environment — why neosource jobs declare
packages:instead of choosing a base image.