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
sudoprivileges 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 anX-Scope-OrgIDheader 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. Thestorage_prefixvalues keep blocks, rules and Alertmanager state in separate folders of that bucket.endpointis the S3 host name withouthttps://.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: 1ydeletes 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:
- Go to Connections > Data sources > Add data source and choose Prometheus.
- 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 changedhttp_listen_address). - Under HTTP headers, click Add header, set the name to
X-Scope-OrgIDand the value toteam-ops. - 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.
