Sources: CD's devops wiki
02-kafka-rabbitmq/01-kafka-overview.mdand02-lab-kafka-with-docker-compose.md, updated to Kafka 4.x (KRaft, no ZooKeeper) and AKHQ, checked against the Apache Kafka docs and AKHQ releases on 2026-10-28. Lab names only.
Friday evening at Contoso's web shop. The inventory service restarts for a patch. For 40 seconds the order service, which calls inventory directly, gets connection errors and throws the orders away. Nobody finds out until Monday, when 212 customers ask where their stuff is. Nothing was hacked. Two services were simply holding hands too tightly.
TL;DR
- Problem: services that call each other directly fail together. One restart, one slow dependency, one traffic spike, and work is lost.
- Who it hurts: customers (lost orders), the business (refunds, reputation), developers on call (every outage is a 2 a.m. mystery), and anyone trying to add a new service without touching the old ones.
- Fix: put a broker in the middle. Producers write events to a topic; consumers read when they are ready. This post runs Apache Kafka 4.1 and the AKHQ web UI in one Docker Compose file.
- Cost: two containers, about 1 GB of RAM, zero licence. The 2023 way (ZooKeeper + broker + Confluent Control Center) was three containers, several GB, and a commercial UI.
- Prove it: send three orders, stop the consumer, send three more, start it again. Nothing is lost.
Table of contents
- 1. The problem
- 2. Who gets hurt
- 3. Cost and value
- 4. Build it: one Compose file
- 5. Prove it works
- Gotchas
- Checklist
- What changed recently
- Sources
1. The problem
Direct calls between services are like phoning a colleague every time you need something. If they are at lunch, you wait. If they quit, you are stuck. If 500 people phone at once, nobody gets through.
A message broker is the office mailbox. You drop the letter and walk away. The colleague empties the mailbox when they are back. Letters wait in order. Nobody loses a letter because someone was at lunch.
Kafka is that mailbox, built for millions of letters a day. The words you need (from my notes):
| Word | Plain meaning |
|---|---|
| Event | One letter: "Order 1042 was placed", with a key, a value and a timestamp |
| Producer | The service that drops the letter (order service) |
| Topic | A named mailbox (orders, payments) |
| Partition | A mailbox split into slots so several people can empty it in parallel |
| Consumer / consumer group | The services that read; a group shares the slots between its copies |
| Broker | The Kafka server that stores and serves it all |
2. Who gets hurt
| Who | What they lose without a broker |
|---|---|
| Customers | Orders, bookings, messages silently dropped during a restart or spike |
| The business | Refunds, support tickets, "the site is unreliable" reviews |
| Developers on call | A failure in service A shows up as an error in service B, C and D. Root cause takes hours. |
| Architecture | Adding a new consumer (analytics, fraud check) means changing the producer again |
| Security / audit | No durable record of what happened in which order; replay after an incident is impossible |
3. Cost and value
What it costs to run this lab (checked 2026-10-28):
| Stack | Containers | RAM on my laptop | Licence |
|---|---|---|---|
| 2023 lab (my old notes): ZooKeeper + Kafka + Confluent Control Center | 3 | several GB; Control Center alone is the heavy one | Control Center is Confluent's commercial UI (free only for a single-broker dev setup) |
| This post: Kafka 4.1 (KRaft) + AKHQ | 2 | about 1 GB | Apache 2.0 for both. Free. |
Kafka 4.0 (March 2025) removed ZooKeeper for good; the broker now coordinates itself with KRaft. AKHQ is an open-source UI that does what most people used Control Center for: topics, messages, consumer groups, lag.
What it is worth: the inventory restart above costs 212 lost orders. With a broker, the same restart costs nothing; the orders wait 40 seconds in the topic. Even on a laptop, learning this pattern is the difference between "we lost data" and "we were a bit late".
Managed alternatives, when the lab becomes real: every cloud sells Kafka as a service (Azure Event Hubs has a Kafka-compatible endpoint, AWS MSK, Confluent Cloud). They start at a few dollars a day for the smallest tier and remove the operations work. The protocol is the same, so what you learn here carries over.
4. Build it: one Compose file
Folder kafka-lab/, file compose.yaml:
services: kafka: image: apache/kafka:4.1.0 container_name: kafka ports: - "29092:29092" # for clients on your laptop environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_HOST://:29092 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_NUM_PARTITIONS: 3 KAFKA_LOG_DIRS: /var/lib/kafka/data volumes: - kafka-data:/var/lib/kafka/data akhq: image: tchiotludo/akhq:0.28.0 container_name: akhq depends_on: - kafka ports: - "8080:8080" environment: AKHQ_CONFIGURATION: | akhq: connections: lab-kafka: properties: bootstrap.servers: "kafka:9092" volumes: kafka-data: Three things in that file matter more than the rest:
- Two listeners.
kafka:9092is what containers (AKHQ) use.localhost:29092is what your laptop uses. Get the advertised listener wrong and clients connect, then time out on the first message. - Fixed versions.
4.1.0and0.28.0, notlatest. My notes already said this in 2023; it is still the number one cause of "it worked yesterday". - A named volume. Without
kafka-data, everydocker compose downdeletes every topic. (See my Docker volumes post.)
Run it:
docker compose up -d docker compose ps Both containers show running. First run pulls about 500 MB. Open http://localhost:8080: AKHQ lists the lab-kafka cluster with one node.
Create the topic:
docker exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 \ --create --topic orders --partitions 3 --replication-factor 1 5. Prove it works
Terminal 1, the consumer (the inventory service):
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \ --bootstrap-server localhost:9092 --topic orders --group inventory --from-beginning Terminal 2, the producer (the order service):
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \ --bootstrap-server localhost:9092 --topic orders --property parse.key=true --property key.separator=: Type three orders, one per line: 1042:placed, 1043:placed, 1044:placed. They appear in terminal 1 at once.
Now the Friday-evening test: stop the consumer (Ctrl+C in terminal 1), type three more orders, wait 40 seconds, start the consumer again with the same --group inventory. The three orders arrive, in order. Nothing was lost; the group remembered where it stopped.
In AKHQ, Consumer Groups → inventory shows lag 3 while the consumer is down and 0 after it catches up. That number is the one you will put on a dashboard later.
Gotchas
- Advertised listeners.
localhost:29092for the laptop,kafka:9092inside Compose. Mix them up and you get "connected" followed by timeouts. -
latestis not a version. Pin both images. - No volume, no data.
docker compose downwithout the named volume wipes the topics. - Replication factor 1 is a lab setting. Three brokers and
min.insync.replicas: 2for anything real; my Kubernetes notes do that with Strimzi. - Same key, same partition. Use the order ID as the key so events for one order stay in order.
- Port 8080 is popular. If AKHQ does not start, something else owns the port.
- Low on RAM? Drop AKHQ and use
kafka-topics.sh; the broker alone is enough to learn on.
Checklist
- [ ]
compose.yamlwith pinnedapache/kafka:4.1.0andtchiotludo/akhq:0.28.0 - [ ] Two listeners: internal
kafka:9092, hostlocalhost:29092 - [ ] Named volume for
/var/lib/kafka/data - [ ]
docker compose ps: both running - [ ] Topic
orders, 3 partitions, visible in AKHQ - [ ] Produce 3, consume 3
- [ ] Stop consumer, produce 3, restart consumer: 3 arrive, lag back to 0
What changed recently
Checked on 2026-10-28:
- My 2023 lab used ZooKeeper and Confluent Control Center. Kafka 4.0 (March 2025) removed ZooKeeper; the
apache/kafkaimage runs KRaft by default. Control Center is replaced by AKHQ here (Apache 2.0, version 0.28.0, August 2026). - The current Apache Kafka release is 4.4; 4.1.0 is used because it matches my Kubernetes notes. Either works with this file.
- The official
apache/kafkaimage documents theKAFKA_*environment variables used above; the extraPLAINTEXT_HOSTlistener and the named volume are my additions.
Sources
- Apache Kafka quickstart
- apache/kafka image on Docker Hub (compose examples)
- AKHQ releases
- Docker volumes post (why the named volume)
Thanks for reading! I'm Cuong, and on CD Security Lab I rebuild real Microsoft security cases in a safe lab and explain them simply. Follow along:
- ▶️ YouTube (daily Shorts + explainers): CD Security Lab
- 💼 LinkedIn: Cuong Do
- 📘 Facebook: CD Security Lab
- 📸 Instagram: @cdsecuritylab
- 📝 More posts like this: Dev.to
Got a question about your own tenant? Drop it in the comments.




