Skip to content

Getting Started with K3s (K3d)

K3s is a lightweight, certified Kubernetes distribution designed for resource-constrained environments. KSail uses K3d to run K3s clusters in Docker containers, giving you fast cluster creation with batteries-included defaults like a built-in load balancer, local storage provisioner, and metrics server.

K3s is a minimal Kubernetes distribution that bundles everything needed into a single binary. K3d wraps K3s in Docker containers so you can run full clusters locally. Together they offer:

  • Minimal footprint: Single-binary K3s uses less memory and CPU than upstream Kubernetes
  • Batteries included: ServiceLB, local-path-provisioner, and metrics-server are built in
  • Fast cluster creation: Clusters ready in 15–30 seconds
  • Docker-only: Runs entirely in Docker containers (no VM overhead)
  • Multi-node support: Simulate production topologies locally
  • Native configuration: Uses standard k3d.yaml files (no lock-in)

K3s (K3d) is ideal for:

  • Quick local development: Fast iteration without heavyweight infrastructure
  • Resource-constrained machines: Laptops or CI runners with limited CPU/RAM
  • CI/CD pipelines: Ephemeral test clusters that start in seconds
  • Edge and IoT prototyping: K3s targets resource-constrained deployments by design
  • Multi-node testing: Simulate production topologies with workers

Consider alternatives if you need:

  • 100% upstream compatibility: K3s omits some alpha/beta APIs; Vanilla (Kind) uses unmodified Kubernetes
  • Production immutability: Talos offers immutable infrastructure and enhanced security
  • Virtual cluster isolation: VCluster provides tenant isolation without separate nodes
  • Cloud deployments: Talos supports Hetzner Cloud; K3s currently only supports Docker locally

Create your first K3s cluster in under 30 seconds.

  • Docker Desktop or Docker Engine installed and running
  • docker ps command works
Terminal window
ksail cluster init \
--name my-cluster \
--distribution K3s \
--control-planes 1 \
--workers 2

This creates:

  • ksail.yaml β€” KSail configuration
  • k3d.yaml β€” K3d-specific cluster configuration
Terminal window
ksail cluster create

KSail will:

  1. Generate K3d configuration from ksail.yaml
  2. Create Docker containers as Kubernetes nodes
  3. Install K3s inside the containers
  4. Configure kubectl context
  5. Apply additional components (cert-manager, policy engine, etc.) if configured

Expected output:

βœ“ Creating K3s cluster with K3d...
βœ“ Cluster ready!
Cluster: my-cluster
Nodes: 3 (1 control plane, 2 workers)
Provider: Docker
Terminal window
# Check cluster info
ksail cluster info
# View nodes
kubectl get nodes
# Expected output:
# NAME STATUS ROLES AGE VERSION
# k3d-my-cluster-server-0 Ready control-plane,master 2m v1.31.x+k3s1
# k3d-my-cluster-agent-0 Ready <none> 2m v1.31.x+k3s1
# k3d-my-cluster-agent-1 Ready <none> 2m v1.31.x+k3s1
Terminal window
# Create a deployment
kubectl create deployment nginx --image=nginx
# Expose as LoadBalancer (K3s ServiceLB assigns an IP automatically)
kubectl expose deployment nginx --port=80 --type=LoadBalancer
# Verify β€” EXTERNAL-IP should appear within a few seconds
kubectl get svc nginx
Terminal window
# Delete cluster and all resources
ksail cluster delete

The ksail.yaml file controls your cluster:

apiVersion: ksail.io/v1alpha1
kind: Cluster
spec:
cluster:
name: my-cluster
distribution: K3s
provider: Docker
controlPlanes: 1
workers: 2

Customize K3d behavior in k3d.yaml:

apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
name: my-cluster
servers: 1 # control-plane nodes
agents: 2 # worker nodes
ports:
- port: 8080:80@loadbalancer # Map host port 8080 to cluster port 80
- port: 8443:443@loadbalancer # Map host port 8443 to cluster port 443
options:
k3s:
extraArgs:
- arg: --disable=traefik # Disable built-in Traefik ingress
nodeFilters:
- server:*

K3s includes these components by defaultβ€”KSail does not reinstall them:

ComponentNotes
ServiceLB (Klipper-LB)LoadBalancer support via host ports
local-path-provisionerDynamic PersistentVolume provisioning
Metrics Serverkubectl top and HPA support
CoreDNSCluster DNS
TraefikIngress controller (can be disabled)

Configure registry mirrors to avoid rate limits. KSail enables docker.io, ghcr.io, quay.io, and registry.k8s.io mirrors by default. Override with --mirror-registry flags:

Terminal window
ksail cluster init --name my-cluster --distribution K3s \
--mirror-registry 'docker.io=https://registry-1.docker.io' \
--mirror-registry 'ghcr.io=https://ghcr.io' \
--mirror-registry 'quay.io=https://quay.io' \
--mirror-registry 'registry.k8s.io=https://registry.k8s.io'

K3d injects registry configuration so all nodes use your mirrors automatically.

Scenario: Develop a microservices application with rapid restart cycles.

Terminal window
# Single-node cluster (fastest startup)
ksail cluster init --name dev --distribution K3s --workers 0
ksail cluster create
# Deploy application
kubectl apply -f k8s/
# Hot reload: rebuild image and restart
docker build -t my-app:dev .
kubectl rollout restart deployment/my-app

Why K3s: Sub-30-second cluster startup, low memory footprint, built-in LoadBalancer.

Scenario: Run integration tests in ephemeral clusters on shared GitHub Actions runners.

Terminal window
# CI pipeline script
ksail cluster init --name ci-test --distribution K3s
ksail cluster create
# Run tests against the cluster
kubectl apply -f test-manifests/
./run-integration-tests.sh
# Cleanup
ksail cluster delete

Why K3s: Fast creation, low resource usage ideal for constrained CI runners.

Scenario: Test GitOps configurations locally before production deployment.

Terminal window
# Initialize with GitOps and a local registry
ksail cluster init \
--name gitops-test \
--distribution K3s \
--gitops-engine Flux \
--local-registry
# Create, push manifests, and reconcile
ksail cluster create
ksail workload push
ksail workload reconcile
# Verify reconciliation
kubectl get kustomizations -A

Why K3s: Built-in storage and load balancer means fewer extra components to install.

Scenario: Test pod scheduling, node affinity, and high-availability patterns.

Terminal window
# 3-node cluster (1 control plane + 2 workers)
ksail cluster init \
--name topology-test \
--distribution K3s \
--control-planes 1 \
--workers 2
ksail cluster create
# Label nodes and test affinity
kubectl label nodes k3d-topology-test-agent-0 zone=us-east-1a
kubectl label nodes k3d-topology-test-agent-1 zone=us-east-1b

Why K3s: Worker nodes can be added/removed in-place with ksail cluster update.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Docker Host β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ K3d Load Balancer (Docker Container) β”‚ β”‚
β”‚ β”‚ β€’ Routes traffic to control-plane nodes β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Control Plane Node (Docker Container) β”‚ β”‚
β”‚ β”‚ β€’ k3s server process (includes all control- β”‚ β”‚
β”‚ β”‚ plane components in one binary) β”‚ β”‚
β”‚ β”‚ β€’ etcd (embedded) β”‚ β”‚
β”‚ β”‚ β€’ ServiceLB (Klipper-LB) β”‚ β”‚
β”‚ β”‚ β€’ local-path-provisioner β”‚ β”‚
β”‚ β”‚ β€’ metrics-server β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Worker Node 1 β”‚ β”‚ Worker Node 2 β”‚ β”‚
β”‚ β”‚ β€’ k3s agent β”‚ β”‚ β€’ k3s agent β”‚ β”‚
β”‚ β”‚ β€’ containerd β”‚ β”‚ β€’ containerd β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. K3d Load Balancer: A dedicated container routes kubectl and API traffic to control-plane nodes
  2. Single Binary: K3s bundles all Kubernetes control-plane components into one process
  3. Embedded etcd: No separate etcd cluster needed for single-node or HA setups
  4. ServiceLB: Klipper-LB allocates node ports to back LoadBalancer services
  5. Storage: local-path-provisioner uses host-path volumes on the node
  • Provisioner: pkg/svc/provisioner/cluster/k3d/ wraps the K3d SDK
  • Infrastructure: Docker provider manages container lifecycle (start/stop)
  • Configuration: Generates k3d.yaml from ksail.yaml declaratively
  • Installers: CNI (Cilium/Calico replaces default Flannel), cert-manager, policy engines
FeatureVanilla (Kind)K3s (K3d)TalosVCluster
Kubernetes TypeUpstreamLightweightUpstreamVirtual
Resource UsageMediumLowMediumVery Low
Startup Time30–60s15–30s60–90s10–20s
LoadBalancerCloud Provider KINDServiceLB (built-in)MetalLB / hcloud-ccmHost cluster LB
Storagelocal-path (optional)local-path (built-in)Hetzner CSI / local-pathHost cluster storage
Metrics ServerOptionalBuilt-inOptionalHost cluster
Production Ready❌ Local only❌ Local onlyβœ… Docker + Cloud❌ Virtual only
Multi-Nodeβœ… Yesβœ… Yesβœ… Yes❌ Single pod
Shell Accessβœ… Docker execβœ… Docker exec❌ No shellβœ… kubectl exec
Worker ScaleRecreateIn-placeIn-placeN/A
GitOps Supportβœ… Fullβœ… Fullβœ… Fullβœ… Full
Best ForUpstream K8s, testingQuick dev, CI/CDProduction, securityMulti-tenancy

Symptom: ksail cluster create fails with Docker errors.

Solutions:

Terminal window
# Check Docker is running
docker ps
# Check available disk space
df -h
# Clean up unused containers and networks
docker system prune
# Retry cluster creation
ksail cluster create

Symptom: kubectl get nodes shows NotReady status.

Solutions:

Terminal window
# Check node conditions
kubectl describe node <node-name>
# Check system pods
kubectl get pods -n kube-system
# Restart K3d cluster
ksail cluster delete
ksail cluster create

Symptom: LoadBalancer service never gets EXTERNAL-IP.

K3s uses ServiceLB (Klipper-LB) which binds to host ports. Ensure the port is not already in use:

Terminal window
# Check existing port bindings
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep k3d
# Use different ports in k3d.yaml
ports:
- port: 9080:80@loadbalancer

Symptom: address already in use error during cluster creation.

Solutions:

Terminal window
# List existing K3d clusters
k3d cluster list
# Delete conflicting cluster
k3d cluster delete <cluster-name>
# Or initialize with a different name
ksail cluster init --name my-cluster-2 --distribution K3s

Symptom: Cluster creation fails with resource errors.

Solutions:

Terminal window
# Reduce node count (K3s uses very little memory per node)
# Edit ksail.yaml:
spec:
cluster:
controlPlanes: 1
workers: 0 # Single-node cluster
# Clean up Docker
docker system prune
# Increase Docker resources (Docker Desktop β†’ Settings β†’ Resources)

Unlike Vanilla (Kind), K3s supports in-place worker node scaling:

# ksail.yaml β€” increase workers
spec:
cluster:
workers: 4
Terminal window
ksail cluster update # Adds/removes worker containers without recreation

Remove K3s default components you don’t need:

k3d.yaml
options:
k3s:
extraArgs:
- arg: --disable=traefik # Remove Traefik ingress
nodeFilters: ["server:*"]
- arg: --disable=servicelb # Remove ServiceLB (use MetalLB instead)
nodeFilters: ["server:*"]

Replace the default Flannel CNI with Cilium:

ksail.yaml
spec:
cluster:
cni: Cilium
# k3d.yaml β€” disable built-in Flannel
options:
k3s:
extraArgs:
- arg: --flannel-backend=none
nodeFilters: ["server:*"]
- arg: --disable-network-policy
nodeFilters: ["server:*"]

K3s version is determined by the K3d image. Pin a specific version in k3d.yaml:

k3d.yaml
image: rancher/k3s:v1.31.0-k3s1

See K3d image tags for available versions.

Expose services on host ports:

k3d.yaml
ports:
- port: 8080:80@loadbalancer # HTTP
- port: 8443:443@loadbalancer # HTTPS
- port: 30080:30080@agent:0 # NodePort on first worker
Terminal window
# Stop cluster (preserves state)
ksail cluster stop
# Start stopped cluster
ksail cluster start
# Scale workers in-place
ksail cluster update # Apply changed worker count from ksail.yaml
# List all K3s clusters
ksail cluster list
# Get cluster details
ksail cluster info

KSail generates standard k3d.yaml files:

Terminal window
# Use K3d CLI directly
k3d cluster create --config k3d.yaml
# List K3d clusters
k3d cluster list

No vendor lock-in β€” configurations are portable and KSail can interact with any K3d cluster accessible via your kubeconfig.

  • Vanilla (Kind): Upstream Kubernetes for maximum compatibility
  • Talos: Immutable infrastructure for production
  • VCluster: Virtual clusters for multi-tenancy