CI/CD integration

Automate container image builds and pushes using your existing CI/CD platform. This guide provides 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 using 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.

Basic configuration

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

name: Build and Push

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

env:
  REGISTRY: ${{ secrets.CF_ORG_ID }}.${{ secrets.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-            

      - 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

In your GitHub repository:

  1. Navigate to Settings > Secrets and variables > Actions
  2. Add the following secrets:
    • CF_ORG_ID: Your organization ID
    • CFCR_REGION: Your registry region (europe, northamerica, or apac)
    • CF_TOKEN_ID: API token ID
    • CF_TOKEN_SECRET: API token secret

Multi-architecture builds

Build images for multiple architectures:

      - 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

Trigger a deployment after pushing the image:

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

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

      - name: Install Cloudfleet CLI
        run: |
          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          

      - name: Configure Cloudfleet CLI
        env:
          CLOUDFLEET_ORGANIZATION_ID: ${{ secrets.CF_ORG_ID }}
          CLOUDFLEET_ACCESS_TOKEN_ID: ${{ secrets.CF_TOKEN_ID }}
          CLOUDFLEET_ACCESS_TOKEN_SECRET: ${{ secrets.CF_TOKEN_SECRET }}
        run: |
          cloudfleet clusters kubeconfig ${{ secrets.CF_CLUSTER_ID }} > kubeconfig.yaml          

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

GitLab CI

GitLab CI integrates natively with GitLab repositories.

Basic configuration

Create .gitlab-ci.yml:

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
  image: bitnami/kubectl:latest
  before_script:
    - |
      curl -fsSL https://downloads.cloudfleet.ai/cli/latest/cloudfleet-linux-amd64.zip -o cloudfleet.zip
      unzip cloudfleet.zip
      chmod +x cloudfleet      
  script:
    - |
      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.yaml      
    - kubectl --kubeconfig=kubeconfig.yaml set image deployment/myapp myapp=$REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Configure variables

In your GitLab project:

  1. Navigate to Settings > CI/CD > Variables
  2. Add the following variables (mark as protected and masked):
    • CF_ORG_ID: Your organization ID
    • CFCR_REGION: Your registry region (europe, northamerica, or apac)
    • CF_TOKEN_ID: API token ID
    • CF_TOKEN_SECRET: API token secret
    • CF_CLUSTER_ID: Target cluster ID (for deployments)

Using Kaniko for rootless builds

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

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 flexible CI/CD pipelines for enterprise environments.

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any

    environment {
        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-org-id', variable: 'CF_ORG_ID'),
                    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.yaml
                        kubectl --kubeconfig=kubeconfig.yaml set image deployment/myapp myapp=${REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}
                    '''
                }
            }
        }
    }
}

Configure credentials

In Jenkins:

  1. Navigate 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-org-id
    • cfcr-region
    • cf-token-id
    • cf-token-secret
    • cf-cluster-id

CircleCI

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

Configuration

Create .circleci/config.yml:

version: 2.1

orbs:
  docker: circleci/docker@2.4

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/cli/latest/cloudfleet-linux-amd64.zip -o cloudfleet.zip
            unzip cloudfleet.zip
            sudo mv cloudfleet /usr/local/bin/            
      - 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.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. Navigate 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:

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/cli/latest/cloudfleet-linux-amd64.zip -o cloudfleet.zip
              unzip cloudfleet.zip
              chmod +x 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.yaml
              kubectl --kubeconfig=kubeconfig.yaml set image deployment/myapp myapp=$(registry)/$(imageName):$(Build.BuildId)              
            displayName: Deploy to CFKE

Configure service connection

  1. Navigate 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

Implement consistent image tagging across pipelines:

# 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 to speed up builds:

# 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 before pushing images:

# GitHub Actions with Trivy
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ 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 using infrastructure as code.

Troubleshooting

Authentication failures

Error: unauthorized: authentication required

Verify:

  • Token ID and secret are correct
  • Token has 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 User role (pull-only). Create a new token with Administrator role.

Rate limiting

If you experience rate limiting during high-volume builds:

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

Next steps