14 min read

Learning Objectives

By the end of this module, you will be able to:

  • Provision an IONOS Container Registry and its access tokens with Terraform using the `ionoscloud_container_registry` and `ionoscloud_container_registry_token` resources
  • Authenticate the Docker CLI to a registry using a token-scoped `docker login` and push multi-stage production images
  • Build a reproducible image pipeline that tags images with the Git SHA and pushes them to the registry endpoint
  • Wire registry credentials into GitHub Actions and GitLab CI as secrets and run `docker login` inside a pipeline
  • Configure garbage collection and vulnerability scanning, and work within the registry's per-registry token model

Unit 3.1: Container Registry and Image Pipelines

Introduction

You are about to containerize TaskBoard's API service and web frontend and you need a registry to push the images to before Managed Kubernetes can pull them. IONOS Container Registry gives you one or more dedicated, authenticated registries that speak the Docker Registry HTTP API V2 and accept any OCI-compliant artifact. Everything you do with it, provisioning, token creation, login, push, and garbage collection, is driven through Terraform, the REST API, and the Docker CLI.

This unit starts at the code level. You will provision a registry as Infrastructure as Code, mint a scoped token, authenticate Docker, and build the build-tag-push pipeline that the rest of Module 3 depends on. Two IONOS-specific constraints shape every example here: authentication is token-based only with no role-based access control and no per-repository ACLs, and the registry hostname does not exist until the registry reaches the Running state. Both will bite you in CI if you ignore them, so they are baked into the code patterns below.

1. Provisioning the Registry with Terraform

The registry is a first-class Terraform resource. You declare a name and a location, apply, and wait for the asynchronous provisioning to complete before the hostname is allocated. The name and location are immutable after creation, so choosing them is a one-way decision per registry.

The registry name must be at most 63 characters. If you try to create a registry with a name that is already taken across the platform, the API returns HTTP 409 Conflict with the message already exists: name is not available. Treat the name as a global handle, not a per-account one, and prefix it with your project to avoid collisions.

1.1 The ionoscloud_container_registry resource

The location must be a supported Container Registry location. Container Registry is available in de/fra (DE/FRA), so your location value is de/fra.

terraform {
  required_providers {
    ionoscloud = {
      source  = "ionos-cloud/ionoscloud"
      version = ">= 6.0.0"
    }
  }
}

provider "ionoscloud" {
  # token read from IONOS_TOKEN env var
}

resource "ionoscloud_container_registry" "taskboard" {
  name     = "taskboard-prod"
  location = "de/fra"

  garbage_collection_schedule {
    days = ["Saturday", "Sunday"]
    time = "19:30:00+00:00"
  }
}

output "registry_hostname" {
  value = ionoscloud_container_registry.taskboard.hostname
}

The hostname output follows the pattern {registry-name}.cr.{location-with-dash}.ionos.com. It is empty until the registry reaches Running, so any downstream step that reads it must run after apply fully completes. The registry has only two lifecycle states, New and Running, and only Running is usable.

1.2 Provisioning via the REST API

If you provision outside Terraform, the registry is created with a POST to the registries endpoint. The same garbageCollectionSchedule, location, and name properties apply.

curl --location \
  --request POST 'https://api.ionos.com/containerregistries/registries' \
  --header "Authorization: Bearer ${IONOS_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "properties": {
      "garbageCollectionSchedule": {
        "days": ["Saturday", "Sunday"],
        "time": "19:30:00+00:00"
      },
      "location": "de/fra",
      "name": "taskboard-prod"
    }
  }'

The 201 response does not include a hostname because the host is not allocated yet. Poll GET /containerregistries/registries/{registryId} until the state is Running before reading the hostname. List calls are token-paginated: pass limit as a query parameter and follow nextPageToken to page through large collections.

2. Tokens and Authentication

Authentication is token-based docker login only. There is no RBAC, no per-team repositories, and no anonymous or public-repository access. A token is the unit of access, and you scope it per registry or per repository path with a set of allowed actions.

2.1 Creating a token with Terraform

A token has a name, a status, an optional expiry, and one or more scopes. Each scope has a type of Registry or Repository, a path, and an action list drawn from pull, push, and delete. The token name must be 3 to 63 characters, start with a-z, end with an alphanumeric character, and contain only alphanumerics and dashes, and it is immutable after creation.

resource "ionoscloud_container_registry_token" "ci_push" {
  container_registry_id = ionoscloud_container_registry.taskboard.id
  name                  = "ci-push"
  status                = "enabled"

  # minimum expiry is 1 hour in the future; on expiry the token is DELETED
  expiry_date = "2027-01-01T00:00:00Z"

  scopes {
    type    = "Repository"
    name    = "taskboard/*"
    actions = ["push", "pull"]
  }
}

output "ci_push_password" {
  value     = ionoscloud_container_registry_token.ci_push.password
  sensitive = true
}

Two behaviors matter for automation. The token password is shown once, so capture it from Terraform state or the API response immediately and store it in your secret manager. When the expiry date is reached the token is deleted, not disabled, so a rotation job must create the replacement before the old one expires or your pipeline loses access mid-deploy.

2.2 Creating a token via the API and logging in

The token endpoint is POST /containerregistries/registries/{registryId}/tokens. Tokens come in two types, a permanent registry access token and a temporary one bounded by expiryDate. The minimum expiry is one hour in the future.

curl --location \
  --request POST "https://api.ionos.com/containerregistries/registries/${REGISTRY_ID}/tokens" \
  --header "Authorization: Bearer ${IONOS_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "properties": {
      "name": "ci-push",
      "status": "enabled",
      "scopes": [
        { "type": "Repository", "name": "taskboard/*", "actions": ["push", "pull"] }
      ]
    }
  }'

The response carries the credentials once. Use the token name as the username and the token password as the password for docker login against the registry hostname.

echo "${CR_TOKEN_PASSWORD}" | docker login taskboard-prod.cr.de-fra.ionos.com \
  --username ci-push \
  --password-stdin

A token lives in one of three states, enabled, disabled, or deleted. Disable a token to revoke access immediately without deleting it; delete it when it is no longer needed.

2.3 Choosing how to mint and use credentials

The following table compares the access paths for registry credentials so you can pick the right one per workflow.

Approach Best for Tooling Credential lifetime
Terraform token resource CI service identities provisioned as code HCL Until expiry_date (deleted on expiry)
API token create Scripted rotation jobs curl/HTTP Until expiry_date or manual delete
docker login Local dev push/pull Docker CLI Session, backed by token

Because scopes are per registry or per repository path and there is no RBAC, model one token per CI pipeline and one per developer rather than sharing a single broad token. Scope CI tokens to push plus pull on the repository paths they own, and scope read-only deploy tokens to pull only.

3. Building and Pushing Images

With a registry running and a token in hand, the image workflow is standard Docker against the registry hostname. The discipline that makes it production-grade is multi-stage builds for small images and immutable Git-SHA tags for traceable deploys.

3.1 Multi-stage Dockerfile for the TaskBoard API

A multi-stage build compiles or installs dependencies in a fat builder stage and copies only the runtime artifacts into a slim final image. Smaller images pull faster on Kubernetes and carry fewer packages for the vulnerability scanner to flag.

FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "taskboard.api:app"]

3.2 Tag with the Git SHA and push

Tag every build with the immutable Git SHA so a deployed image maps back to an exact commit. A latest tag is fine as a convenience pointer, but never deploy from it; deploy from the SHA.

REGISTRY=taskboard-prod.cr.de-fra.ionos.com
GIT_SHA=$(git rev-parse --short HEAD)

docker build -t "${REGISTRY}/taskboard/api:${GIT_SHA}" \
             -t "${REGISTRY}/taskboard/api:latest" .

docker push "${REGISTRY}/taskboard/api:${GIT_SHA}"
docker push "${REGISTRY}/taskboard/api:latest"

The pipeline pattern is build, tag with the Git SHA, push to the registry, then reference the SHA tag in the Kubernetes manifest (covered in Unit 3.2). Note that a token granting push also needs pull: pushing an image requires reading existing layers, so a push-only scope is insufficient and a push token must also carry pull.

4. Garbage Collection and Vulnerability Scanning

Two registry features run server-side and are worth configuring early. Garbage collection reclaims storage from unreferenced layers, and vulnerability scanning surfaces CVEs in your pushed artifacts.

4.1 Garbage collection

Garbage collection frees storage occupied by layer data no longer referenced by any tag. It is disabled by default because the right schedule depends on your push and delete cadence, so you set it explicitly. You already configured it in the Terraform resource in Section 1.1 via the garbage_collection_schedule block with days and a time.

garbage_collection_schedule {
  days = ["Saturday", "Sunday"]
  time = "19:30:00+00:00"
}

During garbage collection the registry enters a read-only state: pulls continue to work but pushes are blocked until the run finishes, so schedule it for a low-traffic window to avoid interrupting CI/CD pushes and deploys. Note that the Docker Registry V2 API cannot delete an entire repository; to remove a repository you call the IONOS Container Registry API directly with DELETE /containerregistries/registries/{id}/repositories/{url-encoded-name}, and the repository name in the URL must be URL-encoded, for example taskboard/api becomes taskboard%2Fapi, or the call returns 404.

4.2 Vulnerability scanning

Vulnerability scanning analyzes pushed artifacts for known CVEs. It is an add-on feature, and once enabled on a registry it cannot be disabled, so enable it deliberately. Scan results are reported per artifact and per repository, including which findings have known fixes, so you can gate promotion of an image on its scan results in CI.

Treat the scan as a quality gate: after the push step, query the scan results for the SHA-tagged artifact and fail the pipeline if findings exceed your threshold before the deploy step proceeds.

5. CI Integration

In CI you do not run an interactive docker login. You store the token credentials as pipeline secrets and feed them to docker login --password-stdin. The registry hostname, token name, and token password are the three values your pipeline needs.

5.1 GitHub Actions

Store CR_HOSTNAME, CR_USERNAME, and CR_PASSWORD as repository or environment secrets, then log in and push in the workflow.

name: build-and-push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to IONOS Container Registry
        run: |
          echo "${{ secrets.CR_PASSWORD }}" | docker login "${{ secrets.CR_HOSTNAME }}" \
            --username "${{ secrets.CR_USERNAME }}" --password-stdin

      - name: Build and push
        run: |
          SHA=$(git rev-parse --short HEAD)
          IMG="${{ secrets.CR_HOSTNAME }}/taskboard/api:${SHA}"
          docker build -t "$IMG" .
          docker push "$IMG"

5.2 GitLab CI

The equivalent GitLab pipeline uses CI/CD variables for the same three values and the Docker-in-Docker service for the build.

build-and-push:
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
  script:
    - echo "$CR_PASSWORD" | docker login "$CR_HOSTNAME" --username "$CR_USERNAME" --password-stdin
    - SHA=$(echo "$CI_COMMIT_SHA" | cut -c1-7)
    - docker build -t "$CR_HOSTNAME/taskboard/api:$SHA" .
    - docker push "$CR_HOSTNAME/taskboard/api:$SHA"
  only:
    - main

Because tokens are the only credential model, rotate the CI token on a schedule and update the secret in lockstep. Each client is rate-limited to 60 requests per second and 100 concurrent connections, which is generous for a single pipeline but worth knowing if you fan out parallel matrix builds that all push at once.

API Reference Quick Card

Key API endpoints for Container Registry:

Method Endpoint Description
GET /containerregistries/registries List registries (paginated via limit + nextPageToken)
POST /containerregistries/registries Create a registry
GET /containerregistries/registries/{registryId} Get registry details (and state/hostname)
POST /containerregistries/registries/{registryId}/tokens Create an access token
DELETE /containerregistries/registries/{id}/repositories/{url-encoded-name} Delete an entire repository (V2 API cannot)

Base URL: https://api.ionos.com Authentication: Authorization: Bearer <token>

Code Lab

Objective: Provision a Container Registry, mint a scoped token, build the TaskBoard API image, and push it with a Git-SHA tag.

Prerequisites:

  • IONOS Cloud account with API token exported as IONOS_TOKEN
  • Terraform and Docker installed locally
  • A Dockerfile for the TaskBoard API (see Section 3.1)

Step 1: Provision the registry and token

export IONOS_TOKEN="your-api-token"
terraform init
terraform apply -auto-approve

Expected output:

ionoscloud_container_registry.taskboard: Creation complete
Outputs:
registry_hostname = "taskboard-prod.cr.de-fra.ionos.com"

Step 2: Read the registry hostname and token password

REGISTRY=$(terraform output -raw registry_hostname)
CR_PASSWORD=$(terraform output -raw ci_push_password)
echo "$REGISTRY"

Expected output:

taskboard-prod.cr.de-fra.ionos.com

Step 3: Log in to the registry

echo "$CR_PASSWORD" | docker login "$REGISTRY" --username ci-push --password-stdin

Expected output:

Login Succeeded

Step 4: Build the image

docker build -t "${REGISTRY}/taskboard/api:$(git rev-parse --short HEAD)" .

Expected output:

=> exporting to image
=> => naming to taskboard-prod.cr.de-fra.ionos.com/taskboard/api:a1b2c3d

Step 5: Push the image

docker push "${REGISTRY}/taskboard/api:$(git rev-parse --short HEAD)"

Expected output:

a1b2c3d: digest: sha256:... size: 1786

Step 6: Verify via the API

REGISTRY_ID=$(terraform output -raw registry_id)
curl -s --request GET \
  "https://api.ionos.com/containerregistries/registries/${REGISTRY_ID}/repositories" \
  --header "Authorization: Bearer ${IONOS_TOKEN}" | jq '.items[].id'

Expected output:

"taskboard/api"

Validation Checklist:

  • [ ] Registry reached Running and exposed a hostname
  • [ ] docker login returned Login Succeeded
  • [ ] Image pushed with a Git-SHA tag and visible via the repositories API

Cleanup:

terraform destroy -auto-approve

Common Pitfalls

Developer mistakes to avoid with Container Registry:

  1. Reading the hostname before the registry is Running

    • Problem: Your pipeline's docker login fails with a DNS resolution error right after terraform apply.
    • Why it happens: The hostname is empty until the registry leaves the New state and reaches Running. Reading it too early yields an empty or unresolvable value.
    • Fix: Gate the login step on the Running state. Poll the registry until ready before logging in:
    until [ "$(curl -s -H "Authorization: Bearer $IONOS_TOKEN" \
      https://api.ionos.com/containerregistries/registries/$REGISTRY_ID \
      | jq -r '.metadata.state')" = "Running" ]; do sleep 5; done
    
  2. Push token without pull permission

    • Problem: docker push fails with an authorization error even though the token has the push action.
    • Why it happens: Pushing reads existing layers to deduplicate, so push requires pull. A push-only scope is rejected.
    • Fix: Always include pull alongside push in the scope:
    actions = ["push", "pull"]
    
  3. Letting a token expire mid-deploy

    • Problem: A CI deploy that worked yesterday now fails authentication, and the token is simply gone.
    • Why it happens: When a token's expiry date is reached it is deleted, not disabled, so there is no grace period and no re-enable path.
    • Fix: Rotate ahead of expiry. Create the replacement token, update the CI secret, then let the old one expire. Never set an expiry shorter than your release cadence, and remember the minimum expiry is one hour in the future.

Summary

You can now provision an IONOS Container Registry as Infrastructure as Code, mint scoped access tokens, authenticate the Docker CLI and CI pipelines against it, and push production-grade multi-stage images tagged by Git SHA. You also know how to configure garbage collection to reclaim storage and enable vulnerability scanning as a promotion gate. This registry is the source of truth for the images that Unit 3.2 will deploy to Managed Kubernetes.

The registry's token-only access model with no RBAC and no per-repository ACLs shapes how you organize credentials: one scoped token per pipeline and per developer, rotated ahead of expiry because expired tokens are deleted outright. Keep the hostname-after-Running constraint in mind for every automated workflow, and the build-tag-push pipeline you built here becomes the first stage of the full CI/CD flow in Unit 3.3.

Key Points:

  • Provision registries with ionoscloud_container_registry; name and location are immutable and the name is globally unique (a duplicate returns HTTP 409)
  • The hostname follows {name}.cr.{location-with-dash}.ionos.com and is empty until the registry is Running
  • Authentication is token-based docker login only, with scopes of Registry or Repository and actions pull, push, delete; push requires pull
  • Token passwords are shown once and expired tokens are deleted, not disabled, so capture and rotate proactively
  • Garbage collection is disabled by default and configured on a schedule; vulnerability scanning is an add-on that cannot be disabled once enabled

Important Terminology:

  • Registry access token: The credential for docker login, scoped per registry or per repository path; comes in permanent and temporary (expiry-bounded) types
  • Scope: A token's permission grant, a type (Registry or Repository), a path, and an action set from pull, push, delete
  • Garbage collection: Server-side reclamation of unreferenced image layers, disabled by default and run on a configured day/time schedule
  • Vulnerability scanning: An add-on that analyzes pushed artifacts for CVEs per artifact and per repository; irreversible once enabled
  • Git-SHA tag: An immutable image tag set to the commit SHA so a deployed image maps to an exact commit

Next Steps

Continue Learning: Unit 3.2: Kubernetes Deployment and Operations

Related Topics: