El stack OTITIA - Arquitectura IT/OT con Docker
OTITIA
Operaciones y Tecnologías de la Información para la Telemetría Industrial Aplicada. Es el nombre del laboratorio que utilizaremos durante todo el proyecto. Todos los contenedores, redes y volúmenes llevan el prefijo otitia- para no colisionar con otros proyectos que tengas en tu máquina.
A lo largo del proyecto vamos a construir un sistema completo de telemetría industrial: unos sensores generan datos, esos datos viajan por un bus de mensajería, se almacenan en una base de datos de series temporales, se visualizan en cuadros de mando y finalmente se interpretan mediante un modelo de lenguaje.
Montar cada una de esas piezas a mano en nuestro equipo sería inviable en 24 horas de clase: versiones de Java, dependencias de Node, servicios de sistema, puertos en conflicto... Por eso todo el laboratorio se despliega mediante Docker Compose, de forma que con un único comando tengamos el entorno completo funcionando, y con otro lo podamos destruir y volver a empezar.
Repaso de conceptos Docker¶
Antes de entrar en el stack, conviene tener claros cuatro conceptos:
- Imagen: plantilla de solo lectura que contiene un sistema de archivos con una aplicación y todas sus dependencias. Por ejemplo,
influxdb:2.7. Las imágenes se descargan de un registro, normalmente Docker Hub. - Contenedor: una instancia en ejecución de una imagen. Es un proceso aislado del resto del sistema, con su propio sistema de archivos, su propia red y sus propios límites de recursos.
- Volumen: espacio de almacenamiento gestionado por Docker que sobrevive a la destrucción del contenedor. Todo lo que escribamos dentro de un contenedor fuera de un volumen se pierde al eliminarlo.
- Red: red virtual que conecta contenedores entre sí. Dentro de una misma red, los contenedores se localizan por su nombre de host, sin necesidad de conocer direcciones IP.
Docker Compose nos permite describir todo lo anterior (qué servicios, con qué imágenes, en qué redes, con qué volúmenes y qué variables) en un único archivo docker-compose.yml declarativo.
El nombre de host importa
Este es el error más frecuente al configurar el stack. Desde dentro de un contenedor, InfluxDB está en http://influxdb:8086. Desde tu navegador, está en http://localhost:8086.
Cuando configures el nodo de InfluxDB en Node-RED, Node-RED es un contenedor: debes usar influxdb, no localhost. Si pones localhost, Node-RED se buscará a sí mismo y obtendrás un error de conexión.
Arquitectura IT/OT¶
El stack no es una lista plana de servicios: reproduce a escala la separación que existe en cualquier planta industrial real entre dos mundos.
- OT (Operational Technology): la tecnología que está en contacto con el proceso físico. Sensores, autómatas, actuadores, el bus de campo. Prioriza el determinismo y la disponibilidad: si el bus se cae, la planta se para.
- IT (Information Technology): la tecnología corporativa. Bases de datos, cuadros de mando, informes, ERP. Prioriza la integridad del dato y la seguridad.
Durante décadas ambos mundos estuvieron físicamente separados. La convergencia IT/OT (que es de lo que va la Industria 4.0) consiste precisamente en conectarlos, pero de forma controlada: no se abre la planta a la red corporativa, sino que se establece un único punto de cruce vigilado.
En nuestro laboratorio eso se traduce en dos redes Docker y un único servicio que pertenece a las dos:
flowchart LR
subgraph OT["Red otitia-ot (planta)"]
NR[Node-RED<br/>:1880]
MQ[Mosquitto<br/>:1883]
end
subgraph BRIDGE[" "]
TG[Telegraf]
N8[N8N<br/>:5678]
end
subgraph IT["Red otitia-it (corporativa)"]
IDB[(InfluxDB 2.7<br/>:8086)]
IDB3[(InfluxDB 3 Core<br/>:8181)]
MY[(MySQL 8.4<br/>:3306)]
GR[Grafana<br/>:3000]
WP[WordPress<br/>:8080]
end
NR -->|publish| MQ
MQ -->|subscribe| TG
MQ -->|subscribe| N8
TG --> IDB
TG --> IDB3
N8 --> MY
NR -.->|escritura directa| IDB
IDB --> GR
IDB3 --> GR
MY --> GR
La línea continua marca el recorrido del dato en su forma final (fase 3): todo pasa por el bus y Telegraf se encarga de la ingesta. La línea discontinua es la fase 1, la escritura directa con la que empezaremos y que iremos abandonando a medida que el diseño madure.
La lectura de este diagrama es la clave del proyecto:
| Servicio | Red OT | Red IT | Papel |
|---|---|---|---|
| Mosquitto | ✅ | ✅ | Broker MQTT. Único puente entre ambos mundos |
| Node-RED | ✅ | ✅ | Adquisición y simulación de sensórica. Vive en planta, pero con acceso a IT |
| Telegraf | ✅ | ✅ | Agente de ingesta: lee del bus, escribe en la TSDB |
| N8N | ✅ | ✅ | Orquestación alternativa a Telegraf |
| InfluxDB 2.7 | ❌ | ✅ | Base de datos de series temporales (principal) |
| InfluxDB 3 Core | ❌ | ✅ | Nueva generación, para comparar |
| MySQL | ❌ | ✅ | Base de datos relacional (alarmas, maestros) |
| Grafana | ❌ | ✅ | Visualización y alertado |
| WordPress | ❌ | ✅ | Publicación de resultados |
Node-RED está en las dos redes, ¿no rompe eso la frontera?
Sí, la rompe. Y está puesto así a propósito, porque durante el proyecto vamos a construir la ingesta en tres fases, cada una motivada por la limitación de la anterior:
- Node-RED → InfluxDB. Escritura directa. Funciona enseguida, pero hay un único consumidor, Node-RED tiene que conocer las credenciales de la base de datos, y si InfluxDB no responde el dato se pierde.
- Node-RED → MQTT → Node-RED → InfluxDB. Aparece el bus y el productor deja de saber quién le escucha. Ahora podemos añadir suscriptores sin tocarlo. Pero la lógica de ingesta sigue siendo código nuestro.
- Node-RED → MQTT → Telegraf → InfluxDB. La ingesta se vuelve declarativa, con buffer, escritura por lotes y reintentos sin escribir código.
La fase 1 es la única que necesita que Node-RED alcance la red IT. Cuando lleguemos a la fase 3 esa conexión ya no hace falta, y al final del proyecto la retiraremos para comprobarlo: dejará de funcionar la fase 1, que a esas alturas es la versión que ya hemos superado, y el resto del sistema seguirá vivo sin enterarse.
Es importante entender que en la fase 3 Telegraf no sustituye a Node-RED: sustituye al consumidor de la fase 2. Node-RED sigue siendo el sensor de principio a fin.
Los servicios, uno a uno¶
Mosquitto¶
Eclipse Mosquitto es el broker MQTT del laboratorio, el sistema nervioso del stack. Todo lo que ocurre en la planta se publica aquí, y todo el que quiera enterarse se suscribe.
- Puerto
1883: MQTT estándar. - Puerto
9001: MQTT sobre WebSockets, útil si algún día quieres consumir el bus desde un navegador. - Configuración en
mosquitto/config/mosquitto.conf, con persistencia activada yallow_anonymous true(aceptable en laboratorio, inaceptable en producción).
Node-RED¶
Herramienta de programación visual low-code donde construiremos los flujos de simulación de sensores, el consumo de APIs externas y la lógica de alertas. Es nuestro sustituto de la sensórica física.
- Puerto
1880. - Está conectado a las redes
oteit, aunque su vía normal de salida es el bus MQTT. - Los flujos se guardan en el volumen
otitia-nodered-data, así que sobreviven a un reinicio del contenedor pero no a undown -v. Exporta siempre tus flujos a JSON.
Telegraf¶
Agente de recolección de métricas de InfluxData. Es puramente declarativo: se configura con un archivo TOML y no tiene lógica de negocio.
En nuestro stack se suscribe a los topics de Mosquitto y escribe simultáneamente en las dos versiones de InfluxDB usando el mismo protocolo de línea. No expone ningún puerto porque nadie le habla: él habla a los demás.
InfluxDB 2.7 y InfluxDB 3 Core¶
Las dos generaciones de la base de datos de series temporales conviven en el stack para poder compararlas:
| Aspecto | InfluxDB 2.7 | InfluxDB 3 Core |
|---|---|---|
| Motor | TSM (propio) | Apache Arrow + DataFusion + Parquet |
| Lenguaje principal | Flux | SQL |
| Lenguaje legado | InfluxQL | InfluxQL (compatibilidad) |
| API de escritura | /api/v2/write |
/api/v3/write_lp |
| Puerto | 8086 | 8181 |
| Contenedor de datos | bucket | database |
| Retención larga en OSS | Ilimitada | Limitada en Core |
| Madurez del ecosistema | Muy alta | En crecimiento |
Trabajaremos sobre la 2.7 porque es estable, tiene interfaz web propia y todo el ecosistema (Telegraf, Grafana, clientes) está sobradamente rodado. La 3 Core queda como anexo comparativo: mismo dato entrando, distinto lenguaje de consulta.
MySQL¶
Base de datos relacional para lo que no es una serie temporal: catálogo de dispositivos, registro de alarmas confirmadas, usuarios. También aloja la base de datos de WordPress.
- Puerto
3306, con límites ajustados (--innodb-buffer-pool-size=128M) para no comerse la RAM del portátil. - Los scripts de
mysql/init/se ejecutan solo la primera vez que se crea el volumen.
N8N¶
Plataforma de automatización de flujos, alternativa a Telegraf. Está en las dos redes para poder suscribirse a MQTT y escribir en MySQL.
Su presencia es deliberadamente redundante: queremos que compares el enfoque declarativo y eficiente de Telegraf con el enfoque visual y flexible de N8N, y que sepas argumentar cuál elegirías según el escenario.
Grafana¶
Plataforma de visualización y alertado. Se conecta a las dos InfluxDB y a MySQL, y es donde construiremos el cuadro de mandos final del proyecto.
- Puerto
3000. - Incluye provisioning: los orígenes de datos y los cuadros de mando pueden definirse como archivos versionables en
grafana/provisioning/ygrafana/dashboards/, en lugar de configurarse a mano.
WordPress¶
Instalación Multisite para publicar resultados y documentación del proyecto. Reutiliza el contenedor MySQL con una base de datos y un usuario propios.
- Puerto
8080. - En local conviene usar
WP_DOMAIN=lvh.meen el.env: ese dominio resuelve siempre a127.0.0.1, lo que permite subdominios (web1.lvh.me:8080) sin tocar/etc/hosts.
Estructura del proyecto¶
otitia/
├── docker-compose.yml
├── .env
├── README.md
├── mosquitto/
│ └── config/mosquitto.conf
├── telegraf/
│ └── telegraf.conf
├── mysql/
│ └── init/01-schema.sql
├── grafana/
│ ├── provisioning/
│ │ ├── datasources/datasources.yml
│ │ └── dashboards/dashboards.yml
│ └── dashboards/
│ ├── 01-telemetria-v2.json
│ ├── 02-telemetria-v3.json
│ └── 03-alarmas-mysql.json
├── wordpress/
│ ├── uploads.ini
│ └── README.md
└── deploy/
├── install.sh
├── otitia.service
└── check-disk.sh
Fíjate en un detalle importante: los datos no están aquí. Los archivos del proyecto son solo configuración; los datos viven en volúmenes gestionados por Docker. Eso significa que puedes versionar toda esta carpeta en Git sin miedo (salvo el .env, que contiene credenciales).
El archivo .env¶
Compose sustituye automáticamente las variables ${VARIABLE} del docker-compose.yml por los valores de este archivo. Así el compose es público y las credenciales quedan aparte.
# === Proyecto ===
COMPOSE_PROJECT_NAME=otitia
# === InfluxDB v2 ===
INFLUXDB_USER=admin
INFLUXDB_PASSWORD=admin12345
INFLUXDB_ORG=iabd
INFLUXDB_BUCKET=telemetria
INFLUXDB_TOKEN=token-v2-para-curso
# === InfluxDB v3 Core ===
INFLUXDB3_NODE_ID=node0
INFLUXDB3_TOKEN=token-v3-para-curso
INFLUXDB3_DATABASE=telemetria
# === MySQL ===
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=iot
MYSQL_USER=iot
MYSQL_PASSWORD=iot
# === Grafana ===
GF_ADMIN_USER=admin
GF_ADMIN_PASSWORD=admin
# === N8N ===
N8N_USER=admin
N8N_PASSWORD=admin
# === WordPress ===
WP_DOMAIN=lvh.me
# === Zona horaria ===
TZ=Europe/Madrid
Nunca subas el .env a un repositorio
Añádelo siempre al .gitignore. Lo habitual es versionar un .env.example con los nombres de las variables y valores vacíos o de ejemplo, para que quien clone el proyecto sepa qué tiene que rellenar.
Anatomía del docker-compose.yml¶
No vamos a reproducir aquí las 300 líneas del archivo, pero sí conviene entender los cuatro mecanismos que utiliza.
Anclas YAML para el logging¶
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Un servicio que lleve horas escribiendo logs puede llenar el disco. Esta configuración limita cada servicio a 3 archivos de 10 MB. La clave x-logging es una extensión (Compose ignora las claves que empiezan por x-), y &default-logging define un ancla YAML que luego cada servicio reutiliza con logging: *default-logging sin repetir el bloque.
Límites de memoria¶
mem_limit: 384m
Cada servicio tiene un techo de memoria. Sin ellos, InfluxDB o MySQL pueden crecer hasta ahogar un portátil de 8 GB. Los valores están calibrados para que el stack completo quepa holgadamente en 16 GB y sobreviva en 8 GB.
Perfiles¶
profiles: ["full", "v2", "v3"]
Los perfiles permiten arrancar subconjuntos del stack. Un servicio con perfiles solo se levanta si activas uno de ellos:
| Perfil | Qué levanta | RAM aprox. |
|---|---|---|
full |
Todo, incluido WordPress | ~4.5 GB |
lite |
Sin InfluxDB ni Telegraf | ~2.2 GB |
v2 |
Con InfluxDB v2, sin v3 ni WordPress | ~3 GB |
v3 |
Con InfluxDB v3, sin v2 ni WordPress | ~3.2 GB |
wp |
Solo WordPress + MySQL | ~1.2 GB |
Durante la mayor parte del proyecto usaremos el perfil v2, que es el mínimo necesario para el pipeline completo.
Redes y volúmenes nombrados¶
networks:
ot:
name: otitia-ot
driver: bridge
it:
name: otitia-it
driver: bridge
volumes:
influxdb_data:
name: otitia-influxdb-data
# ...
Todos los recursos llevan nombre explícito con prefijo otitia-. Sin esto, Docker generaría nombres derivados del directorio (otitia_influxdb_data), lo que dificulta identificarlos con docker volume ls.
Puesta en marcha¶
Arranque¶
# Situarse en el directorio del proyecto
cd otitia
# Levantar el perfil que necesitemos
docker compose --profile v2 up -d
# O el stack completo
docker compose --profile full up -d
La primera vez tardará varios minutos: hay que descargar unos 3 GB de imágenes. Las siguientes veces el arranque es cuestión de segundos.
Verificación¶
# Estado de todos los contenedores
docker compose ps
# Consumo de recursos en tiempo real
docker stats
# Logs de un servicio concreto
docker compose logs -f influxdb
Todos los servicios deben aparecer como running. Si alguno aparece como restarting, se está cayendo en bucle: mira sus logs.
Puntos de acceso¶
| Servicio | URL | Credenciales |
|---|---|---|
| Node-RED | http://localhost:1880 | (sin autenticación) |
| InfluxDB 2.7 | http://localhost:8086 | admin / admin12345 |
| InfluxDB 3 Core | http://localhost:8181 | (sin autenticación) |
| Grafana | http://localhost:3000 | admin / admin |
| N8N | http://localhost:5678 | admin / admin |
| WordPress | http://localhost:8080 | (instalación inicial) |
| MySQL | localhost:3306 |
iot / iot |
| MQTT | localhost:1883 |
(anónimo) |
Comandos habituales¶
# Parar sin borrar datos
docker compose --profile v2 stop
# Parar y eliminar contenedores (los volúmenes se conservan)
docker compose --profile v2 down
# Parar y BORRAR TAMBIÉN LOS DATOS
docker compose --profile v2 down -v
# Reiniciar un único servicio
docker compose restart nodered
# Entrar a un contenedor
docker exec -it otitia-influxdb bash
# Suscribirse al bus MQTT desde el propio broker
docker exec -it otitia-mosquitto mosquitto_sub -t 'otitia/#' -v
down -v borra todo
La opción -v elimina los volúmenes, es decir, todos los datos ingeridos, todos los flujos de Node-RED y todos los cuadros de mando. Úsala solo cuando quieras empezar de cero de forma deliberada. Es la diferencia entre apagar el ordenador y formatearlo.
Versiones de las imágenes¶
Todas las imágenes usan tags fijos, nunca latest. Usar latest significa que un docker compose pull puede introducir una versión nueva que rompa el laboratorio a mitad de curso.
| Servicio | Tag | Notas |
|---|---|---|
eclipse-mosquitto |
2.1 | Rama estable mantenida |
nodered/node-red |
4.1 | Estable rodada |
telegraf |
1.38 | InfluxData publica una minor por trimestre |
influxdb |
2.7 | Última de la serie 2.x |
influxdb |
3-core | Generación nueva |
mysql |
8.4 | Único LTS formal del stack |
n8nio/n8n |
2.11 | Sin LTS, actualizan a diario |
grafana/grafana-oss |
13.0.2 | Sin LTS, entrega mensual |
wordpress |
6.9-php8.4-apache | PHP 8.4 |
(Estado auditado a junio de 2026.)
De todo el ecosistema, solo MySQL ofrece un LTS formal. Para el resto, "fijar la versión y revisarla cada trimestre" es la mejor aproximación disponible a la estabilidad.
Requisitos y avisos por sistema operativo¶
| Escenario | Recomendación |
|---|---|
| Mínimo | 8 GB RAM, 4 núcleos, 20 GB SSD. Funciona, pero justo |
| Recomendado | 16 GB RAM, 4-6 núcleos, 50 GB SSD |
| Cómodo | 32 GB RAM, 8+ núcleos, 100 GB NVMe |
- Windows con WSL2: usa el backend WSL2, no Hyper-V. El proyecto debe vivir dentro del sistema de archivos de WSL (
~/proyectos/otitia), nunca en/mnt/c/...: el rendimiento de entrada/salida cae de forma brutal y el laboratorio se vuelve inusable. - macOS Apple Silicon: funciona perfectamente con imágenes ARM64 nativas.
- macOS Intel: funcional, algo más lento por la virtualización.
- Linux nativo: la mejor experiencia, sin sobrecarga.
En Docker Desktop conviene asignar 6-8 GB de RAM y 2-4 vCPU al motor.
Problemas frecuentes¶
Un puerto ya está ocupado. Si tienes otro InfluxDB o Grafana corriendo, el arranque fallará. Localiza el culpable con docker ps -a o cambia el puerto del lado izquierdo en el compose ("8087:8086").
MySQL se cae nada más arrancar. Si el volumen quedó corrupto de un intento anterior, hay que purgarlo:
docker compose down
docker volume rm otitia-mysql-data
docker compose --profile v2 up -d
Node-RED no conecta con InfluxDB. Casi siempre es el nombre de host: debe ser influxdb, no localhost. Si el nombre es correcto, comprueba que Node-RED sigue teniendo la red it (docker inspect otitia-nodered | grep -A5 Networks); si has hecho el ejercicio de endurecimiento del final del proyecto, ese es precisamente el comportamiento esperado.
Telegraf no escribe nada. Comprueba tres cosas en este orden: que hay mensajes en el bus (mosquitto_sub -t 'otitia/#' -v), que los topics del telegraf.conf coinciden con los que publicas, y que el token del .env es el mismo con el que se inicializó InfluxDB.
Cambio el .env y no pasa nada. Las variables se leen al crear el contenedor. Hay que hacer docker compose up -d --force-recreate. Y ojo: las variables de inicialización de InfluxDB (DOCKER_INFLUXDB_INIT_*) solo se aplican cuando el volumen está vacío; cambiar la contraseña en el .env no cambia la de una instancia ya creada.
Endurecer la frontera IT/OT¶
Al final del proyecto, con el sistema ya montado, haremos un ejercicio de endurecimiento: retirar a Node-RED su acceso a la red corporativa y comprobar que el pipeline sobrevive.
El cambio en el docker-compose.yml consiste en dejar el servicio con una única red:
nodered:
# ...
networks:
- ot # se elimina la línea "- it"
Y aplicarlo recreando solo ese contenedor:
docker compose --profile v2 up -d --force-recreate nodered
No pierdes tus flujos
Recrear el contenedor no toca el volumen otitia-nodered-data, así que todos tus flujos siguen ahí. Lo único que cambia es a qué red está conectado el contenedor.
Tras el cambio, comprueba desde el propio contenedor que la base de datos ya no es alcanzable:
docker exec -it otitia-nodered ping -c2 influxdb
# ping: bad address 'influxdb'
Lo que debe ocurrir a continuación:
- Los flujos que publican en MQTT siguen funcionando: Mosquitto está en las dos redes y es el puente.
- Telegraf sigue ingiriendo: nunca dependió de Node-RED.
- Grafana sigue pintando datos: consulta a InfluxDB, no a Node-RED.
- La fase 1, la escritura directa con
influxdb out, deja de funcionar. Y debe dejar de funcionar: es la versión que ya habíamos superado.
Si además de la fase 1 se te cae algo más, merece la pena investigar por qué: significa que en algún punto introdujiste una dependencia de la planta hacia el mundo IT sin ser consciente de ello.
De laboratorio a producción¶
La configuración que usamos en clase no es segura, y es importante que sepas exactamente por qué:
- Mosquitto acepta conexiones anónimas (
allow_anonymous true). - Las contraseñas y tokens son triviales y están en texto plano.
- Todos los puertos están expuestos a la red.
- No hay cifrado: todo viaja en claro.
Para un despliegue real, los cambios mínimos serían: autenticación en Mosquitto con password_file, credenciales fuertes generadas aleatoriamente, exponer los puertos solo en 127.0.0.1 y poner delante un reverse proxy con HTTPS (Caddy o Traefik), y establecer una política de copias de seguridad de los volúmenes.
En el VPS del aula el stack vive en /opt/otitia, propiedad de un usuario de servicio iotadmin, con una unidad systemd que lo arranca tras cada reinicio, cortafuegos UFW y fail2ban. Tienes el procedimiento completo en deploy/README.md.
Referencias¶
Actividades¶
-
(RABDA.1 / CEBDA.1d / 1p) Documenta el stack: para cada servicio, indica imagen y versión, puertos publicados, redes a las que pertenece, volúmenes que utiliza y su papel dentro del pipeline. Acompáñalo de un diagrama del flujo de datos de extremo a extremo.
-
(RABDA.1 / CEBDA.1d / 1p) Arranca el perfil
v2y comprueba que los servicios responden. Adjunta capturas de Node-RED, InfluxDB UI y Grafana funcionando, junto con la salida dedocker compose psydocker stats. -
(RAMIA.4 / CEMIA.4d / 1p) Justifica la separación IT/OT del laboratorio: qué servicios pertenecen a cada red, cuál actúa de puente y por qué. Explica qué implicaciones tiene que Node-RED disponga de acceso a la red corporativa y en qué escenarios de una planta real lo permitirías y en cuáles no.
-
(RAMIA.4 / CEMIA.4d / 1p) (Al final del proyecto.) Retira la red
itdel servicionodered, recrea el contenedor y documenta qué partes del sistema siguen funcionando y cuáles fallan. Justifica por qué el pipeline principal sobrevive y qué te dice eso sobre el acoplamiento de tu diseño. -
(opcional) Ejecuta
docker compose down -v, vuelve a levantar el stack y comprueba qué has perdido. ¿Qué elementos del proyecto deberías haber exportado antes para poder recuperar tu trabajo?