CI/CD integration

Automate container image builds and pushes with your existing CI/CD platform. This guide gives configuration examples for popular CI/CD systems.

Prerequisites

Before configuring your CI/CD pipeline:

  1. Create an API token with the Administrator role (required for pushing images). See API tokens.

  2. Store credentials securely in your platform’s secret management feature. Never commit tokens to source control.

  3. Note your registry URL: YOUR_ORG_ID.REGION.registry.cloudfleet.dev (regions: europe, northamerica, apac)

GitHub Actions

GitHub Actions is the recommended CI/CD platform for GitHub repositories. The workflow below pushes images with an API token and deploys to CFKE with GitHub’s OIDC identity, so the deploy step stores no Cloudfleet credentials. For the full cluster access setup, including RBAC bindings, see GitHub Actions.

Basic configuration

Create .github/workflows/build-and-push.yml:

yaml
name: Build and Push

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ${{ vars.CF_ORG_ID }}.${{ vars.CFCR_REGION }}.registry.cloudfleet.dev
  IMAGE_NAME: myapp

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

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

      - name: Log in to Cloudfleet Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.CF_TOKEN_ID }}
          password: ${{ secrets.CF_TOKEN_SECRET }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha,prefix=sha-,format=long            

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Configure secrets and variables

In your GitHub repository:

  1. Go to Settings > Secrets and variables > Actions
  2. Add the following variables. They are not sensitive, and storing them as secrets would mask the image name in the logs:
    • CF_ORG_ID: Your organization ID
    • CFCR_REGION: Your registry region (europe, northamerica, or apac)
  3. Add the following secrets:
    • CF_TOKEN_ID: API token ID
    • CF_TOKEN_SECRET: API token secret

Multi-architecture builds

Build images for multiple architectures:

yaml
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

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

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}

Deploy to CFKE after push

Deploy the pushed image in a second job. The job authenticates to the cluster with GitHub’s OIDC token through the configure-kubectl action, so it needs no Cloudfleet CLI and no long-lived API token. Before the first run, bind a role to the workflow’s identity as described in Namespace-scoped deploy access.

yaml
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    permissions:
      id-token: write   # required: lets the job request a GitHub OIDC token
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v5

      - name: Configure kubectl
        uses: cloudfleet-actions/configure-kubectl@v1
        with:
          cluster-id: ${{ vars.CFKE_CLUSTER_ID }}
          region: ${{ vars.CFKE_REGION }}

      - name: Deploy to CFKE
        run: |
          kubectl set image deployment/myapp \
            myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}          

Add CFKE_CLUSTER_ID and CFKE_REGION as repository variables, not secrets. Neither value is sensitive.

GitLab CI

GitLab CI is built into GitLab repositories.

Basic configuration

Create .gitlab-ci.yml:

yaml
stages:
  - build
  - deploy

variables:
  REGISTRY: ${CF_ORG_ID}.${CFCR_REGION}.registry.cloudfleet.dev
  IMAGE_NAME: myapp

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login $REGISTRY -u $CF_TOKEN_ID -p $CF_TOKEN_SECRET
  script:
    - docker build -t $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA .
    - docker push $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA $REGISTRY/$IMAGE_NAME:latest
        docker push $REGISTRY/$IMAGE_NAME:latest
      fi      
  rules:
    - if: $CI_COMMIT_BRANCH

deploy:
  stage: deploy
  extends: .cfke   # defined in the GitLab CI quick start: OIDC ID token and kubeconfig
  script:
    - kubectl set image deployment/myapp myapp=$REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

The deploy job authenticates to the cluster with GitLab’s OIDC ID token, so it needs no Cloudfleet CLI and no long-lived API token. Copy the .cfke hidden job from the GitLab CI quick start into the same .gitlab-ci.yml. Before the first run, bind a role to the job’s identity as described in Namespace-scoped deploy access.

Configure variables

In your GitLab project:

  1. Go to Settings > CI/CD > Variables
  2. Add the following variables. They are not sensitive, so do not mask them:
    • CF_ORG_ID: Your organization ID
    • CFCR_REGION: Your registry region (europe, northamerica, or apac)
    • CFKE_CLUSTER_ID and CFKE_REGION: the target cluster, used by the .cfke job
  3. Add the following variables and mark them as protected and masked:
    • CF_TOKEN_ID: API token ID
    • CF_TOKEN_SECRET: API token secret

Using Kaniko for rootless builds

If your GitLab runners do not support Docker-in-Docker:

yaml
build:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.19.2-debug
    entrypoint: [""]
  script:
    - |
      cat > /kaniko/.docker/config.json << EOF
      {
        "auths": {
          "${REGISTRY}": {
            "username": "${CF_TOKEN_ID}",
            "password": "${CF_TOKEN_SECRET}"
          }
        }
      }
      EOF      
    - /kaniko/executor
        --context $CI_PROJECT_DIR
        --dockerfile $CI_PROJECT_DIR/Dockerfile
        --destination $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
        --destination $REGISTRY/$IMAGE_NAME:latest

Jenkins

Jenkins provides configurable CI/CD pipelines for enterprise environments.

Jenkinsfile (declarative pipeline)

groovy
pipeline {
    agent any

    environment {
        CF_ORG_ID = 'YOUR_ORG_ID'
        CFCR_REGION = 'europe' // europe, northamerica, or apac
        REGISTRY = "${CF_ORG_ID}.${CFCR_REGION}.registry.cloudfleet.dev"
        IMAGE_NAME = 'myapp'
        REGISTRY_CREDS = credentials('cloudfleet-registry')
    }

    stages {
        stage('Build') {
            steps {
                script {
                    docker.build("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_NUMBER}")
                }
            }
        }

        stage('Push') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY}", 'cloudfleet-registry') {
                        docker.image("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_NUMBER}").push()
                        docker.image("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_NUMBER}").push('latest')
                    }
                }
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                withCredentials([
                    string(credentialsId: 'cf-token-id', variable: 'CF_TOKEN_ID'),
                    string(credentialsId: 'cf-token-secret', variable: 'CF_TOKEN_SECRET'),
                    string(credentialsId: 'cf-cluster-id', variable: 'CF_CLUSTER_ID')
                ]) {
                    sh '''
                        export CLOUDFLEET_ORGANIZATION_ID=$CF_ORG_ID
                        export CLOUDFLEET_ACCESS_TOKEN_ID=$CF_TOKEN_ID
                        export CLOUDFLEET_ACCESS_TOKEN_SECRET=$CF_TOKEN_SECRET
                        cloudfleet clusters kubeconfig $CF_CLUSTER_ID --kubeconfig kubeconfig.yaml
                        kubectl --kubeconfig=kubeconfig.yaml set image deployment/myapp myapp=${REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}
                    '''
                }
            }
        }
    }
}

Configure credentials

In Jenkins:

  1. Go to Manage Jenkins > Manage Credentials
  2. Add a Username with password credential:
    • ID: cloudfleet-registry
    • Username: Your API token ID
    • Password: Your API token secret
  3. Add Secret text credentials for:
    • cf-token-id
    • cf-token-secret
    • cf-cluster-id

CircleCI

CircleCI provides cloud-native CI/CD with Docker support.

Configuration

Create .circleci/config.yml:

yaml
version: 2.1

orbs:
  docker: circleci/[email protected]

jobs:
  build-and-push:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - setup_remote_docker:
          version: 24.0.7
      - run:
          name: Build image
          command: |
            docker build -t $CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev/myapp:$CIRCLE_SHA1 .            
      - run:
          name: Push image
          command: |
            echo $CF_TOKEN_SECRET | docker login $CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev \
              --username $CF_TOKEN_ID --password-stdin
            docker push $CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev/myapp:$CIRCLE_SHA1
            if [ "$CIRCLE_BRANCH" == "main" ]; then
              docker tag $CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev/myapp:$CIRCLE_SHA1 \
                $CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev/myapp:latest
              docker push $CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev/myapp:latest
            fi            

  deploy:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - run:
          name: Install Cloudfleet CLI
          command: |
            curl -fsSL https://downloads.cloudfleet.ai/apt/pubkey.gpg | sudo tee /usr/share/keyrings/cloudfleet-archive-keyring.gpg >/dev/null
            echo "deb [signed-by=/usr/share/keyrings/cloudfleet-archive-keyring.gpg] https://downloads.cloudfleet.ai/apt stable main" | sudo tee /etc/apt/sources.list.d/cloudfleet.list
            sudo apt-get update && sudo apt-get install -y cloudfleet            
      - run:
          name: Deploy to CFKE
          command: |
            export CLOUDFLEET_ORGANIZATION_ID=$CF_ORG_ID
            export CLOUDFLEET_ACCESS_TOKEN_ID=$CF_TOKEN_ID
            export CLOUDFLEET_ACCESS_TOKEN_SECRET=$CF_TOKEN_SECRET
            cloudfleet clusters kubeconfig $CF_CLUSTER_ID --kubeconfig kubeconfig.yaml
            kubectl --kubeconfig=kubeconfig.yaml set image deployment/myapp \
              myapp=$CF_ORG_ID.$CFCR_REGION.registry.cloudfleet.dev/myapp:$CIRCLE_SHA1            

workflows:
  build-deploy:
    jobs:
      - build-and-push
      - deploy:
          requires:
            - build-and-push
          filters:
            branches:
              only: main

Configure environment variables

In CircleCI project settings:

  1. Go to Project Settings > Environment Variables
  2. Add:
    • CF_ORG_ID
    • CFCR_REGION
    • CF_TOKEN_ID
    • CF_TOKEN_SECRET
    • CF_CLUSTER_ID

Azure DevOps

Azure Pipelines integrates with Azure DevOps repositories.

Pipeline configuration

Create azure-pipelines.yml:

yaml
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

variables:
  registry: $(CF_ORG_ID).$(CFCR_REGION).registry.cloudfleet.dev
  imageName: myapp

stages:
  - stage: Build
    jobs:
      - job: BuildAndPush
        steps:
          - task: Docker@2
            displayName: Login to registry
            inputs:
              command: login
              containerRegistry: cloudfleet-registry

          - task: Docker@2
            displayName: Build and push
            inputs:
              command: buildAndPush
              repository: $(imageName)
              dockerfile: '$(Build.SourcesDirectory)/Dockerfile'
              containerRegistry: cloudfleet-registry
              tags: |
                $(Build.BuildId)
                $(Build.SourceVersion)
                latest                

  - stage: Deploy
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - job: DeployToCFKE
        steps:
          - script: |
              curl -fsSL https://downloads.cloudfleet.ai/apt/pubkey.gpg | sudo tee /usr/share/keyrings/cloudfleet-archive-keyring.gpg >/dev/null
              echo "deb [signed-by=/usr/share/keyrings/cloudfleet-archive-keyring.gpg] https://downloads.cloudfleet.ai/apt stable main" | sudo tee /etc/apt/sources.list.d/cloudfleet.list
              sudo apt-get update && sudo apt-get install -y cloudfleet
              export CLOUDFLEET_ORGANIZATION_ID=$(CF_ORG_ID)
              export CLOUDFLEET_ACCESS_TOKEN_ID=$(CF_TOKEN_ID)
              export CLOUDFLEET_ACCESS_TOKEN_SECRET=$(CF_TOKEN_SECRET)
              cloudfleet clusters kubeconfig $(CF_CLUSTER_ID) --kubeconfig kubeconfig.yaml
              kubectl --kubeconfig=kubeconfig.yaml set image deployment/myapp myapp=$(registry)/$(imageName):$(Build.BuildId)              
            displayName: Deploy to CFKE

Configure service connection

  1. Go to Project Settings > Service connections
  2. Create a new Docker Registry service connection:
    • Registry type: Others
    • Docker Registry: https://YOUR_ORG_ID.REGION.registry.cloudfleet.dev (replace REGION with europe, northamerica, or apac)
    • Docker ID: Your API token ID
    • Docker Password: Your API token secret
    • Service connection name: cloudfleet-registry
  3. Add pipeline variables for CF_ORG_ID and CFCR_REGION

Best practices

Tagging strategy

Use consistent image tags across pipelines:

yaml
# Semantic version tags for releases
v1.0.0, v1.0, v1

# Branch-based tags for development
main, develop, feature-xyz

# Commit SHA for traceability
sha-a1b2c3d4

# Build number for CI/CD systems
build-123

Caching

Enable Docker layer caching for faster builds:

yaml
# GitHub Actions
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# GitLab CI
build:
  variables:
    DOCKER_BUILDKIT: 1
  script:
    - docker build --cache-from $REGISTRY/$IMAGE_NAME:latest -t $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA .

Security scanning

Integrate vulnerability scanning into your pipeline. This step scans the image that the GitHub Actions workflow pushed for the current commit:

yaml
# GitHub Actions with Trivy
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
    format: 'table'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

Token rotation

Periodically rotate CI/CD tokens:

  1. Create a new token with the same role
  2. Update CI/CD secrets with the new credentials
  3. Verify pipelines work with the new token
  4. Delete the old token

Consider automating this process with infrastructure as code.

Troubleshooting

Authentication failures

Error: unauthorized: authentication required

Verify:

  • Token ID and secret are correct
  • Token has the Administrator role (for pushing)
  • Registry URL matches your organization

Image push denied

Error: denied: requested access to the resource is denied

The token may have the User role (pull-only). Create a new token with the Administrator role.

Rate limiting

If you hit rate limits during high-volume builds:

  • Use image layer caching
  • Use a single base image across builds
  • Consider parallel job limits

Next steps

On this page