Back to Distributed Systems & Kubernetes Series

Skaffold Track Part 2: Pipelines & Profiles

June 6, 2026 Wasil Zafar 32 min read

Skaffold profiles let you use the same skaffold.yaml for local development (Kind/Minikube), staging (Helm deploy), and CI pipelines (kaniko build + registry push). One tool, consistent workflow across all environments.

Table of Contents

  1. Profiles
  2. CI Pipeline Mode
  3. Helm Deployer
  4. Kustomize Deployer
  5. Debug Mode
  6. Exercises
  7. Key Takeaways

Profiles

Profiles let you have one skaffold.yaml that works across multiple environments by overriding specific sections. A profile can replace the build strategy, deployment method, manifests path, or any combination of these — while inheriting everything else from the default config.

Profile Anatomy

# skaffold.yaml with three profiles: dev, staging, ci
apiVersion: skaffold/v4beta11
kind: Config
metadata:
  name: grade-api

# ── DEFAULT CONFIG (used when no profile is activated) ──────
build:
  artifacts:
    - image: myapp
      docker:
        dockerfile: Dockerfile
  local:
    push: false

deploy:
  kubectl: {}

manifests:
  rawYaml:
    - k8s/base/*.yaml

# ── PROFILES ─────────────────────────────────────────────────
profiles:
  # Profile: dev — local Kind/Minikube, no push needed
  - name: dev
    build:
      local:
        push: false
        useBuildkit: true
    # Overrides are merged with default; other sections remain unchanged

  # Profile: staging — build with Kaniko, push to registry, use Helm
  - name: staging
    build:
      artifacts:
        - image: gcr.io/myproject/myapp
          kaniko:
            dockerfile: Dockerfile
      cluster:
        pullSecretName: kaniko-gcr-secret
        namespace: kaniko
    deploy:
      helm:
        releases:
          - name: grade-api
            chartPath: charts/grade-api
            valuesFiles:
              - charts/grade-api/values-staging.yaml
            setValues:
              image.tag: "{{.IMAGE_TAG}}"

  # Profile: ci — Kaniko build, push to registry, raw kubectl, no watch
  - name: ci
    build:
      artifacts:
        - image: gcr.io/myproject/myapp
          kaniko:
            dockerfile: Dockerfile
      cluster:
        pullSecretName: kaniko-gcr-secret
        namespace: kaniko
    manifests:
      rawYaml:
        - k8s/ci/*.yaml

Activation

# Activate with -p flag
skaffold dev -p dev
skaffold run -p staging
skaffold run -p ci

# Activate via environment variable
SKAFFOLD_PROFILE=staging skaffold run

# Auto-activate based on kubeconfig context
# (Add to profile in skaffold.yaml)
profiles:
  - name: staging
    activation:
      - kubeContext: gke_myproject_us-east1_staging
    # ...

# Auto-activate based on environment variable presence
profiles:
  - name: ci
    activation:
      - env: CI=true
    # ...

# Activate multiple profiles simultaneously
skaffold run -p staging -p monitoring

Overriding Build & Deploy

# A profile can override any top-level section using 'patches':
# (Alternative to full section replacement)
profiles:
  - name: dev-with-debug
    patches:
      # Add an environment variable to one container
      - op: add
        path: /build/artifacts/0/docker/buildArgs/DEBUG
        value: "true"
      # Change the replica count in a specific manifest
      # (for raw YAML files, patches affect skaffold.yaml fields, not k8s manifests)
Profile vs environment-specific manifests: For Kubernetes manifest differences (replicas, resource limits, environment variables), prefer Kustomize overlays (separate YAML patch files) rather than putting everything in Skaffold profiles. Profiles are best for changing the build strategy or deploy mechanism — not for tweaking every Kubernetes field per environment.

CI Pipeline Mode

skaffold run

# One-shot build + deploy (no watching, exits when complete)
skaffold run -p ci

# Build only (for parallel CI stages)
skaffold build -p ci --file-output=build.json

# Deploy using build output from previous stage
skaffold deploy -p ci --build-artifacts=build.json

# Complete GitHub Actions example:
# - uses: actions/checkout@v4
# - name: Build
#   run: skaffold build -p ci --file-output=.skaffold/build.json
# - name: Deploy to staging
#   run: skaffold deploy -p staging --build-artifacts=.skaffold/build.json

--default-repo

# Prefix all image names with a registry path
# 'myapp' becomes 'gcr.io/myproject/myapp'
skaffold run --default-repo=gcr.io/myproject

# Or set in skaffold.yaml
build:
  tagPolicy:
    gitCommit: {}
  # All artifacts get prefixed with this registry
  # (Can also use SKAFFOLD_DEFAULT_REPO env var)

# In CI (GitHub Actions):
skaffold run \
  --default-repo=ghcr.io/${{ github.repository_owner }} \
  --tag=${{ github.sha }} \
  -p ci

# Skaffold automatically authenticates using your Docker credential store
# or the GCR/ECR/ACR credential helpers

Exit Codes

# skaffold run exits with:
# 0 — success (build + deploy completed, all health checks passed)
# 1 — build failure
# 2 — deploy failure
# 3 — health check failure (deployment rolled out but pods not healthy)

# In CI, check exit code explicitly
skaffold run -p ci || {
  echo "Skaffold failed with exit code $?"
  kubectl describe pods -l app=myapp
  exit 1
}

# Or use GitHub Actions job status
- name: Deploy
  run: skaffold run -p ci
  # Workflow automatically fails on non-zero exit

Helm Deployer

# deploy.helm in skaffold.yaml
deploy:
  helm:
    releases:
      - name: grade-api           # Helm release name
        chartPath: charts/grade-api  # relative path to chart (or use 'remoteChart')

        # Pull a published chart instead of a local one
        # remoteChart: oci://ghcr.io/your-org/charts/grade-api
        # version: 1.2.3

        namespace: grade-api-dev
        createNamespace: true

        # Values files (applied left to right, later files override earlier)
        valuesFiles:
          - charts/grade-api/values.yaml
          - charts/grade-api/values-dev.yaml

        # Override individual values (highest priority)
        setValues:
          image.repository: myapp
          image.tag: "{{.IMAGE_TAG}}"     # Skaffold replaces with actual tag
          replicaCount: "1"
          ingress.enabled: "true"
          ingress.hostname: "grade-api.local"

        # Pass as JSON/string
        setValueTemplates:
          image.tag: "{{.IMAGE_TAG}}"     # Identical to setValues for simple cases

        # Wait for Helm release to be ready
        wait: true
        timeout: 3m0s

        # Upgrade flags
        upgradeOnChange: true
        cleanupOnFail: true
        skipBuildDependencies: false
# Run with Helm deploy
skaffold dev -p staging

# Verify Helm release
helm status grade-api -n grade-api-dev
helm get values grade-api -n grade-api-dev

Kustomize Deployer

# Use Kustomize overlays instead of raw kubectl
manifests:
  kustomize:
    paths:
      - k8s/overlays/dev      # default

# In a profile, override the kustomize path
profiles:
  - name: staging
    manifests:
      kustomize:
        paths:
          - k8s/overlays/staging
    deploy:
      kubectl:
        flags:
          apply:
            - "--server-side"

  - name: production
    manifests:
      kustomize:
        paths:
          - k8s/overlays/prod
    build:
      artifacts:
        - image: gcr.io/myproject/myapp
          docker:
            dockerfile: Dockerfile
      local:
        push: true    # push to real registry for prod
# Run with Kustomize dev overlay
skaffold dev -p dev
# Skaffold runs: kustomize build k8s/overlays/dev | kubectl apply -f -

# Run with production overlay
skaffold run -p production \
  --default-repo=gcr.io/myproject

# Preview the rendered Kustomize output without deploying
skaffold render -p staging
# Outputs the final manifests that would be applied

Debug Mode

# skaffold debug modifies container images to enable remote debugging
# Language auto-detection: Go, Java (JVM), Node.js, Python
skaffold debug

# Output shows the debug port per container:
# Port forwarding pod/grade-api-xxxxx in namespace default,
#   remote port 56268 -> http://127.0.0.1:56268 [dlv]

# Go debugging via Delve
# Connect with: dlv connect localhost:56268

# Python debugging (debugpy)
# Connect with VS Code Python debugger on port 5678

# JVM debugging (JDWP)
# Connect with VS Code Java debugger on port 5005
# Configure debug port in skaffold.yaml (optional — auto-assigned if omitted)
build:
  artifacts:
    - image: myapp
      docker:
        dockerfile: Dockerfile

# Add IDE launch config for Delve (Go)
# .vscode/launch.json
# {
#   "version": "0.2.0",
#   "configurations": [
#     {
#       "name": "Skaffold Debug (Go)",
#       "type": "go",
#       "request": "attach",
#       "mode": "remote",
#       "host": "localhost",
#       "port": 56268,
#       "substitutePath": [
#         {"from": "${workspaceFolder}", "to": "/app"}
#       ]
#     }
#   ]
# }
Debug vs dev: skaffold debug rebuilds your image with debug agents injected (Delve for Go, debugpy for Python, etc.) and disables file sync (so every code change triggers a full rebuild). Use it when you need breakpoints; use skaffold dev for regular iteration without breakpoints.

Exercises

Exercise 1 — Profiles: Add dev and ci profiles to your skaffold.yaml. The dev profile should use local.push: false and raw kubectl. The ci profile should use local.push: true and a --default-repo. Add auto-activation for ci based on the CI=true env var. Test that CI=true skaffold run uses the CI profile automatically.
Exercise 2 — Helm Deploy: Create a minimal Helm chart for your application. Add a staging profile to your skaffold.yaml that uses deploy.helm. Include setValues for image.tag using the {{.IMAGE_TAG}} template. Run skaffold dev -p staging and verify Helm manages the release with helm ls.
Exercise 3 — Debug Mode: Run skaffold debug on your service. Connect your IDE debugger to the automatically forwarded debug port. Set a breakpoint in a request handler. Trigger the endpoint and verify execution stops at the breakpoint with full variable inspection.

Key Takeaways

Key Takeaways:
  • Profiles let one skaffold.yaml serve local dev (local push, kubectl), staging (Kaniko + Helm), and CI (one-shot run + registry push)
  • Auto-activate profiles by kubeContext or environment variable — no need to remember -p flag
  • skaffold run is the CI command; use --build-artifacts to share build results between pipeline stages
  • --default-repo prefixes all image names with your registry — one flag to make builds push-ready for any registry
  • The Helm deployer integrates setValues with Skaffold's image tagging — image.tag: "{{.IMAGE_TAG}}" automatically uses the built tag
  • skaffold debug injects language-appropriate debuggers and forwards the debug port — connect your IDE's remote debugger directly to the running pod