Un servidor sin monitorización es un servidor ciego. No sabes cuándo se llena el disco, qué contenedor está consumiendo demasiada RAM, o cuántas peticiones 404 está generando tu web. Este artículo documenta cómo configuré el stack completo: Prometheus + Node Exporter + Grafana + Loki + Promtail.
La arquitectura#
[Servidor Italia]
├── node-exporter → métricas del sistema (CPU, RAM, disco, red)
├── docker-stats- → métricas de contenedores (textfile collector)
│ collector
├── prometheus → recolecta y almacena métricas
├── loki → agrega y almacena logs
├── promtail → envía logs de Nginx y syslog a Loki
└── grafana → dashboards de todo lo anteriorTodos los servicios corren en Docker, coordinados por el mismo docker-compose.yml.
Métricas del sistema: Node Exporter#
Node Exporter expone métricas del hardware y del SO. El truco: tiene que correr con network_mode: host para ver las interfaces de red reales del servidor. Si corre en red de Docker, solo ve la interfaz eth0 del contenedor.
node-exporter:
image: prom/node-exporter:v1.8.2
network_mode: host
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
- ./textfile-collector:/textfile:ro
command:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/rootfs
- --web.listen-address=127.0.0.1:9100
- --collector.textfile.directory=/textfileEscucha en 127.0.0.1:9100. Prometheus lo alcanza por 172.17.0.1:9100 (la IP del host desde la red Docker).
Métricas de contenedores: docker stats + textfile collector#
El problema con cAdvisor es que no funciona con Docker 29 y el driver de almacenamiento overlayfs en cgroupv2 — falla con “failed to identify read-write layer ID”.
La solución: un contenedor ligero que ejecuta docker stats cada 30 segundos y escribe el resultado en formato Prometheus en un archivo que Node Exporter lee.
#!/bin/bash
# docker_stats.sh
OUTFILE="/textfile/docker_stats.prom"
TMPFILE="${OUTFILE}.tmp"
{
echo "# HELP docker_container_cpu_percent CPU usage percentage per container"
echo "# TYPE docker_container_cpu_percent gauge"
# ... más definiciones ...
docker stats --no-stream --format \
'{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}' 2>/dev/null | \
while IFS='|' read -r name cpu mem net; do
cpu_val=$(echo "$cpu" | tr -d '%' | tr ',' '.')
# ... conversión de unidades ...
echo "docker_container_cpu_percent{name=\"${name}\"} ${cpu_val}"
echo "docker_container_memory_bytes{name=\"${name}\"} ${mem_used_bytes}"
echo "docker_container_running{name=\"${name}\"} 1"
done
# Contenedores parados
docker ps -a --filter "status=exited" --format '{{.Names}}' 2>/dev/null | \
while read -r name; do
echo "docker_container_running{name=\"${name}\"} 0"
done
} > "$TMPFILE" && mv "$TMPFILE" "$OUTFILE"La escritura atómica (tmp → final) evita que Prometheus lea un archivo a medias.
docker-stats-collector:
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./textfile-collector:/textfile
- ./docker_stats.sh:/docker_stats.sh:ro
entrypoint: sh -c "apk add --no-cache bc > /dev/null 2>&1; while true; do sh /docker_stats.sh; sleep 30; done"Prometheus: recolectar y retener#
prometheus:
image: prom/prometheus:v2.51.2
networks:
- monitoring
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --web.enable-lifecycleConfiguración de scraping:
global:
scrape_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: [localhost:9090]
- job_name: node
static_configs:
- targets: [172.17.0.1:9100]
relabel_configs:
- target_label: host
replacement: italia172.17.0.1 es la IP del host accesible desde la red Docker bridge. Los datos se retienen 30 días.
Logs: Loki + Promtail#
Loki almacena logs sin indexar el contenido completo — solo las etiquetas (labels). Promtail los recoge y los envía con etiquetas como job, host, filename.
promtail:
image: grafana/promtail:3.3.2
user: root
networks:
- monitoring
volumes:
- ./promtail-config.yml:/etc/promtail/config.yml:ro
- ./promtail-data:/tmp/promtail
- /home/tellme/CLAUDE/web/logs:/logs/nginx:ro
- /var/log:/logs/host:roNecesita correr como root para leer /var/log.
Grafana: dashboards#
Grafana se conecta a Prometheus y Loki como data sources. Los dashboards más útiles:
Sistema (Node Exporter):
- CPU total y por núcleo
- RAM usada / libre / cache
- Disco: uso por partición, IOPS, throughput
- Red: tráfico de entrada/salida por interfaz
Contenedores (docker stats):
- CPU % por contenedor
- RAM por contenedor vs límite
- Estado (running/stopped)
- Tráfico de red por contenedor
Logs (Loki):
- Logs de Nginx en tiempo real
- Peticiones por código de estado (200, 301, 404, 500)
- Top de IPs con más peticiones
- Top de rutas más accedidas
Problema: [$__range] en consultas instantáneas de Loki#
Al usar paneles de tipo “stat” o “piechart” con Loki, la variable [$__range] no se resuelve — Grafana devuelve “empty duration string”. La solución es usar una duración fija:
# MAL (en paneles stat/piechart):
sum by(status) (count_over_time({job="nginx"} | pattern ... [$__range]))
# BIEN:
sum by(status) (count_over_time({job="nginx"} | pattern ... [24h]))Los paneles de tipo “time series” sí admiten [$__interval] correctamente.
Seguridad del stack#
- Prometheus y Loki no tienen acceso externo — solo en la red interna
monitoring - Grafana es el único punto de acceso, protegido con Traefik y Let’s Encrypt
GF_AUTH_ANONYMOUS_ENABLED=falseyGF_USERS_ALLOW_SIGN_UP=falseen Grafana- Node Exporter escucha solo en
127.0.0.1, no expuesto en todas las interfaces
Resultado#
Con este stack tienes visibilidad completa del servidor: qué procesos consumen recursos, qué contenedores fallan, qué peticiones recibe tu web y qué errores genera. Todo en dashboards accesibles desde monitor.serviciosrogeliowar.com.