Jaeger is a distributed tracing platform originally developed by Uber that helps developers visualise request flows across microservices, identify latency bottlenecks, and diagnose failures in complex systems. Each request is represented as a trace composed of spans — timed operations — that are collected from instrumented services and assembled into a service map. This guide covers running Jaeger all-in-one with Docker on RHEL 9 for development, sending test traces via the OTLP HTTP endpoint, and understanding the production deployment options that use Cassandra or Elasticsearch as a persistent backend.
Prerequisites
- RHEL 9 server with Docker installed and running
- Firewall access to open ports 16686 (Jaeger UI), 4317 (OTLP gRPC), and 4318 (OTLP HTTP)
- Basic familiarity with Docker and distributed tracing concepts
- Optional: Python 3 or Node.js for the application instrumentation examples
Step 1 — Run Jaeger All-in-One with Docker
The jaegertracing/all-in-one image bundles the Jaeger Collector, Query service, and UI into a single container backed by in-memory storage. It is suitable for development and testing. The COLLECTOR_OTLP_ENABLED=true environment variable activates the OTLP receiver so modern OpenTelemetry SDKs can send traces directly without the legacy Jaeger Thrift protocol.
docker pull jaegertracing/all-in-one:latest
docker run -d
--name jaeger
--restart unless-stopped
-e COLLECTOR_OTLP_ENABLED=true
-p 16686:16686
-p 4317:4317
-p 4318:4318
-p 14250:14250
-p 14268:14268
jaegertracing/all-in-one:latest
# Open the firewall ports
sudo firewall-cmd --permanent --add-port=16686/tcp
sudo firewall-cmd --permanent --add-port=4317/tcp
sudo firewall-cmd --permanent --add-port=4318/tcp
sudo firewall-cmd --reload
# Confirm the container is running
docker ps --filter name=jaeger
Step 2 — Access the Jaeger UI
The Jaeger Query service exposes a web interface on port 16686. Open a browser and navigate to http://<server-ip>:16686. The UI shows a Search page where you can filter traces by service name, operation, and time range. The System Architecture tab renders an auto-generated service dependency graph once multiple services have submitted traces.
# Verify Jaeger is listening on all required ports
ss -tlnp | grep -E '16686|4317|4318'
# Check Jaeger container logs for startup errors
docker logs jaeger 2>&1 | tail -30
# Jaeger UI: http://:16686
# OTLP gRPC: grpc://:4317
# OTLP HTTP: http://:4318
# Jaeger gRPC (model): :14250
Step 3 — Send a Test Trace with curl
Before instrumenting an application, confirm the OTLP HTTP endpoint is accepting traces by posting a minimal JSON payload with curl. After submitting, refresh the Jaeger UI and search for the curl-test-service service to see the trace appear.
curl -s -o /dev/null -w "%{http_code}n"
-X POST http://localhost:4318/v1/traces
-H "Content-Type: application/json"
-d '{
"resourceSpans": [{
"resource": {
"attributes": [{
"key": "service.name",
"value": {"stringValue": "curl-test-service"}
}]
},
"scopeSpans": [{
"spans": [{
"traceId": "aabbccddeeff00112233445566778899",
"spanId": "aabbccddeeff0011",
"name": "curl-test-span",
"kind": 1,
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000001000000000",
"status": {"code": 2}
}]
}]
}]
}'
# Expected output: 200
Step 4 — Instrument a Python Application with OpenTelemetry
Real-world traces come from applications instrumented with an OpenTelemetry SDK. The Python example below installs the SDK packages, creates a tracer that exports spans directly to the Jaeger OTLP HTTP endpoint, and generates a two-span trace representing a parent operation and a child database call.
# Install the OpenTelemetry Python SDK and OTLP exporter
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
# Save this as trace_demo.py and run it: python trace_demo.py
cat > /tmp/trace_demo.py <<'EOF'
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
resource = Resource(attributes={SERVICE_NAME: "python-demo-service"})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("demo")
with tracer.start_as_current_span("process-order") as parent:
parent.set_attribute("order.id", "ORD-9001")
with tracer.start_as_current_span("query-database"):
import time; time.sleep(0.05)
print("Trace sent. Check Jaeger UI for python-demo-service.")
EOF
python /tmp/trace_demo.py
Step 5 — Production Setup with a Persistent Backend
The all-in-one container stores traces in memory and loses all data on restart, making it unsuitable for production. In a production environment, run Jaeger Collector and Query as separate containers and connect them to an Elasticsearch or Cassandra cluster for durable storage. The environment variables below show how to point the Collector at an existing Elasticsearch instance.
# Production: Jaeger Collector backed by Elasticsearch
docker run -d
--name jaeger-collector
-e SPAN_STORAGE_TYPE=elasticsearch
-e ES_SERVER_URLS=http://elasticsearch-host:9200
-e COLLECTOR_OTLP_ENABLED=true
-p 4317:4317
-p 4318:4318
jaegertracing/jaeger-collector:latest
# Production: Jaeger Query (UI)
docker run -d
--name jaeger-query
-e SPAN_STORAGE_TYPE=elasticsearch
-e ES_SERVER_URLS=http://elasticsearch-host:9200
-p 16686:16686
jaegertracing/jaeger-query:latest
Conclusion
You have deployed Jaeger all-in-one on RHEL 9 with OTLP support enabled, verified trace ingestion with a raw curl request, and instrumented a Python application to generate structured traces viewable in the Jaeger UI. The service map and trace timeline views make it straightforward to locate which microservice or database call is responsible for excess latency in a distributed system. When you are ready to move beyond development, migrate to the separate Collector and Query images backed by Elasticsearch to retain traces for longer retention periods and support higher ingestion throughput.
Next steps: How to Set Up OpenTelemetry Collector on RHEL 9, How to Monitor MySQL with Percona Monitoring and Management on RHEL 9, and How to Configure syslog-ng for Centralised Syslog on RHEL 9.