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
Dockerfilefor 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
Runningand exposed a hostname - [ ]
docker loginreturnedLogin 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:
-
Reading the hostname before the registry is Running
- Problem: Your pipeline's
docker loginfails with a DNS resolution error right afterterraform apply. - Why it happens: The hostname is empty until the registry leaves the
Newstate and reachesRunning. Reading it too early yields an empty or unresolvable value. - Fix: Gate the login step on the
Runningstate. 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 - Problem: Your pipeline's
-
Push token without pull permission
- Problem:
docker pushfails with an authorization error even though the token has thepushaction. - Why it happens: Pushing reads existing layers to deduplicate, so push requires pull. A
push-only scope is rejected. - Fix: Always include
pullalongsidepushin the scope:
actions = ["push", "pull"] - Problem:
-
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;nameandlocationare immutable and the name is globally unique (a duplicate returns HTTP 409) - The hostname follows
{name}.cr.{location-with-dash}.ionos.comand is empty until the registry isRunning - Authentication is token-based
docker loginonly, with scopes ofRegistryorRepositoryand actionspull,push,delete;pushrequirespull - 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 (
RegistryorRepository), a path, and an action set frompull,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: