Gestión centralizada de logs con Grafana Loki en un VPS
Despliega Grafana Loki, Promtail y Grafana con Docker Compose en un VPS. Recopila logs de systemd, Docker y Nginx, consúltalos con LogQL y configura la retención para producción.
Logs dispersos entre /var/log y la salida de docker logs dejan de ser manejables cuando ejecutas más de dos servicios. Este tutorial despliega el stack Grafana + Loki + Promtail en un único VPS usando Docker Compose. Recopilarás logs de systemd, contenedores Docker y Nginx, los consultarás con LogQL, configurarás la retención para que los logs no llenen tu disco y asegurarás el stack para un servidor expuesto a internet.
Al final, la API HTTP de Loki estará lista para consultas programáticas. El artículo Análisis de logs con IA usando Ollama en un VPS: detectar anomalías con un LLM local se apoya en esto para alimentar logs a un LLM local para detección de anomalías.
¿Qué hace el stack Grafana + Loki + Promtail?
Grafana Loki es un sistema open-source de agregación de logs que indexa solo las etiquetas (metadatos), no el texto completo de las entradas de log. Esto lo hace mucho más ligero en recursos que Elasticsearch. Loki almacena bloques de logs comprimidos en el sistema de archivos o en almacenamiento de objetos. Junto con Promtail para la recopilación y Grafana para la visualización, forma un stack completo de logging centralizado.
Los tres componentes tienen roles diferenciados:
| Componente | Rol | Consumo de recursos |
|---|---|---|
| Loki | Recibe, almacena e indexa logs. Responde consultas. | 300-600 MB RAM en reposo, hasta 1 GB bajo consultas intensivas |
| Promtail | Descubre fuentes de logs, lee archivos continuamente, envía entradas a Loki | 50-100 MB RAM |
| Grafana | Interfaz web para consultar y visualizar logs a través de Explore | 200-300 MB RAM |
Consumo total del stack: 1-1,5 GB de RAM. Un VPS de 2 GB es el mínimo. Un VPS de 4 GB ofrece margen cómodo para consultas LogQL en conjuntos de datos grandes.
Loki vs Elasticsearch: Elasticsearch indexa cada palabra de cada línea de log, lo que da búsqueda de texto completo pero cuesta 10-20 veces más en RAM y disco. El índice por etiquetas de Loki significa que primero filtras por etiquetas y luego recorres los bloques que coinciden. Para la mayoría de cargas de trabajo en VPS, es el compromiso adecuado. Si necesitas búsqueda de texto completo sobre terabytes de logs, Loki no es la herramienta indicada.
Requisitos previos
- Un VPS con al menos 2 GB de RAM (4 GB recomendados). Un VPS Virtua Cloud con 4 vCPU y 8 GB de RAM maneja este stack sin problemas.
- Docker y Docker Compose instalados. Si necesitas ayuda con la instalación, consulta .
- Un usuario no root con acceso sudo.
- Conocimientos básicos de terminal y sintaxis YAML.
Verifica que Docker esté funcionando:
docker --version
docker compose version
Deberías ver Docker 24+ y Compose v2+. Si alguno de los comandos falla, Docker no está instalado o falta el plugin de Compose.
¿Cómo despliego Loki con Docker Compose en un VPS?
Crea un directorio de proyecto y tres archivos de configuración: docker-compose.yml, loki-config.yml y promtail-config.yml. El archivo Docker Compose fija todas las imágenes a versiones específicas, establece límites de recursos, configura volúmenes persistentes y vincula Loki solo a localhost.
Estructura del proyecto
mkdir -p ~/loki-stack/{loki-data,promtail-data}
cd ~/loki-stack
El directorio loki-data contiene los chunks, índices y el write-ahead log. El directorio promtail-data almacena el archivo de posiciones de Promtail para reanudar tras reinicios.
Establece la propiedad del directorio de datos de Loki. La imagen Docker de Loki 3.x se ejecuta como UID 10001, no como root. Sin esto, Loki falla al iniciar con "permission denied" al crear subdirectorios:
chown 10001:10001 ~/loki-stack/loki-data
docker-compose.yml
services:
loki:
image: grafana/loki:3.6.7
command: -config.file=/etc/loki/config.yml
volumes:
- ./loki-config.yml:/etc/loki/config.yml:ro
- ./loki-data:/loki
ports:
- "127.0.0.1:3100:3100"
restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
networks:
- loki-net
promtail:
image: grafana/promtail:3.6.7
command: -config.file=/etc/promtail/config.yml
volumes:
- ./promtail-config.yml:/etc/promtail/config.yml:ro
- ./promtail-data:/var/lib/promtail
- /var/log:/var/log:ro
- /var/log/journal:/var/log/journal:ro
- /run/log/journal:/run/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
deploy:
resources:
limits:
memory: 256m
depends_on:
- loki
networks:
- loki-net
grafana:
image: grafana/grafana:11.5.2
volumes:
- grafana-data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin_pw
- GF_SERVER_ROOT_URL=http://localhost:3000
- GF_USERS_ALLOW_SIGN_UP=false
- GF_ANALYTICS_REPORTING_ENABLED=false
secrets:
- grafana_admin_pw
restart: unless-stopped
deploy:
resources:
limits:
memory: 512m
depends_on:
- loki
networks:
- loki-net
secrets:
grafana_admin_pw:
file: ./secrets/grafana_admin_pw
volumes:
grafana-data:
networks:
loki-net:
driver: bridge
Observa que Loki y Grafana escuchan en 127.0.0.1, no en 0.0.0.0. Esto impide el acceso externo. Accederás a Grafana a través de un túnel SSH o un reverse proxy. Exponer Loki o Grafana directamente a internet es un error frecuente que aparece en la mayoría de tutoriales en línea.
La política restart: unless-stopped asegura que cada servicio sobreviva a reinicios. Si detienes manualmente un servicio con docker compose stop, permanece detenido. En caso contrario, se reinicia automáticamente.
Los tags de imagen fijos (3.6.7, 11.5.2) previenen actualizaciones inesperadas. Nunca uses :latest en producción. Cuando quieras actualizar, cambia el tag y ejecuta docker compose up -d para descargar la nueva imagen.
Generar la contraseña de admin de Grafana
Nunca uses credenciales por defecto. Genera una contraseña fuerte y almacénala en un archivo de secretos con permisos restringidos:
mkdir -p ~/loki-stack/secrets
openssl rand -base64 32 > ~/loki-stack/secrets/grafana_admin_pw
chmod 644 ~/loki-stack/secrets/grafana_admin_pw
Verifica los permisos:
ls -la ~/loki-stack/secrets/grafana_admin_pw
Deberías ver -rw-r--r--. El archivo necesita ser legible por todos porque Docker Compose (fuera del modo Swarm) monta los secretos basados en archivo con los permisos del archivo fuente. Grafana se ejecuta como UID 472 dentro del contenedor y necesita acceso de lectura. El archivo sigue protegido por su ubicación en un directorio de secretos dedicado, y solo el usuario root del host puede modificarlo. La variable de entorno GF_SECURITY_ADMIN_PASSWORD__FILE le dice a Grafana que lea la contraseña de este archivo al iniciar en lugar de incluirla en el archivo compose.
loki-config.yml
Esta configuración usa TSDB con esquema v13 (valores por defecto de Loki 3.x), almacenamiento en sistema de archivos para despliegue de nodo único, y el compactor para la retención:
auth_enabled: false
server:
http_listen_port: 3100
http_listen_address: 0.0.0.0
log_level: warn
http_server_read_timeout: 30s
http_server_write_timeout: 30s
common:
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
replication_factor: 1
path_prefix: /loki
schema_config:
configs:
- from: "2024-01-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
filesystem:
directory: /loki/chunks
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: filesystem
limits_config:
retention_period: 720h
max_streams_per_user: 10000
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
max_label_names_per_series: 15
ingester:
chunk_encoding: snappy
wal:
dir: /loki/wal
enabled: true
chunk_store_config:
chunk_cache_config:
embedded_cache:
enabled: true
max_size_mb: 100
Decisiones clave en esta configuración:
auth_enabled: falsees seguro aquí porque Loki solo escucha en localhost (red interna Docker + binding en 127.0.0.1). Las configuraciones multi-tenant necesitanauth_enabled: truecon un headerX-Scope-OrgIDen cada petición.retention_period: 720hconserva los logs durante 30 días. Loki 3.x usa por defecto0s(conservar para siempre) si no defines este valor. Tu disco se llenará.schema: v13constore: tsdbes necesario para las funcionalidades de Loki 3.x. Las configuraciones antiguas conboltdb-shipperde tutoriales de Loki 2.x fallarán al iniciar o producirán advertencias de deprecación.chunk_encoding: snappycomprime los chunks con Snappy. Más rápido que gzip, archivos ligeramente mayores. Buen valor por defecto para nodo único donde la CPU es más limitada que el disco.- WAL habilitado: el write-ahead log protege contra la pérdida de datos si Loki se cae durante una escritura. Al reiniciar, Loki reproduce el WAL para recuperar entradas no confirmadas. Verás mensajes "WAL replay" en los logs al iniciar. Esto es normal.
max_label_names_per_series: 15corresponde al valor por defecto de Loki 3.x. Mantén baja la cardinalidad de etiquetas. Etiquetas comouser_idorequest_idcrean demasiados streams y degradan el rendimiento.
¿Cómo configuro Promtail para recopilar logs del journal de systemd?
Promtail recopila logs de múltiples fuentes y los envía a Loki. La configuración siguiente recopila de tres fuentes en jobs scrape_configs separados: journal de systemd, contenedores Docker y archivos de logs de Nginx.
Aviso de fin de vida de Promtail: Promtail alcanzó su fin de vida el 2 de marzo de 2026. Grafana Alloy es el sucesor oficial. Este tutorial usa Promtail porque los conceptos de configuración se trasladan directamente a Alloy, y millones de despliegues aún lo utilizan. Consulta la sección "¿Está Promtail obsoleto?" más abajo para los pasos de migración.
promtail-config.yml
server:
http_listen_port: 9080
log_level: warn
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# --- Systemd journal ---
- job_name: journal
journal:
max_age: 12h
labels:
job: systemd-journal
relabel_configs:
- source_labels: ['__journal__systemd_unit']
target_label: unit
- source_labels: ['__journal_priority_keyword']
target_label: severity
- source_labels: ['__journal__hostname']
target_label: hostname
# --- Docker containers ---
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.+)'
target_label: container
- source_labels: ['__meta_docker_container_log_stream']
target_label: stream
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: compose_service
# --- Nginx access and error logs ---
- job_name: nginx
static_configs:
- targets:
- localhost
labels:
job: nginx
type: access
__path__: /var/log/nginx/access.log
- targets:
- localhost
labels:
job: nginx
type: error
__path__: /var/log/nginx/error.log
El archivo positions.yaml registra hasta dónde ha leído Promtail en cada fuente. Si Promtail se reinicia, retoma donde lo dejó en lugar de reenviar logs antiguos o perder nuevos.
Cómo funciona el job de journal
El bloque journal lee directamente del journal de systemd a través de la API del journal. El ajuste max_age: 12h indica a Promtail que solo ingiera entradas del journal de las últimas 12 horas en el primer inicio. Sin esto, Promtail intentaría ingerir todo el historial del journal, que puede ser de gigabytes en servidores que llevan mucho tiempo funcionando.
Los relabel_configs extraen metadatos de las entradas del journal hacia etiquetas Loki. __journal__systemd_unit se convierte en la etiqueta unit (ej. sshd.service, nginx.service). __journal_priority_keyword se convierte en severity (ej. warning, err, info). Estas etiquetas permiten filtrar logs eficientemente en LogQL sin escanear cada línea.
Para que funcione el scraping del journal, el contenedor Promtail necesita dos montajes de volumen: /run/log/journal (o /var/log/journal si tu sistema persiste los logs) y /etc/machine-id. El ID de máquina identifica qué journal leer.
Limitación de la imagen Docker: La imagen Docker estándar
grafana/promtailno está compilada con soporte para el journal de systemd. Si vessupport for reading the systemd journal is not compiled into this build of promtailen los logs, el scraping del journal no funcionará desde el contenedor Docker. Tienes dos opciones:
- Instalar Promtail como binario en el host en lugar de usar la imagen Docker. Descárgalo desde la página de releases de Loki y ejecútalo directamente en el host, donde tiene acceso nativo a la API del journal.
- Usar Grafana Alloy (ver la sección de migración más abajo), que soporta scraping del journal en su imagen Docker.
Los jobs de scraping de Docker y archivos Nginx funcionan correctamente en la imagen Docker. Solo el scraping del journal requiere el binario del host o Alloy.
¿Cómo recopilo logs de contenedores Docker con Promtail?
El job docker usa Docker service discovery para descubrir automáticamente todos los contenedores en ejecución en el host. Promtail se conecta al daemon Docker vía /var/run/docker.sock y consulta nuevos contenedores cada 5 segundos. Cuando un contenedor arranca o se detiene, Promtail comienza o deja de seguir sus logs automáticamente.
Los relabel_configs extraen metadatos útiles:
__meta_docker_container_namese convierte en la etiquetacontainer. La regex'/(.+)'elimina el/inicial que Docker añade a los nombres de contenedores.__meta_docker_container_log_streamse convierte en la etiquetastream(stdoutostderr).__meta_docker_container_label_com_docker_compose_serviceextrae el nombre del servicio Compose (ej.loki,grafana). Esta etiqueta solo existe para contenedores gestionados por Docker Compose.
No necesitas configurar contenedores individuales. Todo contenedor con un driver de logs que escriba en el sistema de archivos (el driver json-file por defecto) será descubierto. Si ejecutas un contenedor de base de datos, una app web y una caché, los tres aparecen automáticamente en Loki bajo sus nombres de contenedor.
Para excluir contenedores específicos de la recopilación de logs, añade una etiqueta Docker y filtra sobre ella:
relabel_configs:
# ... existing relabel rules ...
- source_labels: ['__meta_docker_container_label_logging']
regex: 'disabled'
action: drop
Luego en el contenedor que quieres excluir:
noisy-service:
image: some/image
labels:
logging: "disabled"
¿Cómo recopilo los logs de acceso y error de Nginx con Promtail?
El job nginx usa static_configs con la etiqueta __path__ para seguir archivos de log específicos. A diferencia del service discovery de Docker, requiere conocer las rutas de los archivos de antemano. Nginx escribe por defecto en /var/log/nginx/access.log y /var/log/nginx/error.log.
La etiqueta type distingue entre logs de acceso y de error. Esto permite consultarlos por separado en LogQL:
{job="nginx", type="access"} # solo logs de acceso
{job="nginx", type="error"} # solo logs de error
{job="nginx"} # ambos
Si Nginx no está instalado en el host, Promtail registra una advertencia sobre archivos faltantes pero continúa recopilando de otras fuentes. Es inofensivo. Elimina el job nginx de la configuración si no usas Nginx.
Para Nginx ejecutándose dentro de un contenedor Docker, tienes dos opciones. Puedes usar el service discovery de Docker (el stdout/stderr del contenedor se capturará automáticamente). O puedes montar el directorio de logs de Nginx como volumen compartido y usar el scraper de archivos estáticos. El enfoque Docker es más simple. El enfoque por archivo te da etiquetas separadas type: access y type: error.
Iniciar el stack
cd ~/loki-stack
docker compose up -d
Verifica que los tres contenedores estén funcionando:
docker compose ps
Deberías ver loki, promtail y grafana con estado Up. Si algún servicio muestra Restarting, revisa sus logs:
docker compose logs <service-name> --tail=30
Comprueba que Loki está listo:
curl -s http://127.0.0.1:3100/ready
Salida esperada: ready. Si obtienes Ingester not ready: waiting for 15s after being ready, espera 15 segundos y reintenta. Loki necesita tiempo para inicializar el ring del ingester.
Comprueba los targets de Promtail:
docker compose logs promtail --tail=20
Busca líneas que muestren targets descubiertos. Deberías ver entradas para el journal, el socket Docker y las rutas de logs de Nginx. No deberían aparecer líneas con level=error.
¿Cómo verifico que los logs aparecen en Grafana Explore?
Abre un túnel SSH para acceder a Grafana desde tu máquina local. Usamos un túnel porque Grafana escucha en localhost en el VPS y no está expuesto a internet.
ssh -L 3000:127.0.0.1:3000 user@your-vps-ip
Abre http://localhost:3000 en tu navegador. Inicia sesión con el usuario admin y la contraseña de ~/loki-stack/secrets/grafana_admin_pw. Léela con:
cat ~/loki-stack/secrets/grafana_admin_pw
Añadir Loki como fuente de datos
- Ve a Connections > Data Sources > Add data source
- Selecciona Loki
- Establece la URL en
http://loki:3100(este es el hostname interno de Docker, no localhost) - Haz clic en Save & test
Deberías ver "Data source successfully connected." Si falla, verifica que ambos contenedores estén en la misma red Docker (loki-net).
Ejecutar tu primera consulta
- Ve a Explore (icono de brújula en la barra lateral)
- Selecciona la fuente de datos Loki
- Cambia al modo Code (no Builder) e introduce esta consulta LogQL:
{job="systemd-journal"} |= "ssh"
Esto muestra todas las entradas del journal de systemd que contienen "ssh". Si ves líneas de log, el pipeline completo funciona: journal -> Promtail -> Loki -> Grafana.
Prueba una consulta de contenedor Docker:
{compose_service="loki"}
Esto devuelve los propios logs de Loki, recopilados por Promtail a través del service discovery de Docker.
Y una consulta Nginx (si Nginx está instalado y generando logs):
{job="nginx", type="error"}
Si Grafana muestra "No data", espera 2-3 minutos. Loki necesita tiempo para ingerir e indexar el primer lote de logs.
¿Cuáles son las consultas LogQL más útiles para logs de servidor?
LogQL tiene dos tipos de consultas: las consultas de logs devuelven líneas de log, las consultas de métricas devuelven valores numéricos. Ambas comienzan con un selector de stream ({label="value"}) que elige qué streams de logs escanear, y luego añaden filtros y parsers para refinar los resultados.
Selectores de stream y filtros de línea
# Todos los logs del daemon SSH
{unit="sshd.service"}
# Líneas que contienen "Failed password"
{unit="sshd.service"} |= "Failed password"
# Líneas que NO contienen "Accepted"
{unit="sshd.service"} != "Accepted"
# Coincidencia regex: direcciones IP
{unit="sshd.service"} |~ "\\d+\\.\\d+\\.\\d+\\.\\d+"
# Coincidencia sin distinguir mayúsculas
{job="nginx", type="error"} |~ "(?i)timeout"
Los filtros de línea (|=, !=, |~, !~) se ejecutan después del selector de stream. Escanean el contenido de las líneas de log. Varios filtros se encadenan y todos deben coincidir:
{unit="sshd.service"} |= "Failed" |= "root"
Esto encuentra líneas que contienen tanto "Failed" como "root".
Parsers
Los parsers extraen campos estructurados de líneas de log no estructuradas. Una vez parseados, puedes filtrar por campos extraídos como status >= 500 en lugar de usar regex. Elige el parser adecuado para tu formato de log:
| Parser | Sintaxis | Adecuado para | Notas de rendimiento |
|---|---|---|---|
logfmt |
| logfmt |
Logs clave=valor (systemd, apps Go) | El más rápido. Sin regex. |
json |
| json |
Logs estructurados en JSON | Rápido. Parsing JSON nativo. |
pattern |
| pattern "<pattern>" |
Logs con formato fijo (Nginx combined, Apache) | Rápido. Extracción posicional. |
regexp |
| regexp "<regex>" |
Formatos irregulares, estructuras mixtas | El más lento. Usar como último recurso. |
Usa logfmt o json cuando tus logs ya tienen estructura. Usa pattern para formatos conocidos como el log combined de Nginx. Usa regexp solo cuando nada más funcione, porque el parsing con regex es significativamente más lento en streams de alto volumen.
Log de acceso Nginx con parser pattern:
{job="nginx", type="access"}
| pattern "<ip> - - [<timestamp>] \"<method> <uri> <_>\" <status> <bytes>"
| status >= 500
Esto parsea el formato combined de Nginx en campos nombrados y filtra errores 5xx. El placeholder <_> descarta campos que no necesitas (la versión HTTP en este caso).
Parser de logs JSON:
{compose_service="myapp"}
| json
| level="error"
| line_format "{{.timestamp}} {{.message}}"
La etapa line_format reformatea la salida. Útil cuando los logs JSON son ruidosos y quieres una salida más limpia en Grafana.
Métricas a partir de logs
Las consultas de métricas convierten líneas de log en números. Alimentan dashboards de Grafana y reglas de alerta:
# Tasa de inicios de sesión SSH fallidos por minuto en la última hora
rate({unit="sshd.service"} |= "Failed password" [5m])
# Total de errores Nginx 5xx en ventanas de 5 minutos
count_over_time(
{job="nginx", type="access"}
| pattern "<_> - - [<_>] \"<_> <_> <_>\" <status> <_>"
| status >= 500
[5m]
)
# P95 del tiempo de respuesta desde logs JSON de una app
# unwrap extrae un campo numérico para la agregación
quantile_over_time(0.95,
{compose_service="myapp"}
| json
| unwrap response_time_ms
| __error__=""
[5m]
) by (endpoint)
# Bytes servidos por segundo por Nginx
sum(rate(
{job="nginx", type="access"}
| pattern "<_> - - [<_>] \"<_> <_> <_>\" <_> <bytes>"
| unwrap bytes
| __error__=""
[5m]
))
El filtro | __error__="" después de unwrap descarta líneas donde la extracción numérica falló (valores no numéricos, campos faltantes). Sin él, esas líneas producen silenciosamente valores cero que distorsionan tus resultados. Incluye siempre este filtro después de unwrap.
El rango [5m] define el tamaño de la ventana. Rangos más cortos (1m) dan datos más granulares pero más ruidosos. Rangos más largos (15m, 1h) suavizan los picos. Para dashboards, 5m es un buen punto de partida.
¿Cómo configuro la retención de logs en Loki 3.x?
En Loki 3.x, la retención la gestiona el compactor. Establece retention_period en limits_config y habilita el compactor con retention_enabled: true. La retención por defecto en Loki 3.0+ es 0s (conservar para siempre), así que debes configurarlo explícitamente o tu disco se llenará.
El loki-config.yml de arriba ya incluye la retención. Así interactúan los parámetros:
compactor:
retention_enabled: true # Debe ser true, o el compactor solo compacta (sin borrado)
retention_delete_delay: 2h # Esperar 2h después de marcar chunks antes de borrar
compaction_interval: 10m # Con qué frecuencia se ejecuta el compactor
limits_config:
retention_period: 720h # 30 días como valor global por defecto
El compactor se ejecuta como parte del proceso Loki en modo nodo único. Escanea el índice TSDB, identifica chunks más antiguos que el período de retención, los marca para borrado y los elimina después del retention_delete_delay. El retraso te da una ventana de recuperación si la retención está mal configurada.
Retención por stream
Puedes establecer diferentes períodos de retención para distintos streams de logs. Los logs de alto volumen y bajo valor (como la salida de debug) pueden expirar antes:
limits_config:
retention_period: 720h
retention_stream:
- selector: '{job="nginx", type="access"}'
priority: 1
period: 336h # 14 días para logs de acceso
- selector: '{severity="debug"}'
priority: 2
period: 72h # 3 días para logs de debug
Los valores de prioridad más altos ganan cuando varios selectores coinciden con el mismo stream. Un log de acceso Nginx de nivel debug coincide con ambas reglas. La prioridad 2 gana, así que obtiene retención de 3 días.
Dimensionamiento de la retención
Estima el uso de disco antes de comprometerte con un período de retención. Loki comprime bien los logs (ratio 5-10x con Snappy), pero los números se acumulan en servidores activos:
| Volumen bruto/día | Comprimido (est.) | Retención 7 días | Retención 30 días | Retención 90 días |
|---|---|---|---|---|
| 100 MB | ~15 MB | ~105 MB | ~450 MB | ~1,35 GB |
| 500 MB | ~75 MB | ~525 MB | ~2,25 GB | ~6,75 GB |
| 1 GB | ~150 MB | ~1,05 GB | ~4,5 GB | ~13,5 GB |
| 5 GB | ~750 MB | ~5,25 GB | ~22,5 GB | ~67,5 GB |
Son estimaciones. La compresión real depende del contenido de los logs. Los logs repetitivos (logs de acceso con rutas similares) se comprimen mejor que la salida de debug aleatoria. Monitorea el uso real con:
du -sh ~/loki-stack/loki-data/
Ejecuta esto semanalmente para detectar crecimiento inesperado antes de quedarte sin espacio en disco.
¿Cómo optimizo Loki para producción en un VPS único?
Una configuración Loki por defecto funciona para pruebas pero necesita ajustes para un VPS de producción. Los cambios siguientes reducen picos de memoria, protegen contra streams de logs descontrolados y refuerzan el stack para un servidor expuesto a internet.
Vincular Loki a localhost
Ya hecho en el docker-compose.yml de arriba (127.0.0.1:3100:3100). Verifica después del despliegue:
ss -tlnp | grep 3100
Deberías ver 127.0.0.1:3100, no 0.0.0.0:3100. Haz lo mismo para Grafana en el puerto 3000.
Reglas de firewall
Si usas ufw, bloquea el acceso externo a los puertos de logging:
sudo ufw deny 3100/tcp comment "Loki - localhost only"
sudo ufw deny 3000/tcp comment "Grafana - localhost only"
sudo ufw status numbered
Como los puertos ya escuchan en localhost, el firewall es defensa en profundidad. Si alguien cambia accidentalmente el archivo compose para escuchar en 0.0.0.0, el firewall sigue bloqueando el acceso externo.
Ocultar información de versión
La divulgación de versiones ayuda a los atacantes a explotar vulnerabilidades conocidas. El GF_ANALYTICS_REPORTING_ENABLED=false en el archivo compose ya deshabilita la telemetría de Grafana. El endpoint /loki/api/v1/status/buildinfo de Loki expone detalles de versión, pero como Loki escucha en localhost, solo procesos locales pueden acceder.
Si pones Grafana detrás de un reverse proxy (Nginx, Caddy), añade estas configuraciones Nginx:
server_tokens off;
proxy_hide_header X-Powered-By;
Cambios importantes en Loki 3.x
Si estás migrando desde una configuración Loki 2.x o siguiendo un tutorial antiguo, ten en cuenta estos cambios:
| Cambio | Valor por defecto Loki 2.x | Valor por defecto Loki 3.x | Acción requerida |
|---|---|---|---|
| Esquema | v11/v12 | v13 | Usa schema: v13 con store: tsdb |
| Index store | boltdb-shipper |
tsdb |
Migra a TSDB (BoltDB obsoleto) |
| Retención | 0s (conservar siempre) | 0s (conservar siempre) | Establece retention_period explícitamente |
| Metadatos estructurados | Deshabilitado | Habilitado | Requiere esquema v13 |
| Max etiquetas por serie | 30 | 15 | Reduce la cardinalidad de etiquetas o aumenta el límite |
| Imagen Docker | Incluye shell BusyBox | Sin shell | No se puede ejecutar docker exec en el contenedor |
La imagen Docker sin shell de Loki 3.6+ significa que no puedes ejecutar docker exec -it loki sh para depuración. En su lugar, revisa logs con docker compose logs loki y disponibilidad con curl http://127.0.0.1:3100/ready.
Sistema de archivos vs almacenamiento de objetos
Para un VPS único, el almacenamiento en sistema de archivos es lo correcto. El almacenamiento de objetos (S3, GCS, MinIO) añade complejidad y latencia que solo merece la pena cuando necesitas:
- Múltiples instancias Loki compartiendo los mismos datos
- Almacenamiento ilimitado más allá del disco de tu VPS
- Replicación entre regiones
Quédate con almacenamiento en sistema de archivos hasta que superes un solo nodo.
Monitorear el stack
Comprueba el uso de recursos de los contenedores en ejecución:
docker stats --no-stream
Revisa los logs de Loki en busca de advertencias y errores:
docker compose logs loki --tail=50 | grep -E "level=(error|warn)"
Problemas comunes y soluciones:
stream limit exceeded: aumentamax_streams_per_userenlimits_config. Normalmente causado por etiquetas de alta cardinalidad.ingestion rate limit reached: aumentaingestion_rate_mb. Ocurre durante picos de logs (despliegues, tormentas de errores).WAL replay: normal al iniciar. Loki está recuperando escrituras no confirmadas del write-ahead log.- Alto uso de memoria: reduce
max_size_mbenchunk_cache_configo disminuyeingestion_burst_size_mb.
¿Está Promtail obsoleto? ¿Debería usar Grafana Alloy en su lugar?
Sí. Promtail entró en soporte a largo plazo el 13 de febrero de 2025 y alcanzó su fin de vida el 2 de marzo de 2026. No se publicarán más actualizaciones, correcciones de errores ni parches de seguridad. Grafana Alloy es el reemplazo oficial. Es la distribución de Grafana Labs del colector OpenTelemetry y gestiona logs, métricas, trazas y datos de profiling en un único agente.
Por qué este tutorial aún usa Promtail
Los conceptos de configuración de Promtail se trasladan directamente a Alloy. Los scrape configs, reglas de relabel y etapas de pipeline funcionan igual. Aprender Promtail sigue siendo útil porque:
- Millones de despliegues existentes lo usan
- La herramienta de migración de Alloy convierte configs de Promtail automáticamente
- Entender Promtail facilita la depuración de configuraciones de Alloy
- El volumen de búsqueda de tutoriales de Promtail sigue siendo alto, y los conceptos son transferibles
Migrar a Alloy
Convierte tu configuración de Promtail al formato Alloy con un comando:
alloy convert --source-format=promtail --output=alloy-config.alloy promtail-config.yml
Esto genera un archivo de configuración en formato Alloy. Revisa la salida antes de desplegar. El convertidor maneja la mayoría de los casos pero puede necesitar ajustes manuales para etapas de pipeline personalizadas.
Luego reemplaza el servicio Promtail en docker-compose.yml:
alloy:
image: grafana/alloy:v1.14.1
command:
- run
- /etc/alloy/config.alloy
- --server.http.listen-addr=0.0.0.0:12345
volumes:
- ./alloy-config.alloy:/etc/alloy/config.alloy:ro
- /var/log:/var/log:ro
- /var/log/journal:/var/log/journal:ro
- /run/log/journal:/run/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
depends_on:
- loki
networks:
- loki-net
Elimina el bloque del servicio promtail y ejecuta docker compose up -d. Alloy comenzará a recopilar las mismas fuentes de logs usando la configuración convertida.
Para nuevos despliegues desde cero, usa Alloy desde el principio. Para instalaciones Promtail existentes, planifica la migración sin urgencia inmediata. El binario de Promtail sigue funcionando. Fija el tag de la imagen (grafana/promtail:3.6.7) para controlar lo que se ejecuta.
¿Cómo consulto los logs de Loki programáticamente a través de la API HTTP?
Loki expone una API REST para consultas programáticas de logs. Así es como integras Loki con scripts, pipelines de alerta o la capa de análisis de logs con IA cubierta en Análisis de logs con IA usando Ollama en un VPS: detectar anomalías con un LLM local.
La API acepta las mismas consultas LogQL que usas en Grafana. El endpoint principal es /loki/api/v1/query_range para consultas con rango de tiempo.
Consultar logs recientes
END=$(date +%s)000000000
START=$(( $(date +%s) - 3600 ))000000000
curl -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode "query={unit=\"sshd.service\"} |= \"Failed password\"" \
--data-urlencode "start=$START" \
--data-urlencode "end=$END" \
--data-urlencode "limit=50" | jq .
Los parámetros start y end usan timestamps Unix en nanosegundos. La aritmética shell de arriba calcula "ahora menos 1 hora" y añade nueve ceros para precisión en nanosegundos.
La respuesta es JSON:
{
"status": "success",
"data": {
"resultType": "streams",
"result": [
{
"stream": {"unit": "sshd.service", "severity": "info"},
"values": [
["1710850000000000000", "Mar 19 14:00:00 vps sshd[1234]: Failed password for root from 203.0.113.5 port 22"]
]
}
]
}
}
Cada valor es un par [timestamp_nanosegundos, línea_de_log]. Es el formato exacto que parsearás cuando alimentes logs a un LLM local para análisis.
Consultar etiquetas y streams
Listar todos los nombres de etiquetas:
curl -s "http://127.0.0.1:3100/loki/api/v1/labels" | jq .
Listar valores de una etiqueta específica:
curl -s "http://127.0.0.1:3100/loki/api/v1/label/unit/values" | jq .
Estos endpoints son útiles para construir consultas dinámicas en scripts de automatización. Puedes enumerar todas las unidades y luego consultar cada una en busca de errores.
Consultas instantáneas
Para consultas "justo ahora" sin rango de tiempo, usa /loki/api/v1/query:
curl -s "http://127.0.0.1:3100/loki/api/v1/query" \
--data-urlencode 'query=count_over_time({job="systemd-journal"} |= "error" [1h])' | jq .
Esto devuelve un único punto de datos: el recuento de errores del journal en la última hora. Útil para comprobaciones de salud y scripts de monitoreo.
Resolución de problemas
Promtail muestra "permission denied" para el socket Docker:
El contenedor Promtail necesita acceso de lectura a /var/run/docker.sock. Comprueba los permisos del socket en el host:
ls -la /var/run/docker.sock
El socket pertenece normalmente a root:docker. La imagen Promtail se ejecuta como root por defecto, así que generalmente funciona. Si ejecutas Promtail con un usuario personalizado, ese usuario debe estar en el grupo docker.
No aparecen logs del journal:
Primero, busca en los logs de Promtail el mensaje support for reading the systemd journal is not compiled into this build of promtail. Si lo ves, la imagen Docker no soporta scraping del journal. Instala Promtail como binario en el host o cambia a Grafana Alloy (ver secciones anteriores).
Si Promtail se ejecuta como binario en el host con soporte de journal, verifica que el directorio del journal existe:
ls -la /var/log/journal/
Si no existe, systemd está usando almacenamiento de journal volátil (solo en memoria). Habilita almacenamiento persistente:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
Luego actualiza el montaje de volumen de Promtail en docker-compose.yml de /run/log/journal a /var/log/journal y reinicia:
docker compose up -d promtail
Loki reporta "too many outstanding requests":
La carga de consultas supera la capacidad de Loki. Reduce el rango de tiempo de tus consultas o añade límites:
limits_config:
max_query_parallelism: 16
max_query_series: 500
Grafana muestra "Data source connected, no labels found":
Loki necesita unos minutos para ingerir e indexar los primeros logs. Espera 2-3 minutos y reintenta la consulta. Verifica que Loki está listo:
curl -s http://127.0.0.1:3100/ready
Revisar los logs de todos los servicios a la vez:
docker compose logs -f --tail=50
Esto sigue los tres servicios. Filtra los problemas:
docker compose logs --tail=100 | grep -i error
Próximos pasos
Tu pipeline Loki está recopilando logs de systemd, Docker y Nginx. Puedes consultarlos con LogQL en Grafana o a través de la API HTTP.
Desde aquí:
- Análisis de logs con IA usando Ollama en un VPS: detectar anomalías con un LLM local Alimenta estos logs a un LLM local para detección de anomalías y alertas automatizadas
- AIOps en un VPS: gestión de servidores con IA y herramientas open source Visión general del stack AIOps autoalojado
- [-> self-host-signoz-openobserve-vps] Si prefieres un APM todo en uno al stack de Grafana
Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.
¿Listo para probarlo?
Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.
Ver planes VPS