Skip to main content

Push images to the Cloider registry from CI/CD

A CI/CD pipeline builds your image on every push to git, tags it, pushes it to the Cloider Container Registry, and optionally deploys it. This page gives complete, working pipelines for GitHub Actions and GitLab CI, plus the pieces you need for any other CI system.

Before you begin​

  • A project, for example acme. See Create a project.
  • A registry token for the pipeline with Pull & push permission, scoped to the projects it pushes to. Create one per pipeline, so you can revoke it alone.
  • Your account's registry username (shown with the token).

Step 1: Create a token for the pipeline​

  1. Open Registry, then the Tokens tab, and click Create Token.
  2. Name: ci-deploy. Projects: Only specific projects, acme. Permission: Pull & push. Expires: 1 year.
  3. Click Create token and copy the Username and Secret.

Step 2: Store the login as CI secrets​

Store the username and secret as masked secrets in your CI system, never in the repository:

CI systemWhereNames used on this page
GitHub ActionsRepository Settings, Secrets and variables, ActionsCLOIDER_REGISTRY_USERNAME, CLOIDER_REGISTRY_PASSWORD
GitLab CIProject Settings, CI/CD, Variables (masked, protected)CLOIDER_REGISTRY_USERNAME, CLOIDER_REGISTRY_PASSWORD

With the GitHub CLI:

gh secret set CLOIDER_REGISTRY_USERNAME --body "acme"
gh secret set CLOIDER_REGISTRY_PASSWORD # paste the secret when asked

GitHub Actions​

This workflow builds a multi-platform image on every push to main and on version tags, tags it with the commit SHA, the branch and the version, caches layers in the registry, and pushes it.

.github/workflows/image.yml
name: Build and push image

on:
push:
branches: [main]
tags: ["v*"]
pull_request:

env:
REGISTRY: registry.cloider.com
IMAGE: registry.cloider.com/acme/api

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out
uses: actions/checkout@v4

- name: Set up QEMU (for arm64 builds)
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the Cloider registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.CLOIDER_REGISTRY_USERNAME }}
password: ${{ secrets.CLOIDER_REGISTRY_PASSWORD }}

- name: Work out tags
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=sha,prefix=sha-
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
cache-to: ${{ github.event_name != 'pull_request' && format('type=registry,ref={0}:buildcache,mode=max', env.IMAGE) || '' }}

What each part does:

  • Pull requests build the image to check it compiles but don't log in or push.
  • docker/metadata-action turns the git event into tags: sha-4f2a9c1 for every commit, main for the branch, and 1.4.0 and 1.4 for a v1.4.0 git tag.
  • cache-to / cache-from keep the build cache in registry.cloider.com/acme/api:buildcache, so later builds reuse unchanged layers. The cache counts toward the project's storage.
  • For amd64 only, drop the QEMU step and set platforms: linux/amd64. Single platform builds are much faster.
Immutable tags and CI

If the project has Immutable tags turned on, pushing the main or buildcache tag a second time fails. Either push only unique tags (commit SHA and versions), or use an immutability rule that protects only version tags such as [0-9]*.

Deploy to a Cloider Kubernetes cluster from GitHub Actions​

Add a job that points kubectl at the cluster with the Cloider CLI and rolls out the new image. It needs a Cloider API token with write access, stored as the CLOIDER_TOKEN secret, and the cluster must already have a pull secret.

.github/workflows/image.yml (continued)
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
CLOIDER_TOKEN: ${{ secrets.CLOIDER_TOKEN }}
steps:
- name: Install the Cloider CLI
run: curl -fsSL https://cloider.com/install.sh | sh

- name: Get the kubeconfig
run: |
mkdir -p "$HOME/.kube"
cloider cluster kubeconfig production -f "$HOME/.kube/config"

- name: Roll out the new image
run: |
SHORT_SHA="${GITHUB_SHA::7}"
kubectl set image deployment/api api=registry.cloider.com/acme/api:sha-$SHORT_SHA -n shop
kubectl rollout status deployment/api -n shop --timeout=5m

The full tutorial, with tests and environments, is in CI/CD with GitHub Actions.

GitLab CI​

This pipeline builds with Docker in Docker, tags the image with the commit SHA (and latest on the default branch, and the version on git tags), and pushes it.

.gitlab-ci.yml
stages:
- build
- deploy

variables:
REGISTRY: registry.cloider.com
IMAGE: registry.cloider.com/acme/api
DOCKER_TLS_CERTDIR: "/certs"

build:
stage: build
image: docker:27
services:
- docker:27-dind
before_script:
- echo "$CLOIDER_REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$CLOIDER_REGISTRY_USERNAME" --password-stdin
script:
- docker pull "$IMAGE:latest" || true
- docker build --cache-from "$IMAGE:latest" -t "$IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$IMAGE:$CI_COMMIT_SHORT_SHA"
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag "$IMAGE:$CI_COMMIT_SHORT_SHA" "$IMAGE:latest"
docker push "$IMAGE:latest"
fi
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag "$IMAGE:$CI_COMMIT_SHORT_SHA" "$IMAGE:${CI_COMMIT_TAG#v}"
docker push "$IMAGE:${CI_COMMIT_TAG#v}"
fi
rules:
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_TAG

deploy:
stage: deploy
image: alpine:3.20
needs: [build]
before_script:
- apk add --no-cache curl kubectl
- curl -fsSL https://cloider.com/install.sh | sh
script:
- mkdir -p "$HOME/.kube"
- cloider cluster kubeconfig production -f "$HOME/.kube/config"
- kubectl set image deployment/api api="$IMAGE:$CI_COMMIT_SHORT_SHA" -n shop
- kubectl rollout status deployment/api -n shop --timeout=5m
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Set CLOIDER_REGISTRY_USERNAME, CLOIDER_REGISTRY_PASSWORD and, for the deploy job, CLOIDER_TOKEN (a Cloider API token) as masked CI/CD variables. The docker:dind service needs a runner that allows privileged containers.

Multi-platform builds with buildx on GitLab​

Replace the script of the build job with a buildx build:

.gitlab-ci.yml (build job)
script:
- docker run --privileged --rm tonistiigi/binfmt --install arm64
- docker buildx create --use --name ci
- >
docker buildx build
--platform linux/amd64,linux/arm64
--cache-from type=registry,ref=$IMAGE:buildcache
--cache-to type=registry,ref=$IMAGE:buildcache,mode=max
-t "$IMAGE:$CI_COMMIT_SHORT_SHA"
--push .

Without Docker in Docker: Buildah​

On runners that don't allow privileged containers, Buildah builds and pushes without a Docker daemon:

.gitlab-ci.yml (Buildah build job)
build:
stage: build
image: quay.io/buildah/stable:latest
variables:
STORAGE_DRIVER: vfs
BUILDAH_FORMAT: docker
script:
- echo "$CLOIDER_REGISTRY_PASSWORD" | buildah login -u "$CLOIDER_REGISTRY_USERNAME" --password-stdin "$REGISTRY"
- buildah build -t "$IMAGE:$CI_COMMIT_SHORT_SHA" .
- buildah push "$IMAGE:$CI_COMMIT_SHORT_SHA"

Use a private image as a job image​

To run a CI job inside an image from your Cloider project, give the runner a pull login with the DOCKER_AUTH_CONFIG variable. Its value is Docker's config.json, which a token's Download JSON button gives you:

DOCKER_AUTH_CONFIG
{
"auths": {
"registry.cloider.com": {
"auth": "YWNtZTp4eHh4eHh4eHh4eHh4eHh4"
}
}
}

auth is base64("<username>:<secret>"). Use a Pull only token for this. Then:

test:
image: registry.cloider.com/acme/ci-tools:1.2
script:
- make test

The full tutorial is in CI/CD with GitLab.

Other CI systems​

Every CI system follows the same three steps: log in with the token, build, push.

echo "$CLOIDER_REGISTRY_PASSWORD" | docker login registry.cloider.com -u "$CLOIDER_REGISTRY_USERNAME" --password-stdin
docker build -t "registry.cloider.com/acme/api:$GIT_SHA" .
docker push "registry.cloider.com/acme/api:$GIT_SHA"
bitbucket-pipelines.yml
image: atlassian/default-image:4

pipelines:
branches:
main:
- step:
name: Build and push
services:
- docker
script:
- echo "$CLOIDER_REGISTRY_PASSWORD" | docker login registry.cloider.com -u "$CLOIDER_REGISTRY_USERNAME" --password-stdin
- docker build -t registry.cloider.com/acme/api:${BITBUCKET_COMMIT:0:7} .
- docker push registry.cloider.com/acme/api:${BITBUCKET_COMMIT:0:7}

Keep CI images under control​

A pipeline that pushes on every commit fills a project quickly. Two features keep it in check:

  • A retention policy that keeps, for example, the 20 most recently pushed sha-* images and every version tag, run daily.
  • The nightly cleanup, which removes images left without any tag. When CI moves latest or main to a new image, the old image keeps its sha-* tag, so it stays until retention removes it.

Example retention rules for a CI project:

cat > rules.json <<'EOF'
[
{"repositories": {"match": "matching", "patterns": ["**"]},
"tags": {"match": "matching", "patterns": ["sha-*"]},
"keep": "latest_pushed", "count": 20, "untagged": true},
{"repositories": {"match": "matching", "patterns": ["**"]},
"tags": {"match": "excluding", "patterns": ["sha-*"]},
"keep": "always"}
]
EOF
cloider registry retention set acme -f rules.json
cloider registry retention schedule acme --every daily --hour 3

Troubleshooting​

Error in the pipelineCause and fix
unauthorized on docker loginWrong username or secret, or the token expired or was deleted. Check the CI variables; the username is your registry username, not your email
denied: requested access to the resource is denied on pushThe token is Pull only, or doesn't cover the project in the image name
Push fails because the tag is immutableThe project or a rule makes the tag immutable. Push a unique tag (commit SHA) or change the rule
Push fails with a quota errorThe project is at its storage limit. Add a retention policy or raise the limit
Cannot connect to the Docker daemon (GitLab)The docker:dind service is missing or the runner doesn't allow privileged mode. Use Buildah instead
Multi-platform build is slowarm64 is emulated with QEMU on amd64 runners. Build amd64 only, or use native arm64 runners

More on Troubleshooting.

Next steps​