Skaffold’s custom deploy plugins let you escape the confines of kubectl and Helm, giving you granular control over your application deployments.

Let’s see this in action. Imagine you have a simple Go web server and you want to deploy it to Kubernetes using a custom manifest generation tool.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from my custom deployed app!")
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

We’ll also create a simple Go program that acts as our custom deployer. This program will take a Go struct representing our deployment configuration and generate a Kubernetes Deployment and Service YAML.

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"text/template"
)

type DeployConfig struct {
	ImageTag string `json:"imageTag"`
	Replicas int32  `json:"replicas"`
	Port     int32  `json:"port"`
}

const deploymentTemplate = `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-custom-app
spec:

  replicas: {{.Replicas}}

  selector:
    matchLabels:
      app: my-custom-app
  template:
    metadata:
      labels:
        app: my-custom-app
    spec:
      containers:
      - name: my-custom-app

        image: my-docker-registry/my-custom-app:{{.ImageTag}}

        ports:

        - containerPort: {{.Port}}

`

const serviceTemplate = `
apiVersion: v1
kind: Service
metadata:
  name: my-custom-app-service
spec:
  selector:
    app: my-custom-app
  ports:
  - protocol: TCP
    port: 80

    targetPort: {{.Port}}

  type: LoadBalancer
`

func main() {
	if len(os.Args) < 2 {
		log.Fatalf("Usage: %s <deploy-config.json>", os.Args[0])
	}

	configFilePath := os.Args[1]
	configData, err := ioutil.ReadFile(configFilePath)
	if err != nil {
		log.Fatalf("Failed to read config file %s: %v", configFilePath, err)
	}

	var config DeployConfig
	err = json.Unmarshal(configData, &config)
	if err != nil {
		log.Fatalf("Failed to unmarshal deploy config: %v", err)
	}

	// Generate Deployment YAML
	deployTmpl, err := template.New("deployment").Parse(deploymentTemplate)
	if err != nil {
		log.Fatalf("Failed to parse deployment template: %v", err)
	}
	err = deployTmpl.Execute(os.Stdout, config)
	if err != nil {
		log.Fatalf("Failed to execute deployment template: %v", err)
	}

	// Generate Service YAML
	serviceTmpl, err := template.New("service").Parse(serviceTemplate)
	if err != nil {
		log.Fatalf("Failed to parse service template: %v", err)
	}
	err = serviceTmpl.Execute(os.Stdout, config)
	if err != nil {
		log.Fatalf("Failed to execute service template: %v", err)
	}
}

First, you’ll need to build your application and push it to a container registry. For this example, let’s assume you’ve built an image named my-docker-registry/my-custom-app and tagged it latest.

Next, you’ll create a skaffold.yaml file to orchestrate this. The key here is the deploy section, specifically using custom and pointing to your deployer executable.

apiVersion: skaffold/v2beta28
kind: Config
metadata:
  name: skaffold-custom-deploy-example

build:
  artifacts:
    my-app:
      docker:
        dockerfile: Dockerfile
  local: {}

deploy:
  custom:
    - deploy:
        command: "go run ./deployer/main.go" # Path to your deployer executable
        flags:
          - "--config=deployer/config.json" # Path to your deploy configuration file

And here’s our deployer/config.json:

{
  "imageTag": "latest",
  "replicas": 2,
  "port": 8080
}

Now, when you run skaffold dev, Skaffold will:

  1. Build your application image.
  2. Pass the image tag (obtained from the build step) to your custom deployer.
  3. Execute your deployer command, which in turn reads the config.json, generates the Kubernetes manifests, and prints them to standard output.
  4. Skaffold then pipes these generated manifests to kubectl apply.

The problem this solves is that kubectl and Helm, while powerful, can be rigid. kubectl requires you to manage raw YAML, and Helm has its own templating language and structure. Custom deploy plugins allow you to use any templating engine or logic you prefer – be it Go’s text/template, Jinja2 in Python, or even a custom DSL – and integrate it seamlessly into Skaffold’s build-deploy loop. This gives you maximum flexibility for complex deployment strategies, dynamic configuration based on build artifacts, or integration with proprietary deployment systems.

The mental model to build is that Skaffold’s deploy.custom section essentially acts as a black box. You provide an executable and arguments, and Skaffold expects that executable to print valid Kubernetes manifests to stdout. Skaffold doesn’t care how those manifests are generated, only that they are valid and arrive on time. This decoupling is the core of its power. You can swap out your entire deployment logic without changing Skaffold’s core configuration, as long as your new logic adheres to the "print manifests to stdout" contract.

When Skaffold executes a custom deploy, it captures the standard output of your deploy command. This output is then treated as a set of Kubernetes manifests that Skaffold will apply to your cluster using kubectl apply. If your deploy command outputs multiple YAML documents (separated by ---), Skaffold will apply them all as distinct resources. This is how our simple Go deployer can generate both a Deployment and a Service in a single execution.

The next concept you’ll run into is managing the output of your custom deployer. While Skaffold applies everything to stdout, you might want to save these generated manifests for auditing or debugging. You could redirect the output of skaffold dev to a file, or, more elegantly, modify your custom deployer to write to a specific file alongside printing to stdout, which Skaffold would then pick up.

Want structured learning?

Take the full Skaffold course →