Homelab
Observabilidad en casa: Grafana, Prometheus y Loki sin ahogar el NAS
11 min de lectura
Puntos clave
- Observabilidad es poder responder qué pasa y por qué: métricas, logs y alertas.
- Prometheus guarda métricas, Loki guarda logs y Grafana los enseña; los recolectores alimentan a ambos.
- En un NAS modesto el stack completo compite por la RAM con el resto de servicios.
- Solución: el plano de control en un portátil con k3s y solo los recolectores en el NAS.
- Retención y límites de memoria desde el primer día, y una alerta que llegue a alguien.
Durante meses mi forma de saber si algo iba mal en el NAS era esperar a que dejara de funcionar. Luego entraba por SSH, miraba docker ps y leía logs a mano. Funciona, pero siempre llegas tarde.
La observabilidad cambia eso: el sistema te cuenta cómo está, te enseña la historia y te avisa antes de que te enteres por las malas. En este capítulo explico el stack que uso, cómo lo tuve en el NAS, por qué tuve que sacarlo de ahí y cómo montarlo desde cero.
Qué es observabilidad (en corto)
Son tres fuentes de información que responden a tres preguntas:
- Métricas: números en el tiempo. ¿Cuánta CPU, RAM y disco usa cada cosa? ¿Desde cuándo?
- Logs: lo que escriben los servicios. ¿Qué pasó exactamente a las 03:12?
- Alertas: reglas sobre las métricas. ¿Hay algo que deba saber ahora?
Qué vas a montar
| Pieza | Qué hace |
|---|---|
| Prometheus | Pregunta a los exportadores cada pocos segundos y guarda las métricas |
| node-exporter | Publica las métricas del host: CPU, memoria, disco, red |
| cAdvisor | Publica las métricas de cada contenedor |
| Loki | Guarda los logs, indexados por etiquetas |
| Promtail | Lee los logs de los contenedores y los envía a Loki |
| Grafana | Dashboards y exploración de métricas y logs |
En mi stack original también estaba Tempo, para trazas distribuidas de mis aplicaciones. Para empezar no hace falta.
Requisitos
- Docker y Docker Compose.
- Unos 1,5 GB de RAM libres si lo montas todo en la misma máquina.
- Caddy delante para Grafana, con el panel solo en LAN/VPN (ver la serie “Mi NAS paso a paso”).
La historia real: del NAS al portátil
Empezó en el NAS
Monté el stack completo en el NAS con Docker Compose, con límites de memoria para cada servicio y retención de 15 días o 2 GB en Prometheus. Como el NAS es modesto (un Celeron de cuatro núcleos), lo encendía bajo demanda: lo levantaba para enseñarlo y lo paraba para liberar RAM.
Cuando lo medí con todo levantado, el stack no era pesado: unos 670 MB de RAM real. El problema era el margen del NAS: con el resto de servicios le quedaban unos 2 GB libres y ya estaba usando 2 GB de swap. Cualquier pico empujaba a todo el sistema hacia el disco.
Por qué no k3s en el NAS
Quería aprender Kubernetes con un caso propio, así que lo primero fue intentar k3s en el NAS. Lo descarté tras revisar tres cosas del sistema del NAS:
- Un kernel antiguo (4.4).
- Un esquema de cgroups no estándar.
- El módulo overlayfs no disponible, que containerd necesita.
Y aunque hubiera arrancado, el plano de control de k3s habría dejado al NAS en swap permanente.
La arquitectura resultante
La solución fue dividir. Hay piezas que tienen que estar en el NAS porque leen el propio NAS (/proc, /sys, los logs de los contenedores). El resto no depende del host y se puede ir a otra máquina.
| Pieza | Dónde vive | Por qué |
|---|---|---|
| node-exporter | NAS (Docker) | Lee el hardware y el sistema del NAS |
| cAdvisor | NAS (Docker) | Lee los contenedores del NAS |
| Promtail | NAS (Docker) | Lee los logs del NAS y los empuja a Loki |
| Prometheus | Portátil (k3s) | Va a buscar las métricas al NAS por la red |
| Loki | Portátil (k3s) | Recibe los logs |
| Grafana | Portátil (k3s) | Se publica a través del Caddy del NAS |
NAS (Docker) Portátil (k3s)
┌──────────────────┐ ┌──────────────────────┐
│ node-exporter │ ◄── scrape ───── │ Prometheus │
│ cAdvisor │ ◄── scrape ───── │ │
│ Promtail ────────┼───── push ─────► │ Loki │
│ Caddy ───────────┼──── proxy ─────► │ Grafana │
└──────────────────┘ └──────────────────────┘
En k3s usé el chart de Helm kube-prometheus-stack (Prometheus, Grafana y Alertmanager en un paquete) y el chart de Loki en modo de un solo binario. El resultado: el NAS dejó de reservar ~1,5 GB de límites de memoria del plano de control a cambio de ~250 MB de los recolectores.
# En el portátil: instala k3s (trae Traefik y almacenamiento local)
curl -sfL https://get.k3s.io | sh -
sudo kubectl get nodes
# Repos de Helm y el stack de métricas en su propio namespace
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace observability --create-namespace
Si empiezas, no hace falta todo esto. Monta primero el stack con Compose en una máquina con RAM de sobra; la división puede esperar.
Paso 1: el stack con Docker Compose
Estructura de carpetas:
observability/
├── docker-compose.yml
├── .env
├── prometheus/
│ ├── prometheus.yml
│ └── alerts.yml
├── loki/
│ └── loki.yml
├── promtail/
│ └── promtail.yml
└── grafana/
└── provisioning/
└── datasources/
└── datasources.yml
El docker-compose.yml:
services:
prometheus:
image: prom/prometheus:v3.10.0
container_name: prometheus
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=15d # borra datos de más de 15 días
- --storage.tsdb.retention.size=2GB # o cuando ocupen más de 2 GB
volumes:
- ./prometheus:/etc/prometheus:ro
- prometheus-data:/prometheus
extra_hosts:
- "host.docker.internal:host-gateway" # para llegar a node-exporter en el host
mem_limit: 512m
node-exporter:
image: prom/node-exporter:v1.9.1
container_name: node-exporter
restart: unless-stopped
command:
- --path.rootfs=/host
volumes:
- /:/host:ro
network_mode: host # ve la red real del host; escucha en :9100
pid: host
mem_limit: 64m
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.52.1
container_name: cadvisor
restart: unless-stopped
privileged: true
command:
- --docker_only=true
- --housekeeping_interval=30s # menos CPU que el valor por defecto
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
mem_limit: 256m
loki:
image: grafana/loki:3.4.2
container_name: loki
restart: unless-stopped
command: -config.file=/etc/loki/loki.yml
volumes:
- ./loki/loki.yml:/etc/loki/loki.yml:ro
- loki-data:/loki
mem_limit: 256m
promtail:
image: grafana/promtail:3.4.2
container_name: promtail
restart: unless-stopped
command: -config.file=/etc/promtail/promtail.yml
volumes:
- ./promtail/promtail.yml:/etc/promtail/promtail.yml:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- promtail-data:/data
depends_on:
- loki
mem_limit: 128m
grafana:
image: grafana/grafana:12.3.0
container_name: grafana
restart: unless-stopped
env_file: .env # usuario y contraseña de administrador
environment:
GF_USERS_ALLOW_SIGN_UP: "false"
GF_ANALYTICS_REPORTING_ENABLED: "false"
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- grafana-data:/var/lib/grafana
ports:
- "127.0.0.1:3500:3000" # solo el proxy llega a Grafana
mem_limit: 256m
volumes:
prometheus-data:
loki-data:
promtail-data:
grafana-data:
Uso volúmenes con nombre para los datos: Prometheus, Loki y Grafana no se ejecutan como root dentro del contenedor, y con carpetas del host es fácil acabar con errores de permisos.
En un NAS Synology, los contenedores no están en /var/lib/docker. En el mío, cAdvisor y Promtail tuvieron que leer de /volume1/@docker y /volume1/@docker/containers. Comprueba la ruta de tu sistema.
El .env (sin valores en el ejemplo; ponlos tú y haz chmod 600 .env):
GF_SECURITY_ADMIN_USER=
GF_SECURITY_ADMIN_PASSWORD=
Paso 2: la configuración
prometheus/prometheus.yml, qué medir y cada cuánto:
global:
scrape_interval: 30s # cada cuánto pregunta a los exportadores
evaluation_interval: 30s # cada cuánto evalúa las alertas
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: node
static_configs:
- targets: ["host.docker.internal:9100"]
- job_name: cadvisor
static_configs:
- targets: ["cadvisor:8080"]
loki/loki.yml, un Loki de un solo nodo con retención de 7 días:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
compactor:
working_directory: /loki/compactor
retention_enabled: true # sin esto, los logs no se borran nunca
delete_request_store: filesystem
limits_config:
retention_period: 168h # 7 días
promtail/promtail.yml, que lee los logs de todos los contenedores:
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /data/positions.yaml # recuerda por dónde iba tras un reinicio
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: docker
static_configs:
- targets: [localhost]
labels:
job: docker
__path__: /var/lib/docker/containers/*/*-json.log
pipeline_stages:
- docker: {} # extrae el mensaje y la hora del formato JSON de Docker
Grafana aprovisiona sus fuentes de datos desde un fichero, así no hay que configurarlas a mano. grafana/provisioning/datasources/datasources.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
Un aviso sobre Promtail: es lo que yo uso, pero Grafana lo ha dado por obsoleto en favor de Grafana Alloy. Para un montaje nuevo a largo plazo, valora Alloy como agente de logs.
Paso 3: tu primer dashboard
docker compose up -d
docker compose ps
Entra en Grafana a través del proxy con el usuario del .env. Después:
- En Dashboards, elige importar un dashboard.
- Escribe el ID 1860 (“Node Exporter Full”, publicado en grafana.com) y selecciona la fuente Prometheus.
- Ya tienes CPU, memoria, disco y red del host.
Para los logs, abre Explore, elige Loki y prueba una consulta como {job="docker"} |= "error".
Paso 4: tu primera alerta
prometheus/alerts.yml, dos reglas que valen para cualquier servidor:
groups:
- name: homelab
rules:
- alert: HostCaido
expr: up{job="node"} == 0
for: 2m # evita avisos por un fallo puntual
labels:
severity: critical
annotations:
summary: "node-exporter no responde desde hace 2 minutos"
- alert: DiscoCasiLleno
expr: |
node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}
/ node_filesystem_size_bytes{fstype!~"tmpfs|overlay"} < 0.10
for: 15m
labels:
severity: warning
annotations:
summary: "Queda menos de un 10 % de disco en {{ $labels.mountpoint }}"
Recarga Prometheus con docker compose restart prometheus y abre su página de alertas: verás las reglas en estado inactivo.
Una alerta que solo se ve en una página no avisa a nadie. Me pasó: en el NAS, el receptor de alertas estaba vacío y todas se descartaban en silencio. Desde que el stack vive en el portátil, las alertas se envían por ntfy y tengo reglas propias para el NAS caído, el disco o las copias de seguridad. Para enviarlas necesitas Alertmanager (viene en kube-prometheus-stack) o los puntos de contacto de Grafana. En el próximo capítulo monto un canal de avisos por Telegram.
El caso real: tres días sin observabilidad
La migración tuvo un tropiezo que enseña más que la parte técnica. Cuando moví el plano de control, paré el stack del NAS pensando en arrancar solo los recolectores. No lo hice. Durante tres días no hubo métricas ni logs, y mi documentación seguía diciendo que el stack del NAS “seguía funcionando” como respaldo.
Dos errores más aparecieron al arrancar los recolectores:
- Conflicto de puerto. Para que el Prometheus del portátil llegara a cAdvisor, había que publicar su puerto. El 8080 ya lo usaba Caddy en el NAS. Publicarlo ahí habría roto el acceso web. Lo publiqué en otro puerto (
8085:8080). depends_onresucitando lo migrado. Promtail depende de Loki en el compose. Undocker compose up -d promtaillevantaba también el Loki local, que era justo el que había migrado. La solución es--no-deps:
docker compose up -d --no-deps node-exporter cadvisor promtail
La lección: una documentación que no se comprueba contra la realidad es peor que no tener documentación, porque te da una falsa sensación de seguridad.
Problemas frecuentes
| Síntoma | Causa probable | Solución |
|---|---|---|
El target node aparece DOWN |
Prometheus no llega al host | Revisa extra_hosts y el cortafuegos del puerto 9100 |
permission denied en /prometheus o /loki |
Carpeta del host con otro dueño | Usa volúmenes con nombre |
| Promtail no envía nada | Ruta de logs distinta (p. ej. en Synology) | Monta la ruta real de los contenedores |
| Loki ocupa cada vez más | Retención no activada | retention_enabled y retention_period |
| El NAS va lento con el stack arriba | Falta de RAM, uso de swap | Límites de memoria o mueve el plano de control |
| Arranca un Loki que no querías | depends_on en Promtail |
docker compose up -d --no-deps ... |
Conclusión
Tener métricas y logs en un solo sitio me cambió la forma de diagnosticar: ya no adivino, miro. Las lecciones que me llevo: limita la memoria y la retención desde el principio, separa lo que tiene que vivir en el host de lo que puede irse a otra máquina y verifica que lo que dice tu documentación es cierto.
En el próximo capítulo monto Diun para saber cuándo hay imágenes nuevas y un bot de Telegram para que los avisos lleguen al móvil.
Preguntas frecuentes
¿Necesito Kubernetes para tener observabilidad en casa?
No. Con Docker Compose en una sola máquina funciona perfectamente. Yo moví el plano de control a k3s por falta de RAM en el NAS y para aprender Kubernetes con un caso real.
¿Cuánta memoria necesita este stack?
En mi caso, el stack completo usaba unos 670 MB reales. El problema no era su peso, sino el margen que dejaba al NAS: fíjale límites y vigila el uso de swap.
¿Por qué no instalé k3s directamente en el NAS?
Porque el sistema de mi NAS usa un kernel antiguo, un esquema de cgroups no estándar y no carga overlayfs. k3s habría fallado o quedado muy frágil.
Fuentes
Proyecto relacionado
Infraestructura Pesoz
Homelab self-hosted en producción 24/7: un NAS Synology con ~30 servicios en Docker tras Caddy + CrowdSec y un portátil con GPU como servidor de IA y observabilidad en k3s. VPN WireGuard, GitOps con Forgejo y estado en vivo con Uptime Kuma.




