Skip to main content

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.managedFields
  • metadata.resourceVersion
  • metadata.uid
  • metadata.creationTimestamp
  • metadata.generation
  • status (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

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:

SecretHow to get it
KUBECONFIGBase64-encode the kubeconfig you downloaded: base64 -w 0 < my-cluster-kubeconfig.yml
SECRET_KEY_BASECopy from Canine's environment variables
DATABASE_PASSWORDCopy 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.

Step 4: Disable Canine and Verify

  1. Disable autodeploy in Canine for the project so both systems aren't deploying
  2. Push a commit to main and confirm the pipeline deploys successfully
  3. 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:

    - 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

Reference: Canine to Kubernetes Mapping

CanineWhat'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)
DomainHost rules inside ingress.yaml
Health Check URLlivenessProbe / readinessProbe in deployment
Resource Constraintsresources.requests / resources.limits in deployment
Replicasspec.replicas in deployment
Pre-deploy CommandKubernetes Job in CI (see above)
Autodeploy on pushon: push / only: main trigger in CI
Using Portainer?

If your cluster is managed through Portainer, see Ejecting with Portainer for how to use a Portainer API token instead of a raw kubeconfig.