Transforming vinD to vinP (vCluster in Podman) on macOS!
Introduction
I always wanted to test vinD which seems more complete than Kind for a local test and deployment. However the name is "vind - vCluster in Docker" and I use Podman... so I put IBM Bob into work to make all what it takes to use vinD with Podman locally.
While vinD was explicitly designed as a Docker-native tool that expects standard Docker socket endpoints and CLI responses, migrating the setup to a macOS workstation running Podman Desktop is entirely feasible with a few targeted compatibility shims and VM adjustments.
This post walks through the architectural considerations, step-by-step implementation, custom compatibility layers, and deployment manifests required to bring vinD up and running seamlessly on Podman.
TL;DR: What is vinD?
tl;dr — vinD (vCluster in Docker) gives you a real, lightweight Kubernetes cluster running inside a single container in seconds.
-
Real Kubernetes Control Plane: Unlike standard namespaces,
vcluster createprovisions a fully isolated, dedicated Kubernetes control plane (API server, etcd, controller manager) inside one host container. -
High-Speed Provisioning & Resource Efficiency: Starts in ~15 seconds with a tiny RAM footprint (~150 MB). It supports sleep/wake capabilities (
vcluster pause/vcluster resume) so an idle cluster uses zero CPU and zero memory. - Full Tenant Isolation: Multiple isolated vClusters can run side-by-side on a single host machine with separate CRDs, RBAC, and Kubernetes API versions without interfering with each other.
-
Native Tooling: You interact with vinD using standard
kubectl,helm, andvclusterCLI commands just as you would with EKS, GKE, or Kind.
Excerpt from vinD Github;
🎯 What is vind?
vind (vCluster in Docker) is an open-source way to run Kubernetes clusters directly as Docker containers. Built on top of vCluster, vind combines the power of virtual Kubernetes clusters with the simplicity of Docker, creating isolated Kubernetes environments that are perfect for development, testing, and CI/CD pipelines.
Note: vind uses vCluster's Private Nodes mode internally. This is automatically enabled when using the Docker driver and is required for proper operation. This is expected behavior, not a configuration issue.
Why vind?
- 🚀 Faster than KinD - Optimized container-based architecture
- 💤 Sleep & Wake - Pause clusters to save resources, resume in under 3 seconds
- 🎨 Built-in UI - Free vCluster Platform UI for cluster management
- ⚡ Load Balancers OOB - Automatic LoadBalancer services without extra setup
- 🐳 Docker Native - Leverages Docker's networking and storage
- 🔄 Pull-through Cache - Faster image pulls via local Docker daemon
- 🌐 Hybrid Nodes - Join external nodes (even cloud instances) via VPN
- 📸 Snapshots - Save cluster state to OCI registries, S3, or local files and restore them
- 🔧 In-Place K8s Upgrades - Upgrade Kubernetes version without deleting the cluster
Architecture Overview
High-Level Architecture
The following diagram illustrates how the macOS host environment interfaces with the underlying Podman VM and vinD control plane container:
Key Differences & Podman Compatibility Shims
To bridge the gap between vcluster (which assumes a native Docker daemon) and Podman, four specific adjustments are required:
Custom Docker-to-Podman CLI Shim: When probing network subnets,
vclusterissuesdocker network inspect --format '{{.IPAM.Config}}'. Podman formats network inspect JSON under.subnets[].subnet. A lightweight shim intercepts this call and formats the JSON output accordingly.br_netfilterKernel Module: Flannel CNI requires thebr_netfiltermodule loaded in the Podman Machine VM. Loading this module explicitly prevents CNI initialization failure.DOCKER_HOSTSocket Redirection: Standard API interactions route through Podman’s Docker-compatible API socket (unix:///.../podman.sock).Architecture-Aware Image Build (
TARGETARCH): Multi-stage container builds must respect the Apple Silicon (arm64) execution environment of the Podman VM to avoid runtimeexec format errorissues.
**Attention:* Installing vinD on macOS using Homebrew standard commands is not sufficient on its own*. Because the loft-sh/tap repository isn't trusted by default, I used **WailBrew* to explicitly flag and manage the tap as a trusted runnable source.*
Implementation
Application Source & Multi-Stage Containerfile
A lightweight HTTP service written in Go serves as the test workload.
-
app/main.go
// Package main provides a minimal HTTP Hello World server.
// Port is controlled via the PORT environment variable (default: 8081).
package main
import (
"fmt"
"log"
"net/http"
"os"
)
// getPort returns the port to listen on.
// It reads from the PORT environment variable; falls back to 8081.
func getPort() string {
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}
return port
}
// helloHandler writes "Hello, World!" to the response.
func helloHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Request received: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
fmt.Fprintln(w, "Hello, World!")
}
// healthHandler provides a basic liveness probe endpoint.
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "OK")
}
// newMux builds and returns the HTTP ServeMux with all routes registered.
func newMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/", helloHandler)
mux.HandleFunc("/healthz", healthHandler)
return mux
}
func main() {
port := getPort()
addr := ":" + port
mux := newMux()
log.Printf("Starting Hello World server on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
app/Containerfile
# Stage 1 — Builder
FROM docker.io/library/golang:1.21-alpine AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN go test ./... -v
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH:-$(go env GOARCH)} \
go build -ldflags="-s -w" -o /out/helloworld .
# Stage 2 — Runtime
FROM scratch
COPY --from=builder /out/helloworld /helloworld
EXPOSE 8081
ENV PORT=8081
USER 65534:65534
ENTRYPOINT ["/helloworld"]
Kubernetes Deployment Manifests
The service is exposed on NodePort 30080, which maps to the host.
-
manifests/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: helloworld
labels:
app.kubernetes.io/managed-by: vind
-
manifests/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld
namespace: helloworld
labels:
app: helloworld
app.kubernetes.io/name: helloworld
app.kubernetes.io/version: "1.0.0"
spec:
replicas: 1
selector:
matchLabels:
app: helloworld
template:
metadata:
labels:
app: helloworld
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
containers:
- name: helloworld
image: localhost/helloworld:latest
imagePullPolicy: Never
ports:
- containerPort: 8081
protocol: TCP
name: http
env:
- name: PORT
value: "8081"
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "200m"
memory: "64Mi"
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 3
periodSeconds: 5
-
manifests/service.yaml
apiVersion: v1
kind: Service
metadata:
name: helloworld
namespace: helloworld
labels:
app: helloworld
app.kubernetes.io/name: helloworld
spec:
type: NodePort
selector:
app: helloworld
ports:
- name: http
port: 80
targetPort: 8081
nodePort: 30080
protocol: TCP
- and even a Load Balancer (manifests/loadbalancer-service.yaml)
---
# =============================================================================
# Kubernetes Service — helloworld (LoadBalancer type)
#
# vinD provides automatic LoadBalancer support out-of-the-box.
# The EXTERNAL-IP is assigned from the Podman/Docker bridge network.
# Apply this INSTEAD of service.yaml if you prefer LoadBalancer over NodePort.
#
# Apply with:
# kubectl apply -f manifests/loadbalancer-service.yaml
# =============================================================================
apiVersion: v1
kind: Service
metadata:
name: helloworld-lb
namespace: helloworld
labels:
app: helloworld
app.kubernetes.io/name: helloworld
spec:
type: LoadBalancer
selector:
app: helloworld
ports:
- name: http
port: 80 # External port
targetPort: 8081 # Container port
protocol: TCP
Orchestration & Podman Shim Setup Script
The orchestration script manages binary downloads, setting up the docker CLI translation shim, enabling br_netfilter in the Podman VM, creating the vCluster instance, importing images into containerd, and deploying manifests.
-
scripts/setup.sh
#!/usr/bin/env bash
set -euo pipefail
CLUSTER_NAME="${CLUSTER_NAME:-vind-helloworld}"
APP_IMAGE="${APP_IMAGE:-localhost/helloworld:latest}"
echo "==> 1. Ensuring vcluster CLI is installed..."
if ! command -v vcluster &> /dev/null; then
mkdir -p "$HOME/.local/bin"
curl -fsSL "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-darwin-arm64" \
-o "$HOME/.local/bin/vcluster"
chmod +x "$HOME/.local/bin/vcluster"
export PATH="$HOME/.local/bin:$PATH"
fi
echo "==> 2. Detecting Podman Socket..."
PODMAN_SOCK=$(podman info --format '{{.Host.RemoteSocket.Path}}' 2>/dev/null || true)
if [ -z "$PODMAN_SOCK" ]; then
PODMAN_SOCK="$HOME/.local/share/containers/podman/machine/qemu/podman.sock"
fi
export DOCKER_HOST="unix://${PODMAN_SOCK}"
echo "==> 3. Creating docker -> podman CLI shim with IPAM translation..."
mkdir -p "$HOME/.local/bin"
cat << 'EOF' > "$HOME/.local/bin/docker"
#!/usr/bin/env bash
if [[ "$*" == *"network inspect"* ]] && [[ "$*" == *"IPAM"* ]]; then
NET_NAME="${@:$#}"
SUBNET=$(podman network inspect "$NET_NAME" --format '{{range .subnets}}{{.subnet}}{{end}}' 2>/dev/null)
echo "[{\"Subnet\":\"${SUBNET}\"}]"
exit 0
fi
exec podman "$@"
EOF
chmod +x "$HOME/.local/bin/docker"
export PATH="$HOME/.local/bin:$PATH"
echo "==> 3b. Loading br_netfilter module in Podman VM..."
podman machine ssh "sudo modprobe br_netfilter" || true
podman machine ssh "echo 'br_netfilter' | sudo tee /etc/modules-load.d/br_netfilter.conf" || true
echo "==> 4. Configuring vCluster driver..."
vcluster use driver docker
echo "==> 5. Creating vinD cluster..."
if ! vcluster list | grep -q "$CLUSTER_NAME"; then
vcluster create "$CLUSTER_NAME" -f vcluster.yaml
fi
echo "==> 6. Building Go application image..."
podman build -t "$APP_IMAGE" -f app/Containerfile app/
echo "==> 7. Importing image into cluster containerd..."
podman save "$APP_IMAGE" | docker exec -i "vcluster.cp.${CLUSTER_NAME}" ctr --namespace k8s.io images import -
echo "==> 8. Applying Kubernetes manifests..."
kubectl apply -f manifests/namespace.yaml
kubectl apply -f manifests/deployment.yaml
kubectl apply -f manifests/service.yaml
echo "==> 9. Waiting for deployment readiness..."
kubectl rollout status deployment/helloworld -n helloworld --timeout=60s
echo "==> Setup complete! Test endpoint:"
echo "curl http://localhost:30080"
Conclusion
By addressing the four core points of divergence between Docker and Podman environments—CLI output formatting, kernel module presence in the underlying machine VM, platform-native image builds, and API socket location—vinD runs efficiently on top of Podman.
This setup offers a fast local deployment pipeline, retaining vCluster's single-container control plane model while operating fully within open-source, Podman-based development tooling.
Thanks for reading 🍇
Links
- vinD: https://github.com/loft-sh/vind
- Podman: https://github.com/podman-container-tools/podman
- Code repo for this post: https://github.com/aairom/vinD-Podman
- IBM Bob: https://bob.ibm.com/







Top comments (0)