DEV Community

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Posted on

Running vinD with Podman: Local Kubernetes without Docker Desktop

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 create provisions 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, and vcluster CLI 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:

  1. Custom Docker-to-Podman CLI Shim: When probing network subnets, vcluster issues docker 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.

  2. br_netfilter Kernel Module: Flannel CNI requires the br_netfilter module loaded in the Podman Machine VM. Loading this module explicitly prevents CNI initialization failure.

  3. DOCKER_HOST Socket Redirection: Standard API interactions route through Podman’s Docker-compatible API socket (unix:///.../podman.sock).

  4. Architecture-Aware Image Build (TARGETARCH): Multi-stage container builds must respect the Apple Silicon (arm64) execution environment of the Podman VM to avoid runtime exec format error issues.

**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)
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)