Grafana Mimir is a multi-tenant, horizontally scalable metrics database for Prometheus data. Prometheus servers push samples to Mimir with remote write, Mimir stores them as blocks in object storage, and Grafana queries them through a Prometheus-compatible API. In this tutorial you will install Mimir 3 in monolithic mode (all components in one process) on Ubuntu 24.04, back it with an S3-compatible bucket, send metrics from Prometheus, and configure tenants, per-tenant limits and recording rules.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS (x86_64), for example a CubePath VPS, with at least 4 GB of RAM.
  • A non-root user with sudo privileges and UFW enabled.
  • An empty bucket on an S3-compatible object storage service, with an access key and secret key that can read, write, list and delete objects in it.
  • Prometheus installed on the same server from the Ubuntu package (sudo apt install prometheus), or on another server that can reach this one over a private network.
  • Grafana, if you want to complete Step 6.

Monolithic mode is the right starting point for small and medium installations. The same binary and configuration can later run as separate components (-target=distributor, -target=ingester, and so on) when you need to scale them independently.

Step 1 - Installing the Mimir binary

Mimir is published as a single binary on its GitHub releases page, together with a SHA-256 checksum file. Download both (check the releases page for the latest version):

cd /tmp
MIMIR_VERSION=3.2.1
curl -fLO "https://github.com/grafana/mimir/releases/download/mimir-${MIMIR_VERSION}/mimir-linux-amd64"
curl -fLO "https://github.com/grafana/mimir/releases/download/mimir-${MIMIR_VERSION}/mimir-linux-amd64-sha-256"

The checksum file contains only the hash, so pass the file name explicitly when verifying:

echo "$(cat mimir-linux-amd64-sha-256)  mimir-linux-amd64" | sha256sum -c
mimir-linux-amd64: OK

Install the binary and check the version:

sudo install -m 0755 mimir-linux-amd64 /usr/local/bin/mimir
mimir --version
Mimir, version 3.2.1 (branch: HEAD, revision: e49585d4)

Create a system user, a data directory and a configuration directory:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin mimir
sudo install -d -o mimir -g mimir -m 0750 /var/lib/mimir
sudo install -d -o root -g mimir -m 0750 /etc/mimir

Step 2 - Writing the configuration

Mimir reads a single YAML file. Create it:

sudo nano /etc/mimir/mimir.yaml

Paste the following configuration and replace the your_* placeholders with your bucket details:

multitenancy_enabled: true

usage_stats:
  enabled: false

server:
  http_listen_address: 127.0.0.1
  http_listen_port: 9009
  grpc_listen_port: 9095
  log_level: info

common:
  storage:
    backend: s3
    s3:
      endpoint: your_s3_endpoint
      region: your_region
      bucket_name: your_bucket
      access_key_id: your_access_key
      secret_access_key: your_secret_key

blocks_storage:
  storage_prefix: blocks
  tsdb:
    dir: /var/lib/mimir/tsdb
  bucket_store:
    sync_dir: /var/lib/mimir/tsdb-sync

ruler_storage:
  storage_prefix: ruler

alertmanager_storage:
  storage_prefix: alertmanager

ruler:
  rule_path: /var/lib/mimir/ruler

compactor:
  data_dir: /var/lib/mimir/compactor
  sharding_ring:
    kvstore:
      store: memberlist

distributor:
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: memberlist

ingester:
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: memberlist
    replication_factor: 1

store_gateway:
  sharding_ring:
    replication_factor: 1

limits:
  ingestion_rate: 50000
  max_global_series_per_user: 1000000
  compactor_blocks_retention_period: 1y

runtime_config:
  file: /etc/mimir/runtime.yaml

The important parts:

  • multitenancy_enabled: true: every write and read must carry an X-Scope-OrgID header that names the tenant. Each tenant's data is stored and limited separately.
  • usage_stats.enabled: false: stops Mimir from sending anonymous usage statistics to Grafana Labs.
  • server.http_listen_address: 127.0.0.1: the HTTP API (remote write and queries) is only reachable locally. Mimir has no authentication of its own, and anyone who can reach it can pick any tenant ID.
  • common.storage: one S3 bucket for everything. The storage_prefix values keep blocks, rules and Alertmanager state in separate folders of that bucket. endpoint is the S3 host name without https://.
  • replication_factor: 1: a single node keeps one copy of incoming data. With three or more ingesters you would raise it to 3.
  • limits: defaults for every tenant. compactor_blocks_retention_period: 1y deletes blocks older than a year.

Now create the runtime configuration file. Mimir reloads it periodically without a restart, which makes it the place for per-tenant overrides:

sudo nano /etc/mimir/runtime.yaml
overrides:
  team-ops:
    ingestion_rate: 100000
    max_global_series_per_user: 3000000
    compactor_blocks_retention_period: 2y

The tenant team-ops gets higher limits and two years of retention; every other tenant uses the defaults from mimir.yaml.

Both files contain or control access to credentials, so restrict them to the mimir group:

sudo chown root:mimir /etc/mimir/mimir.yaml /etc/mimir/runtime.yaml
sudo chmod 640 /etc/mimir/mimir.yaml /etc/mimir/runtime.yaml

Step 3 - Running Mimir as a systemd service

Create the unit file:

sudo nano /etc/systemd/system/mimir.service
[Unit]
Description=Grafana Mimir
Documentation=https://grafana.com/docs/mimir/latest/
After=network-online.target
Wants=network-online.target

[Service]
User=mimir
Group=mimir
ExecStart=/usr/local/bin/mimir -config.file=/etc/mimir/mimir.yaml
Restart=on-failure
RestartSec=10
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/mimir

[Install]
WantedBy=multi-user.target

Start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now mimir

Mimir takes up to a minute to become ready, because the rings have to settle. Check the readiness endpoint:

curl http://127.0.0.1:9009/ready
ready

If it prints a message about services that are not running yet, wait a few seconds and try again. On startup Mimir also checks that it can reach the bucket; any problem shows up in sudo journalctl -u mimir.

Mimir listens for gRPC on port 9095 and for cluster membership (memberlist) on port 7946 on all interfaces. On a single node nothing outside the server needs them, so make sure UFW is not allowing them:

sudo ufw status

Do not add rules for ports 9009, 9095 or 7946. If you later add more Mimir nodes, allow those ports only from the other nodes' private IP addresses.

Step 4 - Sending metrics from Prometheus

Prometheus pushes to Mimir's /api/v1/push endpoint. Open the Prometheus configuration:

sudo nano /etc/prometheus/prometheus.yml

Add a remote_write block at the top level of the file, next to global and scrape_configs:

remote_write:
  - url: http://127.0.0.1:9009/api/v1/push
    headers:
      X-Scope-OrgID: team-ops

The header assigns all samples from this Prometheus server to the tenant team-ops. Validate and reload Prometheus:

promtool check config /etc/prometheus/prometheus.yml
sudo systemctl reload prometheus

After a minute, confirm that Prometheus is sending samples without failures:

curl -s http://localhost:9090/api/v1/query \
  --data-urlencode 'query=rate(prometheus_remote_storage_samples_failed_total[5m])'

The value should be 0. For the opposite view, ask Mimir itself. Its Prometheus-compatible API lives under /prometheus:

curl -s -H 'X-Scope-OrgID: team-ops' http://127.0.0.1:9009/prometheus/api/v1/query \
  --data-urlencode 'query=count(up)'
{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1790242200,"2"]}]}}

The number is how many scrape targets Prometheus has. Without the header, Mimir refuses the request:

curl -s http://127.0.0.1:9009/prometheus/api/v1/query --data-urlencode 'query=up'
no org id

Step 5 - Adding recording rules

Mimir includes a ruler that evaluates recording and alerting rules per tenant and writes the results back into the same tenant. Rules are uploaded through the API, one rule group at a time, into a namespace. Create a rule group file:

nano node-rules.yaml
name: node
rules:
  - record: instance:node_cpu:rate5m
    expr: sum by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m]))

Upload it into the namespace infra for the tenant team-ops:

curl -s -X POST http://127.0.0.1:9009/prometheus/config/v1/rules/infra \
  -H 'X-Scope-OrgID: team-ops' \
  -H 'Content-Type: application/yaml' \
  --data-binary @node-rules.yaml

List the tenant's rules to confirm:

curl -s -H 'X-Scope-OrgID: team-ops' http://127.0.0.1:9009/prometheus/config/v1/rules
infra:
    - name: node
      rules:
        - record: instance:node_cpu:rate5m
          expr: sum by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m]))

The rule groups are stored in the bucket under the ruler prefix, so they survive restarts. This example only returns data if Prometheus scrapes Node Exporter (sudo apt install prometheus-node-exporter adds it, and the Ubuntu Prometheus package already has a node job for it).

You can also check that the per-tenant overrides from Step 2 are loaded:

curl -s http://127.0.0.1:9009/runtime_config | head -8
overrides:
    team-ops:
        ...
        ingestion_rate: 100000
        ingestion_burst_size: 200000

Step 6 - Querying Mimir from Grafana

Grafana talks to Mimir with its built-in Prometheus data source. Because every query needs a tenant, create one data source per tenant:

  1. Go to Connections > Data sources > Add data source and choose Prometheus.
  2. Set Prometheus server URL to http://127.0.0.1:9009/prometheus (or the Mimir server's private address if Grafana runs elsewhere and you changed http_listen_address).
  3. Under HTTP headers, click Add header, set the name to X-Scope-OrgID and the value to team-ops.
  4. Click Save & test.

Dashboards that use this data source see only the team-ops tenant. A header value such as team-ops|team-dev queries several tenants at once, but only if you enable tenant_federation.enabled: true in mimir.yaml.

Troubleshooting

/ready never returns ready. Read the log with sudo journalctl -u mimir -n 100 --no-pager. The message Unable to successfully connect to configured object storage means the S3 endpoint, region, bucket name or keys are wrong, or the server cannot reach the endpoint.

Mimir fails to start with a field ... not found error. A configuration key is misspelled or at the wrong indentation level. Mimir rejects unknown keys on startup, and the error names the key.

Prometheus logs HTTP 401 with no org id. The X-Scope-OrgID header is missing from remote_write. Every write must carry it while multi-tenancy is enabled.

Prometheus logs HTTP 429 or err-mimir-tenant-max-ingestion-rate. The tenant is sending faster than ingestion_rate. Raise it for that tenant in /etc/mimir/runtime.yaml; the change is picked up within a few seconds without a restart.

Samples rejected with err-mimir-max-series-per-user. The tenant hit max_global_series_per_user. Either raise the limit or reduce cardinality at the source (drop high-cardinality labels in Prometheus with metric_relabel_configs).

Conclusion

Mimir is now running in monolithic mode on Ubuntu 24.04, storing Prometheus data in S3 per tenant, with per-tenant limits, recording rules in the ruler, and Grafana reading through the Prometheus API. As next steps, point more Prometheus servers or Grafana Alloy agents at Mimir with their own tenant IDs, put an authenticating reverse proxy in front of port 9009 before exposing it beyond localhost, and add more Mimir nodes with replication_factor: 3 when a single process is no longer enough.