Despliega Apache Kafka en Docker con KRaft en 6 Minutos: Guía Definitiva Paso a Paso
Olvídate para siempre de la complejidad de ZooKeeper. Domina el protocolo de consenso KRaft (KIP-500) en Docker Compose, resuelve de raíz el laberinto de los advertised.listeners y levanta un clúster de streaming ultrarrápido listo para tus pipelines de ingeniería de datos.
Lo que Dominarás en Esta Guía Práctica
El Fin de la Era ZooKeeper: Por Qué KRaft lo Cambia Todo para el Data Engineer
Durante más de una década, cualquier arquitecto o ingeniero de datos que necesitara levantar Apache Kafka en su entorno local o en servidores on-premise debía enfrentarse a una pesadilla recurrente: Apache ZooKeeper. Gestionar dos tecnologías distribuidas simultáneas con ciclos de vida independientes, sincronización de metadatos por duplicado y puertos separados no solo ralentizaba el desarrollo, sino que introducía puntos críticos de desincronización y fallos de estado en producción.
En nuestras auditorías de infraestructura y formaciones avanzadas, he visto a cientos de ingenieros perder horas enteras diagnosticando split-brain scenarios o contenedores de ZooKeeper que se corrompían tras un apagado brusco de la máquina. Con la implementación del estándar KRaft (Kafka Raft Metadata Mode) definido en la propuesta KIP-500 y consolidado como estándar indiscutible (marcando la eliminación total de ZooKeeper en Kafka 4.0), las reglas del juego han cambiado radicalmente.
1. Cómo Funciona KRaft: Consenso Raft Nativo
En lugar de externalizar la elección del nodo líder, el estado de las particiones y las listas de control de acceso (ACLs) a un clúster ajeno de ZooKeeper, KRaft convierte los metadatos en un tópico interno append-only de alta velocidad (denominado @metadata). Un subconjunto selecto de brokers asume el rol de Quorum Controllers ejecutando un algoritmo de consenso basado en Raft.
El resultado para el desarrollador y el arquitecto es inmediato: cero dependencias externas, arranques instantáneos, recuperación ante caídas en milisegundos en vez de minutos, y la posibilidad de desplegar un entorno de streaming completo en Docker con un único servicio ligero. Si deseas explorar en detalle la base teórica de particiones, offsets y streaming a escala, puedes consultar nuestro curso completo de Apache Kafka para Data Engineers.
Matriz Comparativa: Apache Kafka con KRaft vs Clúster Tradicional con ZooKeeper
Al desplegar entornos locales en Docker o clústeres en la nube, las ventajas operativas y de consumo de recursos entre la arquitectura clásica y KRaft son abrumadoras:
| Dimensión Arquitectónica | Kafka Clásico (ZooKeeper) | Kafka Moderno (KRaft Mode) | Impacto en Ingeniería de Datos |
|---|---|---|---|
| Contenedores en Docker | Mínimo 2 (Zookeeper + Kafka Broker) | 1 solo contenedor (Broker + Controller) | Menor huella de red, orquestación simplificada. |
| Tiempo de Arranque (Docker) | 30 a 50 segundos (espera de quórum) | Menos de 3 segundos | Iteraciones de pruebas locales y CI/CD ultrasilenciosas. |
| Consumo de Memoria Base | ~1.8 GB a 2.5 GB RAM (2 JVMs activas) | ~512 MB a 700 MB RAM (1 sola JVM) | Elimina el riesgo de OOMKilled en laptops de desarrollo. |
| Recuperación ante Caída de Líder | De 30 a 120 segundos (recarga de metadatos) | < 500 milisegundos (log replicado en memoria) | Cero interrupción observable en clientes y productores. |
| Límite de Particiones por Clúster | ~200.000 particiones (cuello de botella ZK) | Millones de particiones | Escalabilidad masiva para arquitecturas multi-tenant. |
Demostración en Vídeo: Levanta tu Broker KRaft en 6 Minutos
En el siguiente vídeo de nuestro canal oficial de YouTube te muestro en pantalla, paso a paso y sin saltos de edición, cómo escribir el archivo docker-compose.yml, levantar el contenedor de Kafka en modo KRaft y verificar la publicación y consumo de mensajes utilizando únicamente la línea de comandos:
A continuación, vamos a desglosar el aspecto técnico que más dolores de cabeza genera en la industria: la correcta configuración de los listeners de red.
La Trampa de los Listeners: Por Qué Todos Fallamos al Configurar KAFKA_ADVERTISED_LISTENERS
Si alguna vez has intentado conectar tu código de Python, Go o Java que corre en tu sistema operativo host hacia un contenedor Docker de Kafka y has recibido un error como Connection to node -1 could not be established, has sido víctima de la confusión clásica entre KAFKA_LISTENERS y KAFKA_ADVERTISED_LISTENERS.
1. Anatomía del Handshake de Red en Kafka
A diferencia de una base de datos PostgreSQL o MySQL —donde el cliente se conecta a una IP y un puerto y permanece en ese socket para siempre—, Apache Kafka opera mediante un protocolo de dos fases:
- Fase 1 (Metadatos): El cliente (tu script en host) contacta al
bootstrap-serverenlocalhost:9092pidiendo la lista de líderes para cada partición. - Fase 2 (Conexión Real): El broker responde al cliente entregándole la dirección configurada en
KAFKA_ADVERTISED_LISTENERS. A partir de ese milisegundo, el cliente intentará enviar y leer mensajes exclusivamente a esa dirección devuelta.
¿Dónde reside el error clásico? Si en tu Docker configuras KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092, tu script en la laptop recibe como respuesta: "Habla con el broker en kafka:9092". Como tu sistema operativo local no tiene en su archivo /etc/hosts ni en su DNS la resolución del nombre kafka, la conexión falla por timeout de forma inmediata.
Por el contrario, si configuras únicamente localhost:9092, otros contenedores dentro de la misma red de Docker (como un servicio de Kafka UI, un microservicio consumidor o un conector) no podrán resolver localhost como el broker, porque para ellos localhost es su propio contenedor aislado.
⚠️ El Antipatrón más Costoso en Docker
Nunca definas un único listener si planeas interactuar con Kafka tanto desde tu máquina host (localhost) como desde otros contenedores de la red Docker. La regla de oro es configurar dos listeners independientes con protocolos distintos: uno interno (por ejemplo en el puerto 29092) y uno externo expuesto (en el puerto 9092).
2. La Solución Arquitectónica Dual Listener
Para que el entorno funcione sin fisuras tanto dentro como fuera de Docker, definimos un mapeo de seguridad con tres endpoints bien diferenciados:
CONTROLLER(puerto 29093): Utilizado internamente por el motor KRaft para el consenso Raft entre controladores. Nunca se expone al host.PLAINTEXT(puerto 29092): Utilizado por otros contenedores que comparten la red Docker bridge (por ejemplo:PLAINTEXT://kafka:29092).PLAINTEXT_HOST(puerto 9092): Expuesto hacia tu máquina física conPLAINTEXT_HOST://localhost:9092.
Archivo docker-compose.yml de Producción: KRaft Single-Node Listo para Ejecutar
A continuación tienes la especificación completa, probada y modular de docker-compose.yml. Utiliza la imagen oficial de Confluent Platform configurada con un nodo combinado (broker + controller), límites de memoria JVM Heap para evitar saturar la RAM de tu equipo y volúmenes montados para que no pierdas tus tópicos ni tus mensajes al reiniciar Docker:
version: '3.8'
services:
kafka:
image: confluentinc/cp-kafka:7.7.0
container_name: kafka-kraft
hostname: kafka
ports:
- "9092:9092"
environment:
# 1. Configuración de Roles KRaft
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
# 2. Arquitectura de Listeners Dual (Interno + Externo)
KAFKA_LISTENERS: 'PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
# 3. Identificador de Clúster Determinado para KRaft
CLUSTER_ID: '4L622nShTWWkrFuMmISJAK'
# 4. Políticas de Replicación para Broker Único (Local)
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
# 5. Optimización de Memoria y Rendimiento
KAFKA_LOG_DIRS: '/var/lib/kafka/data'
KAFKA_HEAP_OPTS: "-Xmx512M -Xms512M"
volumes:
- kafka_data:/var/lib/kafka/data
networks:
- kafka-net
volumes:
kafka_data:
driver: local
networks:
kafka-net:
driver: bridge
Despliegue en un Solo Comando
Crea un directorio limpio en tu terminal, guarda el archivo anterior como docker-compose.yml y ejecuta:
# Iniciar el servicio en segundo plano
docker compose up -d
# Verificar el estado y salud del contenedor
docker ps --filter "name=kafka-kraft"
En menos de 4 segundos verás el estado Up con los puertos correctamente expuestos. Ya tienes un motor de streaming KRaft plenamente funcional sin haber tocado ni una sola línea de ZooKeeper.
Verificación End-to-End con la CLI de Kafka: Topics, Producer y Consumer
Para comprobar de forma empírica que el clúster responde con baja latencia y que la persistencia funciona, ejecutaremos los tres comandos esenciales del toolkit oficial de Apache Kafka interactuando directamente dentro del contenedor:
1. Crear un Tópico con Particiones
Accede a la terminal del contenedor o ejecuta directamente el script kafka-topics apuntando al listener local:
docker exec -it kafka-kraft kafka-topics \
--create \
--topic eventos-telemetria \
--partitions 3 \
--replication-factor 1 \
--bootstrap-server localhost:9092
Puedes verificar los detalles del tópico y la distribución de sus particiones con el flag --describe:
docker exec -it kafka-kraft kafka-topics \
--describe \
--topic eventos-telemetria \
--bootstrap-server localhost:9092
2. Enviar Mensajes con kafka-console-producer
Abre un flujo de publicación interactivo. Cada línea que escribas y confirmes con Enter será serializada y almacenada en el log append-only:
docker exec -it kafka-kraft kafka-console-producer \
--topic eventos-telemetria \
--bootstrap-server localhost:9092
> {"sensor_id": "sensor_01", "temperatura": 24.5, "timestamp": 1730000001}
> {"sensor_id": "sensor_02", "temperatura": 26.1, "timestamp": 1730000002}
> {"sensor_id": "sensor_03", "temperatura": 22.8, "timestamp": 1730000003}
(Pulsa Ctrl + C para cerrar el productor).
3. Consumir el Flujo con kafka-console-consumer
En otra terminal, lee los eventos desde el offset inicial (--from-beginning) para comprobar la lectura persistente:
docker exec -it kafka-kraft kafka-console-consumer \
--topic eventos-telemetria \
--from-beginning \
--bootstrap-server localhost:9092
Verás de inmediato los tres payloads JSON en pantalla. El circuito completo de producción, persistencia y consumo queda verificado al 100%.
Buenas Prácticas FinOps y Próximos Pasos hacia Google Cloud
En entornos locales y de laboratorio empresarial, aplicar disciplina FinOps y de gestión de recursos evita que Docker consuma toda la memoria de tu workstation. Presta especial atención a estos dos parámetros:
- Límites de JVM Heap (
KAFKA_HEAP_OPTS): Por defecto, Kafka puede intentar reservar el 50% de la memoria física de tu máquina. Fijar-Xmx512M -Xms512Mes suficiente para procesar decenas de miles de mensajes por segundo en pruebas sin degradar tu sistema. - Retención de Logs en Disco: Si realizas pruebas con grandes volúmenes de datos sintéticos, ajusta
log.retention.hours=12ylog.segment.bytes=104857600(100 MB) para impedir que los volúmenes de Docker llenen tu disco duro.
🛡️ Evolución Arquitectónica hacia la Nube
Levantar Kafka en Docker es el estándar indiscutible para prototipar rápidamente y diseñar contratos de datos. Sin embargo, cuando estos eventos deben procesarse en producción a gran escala, la arquitectura suele evolucionar: puedes integrar Kafka Connect para descargar eventos directamente hacia tablas particionadas en BigQuery o evaluar si tu caso de uso se beneficia de una arquitectura event-driven con Google Cloud Pub/Sub completamente serverless y libre de mantenimiento de infraestructura.
Asimismo, para enriquecer, limpiar y agregar estos streams en ventanas de tiempo (tumbling o sliding windows), el paso natural es conectar estos tópicos con pipelines de streaming con Apache Beam y Dataflow hacia BigQuery, garantizando procesamiento exactly-once a escala global.
Preguntas Frecuentes sobre Apache Kafka en Docker con KRaft (FAQ)
CLUSTER_ID definido, pueden autogenerar un UUID en el primer arranque. No obstante, para entornos locales reproducibles donde montas volúmenes persistentes en tu disco, declarar una variable fija CLUSTER_ID: '4L622nShTWWkrFuMmISJAK' garantiza que el contenedor siempre arranque contra el mismo identificador de almacenamiento sin arrojar errores de discrepancia de clúster.
docker-compose.yml la regla dual de listeners. Define PLAINTEXT_HOST://0.0.0.0:9092 en tus listeners y PLAINTEXT_HOST://localhost:9092 en tus advertised listeners, mapeando el puerto 9092:9092. En tu código de cliente (por ejemplo, con la librería confluent-kafka en Python), especifica bootstrap.servers: 'localhost:9092'. Nunca utilices el nombre de host interno kafka:29092 desde fuera de Docker, ya que tu sistema operativo no podrá resolverlo.
KAFKA_PROCESS_ROLES: 'broker,controller') es la opción más eficiente y económica. Sin embargo, en arquitecturas de producción de misión crítica, la recomendación oficial es segregar funciones: contar con un quórum de 3 o 5 nodos actuando exclusivamente como controller dedicados al consenso Raft, y un grupo independiente de brokers dedicados a la ingestión y entrega masiva de datos a clientes.
