This document describes the local LGTM stack configured for this NixOS machine. The stack is managed declaratively by NixOS, runs as Podman containers under systemd, stores state on the host, and exposes service ports on localhost only.
LGTM means:
- Loki for logs.
- Grafana for dashboards, exploration, and datasource access.
- Tempo for traces.
- Mimir for metrics through the Prometheus-compatible API.
The implementation lives in ../lgtm.nix. It is imported and enabled from ../configuration.nix.
The stack is intended for local development and workstation observability, not production multi-node observability. It uses filesystem-backed storage, single-node service modes, no external object storage, and localhost-only port bindings.
NixOS manages the lifecycle through these systemd units:
| Unit | Purpose |
|---|---|
podman-lgtm-network.service |
Creates the dedicated Podman bridge network. |
podman-lgtm-grafana.service |
Runs Grafana. |
podman-lgtm-loki.service |
Runs Loki. |
podman-lgtm-tempo.service |
Runs Tempo. |
podman-lgtm-mimir.service |
Runs Mimir. |
Podman runs the containers on a dedicated network named lgtm. Grafana connects to the backend services by container DNS names on that network:
| Grafana Datasource | Internal URL |
|---|---|
| Loki | http://lgtm-loki:3100 |
| Tempo | http://lgtm-tempo:3200 |
| Mimir | http://lgtm-mimir:9009/prometheus |
Grafana is the UI for exploring metrics, logs, and traces. It is configured by the lgtm-grafana container and persists its database/plugins under /var/lib/lgtm/grafana.
The module provisions datasources from:
/etc/lgtm/grafana/provisioning/datasources/datasources.yml
The datasource file is bind-mounted directly into the Grafana container. This is intentional: NixOS materializes environment.etc entries through /etc/static symlinks, and bind-mounting the whole provisioning directory can leave symlink targets unavailable inside the container.
Loki stores logs using local filesystem storage under /var/lib/lgtm/loki. The current configuration disables authentication, uses an in-memory ring, enables TSDB schema v13, and stores chunks/rules below /loki inside the container.
Loki is ready when this endpoint returns ready:
curl -fsS http://127.0.0.1:3100/readyTempo stores traces on the local filesystem under /var/lib/lgtm/tempo. It enables OTLP receivers for applications and tools that can export traces using OpenTelemetry.
Published local ports:
| Port | Protocol | Purpose |
|---|---|---|
3200 |
HTTP | Tempo API and readiness endpoint. |
4317 |
OTLP gRPC | Trace ingestion. |
4318 |
OTLP HTTP | Trace ingestion. |
Tempo is ready when this endpoint returns ready:
curl -fsS http://127.0.0.1:3200/readyMimir stores metrics using filesystem-backed storage under /var/lib/lgtm/mimir. It runs in single-process target: all mode and exposes Prometheus-compatible APIs through /prometheus.
Grafana treats Mimir as a Prometheus datasource at:
http://lgtm-mimir:9009/prometheus
Mimir is ready when this endpoint returns ready:
curl -fsS http://127.0.0.1:9009/readyThe module pins explicit image tags for repeatability:
| Component | Image |
|---|---|
| Grafana | grafana/grafana-oss:11.2.0 |
| Loki | grafana/loki:3.2.0 |
| Tempo | grafana/tempo:2.6.1 |
| Mimir | grafana/mimir:2.14.0 |
Update these through services.lgtm.images in the NixOS configuration, then rebuild and switch the system.
All published ports bind to 127.0.0.1. They are accessible from this NixOS environment and, depending on WSL networking behavior, may also be reachable from the Windows host through localhost forwarding.
| Service | URL | Notes |
|---|---|---|
| Grafana | http://127.0.0.1:3000 |
Main UI. |
| Grafana health | http://127.0.0.1:3000/api/health |
Returns database and version status. |
| Loki readiness | http://127.0.0.1:3100/ready |
Returns ready after startup. |
| Tempo readiness | http://127.0.0.1:3200/ready |
Returns ready after startup. |
| Mimir readiness | http://127.0.0.1:9009/ready |
Returns ready after startup. |
| Tempo OTLP gRPC | 127.0.0.1:4317 |
Use for OpenTelemetry gRPC exporters. |
| Tempo OTLP HTTP | http://127.0.0.1:4318 |
Use for OpenTelemetry HTTP exporters. |
Grafana sign-up is disabled, anonymous auth is disabled, and the module does not store real credentials. On a fresh Grafana data directory, use Grafana's default first-login behavior, then change the password immediately.
Enable the stack from ../configuration.nix:
imports = [
./lgtm.nix
];
services.lgtm.enable = true;The module exposes these options:
| Option | Default | Purpose |
|---|---|---|
services.lgtm.enable |
false |
Enables the stack. |
services.lgtm.dataDir |
/var/lib/lgtm |
Host directory for persistent data. |
services.lgtm.listenAddress |
127.0.0.1 |
Address used for published container ports. |
services.lgtm.networkName |
lgtm |
Podman bridge network name. |
services.lgtm.images.grafana |
grafana/grafana-oss:11.2.0 |
Grafana image. |
services.lgtm.images.loki |
grafana/loki:3.2.0 |
Loki image. |
services.lgtm.images.tempo |
grafana/tempo:2.6.1 |
Tempo image. |
services.lgtm.images.mimir |
grafana/mimir:2.14.0 |
Mimir image. |
services.lgtm.ports.grafana |
3000 |
Host Grafana port. |
services.lgtm.ports.loki |
3100 |
Host Loki port. |
services.lgtm.ports.tempoHttp |
3200 |
Host Tempo HTTP port. |
services.lgtm.ports.tempoOtlpGrpc |
4317 |
Host OTLP gRPC port. |
services.lgtm.ports.tempoOtlpHttp |
4318 |
Host OTLP HTTP port. |
services.lgtm.ports.mimir |
9009 |
Host Mimir HTTP port. |
Example override:
services.lgtm = {
enable = true;
dataDir = /var/lib/lgtm;
listenAddress = "127.0.0.1";
ports.grafana = 3000;
images.grafana = "grafana/grafana-oss:11.2.0";
};After changing configuration, format and rebuild:
nixfmt configuration.nix lgtm.nix
sudo nixos-rebuild switch -I nixos-config=/home/rslater/nixos/configuration.nixUse test instead of switch when validating a change temporarily:
sudo nixos-rebuild test -I nixos-config=/home/rslater/nixos/configuration.nixPersistent data is stored below /var/lib/lgtm:
| Path | Owner | Purpose |
|---|---|---|
/var/lib/lgtm/grafana |
UID/GID 472:472 |
Grafana database, plugins, and local state. |
/var/lib/lgtm/loki |
UID/GID 10001:10001 |
Loki chunks, indexes, WAL, and rules. |
/var/lib/lgtm/tempo |
UID/GID 10001:10001 |
Tempo WAL and local trace blocks. |
/var/lib/lgtm/mimir |
UID/GID 10001:10001 |
Mimir TSDB, blocks, rules, alertmanager, and activity data. |
These directories are created by systemd.tmpfiles.rules during activation.
Configuration files are generated below /etc/lgtm:
/etc/lgtm/grafana/provisioning/datasources/datasources.yml
/etc/lgtm/loki/loki.yml
/etc/lgtm/tempo/tempo.yml
/etc/lgtm/mimir/mimir.yml
Do not edit these generated files directly. Change ../lgtm.nix, then rebuild.
systemctl --no-pager --plain --full status \
podman-lgtm-network \
podman-lgtm-grafana \
podman-lgtm-loki \
podman-lgtm-tempo \
podman-lgtm-mimirFor a terse active-state check:
systemctl is-active \
podman-lgtm-grafana \
podman-lgtm-loki \
podman-lgtm-tempo \
podman-lgtm-mimir \
podman-lgtm-networksudo podman ps --filter name=lgtm --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'Inspect the network:
sudo podman network inspect lgtmcurl -fsS http://127.0.0.1:3000/api/health
curl -fsS http://127.0.0.1:3100/ready
curl -fsS http://127.0.0.1:3200/ready
curl -fsS http://127.0.0.1:9009/readyUse systemd for service-level logs:
journalctl --no-pager -u podman-lgtm-grafana -n 100
journalctl --no-pager -u podman-lgtm-loki -n 100
journalctl --no-pager -u podman-lgtm-tempo -n 100
journalctl --no-pager -u podman-lgtm-mimir -n 100Use Podman for container logs:
sudo podman logs --tail 100 lgtm-grafana
sudo podman logs --tail 100 lgtm-loki
sudo podman logs --tail 100 lgtm-tempo
sudo podman logs --tail 100 lgtm-mimirNote that the containers are started with --rm; if a container exits, Podman may remove it. In that case, use journalctl for the unit because the container logs may no longer be available.
Restart the whole stack:
sudo systemctl restart \
podman-lgtm-loki \
podman-lgtm-tempo \
podman-lgtm-mimir \
podman-lgtm-grafanaRestart only Grafana after changing datasource provisioning:
sudo systemctl restart podman-lgtm-grafanasudo systemctl stop \
podman-lgtm-grafana \
podman-lgtm-loki \
podman-lgtm-tempo \
podman-lgtm-mimirThe persistent data in /var/lib/lgtm is not removed when services stop.
The stack provisions storage and query backends, but it does not currently install a host collector such as Grafana Alloy. Applications can still send data directly to the local endpoints.
Configure OpenTelemetry exporters to send traces to Tempo:
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
For gRPC exporters, use:
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
Loki is reachable at:
http://127.0.0.1:3100
Push clients should use Loki's push API path:
http://127.0.0.1:3100/loki/api/v1/push
Mimir exposes Prometheus-compatible APIs at:
http://127.0.0.1:9009/prometheus
Remote write clients should target:
http://127.0.0.1:9009/api/v1/push
or the relevant Mimir/Prometheus remote write endpoint expected by the client version in use. Validate client configuration against the Mimir documentation before wiring production-like workloads.
-
Change the relevant
services.lgtm.images.*value in../configuration.nixor the defaults in../lgtm.nix. -
Review the upstream release notes for breaking changes and configuration migrations.
-
Run:
nixfmt configuration.nix lgtm.nix sudo nixos-rebuild test -I nixos-config=/home/rslater/nixos/configuration.nix -
Verify health endpoints and Grafana datasources.
-
Persist the change:
sudo nixos-rebuild switch -I nixos-config=/home/rslater/nixos/configuration.nix
Stop the stack or accept an application-consistent backup risk, then archive /var/lib/lgtm:
sudo tar -C /var/lib -czf lgtm-backup.tgz lgtmRestore by stopping services, replacing /var/lib/lgtm, preserving ownerships, and restarting services.
This deletes stored dashboards, logs, traces, and metrics. Use carefully.
sudo systemctl stop \
podman-lgtm-grafana \
podman-lgtm-loki \
podman-lgtm-tempo \
podman-lgtm-mimir
sudo rm -rf /var/lib/lgtm
sudo nixos-rebuild switch -I nixos-config=/home/rslater/nixos/configuration.nixThe rebuild recreates the data directories with the expected ownerships.
sudo du -h -d 2 /var/lib/lgtm | sort -h
sudo podman system dfMimir has compactor_blocks_retention_period: 7d in the current config. Tempo has block_retention: 48h. Loki retention is not explicitly configured in the current module, so Loki data can grow until manually cleaned or retention is added.
After image upgrades, remove unused images and layers:
sudo podman image prune
sudo podman system pruneReview what Podman plans to delete before confirming.
Check the unit and logs:
systemctl status podman-lgtm-grafana
journalctl --no-pager -u podman-lgtm-grafana -n 150Confirm the datasource file is visible inside the container:
sudo podman exec lgtm-grafana test -f /etc/grafana/provisioning/datasources/datasources.ymlIf Grafana fails with a datasource provisioning error, verify that ../lgtm.nix bind-mounts the datasource file directly rather than bind-mounting the entire /etc/lgtm/grafana/provisioning directory.
Loki, Tempo, and Mimir can briefly return 503 during startup while their rings and internal modules settle. Wait a few seconds and retry:
curl -fsS http://127.0.0.1:3100/ready
curl -fsS http://127.0.0.1:3200/ready
curl -fsS http://127.0.0.1:9009/readyIf readiness does not recover, check the service logs and filesystem permissions under /var/lib/lgtm.
All services must be attached to the lgtm Podman network. Check:
sudo podman network inspect lgtm
sudo podman inspect lgtm-grafana --format '{{json .NetworkSettings.Networks}}'Restart the network unit and containers if needed:
sudo systemctl restart podman-lgtm-network
sudo systemctl restart podman-lgtm-loki podman-lgtm-tempo podman-lgtm-mimir podman-lgtm-grafanaCheck listeners:
sudo ss -ltnp | grep -E ':3000|:3100|:3200|:4317|:4318|:9009'Change the relevant services.lgtm.ports.* option and rebuild.
- Ports bind to
127.0.0.1by default. Do not changeservices.lgtm.listenAddressto0.0.0.0unless you also add authentication, firewall rules, and a clear access model. - The stack disables anonymous Grafana auth and sign-up, but it does not manage Grafana secrets.
- The Loki, Tempo, and Mimir single-node configs are local-development oriented and are not hardened for shared or production use.
- Avoid storing personal, health, payment, or client-sensitive data in this local stack unless the relevant compliance requirements and retention controls are defined.
- Filesystem-backed Mimir is explicitly suitable for development/testing style deployments, not production-scale observability.
The module and this document are based on the behavior of the running NixOS configuration plus upstream documentation for the individual tools:
- Grafana documentation: https://grafana.com/docs/grafana/latest/
- Grafana provisioning datasources: https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources
- Grafana Loki documentation: https://grafana.com/docs/loki/latest/
- Loki configuration reference: https://grafana.com/docs/loki/latest/configure/
- Grafana Tempo documentation: https://grafana.com/docs/tempo/latest/
- Tempo configuration reference: https://grafana.com/docs/tempo/latest/configuration/
- Grafana Mimir documentation: https://grafana.com/docs/mimir/latest/
- Mimir configuration reference: https://grafana.com/docs/mimir/latest/configure/configuration-parameters/
- OpenTelemetry protocol exporter configuration: https://opentelemetry.io/docs/specs/otel/protocol/exporter/
- NixOS OCI containers option: https://search.nixos.org/options?query=virtualisation.oci-containers
- Podman documentation: https://docs.podman.io/
- NixOS systemd service options: https://search.nixos.org/options?query=systemd.services