This episode turns theory into practice: running Grafana, Loki, Tempo, and Mimir along with the OpenTelemetry Collector and Grafana Alloy with Docker Compose. You'll also connect data sources and send the first telemetry from a sample application.

Enough theory. Starting this episode, you'll build a local LGTM Stack that actually runs — Grafana, Loki, Tempo, and Mimir, plus the OpenTelemetry Collector and Grafana Alloy as telemetry collectors. Everything runs via Docker Compose so it's easy to reproduce and tear down.
This episode is the practical foundation for almost every following episode. When you're done, you'll have a complete observability environment on localhost, connected as data sources in Grafana, and sending the first telemetry from a sample application. Make sure Docker is running before you start.
Create the project directory and the compose.yaml file. The component layout:
mkdir -p lgtm-stack
cd lgtm-stackThe cd lgtm-stack command takes you to the main working directory; all configuration files are created here.
services:
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
tempo:
image: grafana/tempo:latest
ports:
- "3200:3200"
- "4317:4317"
mimir:
image: grafana/mimir:latest
command: ["-config.file=/etc/mimir.yaml"]
ports:
- "9009:9009"
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
ports:
- "4317:4317"
- "4318:4318"
alloy:
image: grafana/alloy:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/containers:/var/lib/docker/containers
- ./config.alloy:/etc/alloy/config.alloy
volumes:
grafana-data:Important note: the volumes: block at the bottom defines a named volume to store Grafana data. Mimir needs a config file, so prepare a simple mimir.yaml that enables single-binary mode.
target: all
multitenancy_enabled: false
blocks_storage:
backend: filesystem
filesystem:
dir: /data/blocksThe configuration above disables multi-tenancy to keep things simple locally and stores data blocks on the filesystem. The target: all concept means one process runs all Mimir components at once — you'll learn how to split them in episode 22.
Grafana provides a provisioning mechanism so data sources are available automatically:
apiVersion: 1
datasources:
- name: Mimir
type: prometheus
url: http://mimir:9009/prometheus
access: proxy
- name: Loki
type: loki
url: http://loki:3100
access: proxy
- name: Tempo
type: tempo
url: http://tempo:3200
access: proxyThe data source names Mimir, Loki, and Tempo are used as references in the following episodes. Mimir is exposed as type: prometheus because it is Prometheus-compatible.
Run the entire stack and wait for the containers to become healthy:
docker compose up -d
docker compose psMake sure all services are in Up status. Then verify the health of each backend via the API:
curl -s http://localhost:3000/api/health
curl -s http://localhost:3100/ready
curl -s http://localhost:3200/ready
curl -s "http://localhost:9009/ready"If curl -s http://localhost:3100/ready returns ready, Loki is ready to receive logs. Do the same for the other three endpoints.
Open http://localhost:3000, log in with admin / admin, then open the Connections → Data sources menu. You'll see Mimir, Loki, and Tempo already registered. If not, recheck the provisioning and run docker compose restart grafana.
Tip
Use the Docker service name as the hostname when connecting between containers, for example http://mimir:9009. This name is only valid inside the Compose network, while from the host you use localhost.
To send the first telemetry, create a small Python application that exports traces via OTLP to the Collector:
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider(resource=Resource.create({"service.name": "checkout"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317")))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("checkout")
with tracer.start_as_current_span("checkout.process"):
passpip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
python3 app.pyRun the application several times, then open Grafana → Explore, select the Tempo data source, and you'll find a trace named checkout.process. This is your first telemetry successfully flowing: app → OTLP → Collector → Tempo → Grafana.
In episode 4 you successfully built a complete LGTM Stack environment locally: running six containers with Docker Compose, configuring single-binary Mimir, connecting Grafana data sources via provisioning, verifying backend health, and sending the first trace from a sample application.
The key takeaways:
In the next episode 5 we'll discuss Grafana fundamentals — server and plugin architecture, UI navigation, creating your first dashboard and panels, variables and templating, up to the differences between the PromQL, LogQL, and TraceQL query editors. Your environment is now alive, so let's learn how to read it correctly.