Skip to content

How to Deploy Kafka in Kubernetes with Helm (Strimzi) and Kafdrop (2026)

Last updated on October 26, 2026

Updated October 2026. The 2022 version of this post installed bitnami/kafka. Bitnami stopped publishing free versioned images in 2025, and Kafka 4 dropped ZooKeeper entirely, so that chart is no longer a good starting point. This version installs the Strimzi operator from its Helm chart, creates a single-node KRaft cluster and puts Kafdrop on top. Tested on Kubernetes 1.36 (k3s) with Helm 3.22, Strimzi 1.3.0, Kafka 4.3.1 and Kafdrop 4.3.0.

This is the quick path for development and testing: one node that is both controller and broker. For a three-node production cluster see Deploy Apache Kafka in KRaft Mode with Strimzi.

1. Install the Strimzi operator with Helm

kubectl create namespace kafka
helm install strimzi oci://quay.io/strimzi-helm/strimzi-kafka-operator \
  --version 1.3.0 -n kafka
kubectl -n kafka rollout status deploy/strimzi-cluster-operator

Expected result: deployment "strimzi-cluster-operator" successfully rolled out. Helm installs the CRDs and the RBAC for the namespace you install into, so none of the manual RoleBinding patches from the YAML install are needed.

2. Create a single-node KRaft cluster

Save as kafka.yaml. The KafkaNodePool defines the pod (one replica that is controller and broker at the same time) and its storage; the Kafka resource defines the cluster:

apiVersion: kafka.strimzi.io/v1
kind: KafkaNodePool
metadata:
  name: dual-role
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster
spec:
  replicas: 1
  roles:
    - controller
    - broker
  storage:
    type: jbod
    volumes:
      - id: 0
        type: persistent-claim
        size: 10Gi
        kraftMetadata: shared
---
apiVersion: kafka.strimzi.io/v1
kind: Kafka
metadata:
  name: my-cluster
  namespace: kafka
spec:
  kafka:
    version: 4.3.1
    metadataVersion: 4.3-IV0
    listeners:
      - name: plain
        port: 9092
        type: internal
        tls: false
    config:
      offsets.topic.replication.factor: 1
      transaction.state.log.replication.factor: 1
      transaction.state.log.min.isr: 1
      default.replication.factor: 1
      min.insync.replicas: 1
  entityOperator:
    topicOperator: {}
    userOperator: {}
kubectl apply -f kafka.yaml
kubectl -n kafka wait kafka/my-cluster --for=condition=Ready --timeout=600s
kubectl -n kafka get pods,pvc,svc

Expected result: kafka.kafka.strimzi.io/my-cluster condition met, the pods my-cluster-dual-role-0 and my-cluster-entity-operator-... running, a 10Gi PVC Bound and the service my-cluster-kafka-bootstrap on port 9092. Applications inside the cluster connect to my-cluster-kafka-bootstrap.kafka.svc:9092.

3. Create a topic the Kubernetes way

With the Topic Operator running, topics are Kubernetes resources too:

apiVersion: kafka.strimzi.io/v1
kind: KafkaTopic
metadata:
  name: orders
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster
spec:
  partitions: 3
  replicas: 1
kubectl apply -f topic.yaml
kubectl -n kafka wait kafkatopic/orders --for=condition=Ready

4. Send and read a few messages

printf "order-1\norder-2\norder-3\n" | kubectl -n kafka exec -i my-cluster-dual-role-0 -- \
  bin/kafka-console-producer.sh --bootstrap-server my-cluster-kafka-bootstrap:9092 --topic orders

kubectl -n kafka exec my-cluster-dual-role-0 -- \
  bin/kafka-console-consumer.sh --bootstrap-server my-cluster-kafka-bootstrap:9092 \
  --topic orders --from-beginning --max-messages 3

Expected result: order-1, order-2 and order-3 (the order can differ, the topic has three partitions).

5. Deploy Kafdrop

Kafdrop is a small web UI for browsing brokers, topics, partitions and messages. Save as kafdrop.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kafdrop
  namespace: kafka
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kafdrop
  template:
    metadata:
      labels:
        app: kafdrop
    spec:
      containers:
        - name: kafdrop
          image: obsidiandynamics/kafdrop:4.3.0
          env:
            - name: KAFKA_BROKERCONNECT
              value: my-cluster-kafka-bootstrap:9092
            - name: JVM_OPTS
              value: "-Xms64m -Xmx256m"
          ports:
            - name: http
              containerPort: 9000
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: http
            initialDelaySeconds: 20
          resources:
            requests:
              cpu: 50m
              memory: 256Mi
            limits:
              memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: kafdrop
  namespace: kafka
spec:
  selector:
    app: kafdrop
  ports:
    - name: http
      port: 9000
      targetPort: http
kubectl apply -f kafdrop.yaml
kubectl -n kafka rollout status deploy/kafdrop
kubectl -n kafka port-forward svc/kafdrop 9000:9000

Open http://localhost:9000. The topic list shows orders with 3 partitions next to the internal __consumer_offsets, and /actuator/health answers {"status":"UP"}.

Two things changed compared with the 2022 manifest. The old Deployment labelled the Kafdrop pods app: kafka, a label that easily collides with other workloads and makes the Service select pods it should not; here everything is labelled app: kafdrop. It also exposed the UI through a LoadBalancer, although Kafdrop has no login and lets anyone read every message. Keep it on ClusterIP and use port-forward, or put it behind an ingress with authentication.

Gotchas

  • Strimzi 1.x serves only kafka.strimzi.io/v1. Manifests written for 0.4x with apiVersion: kafka.strimzi.io/v1beta2 are rejected until you change the API version.
  • No ZooKeeper. Kafka 4 runs only in KRaft mode; guides that create a ZooKeeper ensemble are for Kafka 3 and older.
  • Single node means replication factor 1 everywhere, including the internal topics in config. Without them Kafka tries to create __consumer_offsets with three replicas on a single broker and consumer groups fail.
  • version and metadataVersion go together. When you upgrade Kafka, bump version first and metadataVersion only after the cluster runs fine on the new version.
  • Pin image tags. obsidiandynamics/kafdrop:latest moves under you; the manifest above uses 4.3.0.

Takeaways

  • helm install strimzi oci://quay.io/strimzi-helm/strimzi-kafka-operator replaces the Bitnami chart.
  • A KafkaNodePool with both roles plus a Kafka resource gives you a working KRaft cluster in about two minutes.
  • Topics are KafkaTopic resources, and Kafdrop stays private behind port-forward.

Links

Published inKubernetes