# Container Registry

DanubeData operates a managed OCI registry at `cr.danubedata.ro` that your CI/CD
can push images to and your Rapids can pull from — no third-party credentials
required for your own team's images. Storage is backed by our object-storage
tier; pulls from inside the platform stay on the cluster LAN (free).

## What you get

- A registry namespace scoped to your team, hosted at
  `cr.danubedata.ro/{your-team-slug}/...`.
- **Token-only authentication.** Same flow as DigitalOcean: you generate one
  token, use it as the Docker password, and put any string (typically your
  account email) in the username field. We identify the key by hashing the
  token — the username is informational only.
- Short-lived (5 min) JWTs under the hood, so a revoked key stops working
  within minutes.
- Auto-wired Rapid integration — your team's first-party images appear under
  "DanubeData Container Registry" in the Rapid credential dropdown without any
  setup.
- A free Starter plan (1 repository, 500 MB) on every team by default; paid
  plans for more repositories and storage.

Your team slug is the same `tenant_name` shown in **Team Settings → General**.
The rest of this page assumes your team slug is `acme` and your account email
is `you@example.com`.

## Create an access key

1. Go to **Container Registry** and choose **New access key** at the top of the page.
2. Enter a **Name** (e.g. `github-actions-prod`).
3. Pick the **Access**:
   - **Push and pull** — for CI pipelines that build and publish images.
   - **Pull only** — for read-only consumers (deploy targets, mirrors).
4. Click **Create access key**, then [confirm it's you](https://docs.danubedata.ro/account-settings#confirm-its-you) when asked.

We email you whenever a registry access key is created.

The **Token** is shown once. Copy or download it now — we never display it
again. The token is stored hashed in our database; even root access to the
database does not expose it.

Tokens look like `cr_aBcD12ef...` — the `cr_` prefix lets credential scanners
(`git-secrets`, `trufflehog`) and humans spot them on sight in `.env` files,
CI logs, or `~/.docker/config.json`.

You can create as many keys as you want. To revoke one, open it under
**Container Registry → Access keys** and choose **Revoke**; the next
`docker push` against the revoked key fails within 5 minutes. Only the team
owner creates, sees and revokes access keys.

> **No more "username" to copy.** Earlier keys had an auto-generated username
> like `dd-acme-github-actions-prod-3f9k2a`. New keys still have one for
> back-compat (visible under the "Show the auto-generated username" disclosure
> in the reveal modal), but you don't need it — any value works in the `-u`
> field. Your account email is the recommended default.

## Log in from Docker

Use your account email as the username — Docker requires *something* in `-u`,
and your email is memorable and not secret. The token is the password.

```bash
echo "$REGISTRY_TOKEN" | docker login cr.danubedata.ro \
  -u you@example.com --password-stdin
```

For interactive use:

```bash
docker login cr.danubedata.ro -u you@example.com
# Password: (paste the cr_... token)
```

Any string works for the username, so this is also valid:

```bash
echo "$REGISTRY_TOKEN" | docker login cr.danubedata.ro -u _token --password-stdin
```

## Push an image

Tag the image under your team slug and push:

```bash
docker build -t cr.danubedata.ro/acme/hello:1.0.0 .
docker push cr.danubedata.ro/acme/hello:1.0.0
```

The first path segment **must** be your team slug (`acme` in the example).
Pushes to any other namespace are rejected with `denied: requested access to
the resource is denied`.

You can organise your images however you like under that prefix:

```
cr.danubedata.ro/acme/web:1.0.0
cr.danubedata.ro/acme/web:latest
cr.danubedata.ro/acme/worker:1.0.0
cr.danubedata.ro/acme/staging/web:abc1234
```

## Use the image in a Rapid

1. In the Rapid Create or Edit form, set **Deployment type** to **Docker image**.
2. **Image:** `cr.danubedata.ro/acme/hello:1.0.0`
3. **Registry credential:** pick **DanubeData Container Registry** from the
   dropdown — it's pre-seeded for every team and authenticates against your
   first-party registry.
4. Save / deploy. Knative pulls the image using a short-lived pull-only
   credential the platform manages for you.

You do **not** need to create or rotate any credentials for first-party pulls.
The platform handles that.

## Push from GitHub Actions

```yaml
name: build-and-push
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Log in to DanubeData
        uses: docker/login-action@v3
        with:
          registry: cr.danubedata.ro
          username: ${{ secrets.DD_REGISTRY_USERNAME }}  # your email — not secret
          password: ${{ secrets.DD_REGISTRY_TOKEN }}      # the cr_... token
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            cr.danubedata.ro/acme/hello:${{ github.sha }}
            cr.danubedata.ro/acme/hello:latest
```

Add `DD_REGISTRY_USERNAME` (your account email or any string) and
`DD_REGISTRY_TOKEN` (the `cr_...` token) under **Settings → Secrets and
variables → Actions** in your GitHub repo. Only the token needs to be a
secret; the username can also live in plain `vars`.

## Push from GitLab CI

```yaml
build:
  image: docker:24
  services:
    - docker:24-dind
  stage: build
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - echo "$DD_REGISTRY_TOKEN" | docker login cr.danubedata.ro -u "$DD_REGISTRY_USERNAME" --password-stdin
  script:
    - docker build -t cr.danubedata.ro/acme/hello:$CI_COMMIT_SHORT_SHA .
    - docker push cr.danubedata.ro/acme/hello:$CI_COMMIT_SHORT_SHA
```

Define `DD_REGISTRY_USERNAME` (your account email) and `DD_REGISTRY_TOKEN`
(the `cr_...` token) under **Settings → CI/CD → Variables** (mark the token
as **Masked** and **Protected**).

## Push from Bitbucket Pipelines

```yaml
image: atlassian/default-image:4
pipelines:
  branches:
    main:
      - step:
          name: Build and push
          services: [docker]
          script:
            - echo "$DD_REGISTRY_TOKEN" | docker login cr.danubedata.ro -u "$DD_REGISTRY_USERNAME" --password-stdin
            - docker build -t cr.danubedata.ro/acme/hello:$BITBUCKET_COMMIT .
            - docker push cr.danubedata.ro/acme/hello:$BITBUCKET_COMMIT
```

Define `DD_REGISTRY_USERNAME` (your account email) and `DD_REGISTRY_TOKEN`
(the `cr_...` token) under **Repository settings → Repository variables**
(mark the token as **Secured**).

## Tag conventions

- Use **immutable**, sortable tags (commit SHA, semver) for production
  Rapids — Kubernetes will not re-pull `:latest` once a pod has cached it.
- `:latest` is fine for development. For production we recommend pinning to
  a SHA or a semver tag.
- Tag names must match `[a-z0-9]+(?:[._-][a-z0-9]+)*` per path segment
  (Distribution's standard rule). Uppercase letters are not allowed in the
  registry path; they are allowed in tags.
- We do not currently overwrite-protect tags. Pushing the same tag twice
  replaces the manifest. Future plans include "immutable tag" toggles.

## Delete images

From the Container Registry UI:

- **Delete a single image** — open the repository (**Container Registry →
  Repositories → {repo}**), click the image and choose **Delete** in the panel
  that opens. The repository stays.
- **Delete multiple images** — on the repository's page, tick the images you
  want to remove and click **Delete** above the list. Images a Rapid runs are
  left out, and the confirmation names them. The repository stays even if all
  its images are deleted.
- **Delete the whole repository** — on the repository's page, open **More** and
  pick **Delete repository**. All its images are removed and the repository
  drops from your dashboard.

A delete removes the image, not only the tag you chose: other tags that point
at the same image (say `latest` and `1.4.0`) go with it. The console lists
them before you confirm. Deleting is for the team owner.

In all three cases the underlying blob storage is reclaimed at the next
garbage-collection run (daily). Tag and repository deletions are not
recoverable.

**Images a Rapid runs are protected.** A tag that one of your serverless
containers was deployed from — or any tag sharing the same image — cannot be
deleted from the dashboard, the API or the CLI. Knative pins that exact image
into the container's revision, so removing it would break the container's next
cold start. The refusal names the container: redeploy it on another tag or
remove it first. When you really mean it, the API accepts `force=true` and the
CLI `--delete-in-use`.

## Plans & limits

| Plan          | Repositories | Storage | Price       |
|---------------|--------------|---------|-------------|
| Starter       | 1            | 500 MB  | Free        |
| Basic         | 5            | 5 GB    | EUR 3.99/mo |
| Professional  | unlimited    | 100 GB  | EUR 14.99/mo |
| Enterprise    | custom       | custom  | Contact us  |

Change plan under **Container Registry → Plan and usage → Change plan**. A
switch takes effect immediately and is billed at the new plan's rate from then
on. A downgrade to a plan smaller than what you store goes through, but puts
your account on hold: pushes are refused, and so is creating any new resource,
until support lifts the hold. Delete images first to switch without one; the
console says so before you confirm.

## Troubleshooting

### `unauthorized: authentication required`

Your token is wrong, revoked, or expired.

```bash
# Verify the token directly (username can be anything — try your email):
curl -i -u "you@example.com:$DD_REGISTRY_TOKEN" \
  "https://danubedata.ro/oci/token?service=cr.danubedata.ro&scope=repository:acme/hello:pull"
```

A `200 OK` with a JSON `{"token":"...", ...}` body means your credentials are
valid. A `401` means the token is wrong, revoked, or expired — re-issue from
the dashboard.

### `denied: Storage quota exceeded (X MB used of Y MB). Upgrade your plan or delete tags to free space.`

You are at your plan's storage cap. Either upgrade under **Container
Registry → Plan and usage**, or delete unused tags/repositories to reclaim
quota. The next push succeeds immediately after you free space — actual disk
bytes are reclaimed at the nightly GC, but the quota check uses your live
`bytes_used` total which drops as soon as the tag is deleted.

### `denied: Repository limit reached (X of X). Upgrade your plan or delete an existing repository to add a new one.`

You are trying to push to a **new** repository path while at your plan's
repo cap. Either upgrade, or delete an existing repository under
**Container Registry → Repositories**. Pushing additional tags to a
repository you already own is unaffected.

### `denied: requested access to the resource is denied`

The first path segment of your image does not match your team slug. Tag the
image as `cr.danubedata.ro/{your-team-slug}/...` and push again.

### `ImagePullBackOff` on a Rapid

Most common causes, in order:

1. The image tag does not exist (typo, push not yet finished). Check
   **Container Registry → Repositories → {repo}** to confirm
   the tag is listed.
2. The Rapid is configured to pull from a different team's image. First-party
   pulls only work when the team that owns the Rapid matches the team slug in
   the image path.
3. The auto-seeded internal credential is stale (e.g. you renamed the team).
   Trigger a re-deploy of the Rapid; the platform re-materialises the pull
   secret.

The Rapid detail page surfaces the underlying Kubernetes event — it will tell
you exactly which of these is in play.

### Push hangs at the last layer / "retrying in N seconds"

Likely a transient network issue or you are pushing during the nightly GC
window. Docker retries automatically; if it has not resolved after two
minutes, contact support.

## Cross-references

If you need to push from or pull from a **third-party** registry (GHCR,
Docker Hub, GitLab Container Registry, ECR, etc.) into a Rapid, see
[Private Container Registry](https://docs.danubedata.ro/serverless-private-registry).
