Problema

Mantener un homelab con decenas de contenedores Docker distribuidos en varios servidores físicos genera una serie de cuellos de botella que aparecen de forma intermitente: crecimiento inesperado del estado de la cadena, agotamiento de pools de bases de datos, pérdida de conectividad cuando la ruta primaria falla y dificultad para correlacionar logs provenientes de diferentes hosts. Cuando la infraestructura incluye un nodo Ethereum completo, la presión sobre el disco y el ancho de banda aumenta, y cualquier desalineamiento en la configuración de red o firewall puede romper la sincronización de la cadena y los servicios dependientes. El reto es diseñar una arquitectura que permita escalar el número de contenedores sin que la complejidad operacional se vuelva inmanejable.

Causa

  1. Almacenamiento estático y crecimiento de la cadena
    Los clientes de Ethereum (Nethermind, Lighthouse) guardan el estado de la cadena en disco. Un sync tipo snap puede pasar de 600 GB a más de 1 TB en pocos meses. Sin una política de rotación o archivado, el nodo se queda sin espacio y obliga a una re‑sync completa.

  2. Pool de conexiones de PostgreSQL insuficiente
    Cuando varios micro‑servicios comparten una única base, PgBouncer se queda sin max_client_conn y default_pool_size. Cada petición adicional se bloquea, provocando latencias y errores 500 en APIs.

  3. Red de malla sin redundancia de túneles
    Dependencia de un único túnel WireGuard para acceder a los servidores dedicados. Si el enlace de salida (fibra o 5G) se cae, la conectividad desaparece hasta que el túnel se restablece, lo que afecta a los runners de CI y a los exporters de métricas.

  4. Reglas de firewall demasiado restrictivas o conflictivas
    nftables gestionado por un unit de systemd puede colisionar con el servicio nativo, dejando puertos críticos (30303, 9000) cerrados sin notificación. Además, la política drop sin logs de depuración dificulta la detección de bloqueos.

  5. Observabilidad fragmentada
    Logs enviados a OpenSearch desde contenedores sin estandarizar el formato dificultan búsquedas rápidas. La retención indefinida de índices grandes consume espacio y ralentiza las consultas.

Solución

1. Arquitectura de almacenamiento con capas

  • Separar datos de la cadena del resto de los volúmenes. Usa un disco NVMe dedicado para el datadir de Nethermind y otro para los logs y bases de datos.
  • Implementar rotación de snapshots: programa un cron que, cada 30 días, copie el estado actual a un bucket S3 (o MinIO) y luego elimine los archivos locales más antiguos. Mantén al menos dos copias antes de borrar.
  • Planificar una expansión: cuando el uso supere el 70 % del disco, provisiona un RAID0/1 adicional y migra el datadir con rsync --partial.

2. Tuning de PgBouncer y PostgreSQL

  • Aumenta max_client_conn a un valor que cubra la suma de max_connections de todos los micro‑servicios. Un buen punto de partida es 200 + (n_services * 10).
  • Ajusta default_pool_size según la carga esperada por servicio; valores entre 20‑30 suelen ser estables.
  • Habilita ignore_startup_parameters = extra_float_digits para evitar reconexiones innecesarias.
  • Usa métricas de pgbouncer_exporter en Prometheus para alertar cuando el número de clientes activos se acerque al límite.

3. Redundancia de túneles WireGuard

  • Crear dos peers en cada nodo: uno usando la IP pública del servidor principal y otro a través del VPS gateway. Configura PersistentKeepalive = 25 en ambos.
  • En Ansible, define una variable wg_primary y wg_fallback. Un script de systemd verifica la latencia del peer primario cada 10 s y, si supera 500 ms, cambia a la ruta de fallback con wg setconf wg0 /etc/wireguard/fallback.conf.
  • Mantén una regla de firewall que permita tráfico UDP 51820 desde ambas IPs.

4. Gestión segura de nftables

  • Evita que el unit de systemd sobrescriba la tabla existente. Usa ExecStartPre=/usr/sbin/nft -f /etc/nftables.conf y ExecReload=/usr/sbin/nft -f /etc/nftables.conf.
  • Declara una cadena dedicada para Ethereum:
nft add table inet filter
nft 'add chain inet filter eth_chain { type filter hook input priority 0; policy drop; }'
nft add rule inet filter eth_chain ip protocol tcp ip dport {30303, 9000} ct state new accept
nft add rule inet filter eth_chain ip protocol udp ip dport {30303, 9000} ct state new accept
nft add rule inet filter eth_chain ct state established,related accept
nft add rule inet filter eth_chain iif "lo" accept
  • Añade una regla de logging temporal para depurar bloqueos: nft add rule inet filter input log prefix "nft-drop: " drop.

5. Centralizar logs con formato estructurado

  • Instala Fluent Bit fuera de Docker en cada host. Configura Parsers_File para JSON y Tail para los archivos de contenedor en /var/lib/docker/containers/*/*.log.
  • Usa un pipeline que añada etiquetas host, container_name y service.
  • En OpenSearch, crea un ILM (Index Lifecycle Management) que rote índices diarios y elimine los que superen 30 días, manteniendo solo los últimos 7 días en hot storage.
  • Si el volumen supera 10 GB/día, considera migrar a Loki; sin embargo, OpenSearch sigue siendo más flexible para búsquedas de texto completo.

6. Automatización con Ansible

  • Define roles para: wireguard, nftables, docker_compose, pgbouncer, fluent_bit.
  • Usa templates Jinja2 para generar los archivos de configuración con variables de entorno (p.ej., {{ wg_peer_ip }}).
  • Ejecuta ansible-playbook site.yml --check antes de aplicar cambios críticos.

Cuándo aplicar esta solución

  • Síntomas: errores 500 en APIs que dependen de PostgreSQL, alertas de disco lleno en el nodo Ethereum, pérdida de conectividad tras caída de fibra, o demoras en la recolección de métricas.
  • Entorno: homelabs con >50 contenedores distribuidos en al menos dos servidores físicos, uso de nodos blockchain y pipelines CI/CD auto‑hosted.
  • No aplica: entornos con un solo contenedor o sin dependencias de base de datos compartida; en esos casos la complejidad de WireGuard y nftables es innecesaria.

Código

# nftables rules (ver bloque anterior)
# PgBouncer tuning (postgresql.conf)
cat <<EOF >> /etc/pgbouncer/pgbouncer.ini
max_client_conn = 500
default_pool_size = 30
ignore_startup_parameters = extra_float_digits
EOF

# WireGuard fallback script
cat <<'EOS' > /usr/local/bin/wg-fallback.sh
#!/bin/bash
PRIMARY=10.100.0.1
FALLBACK=10.100.0.2
PING=$(ping -c1 -W1 $PRIMARY >/dev/null && echo ok || echo fail)
if [[ $PING == fail ]]; then
  wg setconf wg0 /etc/wireguard/fallback.conf
else
  wg setconf wg0 /etc/wireguard/primary.conf
fi
EOS
chmod +x /usr/local/bin/wg-fallback.sh
systemctl enable --now wg-fallback.timer

Verificación

  1. Espacio en disco: df -h /var/lib/nethermind debe mostrar al menos 20 % libre después de la rotación.
  2. PgBouncer: pgbouncer -R muestra total_clients < max_client_conn. Configura una alerta en Prometheus para pgbouncer_pool_size > 0.8 * pgbouncer_max_client_conn.
  3. WireGuard: wg show wg0 debe listar ambos peers y latest handshake reciente (<30 s). Simula la caída de la fibra desconectando la interfaz primaria y verifica que el tráfico se redirige al fallback.
  4. Firewall: nft list chain inet filter eth_chain debe contener las reglas de puerto 30303 y 9000. Genera tráfico de prueba con nc -vz <node_ip> 30303.
  5. Logs: En OpenSearch, busca un mensaje reciente con host:"node-2" y verifica que aparecen los campos container_name y service.

Notas adicionales

  • Manejo de snapshots: evita usar docker commit para crear imágenes de estado; en su lugar, exporta el datadir y vuelve a montar en un contenedor nuevo.
  • Runners de GitHub Actions: configura --rm en los contenedores de runner para que eliminen su workspace automáticamente y evita la acumulación de archivos temporales.
  • 5G APN: en MikroTik, mantén apn=free y habilita ipv6-use-compressed para que IPv4 y IPv6 coexistan sin interrupciones.
  • OpenSearch vs Loki: si la mayor parte de las consultas son búsquedas de texto libre (p.ej., “error 500 en API X”), OpenSearch sigue siendo la mejor opción. Loki brilla cuando los logs son exclusivamente de series temporales y se consultan por etiquetas.
  • Documentación de Ansible: versiona los playbooks en un repositorio Git separado y usa tags semánticos para marcar cambios de infraestructura críticos.

Con esta arquitectura modular y los ajustes descritos, es posible escalar un homelab con decenas de contenedores y un nodo Ethereum sin que la complejidad operativa se convierta en un obstáculo. La clave está en separar responsabilidades (almacenamiento, red, base de datos, observabilidad) y automatizar cada capa con herramientas probadas.