Back to Distributed Systems & Kubernetes Series

Skaffold Track Part 1: Workflow & Hot Reload

June 6, 2026 Wasil Zafar 35 min read

Skaffold is the inner-loop tool for Kubernetes development. It watches your source files, rebuilds your container image when code changes, and syncs or redeploys to the cluster automatically. The result: a change-save-verify cycle measured in seconds rather than minutes.

Table of Contents

  1. What Skaffold Does
  2. Installation & Init
  3. skaffold.yaml Deep Dive
  4. skaffold dev
  5. Build Strategies
  6. Exercises
  7. Key Takeaways

What Skaffold Does

The Dev Loop

Traditional Kubernetes development has a painful inner loop: change code → run docker build → docker tag → docker push → kubectl apply → wait for rollout → check logs. That's 5–8 manual steps and 1–3 minutes per iteration.

Skaffold collapses this into a single background process. When you save a file, Skaffold detects the change and executes the full pipeline automatically. For static assets (HTML, config files), it can even sync files directly into running containers without rebuilding — sub-second feedback.

Skaffold Inner Loop
flowchart LR
    A[Save File] --> B{File sync\neligible?}
    B -->|Yes| C[Sync file\ninto container]
    B -->|No| D[Rebuild image]
    D --> E[Tag with sha256]
    E --> F[Push to registry\nor load into Kind]
    F --> G[kubectl apply /\nHelm upgrade]
    G --> H[Wait for rollout]
    H --> I[Stream logs]
    C --> I
            

skaffold dev vs skaffold run

  • skaffold dev — Continuous watch mode. Builds, deploys, streams logs, port-forwards, and auto-cleans up on Ctrl+C. Designed for daily development.
  • skaffold run — One-shot build + deploy. No watching, no cleanup on exit. Used in CI pipelines.
  • skaffold build — Build only (no deploy). Useful to pre-populate a registry.
  • skaffold deploy — Deploy only using already-built images. Pair with skaffold build in parallel CI stages.

Installation & Init

Install

# macOS
brew install skaffold

# Linux: download binary
curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64
chmod +x skaffold
sudo mv skaffold /usr/local/bin/

# Windows (Chocolatey)
choco install skaffold

# Verify
skaffold version
# v2.13.0

skaffold init

# In a project with a Dockerfile and k8s/ manifests
ls
# Dockerfile  k8s/  main.go  go.mod

# Auto-generate skaffold.yaml from existing project structure
skaffold init

# Interactive prompts:
# ? Which builders would you like to create Kubernetes resources from?
#   [x] Docker (Dockerfile)
# ? Choose the registry to push your image
#   Enter image name: myapp

# Creates skaffold.yaml and optionally modifies k8s/deployment.yaml to
# use the image name Skaffold will manage

# Override image if needed
skaffold init --generate-manifests

# For a monorepo with multiple services
skaffold init --compose-file docker-compose.yml

skaffold.yaml Deep Dive

# skaffold.yaml — complete example for a Go microservice
apiVersion: skaffold/v4beta11
kind: Config
metadata:
  name: grade-api

# ── BUILD SECTION ─────────────────────────────────────────────
build:
  # Default registry for all artifacts
  # Override with --default-repo flag or SKAFFOLD_DEFAULT_REPO env var
  # tagPolicy defines how images are tagged
  tagPolicy:
    sha256: {}            # tag with content hash (deterministic)
    # Alternatives:
    # gitCommit: {}       # git SHA
    # envTemplate:
    #   template: "{{.IMAGE_TAG}}"

  artifacts:
    - image: myapp                    # image name (without registry for local)
      context: .                      # build context directory
      docker:
        dockerfile: Dockerfile        # relative to context
        buildArgs:                    # optional build args
          APP_VERSION: "{{.IMAGE_TAG}}"
      sync:
        # File sync rules — synced directly without rebuild
        infer:
          - "**/*.html"
          - "**/*.css"
          - "static/**"

    # Second service in a multi-service project
    - image: worker
      context: ./worker
      docker:
        dockerfile: Dockerfile.worker

  # Local build: push=false means images stay in local daemon / kind cache
  local:
    push: false
    useBuildkit: true     # faster builds with BuildKit
    concurrency: 0        # 0 = use all CPU cores

# ── DEPLOY SECTION ────────────────────────────────────────────
deploy:
  kubectl:
    defaultNamespace: default
    flags:
      apply:
        - "--server-side"
        - "--field-manager=skaffold"

# ── MANIFESTS ─────────────────────────────────────────────────
manifests:
  rawYaml:
    - k8s/deployment.yaml
    - k8s/service.yaml
    - k8s/ingress.yaml
  # Or use a glob
  # rawYaml:
  #   - k8s/*.yaml

build Section in Detail

# Multi-artifact build with per-artifact settings
build:
  artifacts:
    - image: frontend
      context: ./frontend
      docker:
        dockerfile: Dockerfile
        cacheFrom:
          - frontend:latest   # seed layer cache from previous build
      sync:
        manual:
          - src: "src/static/**"
            dest: /app/static   # path inside the container

    - image: backend
      context: ./backend
      docker:
        dockerfile: Dockerfile
      # No sync — full rebuild needed for Go binary changes

  tagPolicy:
    inputDigest: {}   # hash of all build inputs (content-addressed)


                        

deploy Section in Detail

# Deploy options: kubectl (raw YAML) or helm (Helm chart) or kustomize
deploy:
  # Option 1: Raw kubectl apply
  kubectl: {}   # uses default settings (namespace from kubeconfig)

  # Option 2: Kustomize overlays
  # kustomize:
  #   paths:
  #     - k8s/overlays/dev

  # Option 3: Helm (covered in Part 2)
  # helm:
  #   releases:
  #     - name: grade-api
  #       chartPath: charts/grade-api
  #       valuesFiles:
  #         - values-dev.yaml

  # Lifecycle hooks — run scripts before/after deploy
  lifecycleHooks:
    before:
      - command: ["./scripts/pre-deploy.sh"]
        os: [linux, darwin]
    after:
      - command: ["./scripts/post-deploy.sh"]
        os: [linux, darwin]

manifests Section

# manifests can reference rawYaml, kustomize, or helm
manifests:
  rawYaml:
    - k8s/base/deployment.yaml
    - k8s/base/service.yaml
  # kustomize:
  #   paths:
  #     - k8s/overlays/dev
  # hooks:
  #   before:
  #     - host:
  #         command: ["./gen-manifests.sh"]

skaffold dev

# Start the dev loop (blocks, Ctrl+C to stop and clean up)
skaffold dev

# Output (truncated):
# Generating tags...
#  - myapp -> myapp:sha256-abc123
# Checking cache...
#  - myapp: Not found. Building
# Building [myapp]...
# [myapp]  Sending build context to Docker daemon  15.3MB
# ...
# Tags used in deployment:
#  - myapp -> myapp:sha256-abc123
# Starting deploy...
# Waiting for deployments to stabilize...
#  - deployment/myapp is ready.
# Press Ctrl+C to exit

# Now edit main.go → Skaffold detects change, rebuilds, redeploys automatically
# Edit static/index.html → Skaffold syncs the file into the container directly (no rebuild)

Automatic Port Forwarding

# Add portForward to skaffold.yaml for automatic port exposure during dev
portForward:
  - resourceType: deployment
    resourceName: grade-api
    port: 8080           # container port
    localPort: 4503      # host port (optional — defaults to container port)
    namespace: default

  - resourceType: service
    resourceName: grade-api-svc
    port: 80
    localPort: 8080

  - resourceType: pod
    resourceName: postgres
    port: 5432
    localPort: 5432
# Or use the flag directly
skaffold dev --port-forward

# Port forwards are shown in the output:
# Port forwarding deployment/grade-api in namespace default,
#   remote port 8080 -> http://127.0.0.1:4503

# Access your app on localhost
curl http://localhost:4503/api/health

File Sync Configuration

# Three types of sync in skaffold.yaml artifacts section:

# 1. infer: Skaffold infers destinations from Dockerfile COPY instructions
sync:
  infer:
    - "**/*.html"
    - "**/*.css"
    - "**/*.js"

# 2. manual: Explicit src → dest mapping
sync:
  manual:
    - src: "config/*.yaml"
      dest: /app/config    # inside the running container

    - src: "templates/**"
      dest: /app/templates
      strip: "templates/"  # strip prefix from src when building dest path

# 3. auto: For JVM / Python / Node hot-reloading (requires compatible runner image)
sync:
  auto: true   # language-specific: jib images, ko images, etc.
File sync vs rebuild: File sync copies files into a running container using kubectl cp. It only works for changes that don't require a binary recompilation — static files, templates, config files. For Go/Java/Rust code changes, a full rebuild is always needed. Sync is most powerful for Node.js and Python services where the interpreter picks up file changes at runtime.

Build Strategies

# Strategy 1: Docker (default) — local or remote daemon
build:
  artifacts:
    - image: myapp
      docker:
        dockerfile: Dockerfile
        buildArgs:
          VERSION: "{{.IMAGE_TAG}}"
  local:
    push: false
    useBuildkit: true

# ---

# Strategy 2: Kaniko — builds inside Kubernetes (no Docker daemon on CI)
build:
  artifacts:
    - image: gcr.io/myproject/myapp
      kaniko:
        dockerfile: Dockerfile
        buildArgs:
          VERSION: "{{.IMAGE_TAG}}"
        cache:
          repo: gcr.io/myproject/myapp/cache
  cluster:
    pullSecretName: kaniko-secret     # GCP/AWS/Azure service account
    namespace: kaniko

# ---

# Strategy 3: Buildpacks — no Dockerfile needed (auto-detects language)
build:
  artifacts:
    - image: myapp
      buildpacks:
        builder: paketobuildpacks/builder:base
        env:
          - BP_GO_VERSION=1.22

# ---

# Strategy 4: ko — builds Go binaries into minimal images (no Dockerfile)
build:
  artifacts:
    - image: myapp
      ko:
        main: .          # main package path
        flags:
          - -ldflags=-s -w    # strip debug info
# Run with Kaniko (cluster build)
skaffold dev --profile=ci-kaniko

# Run with Buildpacks
skaffold dev --profile=buildpacks

# Check build strategy is working
skaffold build -v info 2>&1 | head -30

Exercises

Exercise 1 — First skaffold dev: Take any service with a Dockerfile and Kubernetes manifests. Run skaffold init to generate skaffold.yaml. Start skaffold dev --port-forward. Make a code change, save, and observe Skaffold rebuild and redeploy automatically. Measure the time from save to deployment completion.
Exercise 2 — File Sync: Add a sync.infer rule to your skaffold.yaml for HTML templates (templates/**/*.html). In skaffold dev, edit an HTML file and observe that Skaffold syncs the file instead of rebuilding. Compare timing: sync should be near-instant; a rebuild takes 10–30+ seconds.
Exercise 3 — Minikube Integration: Point kubectl at a Minikube cluster. In your skaffold.yaml, set build.local.push: false. Run skaffold dev. Verify that Skaffold detects Minikube and builds images directly into Minikube's Docker daemon (check with eval $(minikube docker-env) && docker images | grep myapp).

Key Takeaways

Key Takeaways:
  • Skaffold eliminates manual build-tag-push-apply steps by watching source files and running the full pipeline automatically
  • skaffold dev is for development (watch + redeploy + logs + cleanup); skaffold run is for CI (one-shot)
  • The skaffold.yaml has three key sections: build (how to build images), deploy (kubectl/helm/kustomize), and manifests (which files to apply)
  • File sync (sync.infer or sync.manual) bypasses rebuilds for static files — essential for fast frontend feedback
  • Build strategies: Docker (default), Kaniko (cluster builds for CI), Buildpacks (no Dockerfile), ko (Go-optimized)
  • Skaffold auto-detects Minikube and Kind clusters and adjusts image loading accordingly — no extra configuration needed

Next in This Track

In Part 2: Pipelines & Profiles, we configure Skaffold profiles to support local development, staging, and CI pipelines from the same skaffold.yaml, and integrate Helm and Kustomize deployers.