# Consume container images

## Prerequisites

- A published image in your Codebahn registry (see [Publish container images](/docs/publish-container-images/))
- Docker or a compatible runtime on the consumer

Every image is private, so every consumer needs a credential that can read the owner. Use a token scoped to **`read:package`** only, so a leaked pull credential can never push or delete. For the registry reference, see [Container registry](/docs/container-registry/). To publish, see [Publish container images](/docs/publish-container-images/).

## Create a read-only token

Log in as the user or machine account that will pull, and create a [personal access token](https://forgejo.org/docs/latest/user/api-usage/#authentication) scoped to **`read:package`**. For unattended consumers (a server, a cluster, a deploy pipeline), use a dedicated machine user, not a personal account.

## Pull from a workstation or server

```bash
echo "$CODEBAHN_TOKEN" | docker login codebahn.net -u "$USERNAME" --password-stdin
docker pull codebahn.net/acme/api:1.4.0
```

On a server, write the credential once into the Docker config and reuse it. The login stores it under `~/.docker/config.json`. Protect that file with file permissions; treat it as a secret.

## Pull in Kubernetes

Create an image pull secret from a `read:package` token, then reference it in the pod or service account.

```bash
kubectl create secret docker-registry codebahn-registry \
  --docker-server=codebahn.net \
  --docker-username="$USERNAME" \
  --docker-password="$CODEBAHN_TOKEN"
```

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  imagePullSecrets:
    - name: codebahn-registry
  containers:
    - name: api
      image: codebahn.net/acme/api:1.4.0
```

Attach the secret to a service account if every pod in a namespace pulls from the registry, so you do not repeat `imagePullSecrets` on each workload.

## Pull in another workflow

A Codebahn job that deploys or runs an image from its **own owner** pulls with the built-in `${{ github.token }}`, the same credential used to publish. There is no secret to store.

```yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Log in to the registry
        run: echo "${{ github.token }}" | docker login codebahn.net -u "${{ github.actor }}" --password-stdin
      - run: docker pull codebahn.net/acme/api:${{ github.ref_name }}
```

To pull an image owned by a **different** user or org, the built-in token does not reach across owners. Log in with a `read:package` machine-user token from a secret instead, and keep that job on protected branches; secrets are withheld from fork pull requests.

```yaml
      - name: Log in to the registry
        run: echo "${{ secrets.REGISTRY_PULL_TOKEN }}" | docker login codebahn.net -u "${{ vars.REGISTRY_USER }}" --password-stdin
```

Pulls from a runner in the same EU region as the registry do not count as internet egress, so in-CI consumption stays cheap.

## Pin what you deploy

Pull an immutable tag (`:1.4.0` or a commit SHA), never `:latest`, for anything you deploy. A moving tag means a redeploy can pull a different image than the one you tested, and a rollback cannot find the old one. Resolve to a digest if you want a deploy to fail closed when an image changes:

```bash
docker pull codebahn.net/acme/api@sha256:<digest>
```

## Rotate and scope

- Use a separate `read:package` token per consumer, so you can revoke one without breaking the others.
- Never reuse a `write:package` token to pull. A pull credential should not be able to push.
- Rotate machine-user tokens on a schedule, and immediately if a node or config is compromised.
