Despliegue de OpenTelemetry Collector y Jaeger para distributed tracing en un VPS
TL;DR
OpenTelemetry Collector recibe trazas de las aplicaciones mediante OTLP, las procesa y las transmite a Jaeger, donde se pueden buscar solicitudes, revisar cadenas de llamadas y encontrar servicios lentos. En esta guía desplegará distributed tracing self-hosted en un VPS con Ubuntu mediante Docker Compose, cerrará los puertos técnicos del firewall, publicará Jaeger UI mediante HTTPS y configurará copias de seguridad de los datos.
- OpenTelemetry Collector recibirá OTLP/gRPC en el puerto
4317y OTLP/HTTP en4318. - Jaeger almacenará las trazas en el almacenamiento persistente local Badger y las mostrará mediante la interfaz web.
- Jaeger UI estará disponible únicamente mediante el reverse proxy HTTPS Caddy, no directamente a través del puerto
16686. - Para las pruebas bastan 2 vCPU, 4 GB de RAM y 40 GB de NVMe; para una carga de production es más razonable comenzar con 4 vCPU y 8 GB de RAM.
- Los endpoint’ы de tracing no deben quedar abiertos a todo Internet: limite las fuentes a las direcciones IP de su infraestructura o a la red VPN.
- Los archivos de configuración, los datos de Jaeger y los Docker volumes deben copiarse periódicamente a un almacenamiento externo.
Qué configuramos y por qué
Distributed tracing es necesario cuando una solicitud de usuario pasa por varios componentes: frontend, API, cola, worker, base de datos, servicio de pagos de terceros o LLM API. Los logs habituales mostrarán eventos individuales y las métricas mostrarán la imagen general de la carga, pero no responderán rápidamente a la pregunta: «¿Por qué una solicitud concreta del usuario tardó 8 segundos y en qué servicio se produjo el retraso?»
Una traza consta de span’ов relacionados. Cada span describe una operación: una solicitud HTTP, una consulta SQL, la publicación de un mensaje en Kafka, la ejecución de un job o una llamada a Redis. Un span tiene una hora de inicio, duración, estado, atributos e identificadores trace_id y span_id. Si los servicios transmiten el trace context mediante encabezados HTTP o una cola, Jaeger reúne la ruta de la solicitud en un árbol único.
En este esquema, OpenTelemetry Collector actúa como una capa intermedia entre las aplicaciones y el almacenamiento de trazas. Las aplicaciones envían los datos al Collector mediante el protocolo estándar OTLP. Collector recibe los datos, añade atributos útiles, limita la memoria, ejecuta batching y exporta las trazas a Jaeger. Gracias a ello, las aplicaciones no dependen de un backend concreto: más adelante, Jaeger se puede sustituir por Tempo, OpenSearch, un almacenamiento compatible con ClickHouse o un servicio cloud sin reescribir la instrumentación.
| Componente | Función | Acceso externo |
|---|---|---|
| Aplicación instrumentada | Crea spans y transmite el contexto entre servicios | No |
| OpenTelemetry Collector Contrib | Recibe, filtra, agrupa y exporta trazas OTLP | Solo desde servidores de confianza o VPN |
| Jaeger all-in-one | Recibe y almacena las trazas y proporciona una API de búsqueda | Solo dentro de la red Docker |
| Caddy | Emite el certificado TLS y publica Jaeger UI de forma segura | TCP 80 y 443 |
| Badger | Almacenamiento embedded local de los datos de Jaeger | No |
Qué obtendrá como resultado
Después de completar las instrucciones, tendrá un dominio del tipo traces.example.com con acceso HTTPS a Jaeger UI. Las aplicaciones podrán enviar trazas OTLP/gRPC a la dirección https://otel.example.com:4317 o a la IP privada de Collector sin TLS, si todos los servicios se encuentran en la red interna o están conectados mediante WireGuard.
En Jaeger podrá seleccionar un servicio, una operación y un intervalo de tiempo, abrir una trace individual, ver el diagrama waterfall de los span’ов, identificar el cuello de botella y pasar a los logs relacionados mediante trace_id. En la configuración añadiremos los atributos deployment.environment y host.name para que production, staging y los distintos servidores no se mezclen en una misma búsqueda.
Self-hosted o cloud-managed tracing
Las plataformas cloud de observabilidad suelen ofrecer un inicio rápido, alertas listas para usar, almacenamiento prolongado y un backend escalable. Sus desventajas son el pago por el volumen de ingestion, la dependencia del proveedor, la transmisión de metadatos técnicos a terceros y el riesgo de un aumento inesperado de la factura durante un pico de tráfico.
OpenTelemetry Collector y Jaeger self-hosted en un VPS son adecuados para equipos pequeños, servicios internos, SaaS en sus primeras etapas, entornos de prueba y proyectos con requisitos de control de datos. Usted elige de forma independiente el retention, el acceso, las copias de seguridad y la ubicación. El precio de esta flexibilidad es la necesidad de supervisar el disco, las actualizaciones, la seguridad y el crecimiento del volumen de trazas.
Jaeger con Badger en este artículo es una opción práctica para un volumen pequeño y medio: decenas de servicios y un número moderado de solicitudes. Para un almacenamiento prolongado, una cardinalidad elevada, un flujo de varios terabytes y varias instancias de Collector, será necesario un backend independiente: OpenSearch, Elasticsearch, ClickHouse u otro production storage compatible. La arquitectura con Collector se mantiene; solo cambian el exporter y el backend de Jaeger.
Qué configuración de VPS se necesita para esta tarea
El consumo de recursos depende no tanto del número de servicios como del número de spans por segundo, del tamaño medio de los atributos y del tiempo de almacenamiento. Una solicitud HTTP puede crear entre 3 y 5 span’ов en una API sencilla, o entre 30 y 100 span’ов en un sistema de microservicios con SQL, Redis, colas y API externas. No incluya el registro del cuerpo de las solicitudes HTTP, tokens, contraseñas ni payload’ов grandes: es peligroso y llena rápidamente el almacenamiento.
| Escenario | CPU | RAM | Disco NVMe | Red | Escala esperada |
|---|---|---|---|---|---|
| Laboratorio, staging, 1–3 aplicaciones | 2 vCPU | 4 GB | 40 GB | 100 Mbps | Hasta varios miles de span/min |
| Production pequeño | 4 vCPU | 8 GB | 100–160 GB | 1 Gbps | 5–30 servicios, sampling moderado |
| SaaS activo o varios entornos | 8 vCPU | 16–32 GB | 300 GB+ | 1 Gbps | Ingestion elevada, retention de 7–30 días |
Para comenzar un despliegue de production, elija 4 vCPU, 8 GB de RAM, 120 GB de NVMe y un puerto de 1 Gbps. Esta reserva permitirá que Collector trabaje con memory limiter y colas, que Jaeger almacene datos en caché y que el sistema no pase a swap durante cargas breves. Como opción neutral puede elegir un VPS con las características indicadas, pero antes de realizar el pedido compruebe el tipo de unidad, el límite de tráfico, la dirección IPv4 y la posibilidad de ampliar el disco sin migración.
Cómo calcular el volumen de disco
Comience midiendo, no suponiendo. Supongamos que la aplicación crea 20 span’ов por solicitud, procesa 10 solicitudes por segundo y que, después de la serialización y los índices, un span ocupa de media entre 1 y 3 KB. Esto supone aproximadamente entre 17 y 52 GB al día, sin contar la compresión, los datos de servicio y el sampling. Con un sampling del 10 %, el volumen se reducirá aproximadamente diez veces, por lo que el sampling es el principal regulador del coste de almacenamiento.
Para el primer mes, reserve como mínimo el doble de espacio que el volumen calculado para el periodo de retention. Esto dejará margen para las actualizaciones de las imágenes Docker, los archivos de copia de seguridad y un crecimiento repentino del tráfico. Configure una alerta cuando el disco alcance el 70 % de ocupación, no el 95 %: Badger y Docker pueden degradarse rápidamente cuando se acaba el espacio libre.
Cuándo se necesita un servidor dedicado en lugar de un VPS
Un VPS es adecuado mientras la carga sea predecible y la latencia del disco no sea crítica. Un servidor dedicado tiene sentido con un ingestion constantemente elevado, periodos de retention prolongados, necesidad de varios NVMe en RAID, requisitos de aislamiento de CPU e IOPS o el uso de un backend pesado como OpenSearch. Un servidor dedicado también es razonable si el sistema de tracing presta servicio a decenas de equipos y se convierte en una plataforma interna de observabilidad.
No traslade Jaeger a un servidor dedicado solo porque en la interfaz hayan aparecido decenas de miles de traces. Primero active head sampling en el SDK, elimine los atributos innecesarios, reduzca el retention, compruebe la velocidad del disco y separe production y staging en servicios distintos. Normalmente esto produce un efecto más notable que aumentar inmediatamente el servidor.
Elección de la ubicación
La ubicación influye en la latencia del envío de telemetría, los requisitos legales y el coste del tráfico. Es mejor colocar Collector cerca de los servicios que envían las trazas: en el mismo centro de datos, región o, al menos, continente. En OTLP/gRPC síncrono, un RTT elevado aumenta los costes adicionales de la aplicación, especialmente cuando la red presenta problemas.
Si los servicios de production se encuentran en distintas regiones, no obligue a cada servicio a enviar datos por Internet a un único Collector. Despliegue instancias locales de Collector junto a las aplicaciones y envíe los datos posteriormente de forma asíncrona o a un backend central. Para empezar basta con un VPS, pero conserve arquitectónicamente la posibilidad de realizar esta separación.
Preparación del servidor
A continuación se utiliza Ubuntu Server 24.04 LTS. Ejecute los comandos como root solo durante la fase de preparación inicial; después, trabaje con un usuario independiente otel. Se supone que dispone de los dominios traces.example.com para Jaeger UI y, si es necesario, otel.example.com para el endpoint OTLP público.
Antes de configurar TLS, cree un registro A traces.example.com que apunte a la dirección IPv4 pública del servidor. Si planea recibir OTLP a través de un dominio independiente, añada también el registro otel.example.com. Puede comprobar el DNS desde el equipo local con el comando dig +short traces.example.com.
Actualización del sistema operativo
Primero instale las actualizaciones acumuladas y las utilidades básicas. Después de actualizar el kernel, reinicie el servidor antes de continuar con la configuración de Docker.
# Actualiza los índices de paquetes e instala las actualizaciones de seguridad.
sudo apt update && sudo apt upgrade -y
# Instala herramientas de diagnóstico, firewall, editor y soporte para repositorios HTTPS.
sudo apt install -y ca-certificates curl gnupg lsb-release \
ufw fail2ban unattended-upgrades jq vim htop dnsutils
# Reinicia el servidor si se actualizó el kernel o las bibliotecas del sistema.
sudo reboot
Usuario, claves SSH y sudo
Cree una cuenta independiente. No utilice una contraseña para SSH: una clave Ed25519 es más fácil de proteger contra brute-force y resulta más cómoda para la automatización. Antes de desactivar el inicio de sesión de root, abra obligatoriamente una segunda sesión SSH y compruebe que el nuevo usuario puede iniciar sesión y ejecutar sudo.
# Crea el usuario otel con un directorio personal y lo añade al grupo sudo.
sudo adduser otel
sudo usermod -aG sudo otel
# Crea el directorio de claves y copia el authorised_keys actual para el nuevo usuario.
sudo install -d -m 700 -o otel -g otel /home/otel/.ssh
sudo cp /root/.ssh/authorized_keys /home/otel/.ssh/authorized_keys
sudo chown otel:otel /home/otel/.ssh/authorized_keys
sudo chmod 600 /home/otel/.ssh/authorized_keys
Edite la configuración de SSH. Si se conecta con un usuario distinto de root o la clave del proveedor se encuentra en otra ubicación, no copie el archivo a ciegas: añada su clave pública mediante ssh-copy-id otel@SERVER_IP desde el equipo local.
# Abre un archivo de configuración SSH independiente para reforzar los ajustes de acceso.
sudo vim /etc/ssh/sshd_config.d/99-hardening.conf
# Comprueba la sintaxis de la configuración y reinicia SSH solo si la comprobación es correcta.
sudo sshd -t && sudo systemctl restart ssh
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
AllowUsers otel
Firewall y Fail2ban
Abra externamente solo SSH, HTTP y HTTPS. No es necesario publicar los puertos 4317, 4318, 16686 y 14268 para todo Internet. Si las aplicaciones se encuentran en otros servidores, más adelante permita OTLP únicamente desde su subred privada o a través de WireGuard.
# Permite SSH antes de activar el firewall; de lo contrario, puede perder el acceso al servidor.
sudo ufw allow OpenSSH
# Permite que Caddy acepte HTTP para la comprobación de ACME y HTTPS para Jaeger UI.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Activa el firewall y muestra las reglas activas.
sudo ufw enable
sudo ufw status verbose
# Activa Fail2ban para proteger SSH contra intentos de inicio de sesión fallidos repetidos.
sudo systemctl enable --now fail2ban
# Comprueba que el jail de SSH está activo.
sudo fail2ban-client status sshd
Una característica importante de Docker: los puertos de los contenedores publicados pueden eludir las reglas habituales de UFW debido a las reglas de iptables creadas por Docker. En estas instrucciones, los puertos OTLP no se publican de forma predeterminada. Si más adelante los añade a la sección ports, limite el acceso mediante reglas independientes en la cadena DOCKER-USER o utilice private networking/VPN.
Instalación del software — paso a paso
Utilizamos Docker Engine 27.x o posterior, Docker Compose Plugin 2.x, OpenTelemetry Collector Contrib 0.120.x y Jaeger all-in-one 1.67.x como un conjunto estable compatible. Antes de actualizar en production, compruebe las etiquetas de imagen disponibles en las release notes oficiales y pruebe la actualización en staging: la versión de Collector y la imagen de Jaeger pueden cambiar de forma independiente.
Instalación de Docker Engine desde el repositorio oficial
No utilice el paquete antiguo docker.io del repositorio estándar de Ubuntu: puede estar considerablemente desactualizado. Los siguientes comandos añaden el repositorio oficial de Docker e instalan el engine junto con el Compose plugin.
# Elimina los paquetes antiguos de Docker que puedan causar conflictos, si estaban instalados.
sudo apt remove -y docker.io docker-doc docker-compose docker-compose-v2 \
podman-docker containerd runc || true
# Crea el directorio para las claves de repositorios APT de terceros.
sudo install -m 0755 -d /etc/apt/keyrings
# Descarga y guarda la clave del repositorio de Docker en formato keyring.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Añade el repositorio APT oficial de Docker para la versión actual de Ubuntu.
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Instala Docker Engine, CLI, containerd y Compose plugin.
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
# Inicia Docker durante el arranque y comprueba la versión instalada.
sudo systemctl enable --now docker
sudo docker version
sudo docker compose version
# Permite que el usuario otel administre Docker sin sudo.
sudo usermod -aG docker otel
Después de añadirlo al grupo docker, cierre la sesión SSH y vuelva a conectarse como otel. La pertenencia al grupo Docker proporciona, en la práctica, privilegios equivalentes a root en el servidor, por lo que solo debe añadir administradores de confianza.
Directorios del proyecto y secretos
Guarde el archivo Compose, las configuraciones y los datos en un directorio predecible. El directorio /opt/observability suele ser más conveniente que el directorio personal: es más fácil incluirlo en las copias de seguridad y no eliminarlo accidentalmente al cambiar de usuario.
# Crea la estructura de directorios para las configuraciones, Caddy y los datos persistentes de Jaeger.
sudo mkdir -p /opt/observability/{otel-collector,caddy,data/jaeger}
sudo chown -R otel:otel /opt/observability
# Accede al directorio de trabajo del proyecto.
cd /opt/observability
# Crea .env con permisos únicamente para el propietario.
umask 077
touch .env
chmod 600 .env
El secreto de esta configuración será el hash de la contraseña para Jaeger UI. Caddy no almacena la contraseña en texto plano. Genere el hash de forma interactiva: el comando solicitará la contraseña y mostrará una cadena que comienza con $2a$ o un hash bcrypt similar.
# Genera un hash bcrypt de la contraseña para la autenticación HTTP Basic en Caddy.
docker run --rm caddy:2 caddy hash-password --plaintext 'SUSTITUYA_POR_UNA_CONTRASEÑA_LARGA'
# Abre el archivo de variables de entorno para introducir los dominios y el hash generado.
vim /opt/observability/.env
No inserte la contraseña real en docker-compose.yml, no envíe .env a un repositorio Git público y no utilice la misma contraseña para SSH y la interfaz web. Para el equipo, es preferible utilizar una VPN o un identity proxy externo y dejar Basic Auth como una capa de protección adicional.
Comprobación de los límites del sistema
Jaeger con Badger utiliza el sistema de archivos de forma más intensiva que un contenedor stateless simple. Antes de iniciarlo, compruebe el espacio libre en disco y desactive swap solo si dispone de suficiente RAM y de una política OOM clara. Para un VPS pequeño, swap resulta útil como búfer de emergencia, pero una actividad constante de swap indica que el servidor no tiene suficiente memoria.
# Muestra el espacio libre, la memoria RAM disponible y la presencia de swap.
df -h /
free -h
swapon --show
# Crea un registro de Docker con un límite de tamaño para evitar que los logs de los contenedores llenen el disco.
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "local",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}
EOF
# Reinicia Docker para aplicar la política de registro.
sudo systemctl restart docker
Configuración de OpenTelemetry Collector, Jaeger y HTTPS
A continuación se muestra una configuración en la que solo se publican hacia el exterior los puertos 80 y 443 de Caddy. Collector está disponible dentro de la red de Docker. Esta es una opción segura si las aplicaciones se ejecutan en el mismo servidor o en la misma red de Docker. Para aplicaciones remotas, más adelante se mostrará cómo abrir con precaución los puertos OTLP para una subred de confianza.
Archivo de variables de entorno
Reemplace el dominio, el correo electrónico y el hash bcrypt. En el valor de CADDY_BASIC_AUTH_HASH, Docker Compose puede interpretar el símbolo $ como una sustitución de variable, por lo que cada $ se reemplaza por un doble $$. Esta es una característica normal de Compose.
# /opt/observability/.env
TRACES_DOMAIN=traces.example.com
[email protected]
# Вставьте результат caddy hash-password, заменив каждый $ на $$.
CADDY_BASIC_AUTH_HASH=$$2a$$14$$REPLACE_WITH_YOUR_BCRYPT_HASH
# Среда добавляется Collector ко всем входящим трейсам.
DEPLOYMENT_ENVIRONMENT=production
Configuración de OpenTelemetry Collector
Collector acepta OTLP mediante gRPC y HTTP, protege el servidor contra el desbordamiento de memoria mediante memory_limiter, agrupa los spans en paquetes y los envía a Jaeger mediante OTLP/gRPC. Importante: el receiver escucha en 0.0.0.0 dentro del contenedor, pero esto todavía no hace que el puerto sea accesible desde Internet mientras no se publique en Compose.
# /opt/observability/otel-collector/config.yaml
extensions:
health_check:
endpoint: 0.0.0.0:13133
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
resource:
attributes:
- key: deployment.environment
value: ${env:DEPLOYMENT_ENVIRONMENT}
action: upsert
- key: host.name
value: tracing-vps
action: upsert
batch:
send_batch_size: 8192
timeout: 5s
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
sending_queue:
enabled: true
num_consumers: 4
queue_size: 10000
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
extensions: [health_check]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [otlp/jaeger]
telemetry:
logs:
level: info
El límite de 512 MiB está calculado para un servidor con 8 GB de RAM. En un VPS con 4 GB, redúzcalo a 256 MiB; en un servidor con 16 GB puede aumentarlo a 1024 MiB, si la carga lo justifica. Cuando alcance el límite, Collector comenzará a rechazar parte de los datos en lugar de provocar un OOM y detener toda la pila.
Docker Compose: Collector, Jaeger y Caddy
Jaeger all-in-one recibe los traces de Collector en el puerto interno 4317. Los directorios de Badger se han trasladado a un bind mount, por lo que eliminar el contenedor no eliminará los datos. La variable BADGER_SPAN_STORE_TTL establece una retención de 168 horas, es decir, 7 días. Ajuste el período de almacenamiento según el espacio disponible y la necesidad de realizar investigaciones.
# /opt/observability/docker-compose.yml
services:
jaeger:
image: jaegertracing/all-in-one:1.67.0
container_name: jaeger
restart: unless-stopped
environment:
COLLECTOR_OTLP_ENABLED: "true"
SPAN_STORAGE_TYPE: badger
BADGER_EPHEMERAL: "false"
BADGER_DIRECTORY_VALUE: /badger/data
BADGER_DIRECTORY_KEY: /badger/key
BADGER_SPAN_STORE_TTL: 168h
MEMORY_MAX_TRACES: "0"
volumes:
- ./data/jaeger:/badger
networks:
- tracing
otel-collector:
image: otel/opentelemetry-collector-contrib:0.120.0
container_name: otel-collector
restart: unless-stopped
command: ["--config=/etc/otelcol-contrib/config.yaml"]
environment:
DEPLOYMENT_ENVIRONMENT: ${DEPLOYMENT_ENVIRONMENT}
volumes:
- ./otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml:ro
depends_on:
- jaeger
networks:
- tracing
caddy:
image: caddy:2.8.4
container_name: caddy
restart: unless-stopped
environment:
TRACES_DOMAIN: ${TRACES_DOMAIN}
ACME_EMAIL: ${ACME_EMAIL}
CADDY_BASIC_AUTH_HASH: ${CADDY_BASIC_AUTH_HASH}
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- jaeger
networks:
- tracing
networks:
tracing:
name: tracing
driver: bridge
volumes:
caddy_data:
caddy_config:
Caddy y acceso HTTPS a Jaeger UI
Caddy obtiene y renueva automáticamente el certificado TLS mediante ACME si el DNS del dominio ya apunta al servidor y los puertos TCP 80 y 443 están accesibles desde el exterior. Basic Auth protege la interfaz contra la visualización accidental de traces. No considere Basic Auth un sistema completo de gestión de acceso: para uso corporativo, añada una VPN, un SSO-proxy o una ACL de red.
# /opt/observability/caddy/Caddyfile
{
email {$ACME_EMAIL}
auto_https prefer_wildcard
}
{$TRACES_DOMAIN} {
encode zstd gzip
basic_auth {
observability {$CADDY_BASIC_AUTH_HASH}
}
reverse_proxy jaeger:16686
header {
-Server
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
log {
output stdout
format json
}
}
Inicio y comprobación de los contenedores
Antes de iniciar, compruebe la sintaxis de Compose. A continuación, descargue las imágenes e inicie los servicios en segundo plano. La primera emisión del certificado TLS puede tardar hasta un minuto; no cambie el DNS ni reinicie Caddy sin necesidad.
# Проверяет подстановку переменных и итоговую конфигурацию Docker Compose.
cd /opt/observability
docker compose config > /tmp/observability-compose-rendered.yml
# Скачивает зафиксированные версии образов и запускает стек в фоне.
docker compose pull
docker compose up -d
# Показывает состояние контейнеров и последние строки журналов.
docker compose ps
docker compose logs --tail=100 otel-collector jaeger caddy
# Проверяет health endpoint Collector из его Docker-сети.
docker exec otel-collector wget -qO- http://127.0.0.1:13133/ || true
# Проверяет, что Jaeger API отвечает внутри контейнера.
docker exec jaeger wget -qO- http://127.0.0.1:16686/api/services
# Проверяет публичный HTTPS endpoint; потребуется пароль Basic Auth.
curl -I -u observability:'ВАШ_ПАРОЛЬ' https://traces.example.com
Abra https://traces.example.com en el navegador, introduzca el usuario observability y la contraseña para la que generó el hash. Si Jaeger UI se carga, pero la lista de servicios está vacía, es normal: los servicios aparecerán solo después de enviar el primer trace.
Cómo enviar un trace de prueba
La prueba más sencilla consiste en iniciar temporalmente el contenedor telemetrygen en la misma red de Docker. Creará spans de prueba y los enviará a Collector. Después, en Jaeger, seleccione el servicio telemetrygen, la operación traces y pulse Find Traces.
# Отправляет 20 тестовых trace в секунду в Collector внутри Docker-сети в течение 10 секунд.
docker run --rm --network tracing \
ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:0.120.0 \
traces --otlp-endpoint otel-collector:4317 \
--otlp-insecure --rate 20 --duration 10s
# Проверяет, что сервис telemetrygen появился в Jaeger API.
docker exec jaeger wget -qO- http://127.0.0.1:16686/api/services | jq .
Conexión de aplicaciones remotas
El enfoque más seguro consiste en conectar los servidores mediante WireGuard, Tailscale o la red privada del proveedor y publicar los puertos de Collector únicamente en la interfaz privada. Si esto no es posible, añada los puertos 4317 y 4318 en Compose, pero permita el acceso únicamente desde la dirección IP conocida de la aplicación. No los abra a 0.0.0.0/0: cualquiera podrá enviar spans basura y llenar su disco.
# Добавьте в сервис otel-collector только при необходимости внешнего OTLP.
# Лучше заменить 10.20.0.10 на приватный IP интерфейса сервера.
ports:
- "10.20.0.10:4317:4317"
- "10.20.0.10:4318:4318"
Para una aplicación en Python, Node.js, Go, Java o .NET, especifique el endpoint mediante variables de entorno. La instrumentación con bibliotecas OpenTelemetry SDK se realiza por separado, pero las variables básicas son prácticamente iguales para todos los lenguajes.
# Пример общих переменных окружения приложения для OTLP/gRPC.
export OTEL_SERVICE_NAME=orders-api
export OTEL_RESOURCE_ATTRIBUTES="service.version=2026.03,deployment.environment=production"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://10.20.0.10:4317"
export OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1"
El sampling 0.1 significa registrar aproximadamente el 10 % de los nuevos traces. El sampler principal conserva la decisión de la solicitud principal en todos los servicios secundarios, por lo que no obtendrá fragmentos de un mismo trace. Para errores importantes, posteriormente puede implementar tail sampling en Collector: permite conservar el 100 % de los traces con errores y las solicitudes lentas, pero requiere más memoria y una configuración más cuidadosa.
Copias de seguridad y mantenimiento
En este despliegue es necesario copiar el directorio /opt/observability/data/jaeger, el archivo Compose, la configuración de Collector, Caddyfile, el archivo .env y los datos de Caddy volume. Estos últimos contienen los certificados TLS y el estado de ACME; sin ellos, Caddy podrá emitir el certificado nuevamente, pero la recuperación será más lenta y puede verse limitada por los rate limits.
No considere la Docker image como una copia de seguridad. La imagen se puede descargar de nuevo, pero los traces en Badger y la configuración no se restaurarán por sí solos. Tampoco almacene la única copia de seguridad en el mismo VPS: la pérdida del disco, una eliminación accidental o la vulneración del servidor destruirán tanto los datos como la copia de seguridad.
Copia de seguridad mediante restic en un almacenamiento compatible con S3
Restic cifra el contenido antes de enviarlo y admite deduplicación, por lo que es adecuado para copias diarias. Cree un bucket en un proveedor S3 externo o en un servidor MinIO independiente. Almacene los datos de acceso en un archivo accesible solo por root, no en la línea de comandos ni en el historial del shell.
# Instala restic desde el repositorio de Ubuntu.
sudo apt install -y restic
# Crea el directorio para los secretos de la copia de seguridad con acceso solo para root.
sudo install -d -m 700 /root/.config/restic
# Crea el archivo de variables de S3 y la contraseña del repositorio.
sudo vim /root/.config/restic/observability.env
sudo chmod 600 /root/.config/restic/observability.env
# /root/.config/restic/observability.env
export RESTIC_REPOSITORY="s3:https://s3.example.net/observability-restic"
export RESTIC_PASSWORD="СГЕНЕРИРУЙТЕ_ОТДЕЛЬНЫЙ_ДЛИННЫЙ_ПАРОЛЬ"
export AWS_ACCESS_KEY_ID="REPLACE_ME"
export AWS_SECRET_ACCESS_KEY="REPLACE_ME"
export AWS_DEFAULT_REGION="us-east-1"
# Inicializa una vez el repositorio restic cifrado.
sudo bash -c 'source /root/.config/restic/observability.env && restic init'
# Crea el directorio para los scripts del administrador.
sudo install -d -m 700 /usr/local/sbin
# Crea el script diario de copia de seguridad.
sudo vim /usr/local/sbin/backup-observability.sh
sudo chmod 700 /usr/local/sbin/backup-observability.sh
#!/usr/bin/env bash
set -euo pipefail
source /root/.config/restic/observability.env
cd /opt/observability
# Detiene la escritura en Badger durante la copia consistente.
docker compose stop jaeger
# Copia las configuraciones, los datos de Jaeger y el Docker volume de Caddy.
restic backup \
/opt/observability/otel-collector \
/opt/observability/caddy \
/opt/observability/data/jaeger \
/opt/observability/docker-compose.yml \
/opt/observability/.env \
/var/lib/docker/volumes/observability_caddy_data \
--tag observability
# Inicia Jaeger incluso si posteriormente falla la limpieza.
docker compose start jaeger
# Conserva 7 conjuntos diarios, 4 semanales y 6 mensuales.
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
# Comprueba la integridad de una parte de los datos del repositorio.
restic check --read-data-subset=5%
Para garantizar el inicio de Jaeger incluso en caso de error, puede añadir al script trap 'docker compose start jaeger' EXIT. Con un volumen pequeño de datos, una breve detención nocturna de Jaeger es aceptable. Para grandes almacenes de production, utilice los mecanismos nativos de snapshot del backend en lugar de copiar archivos de una base de datos en funcionamiento.
# Añade la ejecución diaria de la copia de seguridad a las 03:20 y registra el log en un archivo protegido.
sudo crontab -e
# Línea para el crontab de root:
20 3 * /usr/local/sbin/backup-observability.sh >> /var/log/backup-observability.log 2>&1
Comprobación de la recuperación
Una copia de seguridad sin una prueba de recuperación es solo una suposición. Una vez al mes, restaure los datos en un VPS de prueba independiente o, al menos, en un directorio temporal. Compruebe que, después de iniciar Compose, se abre Jaeger UI y que los servicios y traces antiguos están disponibles en la búsqueda.
# Muestra los snapshots disponibles en el repositorio restic.
sudo bash -c 'source /root/.config/restic/observability.env && restic snapshots'
# Restaura el último snapshot en un directorio temporal para comprobarlo.
sudo mkdir -p /tmp/observability-restore
sudo bash -c 'source /root/.config/restic/observability.env && \
restic restore latest --target /tmp/observability-restore'
Actualizaciones y retention
Para un VPS, realice la actualización durante una maintenance window: haga una copia de seguridad, lea el changelog de las imágenes, actualice un componente, compruebe su health y solo entonces pase al siguiente. El comando docker compose pull && docker compose up -d crea una breve interrupción en la recepción de traces. Los SDK suelen almacenar parte de los datos en búfer, pero no se debe confiar en ello sin realizar pruebas.
El rolling update es necesario cuando no es aceptable perder siquiera unos segundos de telemetría. En ese caso, ejecute al menos dos instancias de Collector detrás de un balanceador, utilice colas y un backend HA para Jaeger. Un Jaeger all-in-one con Badger local no admite un rolling upgrade real sin interrupciones y no constituye una configuración de alta disponibilidad.
# Comprueba el uso del disco, los contenedores y el tamaño de los datos de Jaeger.
df -h /
docker stats --no-stream
sudo du -sh /opt/observability/data/jaeger
# Actualiza las imágenes solo después de crear una copia de seguridad comprobada.
cd /opt/observability
docker compose pull
docker compose up -d
docker compose logs --tail=100
Solución de problemas y FAQ
¿Por qué Jaeger UI se abre, pero la lista de servicios está vacía?
Una lista vacía significa que Jaeger todavía no ha recibido ningún trace válido o que Collector no puede exportar los datos. Primero, realice la prueba con telemetrygen de la sección de configuración. Después, compruebe docker compose logs otel-collector: busque errores exporter, connection refused o deadline exceeded. Asegúrese de que el endpoint del exporter está indicado como jaeger:4317 y de que Jaeger tiene habilitado COLLECTOR_OTLP_ENABLED=true. Compruebe también que la aplicación utiliza precisamente el traces exporter y no solo metrics o logs.
¿Por qué la aplicación recibe connection refused en el puerto 4317?
De forma predeterminada, en este artículo el puerto 4317 no está publicado externamente, por lo que un servidor remoto no puede conectarse a Collector. Esto es intencionado. Para aplicaciones en el mismo host, utilice Docker network y la dirección otel-collector:4317. Para otro servidor, configure WireGuard o private network y, a continuación, añada una publicación segura del puerto en la IP privada. Compruebe también que la aplicación no intenta utilizar el endpoint HTTP 4318 con el protocolo gRPC, o viceversa.
Caddy no obtiene el certificado TLS: ¿qué se debe comprobar?
Compruebe que el registro A del dominio apunta a la IP pública del VPS: dig +short traces.example.com. Después, asegúrese de que los puertos 80 y 443 están permitidos en UFW y en el firewall de red del panel del proveedor. Consulte docker compose logs caddy; los errores de ACME suelen indicar directamente un problema de DNS, un puerto cerrado o la superación del límite de solicitudes de certificados. No utilice un proxy CDN en un modo que interfiera con HTTP challenge hasta confirmar que Caddy ha obtenido el primer certificado.
Collector consume mucha RAM o se reinicia con OOMKilled. ¿Cómo se soluciona?
Primero, confirme la causa con el comando docker inspect otel-collector y consulte el campo OOMKilled. Reduzca limit_mib y spike_limit_mib en el processor memory_limiter, reduzca queue_size, disminuya el sampling en el SDK y desactive los atributos innecesarios. No aumente los límites de memoria indefinidamente: si el flujo de traces supera las capacidades del VPS, Collector simplemente acumulará más datos antes de fallar. Para una carga elevada, añada un segundo Collector y un balanceador.
El disco se llena rápidamente, aunque retention está configurado en 7 días.
Compruebe el tamaño real de Badger mediante du -sh /opt/observability/data/jaeger, los logs de Docker mediante docker system df y los logs del sistema mediante journalctl --disk-usage. Retention no siempre libera espacio de inmediato: la compaction del almacenamiento requiere tiempo. Reduzca el sampling, disminuya BADGER_SPAN_STORE_TTL, elimine las imágenes de Docker no utilizadas con el comando docker image prune y no registre el cuerpo de las solicitudes/respuestas en los atributos de los spans. Es más seguro añadir espacio de disco antes de alcanzar un 90 % de ocupación.
¿Qué configuración mínima de VPS es adecuada?
Para pruebas, un laboratorio doméstico o staging, son suficientes 2 vCPU, 4 GB de RAM, 40 GB de NVMe y 100 Mbps. Este servidor es adecuado para varias aplicaciones con un sampling del 5–20 % y un retention corto. Para production, es preferible utilizar 4 vCPU, 8 GB de RAM y 100–160 GB de NVMe: habrá margen para las colas de Collector, la caché, las actualizaciones y el crecimiento del volumen de datos. Si el disco es un HDD lento, el tracing backend puede ralentizarse incluso con una cantidad suficiente de CPU.
¿Qué elegir: VPS o dedicated para esta tarea?
Comience con un VPS si se trata de un solo proyecto, varios servicios y un retention de hasta varias semanas. El VPS es más fácil de ampliar en CPU, RAM y disco, y OpenTelemetry Collector funciona bien en este entorno. Un dedicated es necesario cuando existe una escritura sostenidamente elevada de spans, requisitos estrictos de IOPS, varios NVMe en RAID, almacenamiento prolongado y un backend pesado en lugar de Badger local. La transición debe justificarse mediante mediciones: carga de CPU, memory pressure, latencia del disco y velocidad de llenado del almacenamiento.
¿Se pueden enviar datos personales, JWT o parámetros SQL a Collector?
No, de forma predeterminada no se recomienda. Los atributos de trace suelen estar disponibles para los desarrolladores, se incluyen en las copias de seguridad y pueden almacenarse durante semanas. Excluya los encabezados Authorization, cookies, contraseñas, tokens, cuerpos completos de las solicitudes, direcciones de correo electrónico y datos de pago. En el SDK, utilice una allowlist de atributos y, en Collector, añada processors para eliminar las claves sensibles. Para SQL, es preferible registrar el tipo de operación y el nombre de la tabla, no la consulta completa con los parámetros del usuario.
¿Por qué un trace consta de un solo span aunque la solicitud pase por varios servicios?
Normalmente, la causa es la pérdida del contexto entre los servicios. Compruebe que el cliente HTTP transmite automáticamente los encabezados W3C Trace Context: traceparent y tracestate. Para las colas, asegúrese de que producer escribe el context en los message headers y que consumer lo extrae antes de crear el span hijo. Todos los servicios deben utilizar un propagator compatible, normalmente tracecontext. Compruebe también que distintos servicios no generan un nuevo root trace debido a que auto-instrumentation está deshabilitado.
Conclusiones y próximos pasos
Ahora dispone de un stack self-hosted de distributed tracing: las aplicaciones envían traces OTLP a OpenTelemetry Collector, Collector los transmite a Jaeger y Jaeger UI está disponible mediante HTTPS con una protección básica. La configuración es adecuada para el primer uso en production con una carga moderada y ofrece control sobre los datos, retention y la infraestructura.
- Instrumente las entradas HTTP principales, las llamadas HTTP salientes, SQL, Redis y las colas; comience con un escenario de usuario crítico.
- Añada sampling basado en reglas: conserve todos los errores y las solicitudes lentas, y registre las solicitudes correctas habituales con una probabilidad menor.
- Al aumentar la carga, distribuya Collector y Jaeger en servidores diferentes, traslade el storage a un backend de production y configure la supervisión de CPU, RAM, disco, queue size y velocidad de ingestion.
Compruebe periódicamente la recuperación de las copias de seguridad, el espacio disponible y la actualidad de las imágenes. El tracing solo es útil cuando se puede encontrar rápidamente el trace necesario precisamente en el momento del incidente.