How to Setup and Run Jaeger With Docker and Docker Compose

Run Jaeger v2 with Docker and Docker Compose: start the all-in-one image, expose OTLP ports 4317/4318, send traces from a Python OpenTelemetry app to Jaeger UI.

Jaeger is an open-source distributed tracing system that helps you monitor and troubleshoot requests as they flow through a microservices architecture. In simple terms, Jaeger shows you where time is spent in a request and which services were involved.

Distributed tracing is crucial in microservices environments because it allows you to:

  • Identify performance bottlenecks
  • Debug and troubleshoot issues across services
  • Understand the flow of requests through your system
  • Optimize your application’s overall performance

Jaeger’s compatibility with OpenTelemetry — a collection of tools, APIs, and SDKs for instrumenting, generating, collecting, and exporting telemetry data — makes it an even more powerful choice for developers. Since Jaeger v2, Jaeger is itself built on the OpenTelemetry Collector and accepts OTLP natively, so any OpenTelemetry-instrumented application can send traces to it without a Jaeger-specific client. If OpenTelemetry is new to you, start with OpenTelemetry Explained: A Beginner’s Guide With Examples.

While Jaeger does add some overhead to your application, it’s designed to be lightweight. The impact can be minimized through proper sampling strategies and configuration. In most cases, the performance impact is negligible compared to the benefits of distributed tracing.

With Jaeger you can:

  • Monitor and troubleshoot distributed workflows
  • Identify performance bottlenecks
  • Track down root causes
  • Analyze service dependencies

Jaeger vs Zipkin

Jaeger and Zipkin are both open-source distributed tracing systems. Jaeger offers adaptive sampling, a scalable architecture, and strong OpenTelemetry support, which makes it a common choice for new projects.

This guide walks you through running Jaeger with Docker or Docker Compose, instrumenting an app with OpenTelemetry, and using the Jaeger UI.

Prerequisites

Setting up Jaeger (all-in-one)

Jaeger v2 ships as a single binary and container image (cr.jaegertracing.io/jaegertracing/jaeger). With no configuration it runs in all-in-one mode: the collector and query roles in one process, backed by in-memory storage. This is perfect for local development and demos (not production).

Upgrading from Jaeger v1? Older versions of this guide used the jaegertracing/all-in-one:1.x image. Jaeger v1 reached end of life on December 31, 2025 and gets no new releases, so use the v2 jaegertracing/jaeger image shown below. The legacy Jaeger agent and Thrift ports (6831, 6832, 14250, 14268) are deprecated; modern apps send spans over OTLP on 4317/4318.

Confirm Docker is installed and running:

docker --version

Pull the Jaeger image (2.22.0 is the latest release at the time of writing; check the Jaeger releases page for newer versions):

docker pull cr.jaegertracing.io/jaegertracing/jaeger:2.22.0

Run the container (foreground; use -d to run in the background):

docker run --rm --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 5778:5778 \
  -p 9411:9411 \
  cr.jaegertracing.io/jaegertracing/jaeger:2.22.0

To run in the background, add -d before the image name.

Run with Docker Compose

Create docker-compose.yml:

services:
  jaeger:
    image: cr.jaegertracing.io/jaegertracing/jaeger:2.22.0
    container_name: jaeger
    ports:
      - "16686:16686" # Jaeger UI
      - "4317:4317" # OTLP gRPC
      - "4318:4318" # OTLP HTTP
      - "5778:5778" # Remote sampling config
      - "9411:9411" # Zipkin compatibility
    restart: unless-stopped

The top-level version: key is obsolete in the Compose Specification and Docker Compose v2 ignores it (with a warning), so it is left out.

Start Jaeger with Docker Compose v2 (the docker compose plugin; the old standalone docker-compose v1 is deprecated):

docker compose up -d

This runs the all-in-one configuration: collector, query, and in-memory storage in one process. Data is not persisted across restarts; use a proper storage backend for production.

  • Open the Jaeger UI at http://localhost:16686 (or http://<server-ip>:16686 if remote). Allow port 16686 in your firewall if you need external access.
  • Send test spans using the OpenTelemetry example below or your own instrumented app.

Jaeger User Interface

Open http://localhost:16686 in your browser. The UI includes:

  • Service Map: A visual representation of the services and their interactions.
  • Tracing: A detailed view of individual traces, showing every span on a timeline.
  • Span: A detailed view of individual spans, including their attributes (tags) and events (logs).
  • Query: A search interface for querying tracing data.

Tip: After you send a few spans, select your service in the dropdown and click Find Traces.

Ports exposed by the Jaeger all-in-one container

The Jaeger v2 container uses several default ports. The most commonly used ones are:

  • 16686/tcp: Jaeger UI and HTTP query API
  • 4317/tcp: OTLP gRPC receiver (OpenTelemetry)
  • 4318/tcp: OTLP HTTP receiver (OpenTelemetry, /v1/traces)
  • 5778/tcp: Remote sampling configuration (/sampling)
  • 9411/tcp: Zipkin compatibility endpoint (must be enabled in the collector configuration)
  • 16685/tcp: gRPC query API

Legacy ports, still listed but deprecated and only needed for old Jaeger-native clients:

  • 14268/tcp: Jaeger Thrift over HTTP (/api/traces)
  • 14250/tcp: Jaeger legacy gRPC collector
  • 6831/udp and 6832/udp: Jaeger agent Thrift receivers (compact and binary)

If you’re using OpenTelemetry, you’ll typically send spans to 4317 or 4318 and view traces in the UI on 16686.

Jaeger Architecture

Jaeger v2 is a customized distribution of the OpenTelemetry Collector. One binary runs in different roles depending on its configuration:

  • OpenTelemetry SDKs: Instrument your application code. They create spans and export them over OTLP. (The old Jaeger client libraries are retired in favour of OpenTelemetry.)
  • Collector: Receives traces from applications, runs them through a processing pipeline, and writes them to a storage backend.
  • Query: Retrieves traces from storage and serves the APIs and the UI.
  • Ingester: Reads spans from Kafka and writes them to storage, for collector → Kafka → ingester setups.
  • All-in-one: Runs the collector and query roles in one process (what this guide uses).
  • UI: A web interface for searching and analyzing traces.

Data flows from your instrumented application, via OTLP, to the Collector and then to storage. The Query service retrieves this data from storage to display in the UI. The separate jaeger-agent from v1 is no longer needed; if you want a host agent or sidecar, the Jaeger docs recommend running a standard OpenTelemetry Collector instead.

Jaeger v2 supports multiple storage options, including Cassandra, Elasticsearch, OpenSearch, ClickHouse, Badger, and in-memory storage (for development). The choice of storage depends on your scalability needs and existing infrastructure.

Sampling plays a crucial role in Jaeger’s architecture. It allows you to control the amount of tracing data you collect, which is essential for managing performance and storage costs in high-traffic systems.

To instrument your application, prefer using the OpenTelemetry SDK and export spans to Jaeger via OTLP.

Example (Python) exporting spans to Jaeger over OTLP gRPC (localhost:4317):

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

resource = Resource.create({"service.name": "my-service"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("TestSpan") as span:
    span.set_attribute("hello", "world")
    # Your code here

You should then see my-service in the Jaeger UI.

Install dependencies:

pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc

Advanced Instrumentation Techniques

Advanced techniques you can use with Jaeger:

  • Custom Samplers: Create samplers that make intelligent decisions about which traces to sample based on your specific needs.
  • Baggage: Use baggage to pass data along the entire trace, which can be useful for correlating information across services.
  • Multiple Spans: Create and manage multiple spans within a single trace to represent different operations or sub-operations.
  • Logging Integration: Integrate Jaeger with your logging system to enhance debugging capabilities.

Jaeger in a Production Environment

When deploying Jaeger in production, consider the following:

  • Scalability: Each role (Collector, Query, Ingester) can be scaled independently. Use load balancers to distribute traffic.
  • Storage: Implement a production-ready storage backend like Elasticsearch, OpenSearch, or Cassandra. Ensure proper sizing and configuration for your expected data volume.
  • Security: Set up secure communication between Jaeger components using TLS. Implement authentication and authorization for the Jaeger UI.
  • Sampling: Implement appropriate sampling strategies based on your traffic patterns and tracing needs. Dynamic sampling can help balance data collection and system performance.

Analyzing Traces with Jaeger UI

The Jaeger UI provides powerful tools for analyzing traces:

  • Use the search functionality to find relevant traces based on service, operation, tags, or duration.
  • Examine the trace timeline to understand the relationships between spans and identify long-running operations.
  • Inspect span details, including tags and logs, to gather context about each operation.
  • Use the comparison view to analyze multiple traces side by side and identify patterns or anomalies.

Best Practices for Effective Jaeger Implementation

To get the most out of Jaeger:

  • Follow consistent naming conventions for services and operations to make searching and filtering easier.
  • Implement appropriate sampling strategies to balance data collection and system performance.
  • Ensure proper error handling and logging within instrumented code to provide context for issues.
  • Regularly review and optimize your tracing implementation to ensure it continues to meet your needs as your system evolves.

Service Performance Monitoring (SPM) with Jaeger

Service Performance Monitoring (SPM) is a feature in Jaeger that provides service-level and operation-level aggregation of key metrics like request rates, error rates, and durations (latencies). It helps identify interesting traces (e.g. high QPS, slow or erroneous requests) without needing to know the service or operation names up-front.

How does SPM work?

SPM works by aggregating span data to produce RED (Request, Error, Duration) metrics. There are two ways to do it in Jaeger v2:

  • Pre-computed metrics: the OpenTelemetry SpanMetrics Connector generates metrics from incoming spans and exports them to a PromQL-compatible backend such as Prometheus, which Jaeger Query reads from.
  • Direct from storage: with Elasticsearch or OpenSearch as trace storage, Jaeger Query can compute RED metrics at query time without a separate metrics pipeline.

Key features of SPM

  • Service-level and operation-level aggregation of request rates, error rates, and latencies (P95, P75, P50)
  • “Impact” metric computed as the product of latency and request rate to highlight high-impact operations
  • Pre-populated Trace search with relevant service, operation and lookback period for interesting traces

Accessing SPM in Jaeger

The SPM feature can be accessed from the “Monitor” tab in the Jaeger UI. The tab is enabled with monitor.menuEnabled=true in the Jaeger UI configuration.

Limitations

SPM does not work with the default in-memory all-in-one setup. It needs either the SpanMetrics Connector plus a metrics backend (e.g. Prometheus), or Elasticsearch/OpenSearch as trace storage. See the Jaeger SPM docs for configuration examples.

Verifying the setup

  • Container: docker ps (or docker compose ps) should show the Jaeger container with ports 16686, 4317, 4318, etc. mapped.
  • UI: Opening http://localhost:16686 should load the Jaeger UI; the Service dropdown may be empty until you send traces.
  • Traces: After running the Python example (or any OTLP sender), select your service name (e.g. my-service) and click Find Traces to see spans.

Summary

You ran Jaeger v2 in all-in-one mode using the jaegertracing/jaeger Docker image (standalone docker run or Docker Compose), with the UI on port 16686 and OTLP receivers on 4317 (gRPC) and 4318 (HTTP). You instrumented a small Python app with the OpenTelemetry SDK and exported spans to Jaeger. For production, run the collector and query roles separately and use a persistent backend (e.g. Elasticsearch, OpenSearch, Cassandra).

For more monitoring stack options, see

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy