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.
:8405.ngx1 y ngx2 usando round robin. Stats en :8404./stub_status en puerto 80.server {
listen 80;
location /stub_status {
stub_status;
}
}
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
Imagen redis:8.2.5-alpine. Autenticación habilitada. Persistencia RDB en named volume redis-master. Bind en 0.0.0.0:6379.
Imagen redis:8.2.5-alpine. Replica al master via replicaof redis-master 6379. Persistencia RDB en named volume redis-slave.
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.
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.
| Parámetro | Valor |
|---|---|
| Imagen base | httpd:2.4-alpine |
| Puerto | 8081→80 |
| Volumen htdocs | webdav → /usr/local/apache2/htdocs/ |
| Volumen uploads | webdav-up → /usr/local/apache2/uploads/ |
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.
# 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/
docker exec -it webdav htdigest -c /usr/local/apache2/user.passwd DAV-upload admin docker cp webdav:/usr/local/apache2/user.passwd ./webdav/
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.
| Variable | Descripción |
|---|---|
| LDAP_ROOT | Sufijo base del directorio. Ej: dc=empresa,dc=com |
| LDAP_ADMIN_USERNAME | Usuario administrador del directorio |
| LDAP_ADMIN_PASSWORD | Contraseña del administrador |
| LDAP_ORGANISATION | Nombre de la organización raíz del árbol |
| Volumen | Ruta en contenedor | Descripción |
|---|---|---|
| ldap_data | /var/lib/ldap | Datos del directorio LDAP |
| ldap_config | /etc/ldap/slapd.d | Configuració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.
haproxy1-int depende de ngx1 y ngx2 con esta condition. Alcanza cuando solo necesitás que el proceso haya iniciado.haproxy1-ssl depende de haproxy1-int con esta condition. Requiere que el contenedor tenga healthcheck definido y lo pase.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 sin bajar el stack completo docker compose up --force-recreate <nombre_servicio> # Limpiar contenedores huérfanos docker compose up --remove-orphans
/metrics. Cada tecnología tiene su propio exporter.:9090.| Contenedor | Imagen | Puerto | Scrape target |
|---|---|---|---|
| ha1-int_exporter | quay.io/prometheus/haproxy-exporter | 9101 | haproxy1-int:8404/stats;csv |
| ha1-ssl_exporter | quay.io/prometheus/haproxy-exporter | 9101 | haproxy1-ssl:8404/stats;csv |
| ngx1_exporter | nginx/nginx-prometheus-exporter | 9113 | ngx1:80/stub_status |
| ngx2_exporter | nginx/nginx-prometheus-exporter | 9113 | ngx2:80/stub_status |
| redis_exporter | oliver006/redis_exporter | 9121 | master / slave / sentinel |
| solr_exporter | noony/prometheus-solr-exporter | 9231 | solr: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"]
# Reload en caliente curl -X POST http://localhost:9090/-/reload # Ver estado de todos los targets http://localhost:9090/targets
| Dashboard | ID | Servicio | Notas |
|---|---|---|---|
| NGINX exporter | 12708 | ngx1, ngx2 | — |
| HAProxy | 12693 | haproxy1-int, haproxy1-ssl | Variable host corregida |
| Redis Dashboard | 763 | redis-master, slave, sentinel | Requiere auth en exporter |
| Solr Monitoring | — | solr | Creado 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 construido manualmente usando las métricas del exporter noony/prometheus-solr-exporter.
| Panel | Métrica | Tipo |
|---|---|---|
| JVM Heap Usage | solr_jvm_memory_heap_usage | Gauge |
| JVM Heap Used | solr_jvm_memory_heap_used | Time series |
| Threads Activos | solr_jvm_threads_runnable_count | Stat |
| Open File Descriptors | solr_jvm_os_openfiledescriptorcount | Stat |
Los servicios se comunican por nombre dentro de la red Docker. URL: http://prometheus:9090.
Cada dashboard agrupa paneles relacionados. Se puede importar por ID desde grafana.com o construir desde cero.
Cada panel tiene una query PromQL y un tipo de visualización: Time series, Gauge, Stat, Table.
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)
Cada capa vive en su propia red. Oracle solo es alcanzable por el puerto 1521 desde la red interna.
La VM Ubuntu reenvía tráfico entre interfaces (enp0s8 → enp0s3), funcionando como gateway.
El tráfico que sale desde Oracle Linux hacia internet aparece con la IP de Ubuntu, no con la de Oracle.
En producción los servidores de base de datos nunca usan DHCP. Toda IP está asignada administrativamente.
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.
| Parámetro | Valor |
|---|---|
| SID | FREE |
| CDB | FREE |
| PDB | FREEPDB1 |
| Puerto listener | 1521 |
| Límites | 2 CPUs · 2 GB RAM · 12 GB datos |
# 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
| Host | IP | Rol |
|---|---|---|
| Windows host | 192.168.56.1 | Acceso SSH y administración |
| VM Ubuntu 24 | 192.168.0.x (bridge, DHCP) | Docker host, internet |
| VM Ubuntu 24 | 192.168.56.10 (host-only, fija) | Gateway para Oracle |
| VM Oracle Linux 8 | 192.168.56.20 (host-only, fija) | Oracle Database 23ai Free |
| Contenedor | Puerto host | Puerto contenedor | Descripción |
|---|---|---|---|
| haproxy1-ssl | 8080 | 443 | HTTPS externo (SSL termination) |
| haproxy1-ssl | 8405 | 8404 | HAProxy SSL stats |
| haproxy1-int | 443 | 80 | Balanceo interno round-robin |
| haproxy1-int | 8404 | 8404 | HAProxy INT stats |
| ngx1 | — | — | Solo red interna Docker |
| ngx2 | — | — | Solo red interna Docker |
| redis-master | — | 6379 | Solo red interna Docker |
| redis-slave | — | 6379 | Solo red interna Docker |
| redis-sentinel | — | 26379 | Solo red interna Docker |
| solr | 8983 | 8983 | Panel admin Solr |
| webdav | 8081 | 80 | WebDAV HTTP |
| ldap | — | 389 / 636 | Solo red interna Docker |
| ha1-int_exporter | — | 9101 | Exporter HAProxy INT → Prometheus |
| ha1-ssl_exporter | — | 9101 | Exporter HAProxy SSL → Prometheus |
| ngx1_exporter | — | 9113 | Exporter nginx ngx1 → Prometheus |
| ngx2_exporter | — | 9113 | Exporter nginx ngx2 → Prometheus |
| redis_exporter | 9121 | 9121 | Exporter Redis → Prometheus |
| solr_exporter | 9231 | 9231 | Exporter Solr → Prometheus |
| prometheus | 9090 | 9090 | Recolección de métricas |
| grafana | 3000 | 3000 | Visualización de métricas |
| traffic-generator | — | — | Generador de tráfico interno |
| Servicio | Host | Puerto | Protocolo |
|---|---|---|---|
| Oracle Database 23ai Free | 192.168.56.20 | 1521 | TCP (TNS) |
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.
| Componente | Threads | Delay base | Operaciones |
|---|---|---|---|
| HAProxy (int) | 5 | 0.3s | GET 80% · POST 15% · HEAD 5% — 10 rutas, User-Agents variados |
| Redis | 4 | 0.1s | SET/GET (mix hits+misses), INCR, LPUSH, HSET, EXPIRE, DEL |
| Solr | 3 | 0.5s | Queries por término, facets, indexado de docs, borrado periódico |
| WebDAV | 2 | 1.0s | PUT, 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.
traffic-generator/
Dockerfile # python:3.12-alpine
generator.py # script principal con workers por componente
requirements.txt # requests, redis-py
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.
# 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
Cada thread corre indefinidamente. El servicio no termina solo — se controla con docker stop/start.
Ajustar *_workers y *_delay en el bloque CFG del script para modificar la carga sin tocar la lógica.
Si la VM se reinicia, el generador vuelve a arrancar automáticamente junto con todo el stack.