Skaffold is a command-line tool from Google that automates the build, push and deploy cycle of Kubernetes applications. In development mode it watches your source code, rebuilds images or copies changed files straight into running containers, redeploys, streams logs and forwards ports, all from one command. In this tutorial you will install Skaffold on Ubuntu 24.04, write a skaffold.yaml for a small web application, iterate on it against a local kind cluster, and add a profile for building and pushing images from CI.

Prerequisites

To follow this guide you need:

  • An Ubuntu 24.04 machine with at least 2 GB of RAM, either your workstation or a CubePath VPS used as a development box.
  • A non-root user with sudo privileges.
  • Docker Engine installed and usable by your user without sudo.
  • kubectl and kind installed. This guide uses a kind cluster because Skaffold loads locally built images into it directly, with no registry.
  • Git, since Skaffold tags images with the current commit by default.

Create the cluster used in this tutorial and check that kubectl points to it:

kind create cluster --name dev
kubectl config current-context
kind-dev

Step 1 - Installing Skaffold

Skaffold is a single static binary. Download the latest Linux release from the official bucket and install it in your PATH:

curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64
sudo install -m 0755 skaffold /usr/local/bin/skaffold
rm skaffold

On an ARM64 machine, replace skaffold-linux-amd64 with skaffold-linux-arm64. Verify the installation:

skaffold version
v2.x.x

Step 2 - Creating a sample application

The application is a static site served by Nginx, which makes it easy to see file sync at work. Create the project and a Git repository for it:

mkdir -p ~/web-app/site ~/web-app/k8s
cd ~/web-app
git init

Create the page:

nano site/index.html
<!doctype html>
<html>
  <head><title>Skaffold demo</title></head>
  <body><h1>Version 1</h1></body>
</html>

Create the Dockerfile:

nano Dockerfile
FROM nginx:1.27-alpine
COPY site/ /usr/share/nginx/html/

Create the Deployment. The image name web-app has no tag: Skaffold replaces it with the exact image it built on every deploy:

nano k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: web-app
          ports:
            - containerPort: 80

Create the Service:

nano k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-app
spec:
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 80

Commit the files so the default Git-based tagger has a commit to work with:

git add .
git commit -m "Initial web app"

Step 3 - Writing skaffold.yaml

skaffold init can generate a starting configuration by detecting Dockerfiles and manifests, but writing it by hand shows what each section does:

nano skaffold.yaml
apiVersion: skaffold/v4beta11
kind: Config
metadata:
  name: web-app
build:
  artifacts:
    - image: web-app
      docker:
        dockerfile: Dockerfile
      sync:
        manual:
          - src: "site/**"
            dest: /usr/share/nginx/html
            strip: "site/"
  local:
    push: false
    useBuildkit: true
manifests:
  rawYaml:
    - k8s/deployment.yaml
    - k8s/service.yaml
deploy:
  kubectl: {}
portForward:
  - resourceType: service
    resourceName: web-app
    port: 80
    localPort: 8080

The sections map to the stages of the loop:

  • build.artifacts defines the image web-app built from the Dockerfile. The sync rule copies any changed file under site/ into /usr/share/nginx/html in the running container instead of rebuilding the image.
  • build.local.push: false keeps images local. With a kind context, Skaffold loads them into the cluster nodes itself.
  • manifests.rawYaml lists the Kubernetes files to render, and deploy.kubectl applies them.
  • portForward exposes the Service on localhost:8080 while Skaffold runs.

If skaffold init or a newer Skaffold release writes a higher apiVersion, keep it: Skaffold reads older schema versions and skaffold fix upgrades a file to the current one.

Step 4 - Running the development loop

Start the loop:

skaffold dev --port-forward
Generating tags...
 - web-app -> web-app:1a2b3c4
Checking cache...
 - web-app: Not found. Building
Building [web-app]...
...
Starting deploy...
 - deployment.apps/web-app created
 - service/web-app created
Waiting for deployments to stabilize...
 - deployment/web-app is ready.
Deployments stabilized in 3.2 seconds
Port forwarding service/web-app in namespace default, remote port 80 -> http://127.0.0.1:8080
Press Ctrl+C to exit
Watching for changes...

In a second terminal, request the page:

curl -s http://127.0.0.1:8080 | grep h1
  <body><h1>Version 1</h1></body>

Now edit site/index.html and change Version 1 to Version 2. In the Skaffold terminal you will see a sync instead of a build:

Syncing 1 files for web-app:1a2b3c4
Watching for changes...

Request the page again and the new text is there within a second or two:

curl -s http://127.0.0.1:8080 | grep h1
  <body><h1>Version 2</h1></body>

Changes to files that are not covered by a sync rule, such as the Dockerfile or the manifests, trigger a full rebuild and redeploy. Skaffold also streams the Nginx access logs from the Pod into this terminal.

Press Ctrl+C to stop. Skaffold deletes the Deployment and Service it created, leaving the cluster clean.

Step 5 - Deploying once with skaffold run

For a one-off deployment that stays running after the command exits, use skaffold run:

skaffold run
kubectl get pods -l app=web-app
NAME                       READY   STATUS    RESTARTS   AGE
web-app-7d9f8c6b5d-x2kqp   1/1     Running   0          12s

Remove it when you are done:

skaffold delete

Step 6 - Adding a profile for a registry

A local kind cluster needs no registry, but a shared or remote cluster does. Profiles adjust the configuration for a context without duplicating it. Add this block at the end of skaffold.yaml:

profiles:
  - name: registry
    patches:
      - op: replace
        path: /build/local/push
        value: true

The profile uses a JSON patch to turn on pushing and leaves everything else as it is. Check the effective configuration with the profile active:

skaffold diagnose --yaml-only -p registry | grep -A2 "local:"
  local:
    push: true
    useBuildkit: true

To push, log in to your registry with docker login registry.your_domain and pass the registry as the default repository. Skaffold prefixes every image name with it, so you do not have to hardcode the registry in skaffold.yaml:

skaffold build -p registry --default-repo=registry.your_domain/your_user

The command builds registry.your_domain/your_user/web-app:<commit> and pushes it.

Step 7 - Using Skaffold in CI

In a pipeline, split building from deploying so the exact images that were tested are the ones deployed. Build and push, writing the resulting image references to a file:

skaffold build -p registry --default-repo=registry.your_domain/your_user --file-output=build.json

Then deploy those artifacts to the cluster configured in the job's kubeconfig, without rebuilding:

skaffold deploy --build-artifacts=build.json

For GitOps workflows, render the final manifests with the built image references instead of applying them, and commit the result to the repository that Argo CD or Flux watches:

skaffold render --build-artifacts=build.json --output=rendered.yaml

Install Skaffold in the CI job with the same curl and install commands from Step 1.

Troubleshooting

Pods fail with ErrImagePull or ImagePullBackOff on kind. Skaffold did not load the image into the cluster. Make sure the kind binary is in your PATH and the context name starts with kind-. For other local clusters, mark the context as local:

skaffold config set --kube-context your_context local-cluster true

File changes trigger a rebuild instead of a sync. The changed file does not match the src pattern, or the pattern is relative to the wrong directory. Patterns are relative to the artifact's context, which is the project root here. Run skaffold dev -v debug to see how Skaffold classifies each change.

Port 8080 is already in use. Skaffold picks the next free port and prints it in the Port forwarding line. Change localPort in skaffold.yaml if you need a fixed one.

skaffold dev exits with a build error. Reproduce it outside Skaffold with docker build -t web-app:test . to separate Dockerfile problems from Skaffold configuration problems.

Conclusion

You installed Skaffold on Ubuntu 24.04 and set up a development loop that rebuilds, redeploys and syncs files into a kind cluster automatically, then added a profile and a build-then-deploy flow for CI. Next, switch manifests.rawYaml to manifests.kustomize or a Helm release to reuse your existing deployment configuration, add more artifacts for multi-service projects, or use skaffold debug to attach a debugger to containers running in the cluster.