Kafka Introduction
Run Kafka locally with Docker Compose in KRaft mode and create your first topic from the CLI.
A quick recap of the log
Kafka is architecturally different from a traditional message broker: instead of a queue that removes messages once they're consumed, Kafka is an append-only, partitioned, replicated log. Consumers track their own position (an offset) in that log, so multiple independent consumer groups can each read — and even replay — the same data at their own pace. The System Design track's Message Queues & Event-Driven Architecture page covers why that architecture matters and when to reach for Kafka over something like RabbitMQ. This page — and the rest of this track — assumes that context and gets straight into running Kafka and using it from real code.
Running Kafka locally with Docker Compose
Modern Kafka (3.3+) no longer needs ZooKeeper — a single broker can run its own metadata quorum using KRaft mode. Here's a real, working single-node setup using the official Apache Kafka image:
services:
kafka:
image: apache/kafka:3.7.0
container_name: kafka
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://kafka:19092,CONTROLLER://kafka:19093,PLAINTEXT_HOST://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:19092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:19093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
The two listener names are worth understanding rather than copy-pasting blindly: PLAINTEXT (on kafka:19092) is what other containers on the same Docker network use to reach the broker — your application container would connect to kafka:19092. PLAINTEXT_HOST (on localhost:9092) is what a client running directly on your machine, outside Docker, uses instead. Mixing these up is the single most common "why can't my producer connect" problem when running Kafka in Docker — a client inside the container network that tries to connect to localhost:9092 will fail, because localhost resolves to the client's own container, not the broker's.
Start it:
docker compose up -d
Creating a topic via the CLI
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh \
--create --topic orders \
--bootstrap-server localhost:9092 \
--partitions 3 --replication-factor 1
Verify it was created, and see how it laid out its partitions:
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh \
--describe --topic orders --bootstrap-server localhost:9092
Topic: orders TopicId: 8f3c... PartitionCount: 3 ReplicationFactor: 1 Configs: ...
Topic: orders Partition: 0 Leader: 1 Replicas: 1 Isr: 1
Topic: orders Partition: 1 Leader: 1 Replicas: 1 Isr: 1
Topic: orders Partition: 2 Leader: 1 Replicas: 1 Isr: 1
With --replication-factor 1 on a single broker, every partition's only replica is itself — fine for local development, never for production (a broker restart would briefly make that partition unavailable, and a broker failure would lose it). The next page covers exactly what "partition" and "replica" mean and why they matter.
Proving it's alive: console producer and consumer
Kafka ships CLI tools for quickly publishing and reading messages without writing any client code — useful for smoke-testing a topic:
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \
--topic orders --bootstrap-server localhost:9092
Type a few lines and press Enter after each — each line becomes one message. In a second terminal:
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \
--topic orders --from-beginning --bootstrap-server localhost:9092
--from-beginning reads the whole topic from offset 0 instead of only new messages published after the consumer started — handy for confirming your test messages actually landed.
Common mistakes
- Connecting a client running on your host machine to the container's internal hostname (
kafka:19092) instead oflocalhost:9092— or the reverse for a client running inside another container. Match the listener to where the client actually runs. - Running with
--replication-factor 1in anything beyond local development — there's no redundancy at all if that one broker goes down. - Forgetting
--from-beginningwhen testing and concluding "nothing is being produced," when actually the consumer simply started after the test messages were already published.