Saltar a contenido

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:

  1. 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.
  2. 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.
  3. 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 y allow_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 ot e it, 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 un down -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/ y grafana/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.me en el .env: ese dominio resuelve siempre a 127.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

  1. (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.

  2. (RABDA.1 / CEBDA.1d / 1p) Arranca el perfil v2 y comprueba que los servicios responden. Adjunta capturas de Node-RED, InfluxDB UI y Grafana funcionando, junto con la salida de docker compose ps y docker stats.

  3. (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.

  4. (RAMIA.4 / CEMIA.4d / 1p) (Al final del proyecto.) Retira la red it del servicio nodered, 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.

  5. (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?