Skip to content
docktionary

Clusters & cloud

CI/CD for containers

The path from a merged commit to a running Pod has a consistent shape, whatever CI system runs it: build an image, scan it, push it, then tell the cluster about the new tag.

A GitHub Actions pipeline, end to end

.github/workflows/deploy.yml
name: build-and-deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Scan image for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: "1"

      - name: Deploy
        run: |
          kubectl set image deployment/docktionary-app \
            app=ghcr.io/${{ github.repository }}:${{ github.sha }}
          kubectl rollout status deployment/docktionary-app

Tag by commit SHA, not "latest"

Tagging every build with the git SHA (or a semantic version plus the SHA) gives every deployed image a unique, traceable tag, and makes kubectl rollout undo meaningful — you can see exactly which commit is running and roll back to the precise previous one, something :latest can't give you.

GitOps: let the cluster pull, don't push to it

Rather than a CI job running kubectl apply directly against production, a GitOps controller (Argo CD, Flux) running inside the cluster watches a git repository of manifests and applies changes itself. CI's job becomes: build the image, then update the image tag in a manifest repo via a pull request. This gives you a full audit trail in git, and a controller already in place to detect and correct drift if someone changes the cluster out-of-band.

terminal
# CI's last step becomes a commit, not a deploy
yq -i '.spec.template.spec.containers[0].image = "ghcr.io/org/app:'"$GITHUB_SHA"'"' \
  manifests/deployment.yaml
git commit -am "deploy app@$GITHUB_SHA"
git push

# Argo CD (already watching the repo) picks up the change and applies it

Scanning images before they ship

Scanning for known CVEs (Trivy, Grype) as a required CI step, with a severity threshold that fails the build, is the cheapest point in the pipeline to catch a vulnerable base image or dependency — far cheaper than catching it once it's already serving traffic.