Anvil includes an OCI-compatible container registry served from the same host as your repositories. You authenticate with a registry token, then push and pull images with Docker, Podman, or any client that speaks the OCI Distribution Specification.

Images are addressed as anvil.fangorn.io/<org>/<repo>:<tag>, for example anvil.fangorn.io/fangorn/anvil:latest. The registry repository is created automatically on the first push.

Authentication

The registry uses the Docker v2 token-auth protocol. A client that hits a registry endpoint without credentials receives a 401 with this header:

WWW-Authenticate: Bearer realm="https://anvil.fangorn.io/v2/auth",service="anvil.fangorn.io"

The client then exchanges Basic Auth credentials at /v2/auth for a JWT (valid 300 seconds) and retries the original request with Authorization: Bearer <jwt>. Docker handles this exchange for you after docker login.

You can authenticate with one of three credential types as the Docker password:

Credential Prefix Where to create it
Registry token anvreg_ Settings -> Registry Tokens
Personal access token anvil_ Settings -> Access Tokens (must include repo:read or repo:write)
CI job token (JWT) Injected automatically by the CI runner — see CI integration

Create a registry token

Registry tokens are separate from your account password and from API personal access tokens. Create one at Settings -> Registry Tokens (https://anvil.fangorn.io/users/settings/registry-tokens).

Each token has:

Field Description
Name A label for the token, shown in the token list.
Scopes Zero or more action:org/repo patterns granting access. Leave empty to grant only what your account permissions already allow.
Expiration Optional date. A token with no expiration stays valid until you delete it.

The plaintext token is shown only once, at creation time. It is prefixed with anvreg_. Copy it immediately; Anvil stores only a SHA-256 hash and cannot show it again.

Scope format

A scope is <action>:<org>/<repo>, where <action> is pull, push, or delete. The repository part may be a single name or * to match every repository in the org. Enter multiple scopes as a comma-separated list.

Scope Grants
pull:fangorn/anvil Pull from fangorn/anvil.
push:fangorn/anvil Push to fangorn/anvil.
pull:fangorn/* Pull from every repository in fangorn.
delete:fangorn/anvil Delete manifests and blobs in fangorn/anvil.

A token grants only the intersection of its scopes and your account’s permissions on each repository. Pushing to a repository that does not yet exist is permitted if your account can create repositories in the org; the registry repository is auto-created as private on first push.

Log in with Docker

Use your Anvil username and the registry token as the password:

docker login anvil.fangorn.io \
--username your-anvil-username \
--password <your-registry-token>

For automation, pass the token on stdin so it does not appear in shell history or process listings:

echo "$ANVIL_REGISTRY_TOKEN" | docker login anvil.fangorn.io \
--username your-anvil-username --password-stdin

Push an image

Tag the image with the registry host, org, and repository, then push:

docker tag myapp:latest anvil.fangorn.io/fangorn/anvil:latest
docker push anvil.fangorn.io/fangorn/anvil:latest

The push uploads blobs (layers and config) followed by the manifest. Anvil verifies each blob digest against its content and rejects a manifest whose media type is not a supported OCI or Docker manifest type.

A tag reference must match ^[a-zA-Z0-9_][a-zA-Z0-9._\-]{0,127}$, or be a sha256: digest. Manifests can also be pushed by digest.

Pull an image

docker pull anvil.fangorn.io/fangorn/anvil:latest

Pulls from a public registry repository do not require authentication. Pulls from a private repository require a token with a matching pull: scope.

CI integration

Anvil CI runners inject registry credentials into every job environment and run docker login for you before pulling any image hosted on the Anvil registry. You do not add a docker login step to your pipeline.

The credentials and the values you reference in image tags come from these environment variables, set by the runner:

Variable Value Example
ANVIL_REGISTRY The registry host. anvil.fangorn.io
ANVIL_REGISTRY_USER The synthetic CI user for this job. ci-job-<job-id>
ANVIL_REGISTRY_TOKEN A scoped JWT, valid one hour, with pull and push on the building repository. (secret)
ANVIL_REPO_FULL_NAME The <org>/<repo> slug of the repository being built. fangorn/anvil
ANVIL_COMMIT_SHA The commit SHA the run is building. 60a2098...

The CI token grants pull and push only on the repository the job is building. To push to a different repository, supply your own registry token as a CI secret and run docker login against it.

The following .anvil.yml builds the image, pushes it tagged with the commit SHA, then tags and pushes latest:

steps:
- name: build-and-push
run: |
docker build -t $ANVIL_REGISTRY/$ANVIL_REPO_FULL_NAME:$ANVIL_COMMIT_SHA .
docker push $ANVIL_REGISTRY/$ANVIL_REPO_FULL_NAME:$ANVIL_COMMIT_SHA
- name: tag-latest
run: |
docker tag $ANVIL_REGISTRY/$ANVIL_REPO_FULL_NAME:$ANVIL_COMMIT_SHA \
$ANVIL_REGISTRY/$ANVIL_REPO_FULL_NAME:latest
docker push $ANVIL_REGISTRY/$ANVIL_REPO_FULL_NAME:latest
depends_on: [build-and-push]

For more on jobs, dependencies, and runner selection, see .anvil.yml and runners.

Manage packages

The web UI surfaces the images stored for an org and per repository, and lets org owners and admins control visibility, storage limits, and retention.

View an org’s packages

The org packages page (https://anvil.fangorn.io/fangorn/packages) lists every registry repository in the org and shows total storage used, repository count, and manifest count.

Repository visibility

Each registry repository is private or public. Toggle it on the repository’s package settings page (https://anvil.fangorn.io/<org>/<repo>/packages/settings). Public repositories allow anonymous pulls; private repositories require a token with a pull: scope.

Storage quotas

An org owner or admin can set a storage quota for the org under Storage Settings on the org packages page. Enter the limit in GB; clear it to allow unlimited storage. When a push would exceed the quota, Anvil rejects the manifest with 413 and the error code DENIED.

Retention policies

Retention policies prune old images automatically. Create them on a repository’s package settings page. A policy must set at least one of the following, and may combine them:

Setting Effect
Keep last N tags Keep at least N tagged manifests; older tagged manifests beyond N become eligible for deletion.
Expire untagged after N days Delete untagged manifests older than N days.
Tag pattern Restrict the policy to tags matching the pattern.

Both values must be greater than zero when set. A policy is enabled when created and can be deleted from the same page. Anvil applies enabled policies on a schedule and reclaims the underlying blob storage once a blob is no longer referenced by any manifest.

Delete images

From a repository’s package settings page you can select tags and delete them, or delete a manifest by digest. Deletion is also available over the OCI API to a token with a delete: scope — see the API reference.

Manage your tokens

Registry tokens are scoped per user and listed at Settings -> Registry Tokens. The list shows each token’s name, prefix, scopes, expiration, and the date it was last used. Revoke a compromised token by deleting it; it stops working immediately.

Troubleshooting

Status Likely cause Fix
401 UNAUTHORIZED Token expired, deleted, or lacks the needed scope. Create or refresh the token, then re-run docker login.
404 NAME_UNKNOWN / MANIFEST_UNKNOWN / BLOB_UNKNOWN Wrong org, repository, tag, or digest. Confirm the path is anvil.fangorn.io/<org>/<repo>:<tag>.
400 DIGEST_INVALID The uploaded content does not match the supplied digest, or the digest parameter is missing on upload completion. Re-push; let your client compute the digest.
400 MANIFEST_INVALID Empty manifest body or an unsupported manifest media type. Push a supported OCI or Docker manifest.
400 TAG_INVALID The tag does not match the allowed format. Use a tag matching [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}.
400 SIZE_LIMIT_EXCEEDED A single blob exceeds the maximum blob size. Reduce layer size, or split the image.
413 DENIED The org storage quota is exceeded. Raise the quota under Storage Settings, or delete unused tags.
429 TOOMANYREQUESTS Too many concurrent uploads for the org. Retry after in-flight uploads finish.

OCI Distribution API reference

All endpoints are served under /v2. Reads require a token with a pull scope (or none for public repositories); writes require push; deletes require delete. Error responses are JSON of the form {"errors": [{"code": "...", "message": "..."}]}.

Method Path Description Success
GET /v2/ Version check. Returns 200 when authenticated, 401 with a Bearer challenge otherwise. 200
GET /v2/auth Exchange Basic Auth credentials for a JWT ({"token", "expires_in": 300, "issued_at"}). 200
GET /v2/_catalog List repositories, cursor-paginated via ?n=&last= (default n=100). 200
GET /v2/<org>/<name>/tags/list List tags, paginated via ?n=&last= (default n=20). 200
HEAD /v2/<org>/<name>/manifests/<reference> Manifest headers (Docker-Content-Digest, Content-Length) without a body. 200
GET /v2/<org>/<name>/manifests/<reference> Get a manifest by tag or digest. 200
PUT /v2/<org>/<name>/manifests/<reference> Push a manifest. Location and Docker-Content-Digest returned. 201
DELETE /v2/<org>/<name>/manifests/<reference> Delete a manifest by tag or digest. 202
HEAD /v2/<org>/<name>/blobs/<digest> Blob metadata (Content-Length, Docker-Content-Digest). 200
GET /v2/<org>/<name>/blobs/<digest> Download a blob. Redirects to a presigned URL. 200 / 307
DELETE /v2/<org>/<name>/blobs/<digest> Delete a blob. 202
POST /v2/<org>/<name>/blobs/uploads/ Initiate a blob upload. Supports cross-repo mount via ?mount=<digest>&from=<org>/<name>. Returns an upload Location and Docker-Upload-UUID. 202 / 201 (mount)
PATCH /v2/<org>/<name>/blobs/uploads/<uuid> Upload one chunk of a chunked upload. Returns the upload Location, Docker-Upload-UUID, and Range. 202
PUT /v2/<org>/<name>/blobs/uploads/<uuid>?digest=<digest> Complete an upload (chunked or monolithic). The digest query parameter is required and is verified against the content. 201
GET /v2/<org>/<name>/referrers/<digest> OCI image index of manifests referring to <digest>. Filter with ?artifactType=. 200

Catalog and tag pagination

/v2/_catalog and /v2/<org>/<name>/tags/list accept n (page size) and last (the final item of the previous page). When more results exist, Anvil returns a Link header with rel="next":

Link: </v2/_catalog?n=100&last=fangorn/anvil>; rel="next"
  • .anvil.yml — pipeline and step syntax for build jobs
  • Runners — self-hosted runners that build and push images
  • CLI reference — managing repositories and tokens from the terminal
  • Webhooks — events emitted by the registry
  • Get started — set up your first repository