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-operatorExpected 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,svcExpected 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: 1kubectl apply -f topic.yaml
kubectl -n kafka wait kafkatopic/orders --for=condition=Ready4. 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 3Expected 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: httpkubectl apply -f kafdrop.yaml
kubectl -n kafka rollout status deploy/kafdrop
kubectl -n kafka port-forward svc/kafdrop 9000:9000Open 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 withapiVersion: kafka.strimzi.io/v1beta2are 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_offsetswith three replicas on a single broker and consumer groups fail. versionandmetadataVersiongo together. When you upgrade Kafka, bumpversionfirst andmetadataVersiononly after the cluster runs fine on the new version.- Pin image tags.
obsidiandynamics/kafdrop:latestmoves under you; the manifest above uses4.3.0.
Takeaways
helm install strimzi oci://quay.io/strimzi-helm/strimzi-kafka-operatorreplaces the Bitnami chart.- A
KafkaNodePoolwith both roles plus aKafkaresource gives you a working KRaft cluster in about two minutes. - Topics are
KafkaTopicresources, and Kafdrop stays private behindport-forward.
