Ejecting from Canine
Move your Canine-managed project to CI/CD in three steps: download your manifests, add them to your repo, and drop in a pipeline file.
Step 1: Download Your Manifests
Go to your cluster page in Canine and click Download All YAML Files. This gives you a zip organized by namespace:
my-cluster/
my-project-namespace/
web.yaml # Deployment
web-service.yaml # Service
web-ingress.yaml # Ingress
my-project.yaml # ConfigMap (env vars)
cleanup.yaml # CronJob (if any)
worker.yaml # Background worker Deployment (if any)
...
You can also download your kubeconfig from the same page via Download Kubeconfig File — you'll need it for CI secrets in Step 3.
Step 2: Add Manifests to Your Repo
Drop the exported YAML files into a k8s/ directory in your application's repository:
my-repo/
k8s/
namespace.yaml
configmap.yaml
deployment.yaml
service.yaml
ingress.yaml
cronjob.yaml # if applicable
worker.yaml # if applicable
Dockerfile
...
Clean up the exported files
Remove auto-generated Kubernetes metadata from each file. Delete these fields:
metadata.managedFieldsmetadata.resourceVersionmetadata.uidmetadata.creationTimestampmetadata.generationstatus(entire section)metadata.annotations.rolloutTimestamp
The remaining YAML is your source of truth going forward.
Secrets
Don't commit your Secret YAML to git. Instead, store each secret value in your CI provider's secret store and create the Kubernetes Secret in the pipeline. This is handled in the workflows below.
Step 3: Add the CI/CD Pipeline
- GitHub Actions
- GitLab CI/CD
Create .github/workflows/deploy.yml:
name: Build and Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
NAMESPACE: my-namespace # your project's namespace
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=raw,value=latest
- id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- name: Configure kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
- name: Create secrets
run: |
kubectl create secret generic ${{ env.NAMESPACE }} \
--namespace=${{ env.NAMESPACE }} \
--from-literal=SECRET_KEY_BASE=${{ secrets.SECRET_KEY_BASE }} \
--from-literal=DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }} \
--dry-run=client -o yaml | kubectl apply -f -
- name: Apply manifests
run: kubectl apply -f k8s/
- name: Update image
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }}"
kubectl set image deployment --all *=$IMAGE --namespace=${{ env.NAMESPACE }}
kubectl set image cronjob --all *=$IMAGE --namespace=${{ env.NAMESPACE }} 2>/dev/null || true
- name: Wait for rollout
run: |
for deploy in $(kubectl get deploy -n ${{ env.NAMESPACE }} -o name); do
kubectl rollout status $deploy -n ${{ env.NAMESPACE }} --timeout=300s
done
Required GitHub Secrets
Add these under your repo's Settings > Secrets and variables > Actions:
| Secret | How to get it |
|---|---|
KUBECONFIG | Base64-encode the kubeconfig you downloaded: base64 -w 0 < my-cluster-kubeconfig.yml |
SECRET_KEY_BASE | Copy from Canine's environment variables |
DATABASE_PASSWORD | Copy from Canine's environment variables |
Add one --from-literal line per secret env var in the "Create secrets" step. GITHUB_TOKEN is provided automatically for ghcr.io.
Create .gitlab-ci.yml in the root of your repository:
stages:
- build
- deploy
variables:
REGISTRY: registry.gitlab.com
IMAGE_NAME: $CI_PROJECT_PATH
NAMESPACE: my-namespace # your project's namespace
build:
stage: build
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA -t $CI_REGISTRY/$IMAGE_NAME:latest .
- docker push $CI_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY/$IMAGE_NAME:latest
only:
- main
deploy:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- mkdir -p $HOME/.kube
- echo "$KUBECONFIG" | base64 -d > $HOME/.kube/config
script:
# Create secrets
- |
kubectl create secret generic $NAMESPACE \
--namespace=$NAMESPACE \
--from-literal=SECRET_KEY_BASE=$SECRET_KEY_BASE \
--from-literal=DATABASE_PASSWORD=$DATABASE_PASSWORD \
--dry-run=client -o yaml | kubectl apply -f -
# Apply manifests
- kubectl apply -f k8s/
# Update image on all deployments and cronjobs
- IMAGE="$CI_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA"
- kubectl set image deployment --all *=$IMAGE --namespace=$NAMESPACE
- kubectl set image cronjob --all *=$IMAGE --namespace=$NAMESPACE 2>/dev/null || true
# Wait for rollout
- |
for deploy in $(kubectl get deploy -n $NAMESPACE -o name); do
kubectl rollout status $deploy -n $NAMESPACE --timeout=300s
done
only:
- main
Required GitLab CI/CD Variables
Add these under your project's Settings > CI/CD > Variables:
| Variable | Type | How to get it |
|---|---|---|
KUBECONFIG | Variable (masked) | Base64-encode the kubeconfig you downloaded: base64 -w 0 < my-cluster-kubeconfig.yml |
SECRET_KEY_BASE | Variable (masked) | Copy from Canine's environment variables |
DATABASE_PASSWORD | Variable (masked) | Copy from Canine's environment variables |
Add one --from-literal line per secret env var in the script. CI_REGISTRY_USER and CI_REGISTRY_PASSWORD are provided automatically for the GitLab Container Registry.
Step 4: Disable Canine and Verify
- Disable autodeploy in Canine for the project so both systems aren't deploying
- Push a commit to
mainand confirm the pipeline deploys successfully - Once confident, remove the project from Canine
Removing a project from Canine does not delete your running workloads — your app stays up.
Pre-deploy Commands (Migrations)
If you had a predeploy_command in Canine (e.g., rake db:migrate), add a step before "Apply manifests" in your pipeline:
- GitHub Actions
- GitLab CI/CD
- name: Run migrations
run: |
kubectl run migrate-${{ github.sha }} \
--namespace=${{ env.NAMESPACE }} \
--image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }} \
--restart=Never --rm --wait \
--override-type=strategic \
--overrides='{"spec":{"containers":[{"name":"migrate-${{ github.sha }}","envFrom":[{"configMapRef":{"name":"${{ env.NAMESPACE }}"}},{"secretRef":{"name":"${{ env.NAMESPACE }}"}}]}]}}' \
--command -- bundle exec rake db:migrate
# Add before "Apply manifests" in the deploy script
- |
kubectl run migrate-$CI_COMMIT_SHORT_SHA \
--namespace=$NAMESPACE \
--image=$CI_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA \
--restart=Never --rm --wait \
--override-type=strategic \
--overrides='{"spec":{"containers":[{"name":"migrate-'"$CI_COMMIT_SHORT_SHA"'","envFrom":[{"configMapRef":{"name":"'"$NAMESPACE"'"}},{"secretRef":{"name":"'"$NAMESPACE"'"}}]}]}}' \
--command -- bundle exec rake db:migrate
Reference: Canine to Kubernetes Mapping
| Canine | What's in the zip |
|---|---|
| Service (web) | deployment.yaml + service.yaml + ingress.yaml |
| Service (background) | deployment.yaml with a command (no Service/Ingress) |
| Service (cron) | cronjob.yaml |
| Environment Variables (config) | configmap.yaml |
| Environment Variables (secret) | Create via CI secrets (not in zip) |
| Domain | Host rules inside ingress.yaml |
| Health Check URL | livenessProbe / readinessProbe in deployment |
| Resource Constraints | resources.requests / resources.limits in deployment |
| Replicas | spec.replicas in deployment |
| Pre-deploy Command | Kubernetes Job in CI (see above) |
| Autodeploy on push | on: push / only: main trigger in CI |
If your cluster is managed through Portainer, see Ejecting with Portainer for how to use a Portainer API token instead of a raw kubeconfig.