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:
Create an API token with the Administrator role (required for pushing images). See API tokens.
Store credentials securely in your platform’s secret management feature. Never commit tokens to source control.
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:
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=maxConfigure secrets and variables
In your GitHub repository:
- Go to Settings > Secrets and variables > Actions
- 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 IDCFCR_REGION: Your registry region (europe,northamerica, orapac)
- Add the following secrets:
CF_TOKEN_ID: API token IDCF_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
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.
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:
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:
- Go to Settings > CI/CD > Variables
- Add the following variables. They are not sensitive, so do not mask them:
CF_ORG_ID: Your organization IDCFCR_REGION: Your registry region (europe,northamerica, orapac)CFKE_CLUSTER_IDandCFKE_REGION: the target cluster, used by the.cfkejob
- Add the following variables and mark them as protected and masked:
CF_TOKEN_ID: API token IDCF_TOKEN_SECRET: API token secret
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:latestJenkins
Jenkins provides configurable CI/CD pipelines for enterprise environments.
Jenkinsfile (declarative pipeline)
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:
- Go to Manage Jenkins > Manage Credentials
- Add a Username with password credential:
- ID:
cloudfleet-registry - Username: Your API token ID
- Password: Your API token secret
- ID:
- Add Secret text credentials for:
cf-token-idcf-token-secretcf-cluster-id
CircleCI
CircleCI provides cloud-native CI/CD with Docker support.
Configuration
Create .circleci/config.yml:
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: mainConfigure environment variables
In CircleCI project settings:
- Go to Project Settings > Environment Variables
- Add:
CF_ORG_IDCFCR_REGIONCF_TOKEN_IDCF_TOKEN_SECRETCF_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/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 CFKEConfigure service connection
- Go to Project Settings > Service connections
- Create a new Docker Registry service connection:
- Registry type: Others
- Docker Registry:
https://YOUR_ORG_ID.REGION.registry.cloudfleet.dev(replace REGION witheurope,northamerica, orapac) - Docker ID: Your API token ID
- Docker Password: Your API token secret
- Service connection name:
cloudfleet-registry
- Add pipeline variables for
CF_ORG_IDandCFCR_REGION
Best practices
Tagging strategy
Use consistent image tags 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-123Caching
Enable Docker layer caching for faster 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 into your pipeline. This step scans the image that the GitHub Actions workflow pushed for the current commit:
# 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:
- Create a new token with the same role
- Update CI/CD secrets with the new credentials
- Verify pipelines work with the new token
- Delete the old token
Consider automating this process with infrastructure as code.
Troubleshooting
Authentication failures
Error: unauthorized: authentication requiredVerify:
- 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 deniedThe 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
- Configure access control for your CI/CD tokens
- Learn about managing artifacts for advanced operations
- Learn about Helm chart storage for GitOps workflows
← Access control