Infraestructura como código — Docker

Dockham City

Una ciudad de microservicios en Docker. Stack completo con balanceo de carga, caché distribuido, búsqueda, observabilidad, generación de tráfico y base de datos externa.

HAProxy SSL + INT Redis Master/Slave/Sentinel Solr 9.10.1 Prometheus + Exporters Grafana Dashboards WebDAV · LDAP Oracle 23ai Free
11 Secciones
01
HAProxy
SSL termination · Balanceo · Stats
02
Redis Cluster
Master · Slave · Sentinel · Auth
03
Solr
9.10.1 · Core · Persistencia
04
WebDAV
Apache · mod_dav · Auth Digest
05
LDAP
OpenLDAP · osixia · Directorio
06
Dependencias de arranque
depends_on · healthcheck · orden
07
Prometheus
Exporters · Scrape · Pipeline
08
Grafana
Dashboards · Redis · Solr · HAProxy
09
Oracle 23ai Free
VM dedicada · Red segmentada · NAT
10
Registro de puertos
VMs · Contenedores · Servicios externos
11
Traffic Generator
Tráfico sostenido · Grafana · Workers
Sección 01

HAProxy

Dos instancias de HAProxy encadenadas: una para SSL termination expuesta al exterior, otra para balanceo interno entre los backends Nginx. Cada instancia tiene su propio Dockerfile y configuración montada via bind mount.

Flujo de tráfico

Arquitectura en dos capas

haproxy1-ssl
Recibe tráfico externo en el puerto 443. Termina SSL y reenvía al HAProxy interno. Stats en :8405.
haproxy1-int
Recibe tráfico del SSL en el puerto 80. Balancea entre ngx1 y ngx2 usando round robin. Stats en :8404.
ngx1 / ngx2
Contenedores Nginx que simulan los servicios Java reales. Solo accesibles dentro de la red interna Docker. Exponen /stub_status en puerto 80.

Stub status en Nginx

server {
    listen 80;

    location /stub_status {
        stub_status;
    }
}

Stats de HAProxy

frontend stats
    bind *:8404
    stats enable
    stats uri /stats
⚠️

Healthcheck en haproxy1-int: la imagen haproxy:2.8 no incluye wget ni curl. El healthcheck usa el binario nativo de HAProxy para validar la configuración.

healthcheck:
  test: ["CMD", "haproxy", "-c", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]
  interval: 5s
  timeout: 3s
  retries: 5
Sección 02

Redis Cluster

Arquitectura master/slave con sentinel para alta disponibilidad. No es un Redis Cluster en sentido estricto — es replicación con failover automático via Sentinel. Imagen Bitnami para el sentinel debido a un bug en Redis 8 con la resolución de hostnames.

Componentes

Tres servicios coordinados

redis-master
Master

Imagen redis:8.2.5-alpine. Autenticación habilitada. Persistencia RDB en named volume redis-master. Bind en 0.0.0.0:6379.

redis-slave
Slave

Imagen redis:8.2.5-alpine. Replica al master via replicaof redis-master 6379. Persistencia RDB en named volume redis-slave.

redis-sentinel
Sentinel

Imagen bitnami/redis-sentinel:latest. Monitorea al master bajo alias mymaster. Configurado via variables de entorno para evitar bug de resolución de hostnames en Redis 8.

ℹ️

¿Por qué Bitnami para el sentinel? En Redis 8, el hostname del master se resuelve al leer sentinel.conf, antes de que la red Docker esté disponible. La imagen Bitnami configura el sentinel via variables de entorno, evitando el problema. El sentinel tiene depends_on con condition: service_healthy sobre master y slave.

Sección 03

Solr

Motor de búsqueda basado en Apache Lucene. Versión 9.10.1 elegida sobre la 10.0.0 aplicando el criterio de no migrar y actualizar simultáneamente.

Configuración

Instalación y core de prueba

El servicio expone el puerto 8983 al host, donde está disponible la interfaz de administración web. Los datos se persisten en un named volume montado en /var/solr dentro del contenedor.

# Crear core de prueba
docker exec -it solr solr create_core -c test

# Verificar que el core persiste ante reinicios
docker compose stop solr
docker compose start solr

Verificación de persistencia: el core test persiste correctamente ante reinicios del contenedor gracias al named volume.

Sección 04

WebDAV

Servidor de archivos compartidos sobre HTTP usando Apache HTTP Server con mod_dav. Autenticación Digest para el endpoint de uploads.

Configuración

Módulos y estructura

ParámetroValor
Imagen basehttpd:2.4-alpine
Puerto8081→80
Volumen htdocswebdav → /usr/local/apache2/htdocs/
Volumen uploadswebdav-up → /usr/local/apache2/uploads/

Módulos Apache habilitados

LoadModule dav_module modules/mod_dav.so
LoadModule dav_fs_module modules/mod_dav_fs.so
LoadModule dav_lock_module modules/mod_dav_lock.so
LoadModule auth_digest_module modules/mod_auth_digest.so
LoadModule socache_dbm_module modules/mod_socache_dbm.so
⚠️

Nota Alpine: se requiere instalar apr-util-dbm_gdbm vía apk porque la imagen Alpine no incluye el driver DBM necesario para DavLockDB.

Verificar funcionamiento

# OPTIONS debe mostrar: PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, DELETE
curl -v -X OPTIONS http://localhost:8081/uploads/

# Upload debe responder 201 Created
curl -v -T archivo.txt --digest -u admin:password http://localhost:8081/uploads/

Regenerar user.passwd

docker exec -it webdav htdigest -c /usr/local/apache2/user.passwd DAV-upload admin
docker cp webdav:/usr/local/apache2/user.passwd ./webdav/
Sección 05

LDAP

Servidor de directorio centralizado de usuarios basado en OpenLDAP. Permite autenticación y autorización centralizada para todos los servicios de la plataforma. Sin puertos expuestos al host — solo accesible dentro de la red Docker.

Imagen y variables

osixia/openldap:1.5.0

No existe imagen oficial en Docker Hub para OpenLDAP. La imagen de Bitnami fue discontinuada. Se utilizó osixia/openldap por ser la más mantenida y documentada de la comunidad.

VariableDescripción
LDAP_ROOTSufijo base del directorio. Ej: dc=empresa,dc=com
LDAP_ADMIN_USERNAMEUsuario administrador del directorio
LDAP_ADMIN_PASSWORDContraseña del administrador
LDAP_ORGANISATIONNombre de la organización raíz del árbol
VolumenRuta en contenedorDescripción
ldap_data/var/lib/ldapDatos del directorio LDAP
ldap_config/etc/ldap/slapd.dConfiguración del servidor slapd
ℹ️

El servicio es accesible únicamente dentro de la red interna Docker por nombre de contenedor (ldap) en los puertos 389 (LDAP) y 636 (LDAPS). No se exponen puertos al host.

Sección 06

Dependencias de arranque

El orden de arranque en Docker Compose sin depends_on es no determinista. HAProxy intenta resolver los nombres de los backends al leer su configuración — si el contenedor destino no está listo, falla con could not resolve address.

Solución

depends_on con conditions

service_started
haproxy1-int depende de ngx1 y ngx2 con esta condition. Alcanza cuando solo necesitás que el proceso haya iniciado.
service_healthy
haproxy1-ssl depende de haproxy1-int con esta condition. Requiere que el contenedor tenga healthcheck definido y lo pase.
healthcheck
La imagen haproxy:2.8 no incluye wget ni curl. Se usa el binario nativo: haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg.
⚠️

Renombrar la carpeta raíz cambia el nombre de la red y el prefijo de los contenedores — los contenedores anteriores quedan huérfanos y hay que eliminarlos manualmente con docker compose up --remove-orphans.

Recrear servicios individualmente

# Recrear sin bajar el stack completo
docker compose up --force-recreate <nombre_servicio>

# Limpiar contenedores huérfanos
docker compose up --remove-orphans
Sección 07

Prometheus — Observabilidad

Stack de observabilidad completo. Prometheus funciona en modo pull — va a buscar las métricas a cada exporter en el intervalo configurado. Un exporter por cada servicio monitoreable.

Pipeline

Flujo de métricas

Servicio
El servicio produce métricas internas (HAProxy stats, Nginx stub_status, Redis INFO, Solr API).
Exporter
Proceso intermediario que traduce las métricas al formato Prometheus y las expone en /metrics. Cada tecnología tiene su propio exporter.
Prometheus
Hace scrape de cada exporter según el intervalo configurado. Almacena las series temporales en su base de datos interna. Accesible en :9090.

Tabla de exporters

ContenedorImagenPuertoScrape target
ha1-int_exporterquay.io/prometheus/haproxy-exporter9101haproxy1-int:8404/stats;csv
ha1-ssl_exporterquay.io/prometheus/haproxy-exporter9101haproxy1-ssl:8404/stats;csv
ngx1_exporternginx/nginx-prometheus-exporter9113ngx1:80/stub_status
ngx2_exporternginx/nginx-prometheus-exporter9113ngx2:80/stub_status
redis_exporteroliver006/redis_exporter9121master / slave / sentinel
solr_exporternoony/prometheus-solr-exporter9231solr:8983
🔧

Fix HAProxy exporter: el scrape URI incluye ;csv. El ; debe pasarse como lista en el compose para evitar que el shell lo interprete como separador de comandos.

command:
  - "--haproxy.scrape-uri=http://haproxy1-int:8404/stats;csv"
🔧

Fix Redis exporter: el master tiene autenticación. Sin REDIS_PASSWORD el exporter recibe NOAUTH Authentication required y reporta redis_up 0. Puerto debe mapearse como fijo 9121:9121 para evitar puerto efímero aleatorio.

redis_exporter:
  image: oliver006/redis_exporter
  ports:
    - "9121:9121"
  environment:
    - REDIS_ADDR=redis://redis-master:6379
    - REDIS_PASSWORD=<password_del_master>
🔧

Fix Solr exporter: la imagen usa flags con doble guión (--). La URL no debe incluir /solr al final — Solr 9 expone la API admin directamente en la raíz del puerto.

solr_exporter:
  image: noony/prometheus-solr-exporter
  ports:
    - "9231:9231"
  command: ["--solr.address", "http://solr:8983"]

Verificar targets

# Reload en caliente
curl -X POST http://localhost:9090/-/reload

# Ver estado de todos los targets
http://localhost:9090/targets
Sección 08

Grafana — Visualización

Grafana no recolecta datos — los lee desde Prometheus y los presenta en dashboards. Datasource configurado apuntando al contenedor Prometheus por nombre de servicio dentro de la red Docker.

Dashboards

Dashboards instalados

DashboardIDServicioNotas
NGINX exporter12708ngx1, ngx2
HAProxy12693haproxy1-int, haproxy1-sslVariable host corregida
Redis Dashboard763redis-master, slave, sentinelRequiere auth en exporter
Solr MonitoringsolrCreado desde cero
🔧

Fix dashboard HAProxy 12693: la variable host busca la etiqueta instance en la métrica haproxy_process_nbproc, que no existe en el exporter legacy. Se corrigió desde Settings → Variables → host cambiando el campo Metric a haproxy_frontend_bytes_in_total.

dashboard solr — creado desde cero

Solr Monitoring — paneles

Dashboard construido manualmente usando las métricas del exporter noony/prometheus-solr-exporter.

PanelMétricaTipo
JVM Heap Usagesolr_jvm_memory_heap_usageGauge
JVM Heap Usedsolr_jvm_memory_heap_usedTime series
Threads Activossolr_jvm_threads_runnable_countStat
Open File Descriptorssolr_jvm_os_openfiledescriptorcountStat

Conceptos clave

Datasource
Conexión a Prometheus

Los servicios se comunican por nombre dentro de la red Docker. URL: http://prometheus:9090.

Dashboard
Colección de paneles

Cada dashboard agrupa paneles relacionados. Se puede importar por ID desde grafana.com o construir desde cero.

Panel
Unidad mínima

Cada panel tiene una query PromQL y un tipo de visualización: Time series, Gauge, Stat, Table.

Sección 09

Oracle Database 23ai Free

Oracle corre fuera de Docker en una VM dedicada con Oracle Linux 8.10. Esta decisión replica el modelo productivo real donde las bases de datos críticas no se contienen: requieren control total de recursos y no se benefician del modelo efímero de los contenedores.

Red segmentada

Arquitectura de red

Windows host      192.168.56.1   (acceso SSH y SQL Developer)
VM Ubuntu 24      192.168.56.10  (Docker + gateway hacia internet)
VM Oracle Linux   192.168.56.20  (Oracle Database, sin internet directo)
Segmentación
Defensa en profundidad

Cada capa vive en su propia red. Oracle solo es alcanzable por el puerto 1521 desde la red interna.

IP forwarding
Ubuntu como router

La VM Ubuntu reenvía tráfico entre interfaces (enp0s8 → enp0s3), funcionando como gateway.

NAT masquerading
iptables

El tráfico que sale desde Oracle Linux hacia internet aparece con la IP de Ubuntu, no con la de Oracle.

IPs fijas
Sin DHCP

En producción los servidores de base de datos nunca usan DHCP. Toda IP está asignada administrativamente.

Reglas iptables para NAT masquerading

sudo iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE
sudo iptables -A FORWARD -i enp0s8 -o enp0s3 -j ACCEPT
sudo iptables -A FORWARD -i enp0s3 -o enp0s8 -m state --state RELATED,ESTABLISHED -j ACCEPT
⚠️

Las reglas de iptables no persisten ante reinicios por defecto. Para hacerlas persistentes usar iptables-persistent.

Información de la instancia

ParámetroValor
SIDFREE
CDBFREE
PDBFREEPDB1
Puerto listener1521
Límites2 CPUs · 2 GB RAM · 12 GB datos

Verificación

# Estado del servicio
/etc/init.d/oracle-free-23ai status

# Conectarse como SYSDBA
su - oracle && sqlplus / as sysdba

# Verificar instancia y PDB
SELECT status FROM v$instance;
SELECT name, open_mode FROM v$pdbs;

# Verificar conectividad desde Ubuntu
curl -v telnet://192.168.56.20:1521
Sección 10

Registro de puertos e IPs

Mapa completo de toda la infraestructura: VMs, contenedores Docker y servicios externos.

VMs e infraestructura

Hosts y redes

HostIPRol
Windows host192.168.56.1Acceso SSH y administración
VM Ubuntu 24192.168.0.x (bridge, DHCP)Docker host, internet
VM Ubuntu 24192.168.56.10 (host-only, fija)Gateway para Oracle
VM Oracle Linux 8192.168.56.20 (host-only, fija)Oracle Database 23ai Free
contenedores docker
ContenedorPuerto hostPuerto contenedorDescripción
haproxy1-ssl8080443HTTPS externo (SSL termination)
haproxy1-ssl84058404HAProxy SSL stats
haproxy1-int44380Balanceo interno round-robin
haproxy1-int84048404HAProxy INT stats
ngx1Solo red interna Docker
ngx2Solo red interna Docker
redis-master6379Solo red interna Docker
redis-slave6379Solo red interna Docker
redis-sentinel26379Solo red interna Docker
solr89838983Panel admin Solr
webdav808180WebDAV HTTP
ldap389 / 636Solo red interna Docker
ha1-int_exporter9101Exporter HAProxy INT → Prometheus
ha1-ssl_exporter9101Exporter HAProxy SSL → Prometheus
ngx1_exporter9113Exporter nginx ngx1 → Prometheus
ngx2_exporter9113Exporter nginx ngx2 → Prometheus
redis_exporter91219121Exporter Redis → Prometheus
solr_exporter92319231Exporter Solr → Prometheus
prometheus90909090Recolección de métricas
grafana30003000Visualización de métricas
traffic-generatorGenerador de tráfico interno
servicios externos
ServicioHostPuertoProtocolo
Oracle Database 23ai Free192.168.56.201521TCP (TNS)
Sección 11

Traffic Generator

Servicio dedicado a generar tráfico sostenido y variado hacia todos los componentes de la arquitectura. No es stress testing — el objetivo es mantener datos reales y continuos en los dashboards de Grafana para análisis y visualización.

Arquitectura

Contenedor interno a la red Docker

El generador corre dentro de la red Docker, accediendo a los servicios directamente por nombre de contenedor. Esto permite alcanzar servicios sin puertos expuestos al host como redis-master, ngx1 y ngx2.

ℹ️

Imagen base: python:3.12-alpine con dependencias requests y redis-py. El script generator.py lanza threads paralelos por componente y corre indefinidamente.

Workers por componente

ComponenteThreadsDelay baseOperaciones
HAProxy (int)50.3sGET 80% · POST 15% · HEAD 5% — 10 rutas, User-Agents variados
Redis40.1sSET/GET (mix hits+misses), INCR, LPUSH, HSET, EXPIRE, DEL
Solr30.5sQueries por término, facets, indexado de docs, borrado periódico
WebDAV21.0sPUT, GET, DELETE, PROPFIND — pool de hasta 50 archivos activos

El delay incluye variación aleatoria de ±40% para evitar tráfico perfectamente uniforme. Cada 15 segundos se loguea un resumen de operaciones ok/err por componente.

Estructura de archivos

traffic-generator/
    Dockerfile         # python:3.12-alpine
    generator.py       # script principal con workers por componente
    requirements.txt   # requests, redis-py

Configuración en docker-compose

traffic-generator:
  container_name: traffic-generator
  build: ./traffic-generator
  environment:
    - STARTUP_WAIT=20   # espera inicial en segundos
  depends_on:
    haproxy1-int:
      condition: service_healthy
    redis-master:
      condition: service_healthy
  restart: unless-stopped

depends_on: el contenedor espera a que haproxy1-int y redis-master pasen su healthcheck antes de arrancar. El STARTUP_WAIT agrega un margen adicional para Solr y WebDAV que no tienen healthcheck definido.

Control del servicio

# Levantar solo el generador (sin bajar el stack completo)
docker compose up -d --build traffic-generator

# Ver logs en tiempo real — resumen cada 15s
docker logs -f traffic-generator

# Pausar sin eliminar el contenedor
docker stop traffic-generator

# Reanudar
docker start traffic-generator
Continuidad
While True por worker

Cada thread corre indefinidamente. El servicio no termina solo — se controla con docker stop/start.

Intensidad
Configurable

Ajustar *_workers y *_delay en el bloque CFG del script para modificar la carga sin tocar la lógica.

Reinicio
unless-stopped

Si la VM se reinicia, el generador vuelve a arrancar automáticamente junto con todo el stack.