Welcome to Chapter 2 of the Kubernetes Operators tutorial. Before you can
run operator-sdk init on the next article, the machine running your
editor needs six things: a recent Go toolchain, kubectl, a container
runtime, the Helm 4 CLI (required for Helm-based and hybrid operators,
useful for debugging Go-based ones), a local Kubernetes cluster, and
the operator-sdk binary itself. This guide walks every step on Linux —
Ubuntu / Debian / RHEL / Fedora / openSUSE — installs Helm 4, brings up
a plain kind cluster, covers the three install methods for
operator-sdk, and points you at ttl.sh as the
zero-setup image-distribution pattern the rest of the tutorial series
uses to get locally-built operator images into the cluster (no local
registry container, no containerdConfigPatches, no hosts.toml
plumbing). Common install errors and their fixes are catalogued at the
end.
The Operator-SDK install stack on Linux: prerequisites, the binary itself, a plain kind cluster, and
ttl.sh/<random>:24has the way to ship locally-built images into that cluster.
If the concepts here look unfamiliar, the Foundations chapter (what is an Operator, Operator vs Controller vs CRD, the reconcile loop, CRDs explained) is the right prerequisite read before installing the toolchain.
A quick mental model: think of it as a kitchen setup
Before a chef bakes their first loaf they need flour, yeast, an oven, a mixing bowl, and the recipe. Forget any one piece and the project stops.
Installing operator-sdk is exactly this — six ingredients laid out in a specific order, with a small footnote about how images travel from your laptop to the cluster:
| Ingredient | Why it's needed |
|---|---|
| Go 1.22+ | The compiler that builds every operator project. |
| kubectl | The kitchen tongs — how you talk to any Kubernetes cluster. |
| Docker or Podman | The oven — runs containers, both your operator and the local cluster. |
| Helm 4 (CLI) | Required for Helm-based / hybrid operator tutorials (chart smoke-test, helm get manifest debugging); harmless for pure-Go operators. |
| kind | The mini-oven that doubles as a Kubernetes cluster on your laptop. Plain kind create cluster is all you need - no special config. |
| operator-sdk | The recipe book — operator-sdk init writes the project skeleton for you. |
For pushing locally-built operator images into the cluster, the tutorial
series uses ttl.sh — a public, ephemeral, zero-setup
container registry. There's nothing to install for it; the Part 1 / Part
2 articles cover the docker push ttl.sh/<random>:24h pattern when you
get there. No local registry container, no kind cluster modifications,
no kind load docker-image (which is brittle on newer Docker).
The next sections install the six ingredients one by one. You only need to do this once per developer machine.
Step 1: Install the prerequisite toolchain
Go 1.22+
operator-sdk's installation guide lists the minimum Go version each release targets - currently Go 1.22+. The distribution-packaged Go is often too old; install the upstream tarball:
GO_VERSION="1.26.3"
curl -fsSL https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
echo 'export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH' >> ~/.profile
. ~/.profile
go versionConfirm the version is at least the one operator-sdk needs:
$ go version
go version go1.26.3 linux/amd64$HOME/go/bin on $PATH is important - it is where go install puts
binaries like controller-gen and kustomize if you ever install them
manually.
kubectl
The cluster the operator will eventually run against decides the kubectl version - try to stay within one minor of the API server:
KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" -o kubectl
chmod +x kubectl
sudo install kubectl /usr/local/bin/
kubectl version --clientSample output:
Client Version: v1.36.1
Kustomize Version: v5.8.1A container runtime
Either Docker or Podman works. The Makefile that operator-sdk init
scaffolds calls the container CLI via a CONTAINER_TOOL variable; set it to
podman if you prefer Podman.
For Docker on Ubuntu / Debian:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
newgrp docker
docker versionSample output:
Client: Docker Engine - Community
Version: 29.2.1
API version: 1.53
Go version: go1.25.6
Git commit: a5c7197
Built: Mon Feb 2 17:17:42 2026
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 29.2.1
API version: 1.53 (minimum version 1.44)
Go version: go1.25.6
Git commit: 6bc6209
Built: Mon Feb 2 17:17:42 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.2.1
GitCommit: dea7da592f5d1d2b7755e3a161be07f43fad8f75
runc:
Version: 1.3.4
GitCommit: v1.3.4-0-gd6d73eb8
docker-init:
Version: 0.19.0
GitCommit: de40ad0For Podman on Fedora / RHEL:
sudo dnf install -y podman
podman versionHelm 4 CLI
The Helm CLI is a hard prerequisite for the Helm-based and Helm-hybrid
operator tutorials (you smoke-test your chart standalone with helm install / helm lint before letting the operator drive it) and is also
useful for Go-based operators (helm get manifest, helm history, and
helm rollback work on any release the operator creates - the
release-Secret format sh.helm.release.v1.<name>.v<rev> is the same in
Helm 3 and Helm 4).
Install the current stable Helm 4 release from the official tarball:
HELM_VERSION="v4.2.0"
curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" -o helm.tar.gz
tar -xzf helm.tar.gz
sudo install linux-amd64/helm /usr/local/bin/
rm -rf linux-amd64 helm.tar.gz
helm versionSample output:
version.BuildInfo{Version:"v4.2.0", GitCommit:"06468084e85c244c712834933d25ea232a4c2093", GitTreeState:"clean", GoVersion:"go1.26.3", KubeClientVersion:"v1.36"}The
snap install helmpackage lags upstream by one or two minor versions, so the tarball flow above is the most portable choice across Linux distributions. On macOS usebrew install helm. The bundledKubeClientVersionis the Kubernetes API version the binary was built against; it does not need to match your cluster's version exactly (Helm follows the standard one-minor skew rule).
kind (Kubernetes in Docker)
kind brings up a real Kubernetes cluster inside a container - ideal for operator development because it boots in seconds and can be destroyed without consequence.
KIND_VERSION=$(curl -fsSL https://api.github.com/repos/kubernetes-sigs/kind/releases/latest | jq -r .tag_name)
curl -fsSL -o kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
chmod +x kind
sudo install kind /usr/local/bin/
kind versionSample output:
kind v0.31.0 go1.25.5 linux/amd64Bring up a single-node cluster you'll use throughout the rest of the
course - no special config, just plain kind create cluster:
kind create cluster --name demo --image kindest/node:v1.31.0
kubectl cluster-info --context kind-demo
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# demo-control-plane Ready control-plane 30s v1.31.0If kind fails to start, the diagnosis is usually the Docker daemon
(systemctl status docker) or memory pressure (kind needs at least 4
GiB free); see the troubleshooting section below.
Step 2: Image distribution with ttl.sh
When you reach Part 1 of the Helm operator tutorial you'll build a
local Docker image of your operator (make docker-build) and need to
get it onto the kind cluster's nodes so the kubelet can pull it.
There are three commonly-used patterns for this; the tutorial series uses the third because it's the only one with zero setup that always works:
| Pattern | Trade-off |
|---|---|
kind load docker-image <img> |
Brittle on newer Docker (24+) - the containerd snapshotter and kind load's ctr import flow disagree on which blobs exist locally, producing the well-known ctr: content digest <sha>: not found error. Often broken; not used in this series. |
Run a local registry:2 container on localhost:5001 and wire kind's containerd to use it via containerdConfigPatches + per-node hosts.toml |
Works well once set up, but has a ~30-line setup script, an extra container to manage, and a "cluster wasn't created with the registry config" footgun that's painful to debug. Documented in kind's local-registry guide if you want it. |
| Push to ttl.sh — a free, public, ephemeral container registry | Zero setup. No signup, no extra containers, no kind cluster modifications. Works with any cluster (kind, EKS, GKE, k3s) the same way. Images are public and auto-expire (1 hour to 30 days, your choice). The pattern used by Part 1 / Part 2 of this series. |
The ttl.sh pattern (what Part 1 / Part 2 use)
You don't need to do anything during install for ttl.sh — there's nothing to install. When the tutorial says "build and push your operator image", the flow is:
# Generate a unique image name (UUID avoids collisions with anyone else using ttl.sh)
export IMG=ttl.sh/demoapp-$(uuidgen):24h
# Build the operator image with that tag
make docker-build IMG="$IMG"
# Push to ttl.sh - any cluster, anywhere, can now pull from this URL
docker push "$IMG"
# Deploy the operator, kubelet pulls from ttl.sh
make deploy IMG="$IMG"24h is the TTL — after 24 hours the image vanishes from ttl.sh, which
is fine because you'll rebuild it before then. Allowed TTL values: 5m,
15m, 30m, 1h, 2h, 6h, 12h, 24h (default), up to 30d.
A smoke test you can run now to confirm ttl.sh works from your cluster
This is optional but proves the path end-to-end before you reach Part 1:
# Tag and push a public nginx image to a unique ttl.sh URL
export TEST_IMG=ttl.sh/nginx-test-$(uuidgen):1h
docker pull nginx:1.27-alpine
docker tag nginx:1.27-alpine "$TEST_IMG"
docker push "$TEST_IMG"
# Run it in the cluster from that URL
kubectl run smoke --image="$TEST_IMG" --restart=Never
kubectl wait --for=condition=Ready pod/smoke --timeout=60s
# pod/smoke condition met
kubectl delete pod smokeIf kubectl wait returns condition met, your kind cluster can reach
ttl.sh and your image-distribution pipeline is ready. The same pattern
works for the operator image you'll build in Part 1.
Privacy reminder. Anything you push to ttl.sh is public. The random UUID in the image name makes collisions and casual discovery very unlikely, but do not push proprietary code, baked-in credentials, or anything sensitive. For production work, swap ttl.sh for a real registry (GHCR, ECR, GAR, ACR, Harbor) with
imagePullSecrets. The Part 1 / Part 2 article flows only use ttl.sh for the tutorial's throwawaydemo-appoperator image.
Internet access required. Both
docker push(from your host) and the kubelet's pull (from inside the kind node) need outbound HTTPS tottl.sh. If your host is offline, the local registry pattern (path 2 above) is the fallback - see the kind upstream docs link.
Step 3: Install Operator-SDK itself
Three supported methods. The binary download is the upstream-recommended method.
Method A - download the official binary (recommended)
export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
export OS=$(uname | awk '{print tolower($0)}')
export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/latest/download
curl -fsSL "${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}" -o operator-sdk
chmod +x operator-sdk
sudo install operator-sdk /usr/local/bin/
operator-sdk versionExpected output:
operator-sdk version: "v1.42.2", commit: "6001c29067051e1a04e829ea033988b904d1845e", kubernetes version: "1.33.1", go version: "go1.25.7", GOOS: "linux", GOARCH: "amd64"If you want to pin a specific release - for instance to match the version
your team's CI uses - replace latest/download with download/v1.40.0 (or
whichever tag).
Method B - Homebrew on Linux
Homebrew on Linux ships
operator-sdk and keeps it reasonably up to date:
brew install operator-sdk
operator-sdk versionThis is the cleanest path if you already use Homebrew for other tools.
Method C - build from source
Useful on older distributions whose system GLIBC cannot run the official binary, or when you want to test a not-yet-released commit:
git clone https://github.com/operator-framework/operator-sdk.git
cd operator-sdk
git checkout v1.40.0
make installmake install places the binary in $GOPATH/bin (defaulting to
$HOME/go/bin), so make sure that directory is on $PATH.
Step 4: Verify the install
Run operator-sdk version and operator-sdk help - both should print
without error:
operator-sdk version
operator-sdk help | head -20Confirm the Helm plugin is available (needed if you plan to scaffold Helm-based or hybrid operators):
operator-sdk init --plugins helm --help 2>&1 | head -n 3
# Initialize a new Helm-based operator project.
# ...The fully-qualified plugin key is
helm.sdk.operatorframework.io/v1; the bare aliashelmresolves to the same plugin. Short forms likehelm/v1are not recognized by the resolver and will error withno plugin could be resolved with key "helm/v1".
A useful smoke-test scaffold (we will throw it away):
mkdir -p /tmp/operator-smoke
cd /tmp/operator-smoke
operator-sdk init --domain example.com --repo example.com/smoke
lsYou should see Dockerfile, Makefile, PROJECT, api/, cmd/,
config/, and internal/. If operator-sdk init finishes cleanly your
toolchain is ready. We will walk this scaffold in detail in
scaffold your first operator project -
delete the smoke directory for now.
rm -rf /tmp/operator-smokeStep 5: Wire your editor (optional but recommended)
Three editor-side bits make life dramatically easier on day one:
gopls- Go language server.go install golang.org/x/tools/gopls@latest.- The VS Code "Go" extension or your editor's Go integration.
kubectlshell completion -source <(kubectl completion bash)or the equivalent for zsh / fish.
For Kubebuilder-style markers (+kubebuilder:validation:Required,
+kubebuilder:default, etc.) to be recognised in editor IntelliSense, install
the Kubebuilder companion extension or rely on the gopls parser - both
work, the extension is friendlier for newcomers.
Common install errors and fixes
operator-sdk: command not found
The binary is not on $PATH. Check where it landed:
which operator-sdk
ls -l /usr/local/bin/operator-sdk
echo $PATH | tr ':' '\n'The fix is usually sudo install operator-sdk /usr/local/bin/ (Method A) or
adding $HOME/go/bin to $PATH (Method C).
operator-sdk: /lib64/libc.so.6: version GLIBC_2.34 not found
The official binary is built against a recent GLIBC; older distributions (CentOS 7, Debian 10, Ubuntu 18.04) ship a GLIBC too old to run it. Two fixes:
- Upgrade the distribution to a release with GLIBC 2.34+ (Ubuntu 22.04+, RHEL 9+, Debian 12+).
- Build operator-sdk from source against your system's GLIBC (Method C above).
exec format error running operator-sdk version
You downloaded the wrong-architecture binary - typically an amd64 binary
on an arm64 machine (M1/M2 Mac running Linux VM, Raspberry Pi, AWS
Graviton). Re-download with the correct ARCH env var.
go: cannot find module after operator-sdk init
You either have an older Go (< 1.22) or GO111MODULE is set to off.
Confirm:
go version
echo $GO111MODULEUpgrade Go or unset GO111MODULE (Go modules are the default since 1.16
and operator-sdk relies on them).
kind cluster fails to start
Three usual causes, in order:
- Docker daemon not running.
sudo systemctl start dockerorsudo systemctl status docker. - Memory pressure. kind needs ~4 GiB free; close other VMs.
vm.max_map_counttoo low. Setsudo sysctl -w vm.max_map_count=262144and persist it in/etc/sysctl.d/.
Diagnose with:
kind export logs /tmp/kind-logs
ls /tmp/kind-logsThe logs from each container in the cluster control plane are dropped there.
error: error loading config: stat /home/user/.kube/config: no such file
kubectl has no kubeconfig yet. Either the kind cluster did not start (see above), or kind's automatic kubeconfig export failed:
kind export kubeconfig --name demo
kubectl get nodescontroller-gen: command not found
controller-gen is pulled in by the Makefile that operator-sdk init
scaffolds, not by the operator-sdk binary itself. The first time you run
make manifests or make generate in a project, the Makefile installs
controller-gen into the project's bin/ directory. If that fails:
GOBIN=$(pwd)/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@latestERROR: failed to load image ... ctr: content digest sha256:...: not found
Symptom (verbatim from a real kind load run on Docker 24+):
$ kind load docker-image nginx:1.27-alpine --name demo
ERROR: failed to load image: command "docker exec --privileged -i demo-control-plane \
ctr --namespace=k8s.io images import --all-platforms --digests \
--snapshotter=overlayfs -" failed with error: exit status 1
Command Output: ctr: content digest sha256:0c423916...: not foundThis is the mismatch between Docker's containerd-snapshotter image store
(default in Docker 24+) and kind load's ctr import flow - the
manifest references blobs that exist in Docker's store but never get
streamed across. The follow-up workaround (docker save -o image.tar && kind load image-archive image.tar) usually fails the same way for the
same reason.
Don't fight kind load - use ttl.sh from Step 2 instead. The whole
tutorial series is wired around docker push ttl.sh/<random>:24h
exactly so you never have to debug this:
export IMG=ttl.sh/nginx-$(uuidgen):1h
docker tag nginx:1.27-alpine "$IMG"
docker push "$IMG"
# reference "$IMG" from your manifest / CR / chart valuesError response from daemon: toomanyrequests: You have reached your pull rate limit
Docker Hub anonymous pulls are capped at 100 per 6 hours per source IP
(200/6h with docker login). On a shared-IP host or after a few kind
cluster re-creates in the same afternoon this exhausts quickly.
Fix: docker login (Docker ID + access token) on the host before
pulling. For the operator image you build locally, push to ttl.sh once
and reference that URL - the cluster pulls from ttl.sh, never from
Docker Hub:
docker login
# OR
export IMG=ttl.sh/myimage-$(uuidgen):24h
docker tag <local-image> "$IMG" && docker push "$IMG"Helm reports an old version after install
If helm version reports a v3.x release after you ran the Helm 4
install snippet, you almost certainly have a stale binary earlier on
$PATH (often the snap package or a /usr/bin/helm shipped by the
distribution). Find which one wins:
which -a helm
# /usr/local/bin/helm <-- the new Helm 4 binary
# /snap/bin/helm <-- the stale Helm 3 binary that's overriding itEither remove the older package (sudo snap remove helm) or re-order
$PATH so /usr/local/bin comes first.
Recap - the verified toolchain
After this guide you should have:
go version # 1.22 or newer
kubectl version --client
docker version # or: podman version
helm version # v4.x.x (current line is v4.2.0)
kind version
operator-sdk version
operator-sdk init --plugins helm --help | head -1 # confirms Helm plugin is wired in
kubectl get nodes # one Ready node from the kind cluster
# end-to-end push -> pull check through ttl.sh
export TEST_IMG=ttl.sh/busybox-$(uuidgen):1h
docker pull busybox:stable
docker tag busybox:stable "$TEST_IMG"
docker push "$TEST_IMG"
kubectl run busybox --image="$TEST_IMG" --restart=Never -- sleep 1
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/busybox --timeout=30s
kubectl delete pod busyboxIf every command succeeds, you are ready to scaffold your first project
in the next article. The exact ttl.sh/<random>:24h pattern is what
the operator tutorials use to ship the operator image you'll build
(make docker-build IMG=ttl.sh/demoapp-$(uuidgen):24h) from your laptop
into the kind cluster.
Frequently Asked Questions
1. How do I install Operator-SDK on Linux?
Three supported methods on Linux: (1) download the official binary from the GitHub releases page and put it on $PATH, (2) install via Homebrew on Linux with brew install operator-sdk, or (3) build from source with make install after cloning the repo. The binary download is the upstream-recommended method and is the only one that gives you reproducible versions on every distribution.2. What are the prerequisites for Operator-SDK?
You need Go 1.22 or newer, kubectl matching your target cluster version, a container runtime (Docker or Podman) for building images, and a local Kubernetes cluster for testing (kind is the recommended choice). controller-gen is pulled in automatically by the Makefile that operator-sdk init scaffolds, so you do not need to install it by hand.3. How do I verify Operator-SDK is installed correctly?
Run operator-sdk version. The expected output names the operator-sdk version, commit hash, kubernetes-api version, and the Go version operator-sdk was built against. If the command is not found, check that the binary is on $PATH and is executable; if it errors with "exec format error", you have downloaded the wrong architecture binary.4. Do I need GOPATH set for Operator-SDK?
You do not need GOPATH set explicitly - Go 1.13+ defaults to $HOME/go. You do need $HOME/go/bin on your $PATH if you plan to install Go binaries (like controller-gen or kustomize) via go install rather than via the Makefile. Add export PATH="$HOME/go/bin:$PATH" to your shell profile.5. How do I install kind for Operator-SDK development?
Download the kind binary from https://kind.sigs.k8s.io/docs/user/quick-start (current release matching kubectl), make it executable, and put it on $PATH. Create a cluster with kind create cluster --name demo. Verify with kubectl get nodes. kind runs the Kubernetes control plane in a Docker container, so the Docker daemon (or Podman with the appropriate socket) must be running.6. Operator-SDK installs but I get "GLIBC_2.34 not found" - how do I fix it?
The official operator-sdk binary is built against a recent GLIBC; on older distributions (CentOS 7, Debian 10, Ubuntu 18.04) the system GLIBC is too old and the binary refuses to run. Fix by either upgrading to a newer distribution release, or building operator-sdk from source against your system's GLIBC: git clone https://github.com/operator-framework/operator-sdk && cd operator-sdk && make install.7. Can I use Podman instead of Docker with Operator-SDK?
Yes. The Makefile that operator-sdk init scaffolds calls a container runtime via the CONTAINER_TOOL variable; set CONTAINER_TOOL=podman in your environment or in the Makefile. kind also supports Podman as the container runtime with KIND_EXPERIMENTAL_PROVIDER=podman (still labelled experimental but works well in practice).8. Do I need Helm CLI installed for Operator-SDK?
For Go-based operators Helm is optional. For Helm-based and Helm-hybrid operators you do need the Helm CLI (use Helm 4 - the current stable line, v4.2.0 as of mid-2026) so you can smoke-test charts standalone with helm install / helm lint before letting the operator drive them. The Helm CLI is also useful for debugging - helm get manifest / helm history / helm rollback work on any release the operator creates, because the release-Secret format (sh.helm.release.v1.NAME.vREV) is unchanged between Helm 3 and Helm 4.9. How does the operator tutorial series get locally-built images into the kind cluster?
Through ttl.sh - a free, public, ephemeral container registry that requires no signup. You tag your built operator image as ttl.sh/10. Is it safe to push my operator image to ttl.sh?
For tutorial work, yes. Anything pushed to ttl.sh is public and discoverable by anyone who knows the image name and tag - generating a random UUID for the name makes it effectively unguessable in practice. Images auto-expire (default 24h, max 30d). Do NOT push real production code, credentials baked into images, or proprietary chart logic to ttl.sh. For anything beyond throwaway tutorial state, use a registry that supports authentication and private images (GHCR with a personal access token is the simplest free option).What's next?
With the toolchain installed, the natural next steps are the rest of Chapter 2:
- Operator-SDK vs Kubebuilder — which framework to pick for a brand-new operator project in {year}, and what shared lineage they have under the hood.
- Scaffold your first operator project —
walk through
operator-sdk initflags, the module path / domain conventions, and the first successfulmake build. - Operator project folder structure —
tour the directories that
operator-sdk initwrites (api/,internal/controller/,config/). - The controller-runtime architecture — the Go library that the project you just scaffolded is built on.
- Create your first CRD with operator-sdk — the next code-writing step once the scaffold is in place.
- Prefer Minikube over kind? If you already followed install Kubernetes with Minikube, you can skip the kind install and use that cluster instead — operator-sdk works against any kubeconfig-reachable cluster.
- Need a refresher on what you are building toward? What is a Kubernetes Operator? is the Chapter 1 starting point and re-reads quickly now that you have the toolchain in hand.

