Pull from GitHub Container Registry (GHCR)
Authenticate Rune to ghcr.io so it can pull private images, using a GitHub personal access token or an existing docker login.
GHCR (ghcr.io) is a container registry hosted by GitHub. Public images
work with no setup — Rune pulls them like any other registry. Private
images need credentials: a GitHub token with read:packages, registered
with runed so the Docker runner can use it on every pull.
This guide covers the two supported flows:
- Direct (recommended) — paste a GitHub PAT into a registry entry.
- From an existing
docker login— reuse~/.docker/config.json.
Both flows result in ghcr.io images pulling without ImageUnreachable
on rune cast.
1. Mint a GitHub token
Generate a Personal Access Token at
github.com/settings/tokens with the read:packages scope:
- Classic PAT: check
read:packages. (repois not required for pulling.) - Fine-grained PAT: under Permissions → Account permissions set Packages to Read-only.
For organization-owned packages, also make sure the token's user has
read access to the package on the Packages → Package settings →
Manage Actions access page. A token can have the right scope but still
get denied: permission_denied if the package itself doesn't grant the
user access.
Test the token from a shell:
echo "$GHCR_PAT" | docker login ghcr.io -u <github-username> --password-stdin
docker pull ghcr.io/<org>/<image>:<tag>
If docker pull works, Rune will work — the same daemon performs both
pulls.
2a. Direct: register the token with runed
There are two equivalent ways to add a registry entry: at runtime via
rune admin registry add, or statically in the runefile.
Runtime (no restart)
Run from the same host as runed (admin commands are localhost-only by
default):
rune admin registry add \
--name ghcr \
--registry ghcr.io \
--type basic \
--username "$GITHUB_USER" \
--password "$GHCR_PAT"
Verify:
rune admin registry list
rune admin registry test ghcr
test performs a real auth handshake and returns ok or the registry
error message verbatim.
Static (in the runefile)
docker:
registries:
- name: ghcr
registry: ghcr.io
auth:
type: basic
username: ${GHCR_USER} # env-expanded at runed start
password: ${GHCR_PAT}
runed reads its config at startup, so changes here require a restart.
Prefer the rune admin registry route once the cluster is running.
:::caution[No password-file field]
The runefile schema does not support password-file: — password
must be inline (use ${ENV} expansion to keep the literal token out of
the file), or sourced from a Rune Secret via auth.fromSecret: <name>.
:::
2b. Reuse an existing docker login
If the host already has working docker login ghcr.io, you can hand the
contents of ~/.docker/config.json to Rune verbatim instead of
re-typing the credentials:
rune admin registry add \
--name ghcr-from-docker \
--registry ghcr.io \
--type dockerconfigjson \
--data dockerconfigjson="$(cat ~/.docker/config.json)"
This is useful when:
- You manage GHCR auth via a CI step that runs
docker loginfor you (e.g.docker/login-actionon a self-hosted runner). - You want to ship one credential blob that covers multiple registries in a single file.
3. Cast a private image
Once the registry is registered, no service-spec change is needed —
image: references just work:
service:
name: api
namespace: default
image: ghcr.io/myorg/api:1.4.0
scale: 1
rune cast api.yaml
If credentials are missing or wrong you will see the inline failure block:
✗ ImageUnreachable
pull access denied for ghcr.io/myorg/api, repository does not exist
or may require 'docker login'
image: ghcr.io/myorg/api:1.4.0
service: default/api
rune logs api -n default --tail=50
That error text is the registry's response, surfaced directly. A credential rejection also names the registry pattern that supplied the credential, so you can tell a wrong credential from a missing one:
the credential configured for registry pattern "ghcr.io" was rejected;
renew it, or if this image is public scope the pattern to your private
repositories or mark it anonymous
If no credential was sent at all — nothing matched, a type: none entry
matched, or the service opted out — the error says that instead:
registry requires credentials; no [[docker.registries]] entry matches
ghcr.io/myorg/api:1.4.0
3b. Scope a credential to a repository path
A registry pattern can carry a repository path, so a credential only
claims the repositories it's for:
docker:
registries:
- name: ghcr-private
registry: ghcr.io/myorg # only ghcr.io/myorg/** uses the PAT
auth:
fromSecret: ghcr-credentials
ghcr.io/someoneelse/app then matches no configured entry — it pulls
with no credential of yours, falling back to any ambient one (see below). Path comparison is segment-aware, so
ghcr.io/myorg does not match ghcr.io/myorg-evil/app.
(Unmatched images can still pick up an ambient credential — the runed
user's ~/.docker/config.json, or a GCE metadata service account. Those
carry no pattern and are tried last; see
docker.registries[].)
When several entries match one image, the most specific wins (deeper path first, then exact host over wildcard host, then longer pattern) — not declaration order. That's what makes a public carve-out possible on a host you otherwise need credentials for:
docker:
registries:
- name: ghcr-private # many private repos across the host
registry: ghcr.io
auth:
fromSecret: ghcr-credentials
- name: ghcr-public # ...but this one is public
registry: ghcr.io/myorg/oss
auth:
type: none
:::note[No automatic anonymous retry]
Rune does not silently retry a rejected pull without credentials. A
public image at the same reference could stand in for the intended
private one, so the downgrade has to be declared — either by scoping the
pattern, by type: none, or by imagePullAnonymous on the service. A
host-only pattern claims every image on that host, public repositories
included: if the credential expires, those pulls fail with a hard
denied rather than falling back to anonymous.
:::
Per-service opt-out: imagePullAnonymous
A service author can force a credential-free pull without touching the server runefile:
service:
name: web
image: ghcr.io/myorg/oss-web:1.0.0
imagePullAnonymous: true
No credential is ever sent for that image, so there is no failed attempt
and no window in which an expired token can break it. Init steps and the
--debug sidecar inherit the flag — they run the same image.
4. Rotate or revoke
Rotate by issuing a new PAT, then:
rune admin registry update \
--name ghcr \
--registry ghcr.io \
--type basic \
--username "$GITHUB_USER" \
--password "$NEW_PAT"
Revoke at github.com/settings/tokens. Existing pulls in flight keep their credentials for the lifetime of that pull; new pulls fail immediately.
To remove a registry entirely:
rune admin registry remove ghcr
Per-service registry selection
Credentials are selected server-side, from docker.registries[]. A
service cannot supply its own — scope the pattern instead, and use
imagePullAnonymous for
the one per-service control there is.
:::caution
rune cast accepts registry: and imageRegistry: keys on a service
spec. Nothing reads them. They validate, so a spec carrying them looks
fine and pulls exactly as if they were absent.
:::
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
ImageUnreachable with pull access denied | Token missing read:packages or package doesn't grant the user access. | Regenerate PAT with the right scope; check Packages → Manage Actions access. |
unauthorized: authentication required | No registry entry matches ghcr.io. | rune admin registry list — add one if missing. |
no [[docker.registries]] entry matches <image> | The image needs credentials and none was sent. Either no pattern claimed it (often one narrowed past the repository), or a type: none entry matched, or the service set imagePullAnonymous: true. | Rule out the last two first. Otherwise widen the pattern or add an entry. |
the credential configured for registry pattern "X" was rejected | A credential was sent and refused. If rune admin registry list has no entry for X, it came from an ambient source and X is just the image host. | Renew it. If the image is public, scope X to your private repos or set imagePullAnonymous: true. |
A public image on GHCR fails with denied | A host-wide credential is being attached to it and has expired. | Scope the pattern or mark the repository anonymous. |
manifest unknown | Image exists but tag doesn't, or org/name is wrong. | docker pull from the same host to confirm. |
| Works locally, fails on the server | Runed wasn't restarted after a static config change, or the env var ($GHCR_PAT) isn't visible to runed. | Use rune admin registry add instead of editing the runefile, or restart runed with the env exported. |
See also
rune admin registry— full subcommand reference.- Configuration reference — full runefile
schema, including the
docker.registries[]section. docker.registries[]— pattern scoping, precedence, and every authtype.imagePullAnonymous— the per-service opt-out.- Errors: ImageUnreachable — what the reason slug means.