How to Install Grafana Mimir on Ubuntu 24.04 VPS: Self-Hosted Horizontally-Scalable Prometheus
Prometheus is the de facto standard for metrics collection, but it has a well-known ceiling: it runs as a single process, stores data on local disk, and was never designed for long-term retention or multi-tenant workloads. Grafana Mimir is the open-source solution to that ceiling. It accepts Prometheus remote_write traffic, stores metrics in object storage (S3, GCS, Azure, or MinIO), and can scale from a single binary on one VPS to a petabyte-scale cluster across dozens of nodes — all while speaking the PromQL you already know.
This guide walks you through deploying Mimir in monolithic mode on a single Ubuntu 24.04 VPS, backed by MinIO or S3 for long-term storage, with Prometheus remote-writing to it, Grafana querying it, Alertmanager firing notifications, and Nginx terminating TLS in front. By the end you will have a production-grade metrics backend that can retain years of data and serve as the single source of truth for every Prometheus instance, exporter, and agent in your fleet.
Skip the manual setup? Our CloudCore Business VPS gives you the CPU, RAM, and NVMe storage Mimir needs for a comfortable production deployment, at a flat monthly price.
Table of Contents
What is Grafana Mimir?
Grafana Mimir is a horizontally scalable, highly available, multi-tenant, long-term storage backend for Prometheus metrics. It was open-sourced by Grafana Labs in 2022 as the successor to Cortex, and it keeps 100% compatibility with the Prometheus query API. Any dashboard, alert, or tool that talks to Prometheus can talk to Mimir without modification — you change the URL, not the queries.
Under the hood Mimir is composed of several microservices: the distributor validates and fans out incoming writes, the ingester buffers recent samples and flushes them to object storage as immutable TSDB blocks, the store-gateway serves historical queries by reading those blocks back, the querier merges results across ingesters and store-gateways, the compactor merges and deduplicates blocks, and the ruler evaluates recording and alerting rules. In a large cluster each of these runs as a separate deployment. For a single VPS you run them all in one binary — "monolithic mode" — which is the focus of this guide.
The real reason to run Mimir is object storage. Prometheus stores data on a local block device, which means retention is capped by your disk size and you lose everything if that disk dies. Mimir flushes every TSDB block to S3-compatible storage after roughly two hours. Object storage is effectively infinite, costs pennies per gigabyte, and is replicated by the provider. You can keep years of metrics at a cost that would be impossible with local disks. And when you need to scale beyond one node, you simply start more Mimir processes pointing at the same bucket.
Why Self-Host Mimir vs Grafana Cloud Metrics?
Grafana Labs sells a managed version of Mimir called Grafana Cloud Metrics. It is a good product — the same team that writes Mimir operates it — but for many teams self-hosting makes more sense:
- Cost at scale is dramatically lower. Grafana Cloud Metrics is billed per active series and per queried sample. A fleet emitting 10 million active series can easily cost $2,000–$8,000 per month on the managed plan. The same workload fits comfortably on a CloudCore Business VPS plus a few dollars of S3 storage.
- Data sovereignty and compliance. When you self-host, your metrics never leave the jurisdiction you put the VPS in. For teams in the EU who care about GDPR, or in regulated industries where metrics can contain sensitive identifiers (customer IDs, request paths, internal hostnames), this is non-negotiable.
- No per-series gotchas. Managed metrics plans charge based on cardinality. A single bad label (a user ID label on a histogram) can 100x your bill overnight. Self-hosted Mimir still has cardinality limits, but you set them and you absorb the cost in infrastructure you already pay for.
- Full feature access. Experimental flags, bleeding-edge releases, custom limits per tenant, and deep debugging access to the binary are yours. On managed plans you get what the vendor exposes in the UI.
- Predictable, flat-rate pricing. A VPS costs the same whether you ingest a thousand samples per second or a hundred thousand. No end-of-month surprises.
- Unified observability stack. If you are already self-hosting Prometheus, Grafana, and Loki on your own VPS, adding Mimir keeps the entire stack in one place under one operational model.
Cost Comparison
| Scenario | Grafana Cloud Metrics (Pro) | Self-Hosted Mimir on CloudCore Business |
|---|---|---|
| 1M active series | ~$400/month | ~EUR 29.99/month VPS + ~$5/month S3 |
| 10M active series | ~$3,500/month | ~EUR 29.99/month VPS + ~$25/month S3 |
| Data retention | 13 months (cap) | Years (you choose) |
| Queried-sample charges | Yes | No |
| Data residency control | Limited regions | Full (any region) |
| Custom limits per tenant | Limited | Full |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to the server
- At least 4 vCPU and 8 GB of RAM for a production monolithic deployment (16 GB recommended for 1M+ active series)
- 100 GB of NVMe SSD for the Mimir WAL, TSDB head, and block cache
- An S3-compatible object storage — either AWS S3, a managed MinIO, or a self-hosted MinIO on the same or a different VPS
- A domain name pointing at the VPS if you want Nginx with TLS
- An existing Prometheus and Grafana install (see How to Install Prometheus on Ubuntu and How to Install Grafana on Ubuntu)
Recommended Plan: CloudCore Business>
Mimir benefits from ample RAM (to keep ingesters healthy), fast local disk (for the WAL), and steady CPU (for compaction). The CloudCore Business plan is the sweet spot for a single-node production deployment:>
- 6–8 vCPU cores
- 16–24 GB RAM
- 200 GB NVMe SSD
- Unmetered bandwidth>
For larger fleets or multi-node clusters, scale horizontally by adding more VPS instances behind the same object storage bucket.
Connect to the VPS:
ssh root@your-server-ipStep 1: Update the System and Create a Service User
Start by updating packages and installing a few utilities:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget tar ca-certificatesCreate a dedicated unprivileged user and the directory layout Mimir will use:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mimir
sudo mkdir -p /etc/mimir /var/lib/mimir /var/log/mimir
sudo chown -R mimir:mimir /var/lib/mimir /var/log/mimir/etc/mimir holds the config file, /var/lib/mimir is the data directory (WAL, TSDB head, blocks cache), and /var/log/mimir is reserved for future log routing.
Step 2: Download and Install the Mimir Binary
Mimir publishes static Linux binaries on each release. Grab the latest amd64 release:
MIMIR_VERSION="2.14.0"
cd /tmp
wget "https://github.com/grafana/mimir/releases/download/mimir-${MIMIR_VERSION}/mimir-linux-amd64"
sudo install -o root -g root -m 0755 mimir-linux-amd64 /usr/local/bin/mimirIf you are on ARM64 (for example a VPS using an Ampere-based instance), swap amd64 for arm64 in the URL.
Verify the binary:
mimir --versionExpected output:
Mimir, version 2.14.0 (branch: HEAD, revision: abcdef1)
build user: root@buildhost
build date: 20260401-00:00:00
go version: go1.22.5
platform: linux/amd64Step 3: Prepare Object Storage (MinIO or S3)
Mimir needs an S3-compatible bucket for blocks, rules, and Alertmanager state. Two common options:
Option A — Use AWS S3
Create a bucket and an IAM user with s3:GetObject, s3:PutObject, s3:DeleteObject, and s3:ListBucket permissions. Record the access key, secret key, bucket name, and region.
Option B — Use Self-Hosted MinIO
If you want everything on infrastructure you control, run MinIO on the same VPS (or a separate one). Follow our dedicated guide: How to Install MinIO on Ubuntu.
Once MinIO is running, create the bucket Mimir will use. Using the mc client:
mc alias set local http://127.0.0.1:9000 minioadmin minioadmin
mc mb local/mimir-blocks
mc mb local/mimir-ruler
mc mb local/mimir-alertmanagerMimir splits its object storage across three logical locations: one for TSDB blocks, one for recording/alerting rules, and one for Alertmanager state. You can use three buckets or one bucket with three prefixes — this guide uses three buckets for clarity.
Export the credentials so the next step has them handy:
export S3_ENDPOINT="127.0.0.1:9000"
export S3_ACCESS_KEY="minioadmin"
export S3_SECRET_KEY="minioadmin"For production, create a dedicated MinIO service account with access only to these three buckets — do not use the root credentials.
Step 4: Create the Mimir Configuration File
Mimir's configuration is YAML. The monolithic mode config below is a sensible starting point for a single-node production deployment. Write it to /etc/mimir/config.yaml:
sudo tee /etc/mimir/config.yaml > /dev/null <<'EOF'
-----------------------------------------------------------------------------
Grafana Mimir monolithic-mode configuration
-----------------------------------------------------------------------------
multitenancy_enabled: trueThe "common" block is inherited by every storage-aware component unless they
override it explicitly. It avoids repeating S3 credentials three times.
common:
storage:
backend: s3
s3:
endpoint: 127.0.0.1:9000
region: us-east-1
access_key_id: minioadmin
secret_access_key: minioadmin
insecure: true # set to false when talking to real AWS S3 or HTTPS MinIORun every Mimir microservice inside one process.
target: all,alertmanager,overrides-exporterBlocks storage: where TSDB blocks are flushed by ingesters and read by
store-gateways and queriers.
blocks_storage:
backend: s3
s3:
bucket_name: mimir-blocks
tsdb:
dir: /var/lib/mimir/tsdb
retention_period: 24h # local retention on the ingester; long-term lives in S3
bucket_store:
sync_dir: /var/lib/mimir/tsdb-syncIngester: receives writes, buffers them in memory + WAL, flushes blocks to S3.
ingester:
ring:
replication_factor: 1 # single-node; raise to 3 when you add more ingesters
kvstore:
store: memberlistDistributor: accepts remote_write, validates labels, hashes series to ingesters.
distributor:
ring:
kvstore:
store: memberlist
# Per-series and per-request safety limits; tune for your fleet.
pool:
health_check_ingesters: trueRuler: evaluates recording + alerting rules, sends alerts to Alertmanager.
ruler:
rule_path: /var/lib/mimir/ruler
alertmanager_url: http://127.0.0.1:9009/alertmanager
ring:
kvstore:
store: memberlistruler_storage:
backend: s3
s3:
bucket_name: mimir-ruler
Alertmanager: built-in, multi-tenant Alertmanager that reads config from S3.
alertmanager:
data_dir: /var/lib/mimir/alertmanager
external_url: https://mimir.example.com/alertmanager
sharding_ring:
replication_factor: 1alertmanager_storage:
backend: s3
s3:
bucket_name: mimir-alertmanager
Compactor: merges small blocks, applies retention, deduplicates across ingesters.
compactor:
data_dir: /var/lib/mimir/compactor
sharding_ring:
kvstore:
store: memberlistStore-gateway: serves historical queries from S3.
store_gateway:
sharding_ring:
replication_factor: 1
kvstore:
store: memberlistMemberlist: the gossip protocol that wires all rings together in one process
(and across multiple Mimir nodes when you scale out).
memberlist:
join_members: []Per-tenant limits. "anonymous" is used when multi-tenancy is disabled;
named tenants inherit these defaults unless overridden in runtime.yaml.
limits:
ingestion_rate: 100000 # samples/sec per tenant
ingestion_burst_size: 200000
max_global_series_per_user: 1500000
compactor_blocks_retention_period: 2y
ruler_max_rules_per_rule_group: 100HTTP + gRPC server ports.
server:
http_listen_port: 9009
grpc_listen_port: 9095
log_level: infoPoint Mimir at a runtime config file so you can change per-tenant limits
without restarting the binary.
runtime_config:
file: /etc/mimir/runtime.yaml
EOFCreate an empty runtime overrides file (you will populate it as you onboard tenants):
sudo tee /etc/mimir/runtime.yaml > /dev/null <<'EOF' overrides: {} EOF
sudo chown -R mimir:mimir /etc/mimir
A few things worth highlighting:
multitenancy_enabled: trueturns on theX-Scope-OrgIDheader requirement. Every write and every query must carry it. If you want single-tenant behavior for now, set this tofalseand Mimir uses the tenant IDanonymousautomatically.replication_factor: 1is correct for a single-node install. When you add more ingesters for HA, bump this to3.compactor_blocks_retention_period: 2yis where long-term retention is actually enforced. Blocks older than this are deleted by the compactor.common.storageis inherited byblocks_storage,ruler_storage, andalertmanager_storage— that is why each of those only needs to overridebucket_name.
Step 5: Create the systemd Service
A systemd unit ensures Mimir starts on boot, restarts on crash, and runs under the mimir user:
sudo tee /etc/systemd/system/mimir.service > /dev/null <<'EOF' [Unit] Description=Grafana Mimir Documentation=https://grafana.com/docs/mimir/ Wants=network-online.target After=network-online.target[Service] Type=simple User=mimir Group=mimir ExecStart=/usr/local/bin/mimir \ -config.file=/etc/mimir/config.yaml Restart=on-failure RestartSec=5 LimitNOFILE=65536
Hardening
NoNewPrivileges=true ProtectSystem=full ProtectHome=true PrivateTmp=true ReadWritePaths=/var/lib/mimir /var/log/mimir
[Install] WantedBy=multi-user.target EOF
Reload systemd, enable, and start:
sudo systemctl daemon-reload
sudo systemctl enable --now mimir
sudo systemctl status mimirExpected output:
● mimir.service - Grafana Mimir
Loaded: loaded (/etc/systemd/system/mimir.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-04-16 12:00:00 UTC; 5s ago
Main PID: 23456 (mimir)
Tasks: 24 (limit: 18923)
Memory: 312.0M
CPU: 2.1sTest the HTTP endpoint:
curl http://localhost:9009/readyExpected output: ready
Check the metrics endpoint (Mimir self-monitors):
curl -s http://localhost:9009/metrics | head -20You should see Prometheus-format metrics starting with cortex_ and mimir_.
Step 6: Configure Prometheus Remote Write
Now point your existing Prometheus instance at Mimir. Edit /etc/prometheus/prometheus.yml and add a remote_write block:
global:
scrape_interval: 15s
external_labels:
cluster: prod-eu
replica: prom-01remote_write:
- url: http://mimir.example.com:9009/api/v1/push
headers:
X-Scope-OrgID: team-platform
queue_config:
max_samples_per_send: 2000
capacity: 10000
max_shards: 50
write_relabel_configs:
# Example: drop noisy Go runtime metrics before shipping
- source_labels: [__name__]
regex: go_gc_.*
action: drop
Keep scraping as before
scrape_configs:
- job_name: node
static_configs:
- targets: ['localhost:9100']Key notes:
X-Scope-OrgIDis the tenant identifier. Pick whatever convention works for you — per team, per environment, per customer. Every series Prometheus ships will be stored under that tenant.external_labelsshould be unique per Prometheus replica. Mimir uses them to deduplicate when you run HA Prometheus pairs.write_relabel_configsis the right place to drop high-cardinality or low-value metrics before they cross the wire — this is the single best lever for controlling your active-series count.
sudo systemctl reload prometheusConfirm writes are flowing by querying Mimir's ingester metrics:
curl -s http://localhost:9009/metrics | grep cortex_ingester_ingested_samples_totalThe counter should be increasing. You can also check Mimir's structured logs:
sudo journalctl -u mimir -fYou should see lines like level=info msg="push request" tenant=team-platform samples=4523 ....
Step 7: Add Mimir as a Grafana Data Source
In Grafana, go to Connections -> Data sources -> Add new data source -> Prometheus. Mimir speaks the Prometheus query API, so that is the correct driver.
Configure:
- Name: Mimir (prod)
- Prometheus server URL:
http://localhost:9009/prometheus(orhttps://mimir.example.com/prometheusonce Nginx is in front) - HTTP Custom Headers: add one named
X-Scope-OrgIDwith the value matching the tenant you want to query — e.g.team-platform
Open the Explore tab and run a test query:
rate(node_cpu_seconds_total[5m])You should see the same data you previously saw in direct-Prometheus mode, only now it is being served from Mimir — which means it will still be there months from now even if the Prometheus instance is rebuilt.
Grafana provisioning (optional)
If you manage Grafana with config files, add the data source declaratively:
# /etc/grafana/provisioning/datasources/mimir.yaml
apiVersion: 1
datasources:
- name: Mimir
type: prometheus
access: proxy
url: http://localhost:9009/prometheus
jsonData:
httpHeaderName1: X-Scope-OrgID
prometheusType: Mimir
prometheusVersion: 2.14.0
secureJsonData:
httpHeaderValue1: team-platformThen restart Grafana.
Step 8: Enable Multi-Tenancy (X-Scope-OrgID)
With multitenancy_enabled: true, Mimir partitions every write, query, and alert by the X-Scope-OrgID header. This is how one Mimir cluster can serve multiple teams, environments, or customers without their data mixing.
Adding a second tenant
Say you want to onboard a second team, team-data. In their Prometheus config:
remote_write:
- url: http://mimir.example.com:9009/api/v1/push
headers:
X-Scope-OrgID: team-dataIn Grafana, either create a second data source with the new tenant header, or use the Grafana Enterprise Mimir data source plugin which supports per-user tenant switching.
Per-tenant limits
Edit /etc/mimir/runtime.yaml to set custom limits per tenant without restarting Mimir:
overrides:
team-platform:
ingestion_rate: 500000
max_global_series_per_user: 5000000
compactor_blocks_retention_period: 2y
team-data:
ingestion_rate: 100000
max_global_series_per_user: 1000000
compactor_blocks_retention_period: 90d
noisy-dev-team:
ingestion_rate: 20000
max_global_series_per_user: 250000
compactor_blocks_retention_period: 14dMimir reloads this file automatically every 10 seconds. Watch the logs to confirm:
sudo journalctl -u mimir | grep "runtime config"This is a powerful pattern — you can give production teams years of retention while capping dev environments at two weeks, all from one file.
Step 9: Configure the Ruler and Alertmanager
Mimir has a built-in Ruler that evaluates Prometheus recording and alerting rules server-side, and a built-in Alertmanager that receives the resulting alerts. This is a big deal: you no longer need to run a separate Prometheus Alertmanager, and recording rules execute on Mimir's own storage (which means they work on long-range queries that would time out on raw Prometheus).
Upload a rule group
Rules are stored in the mimir-ruler bucket, keyed by tenant. The easiest way to manage them is mimirtool.
Install mimirtool:
wget https://github.com/grafana/mimir/releases/download/mimir-2.14.0/mimirtool-linux-amd64
sudo install -o root -g root -m 0755 mimirtool-linux-amd64 /usr/local/bin/mimirtoolCreate a rule file, rules.yaml:
namespace: platform-alerts
groups:
- name: host-health
interval: 1m
rules:
- alert: HighCPUUsage
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 10m
labels:
severity: warning
annotations:
summary: "High CPU on {{ $labels.instance }}"
description: "CPU is above 85% for 10 minutes."
- alert: DiskAlmostFull
expr: (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 > 90
for: 15m
labels:
severity: critical
annotations:
summary: "Disk almost full on {{ $labels.instance }} ({{ $labels.mountpoint }})"
- name: slo-recording
interval: 30s
rules:
# Recording rule: pre-compute a rolling SLI for fast dashboarding
- record: job:http_request_errors:ratio_5m
expr: |
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))Load it into Mimir:
mimirtool rules load rules.yaml \
--address=http://localhost:9009 \
--id=team-platformList the rules to confirm:
mimirtool rules list \
--address=http://localhost:9009 \
--id=team-platformConfigure Alertmanager
Each tenant has its own Alertmanager configuration, also stored in S3. Create alertmanager.yaml:
template_files: {}
alertmanager_config: |
route:
receiver: default
group_by: [alertname, severity]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- match:
severity: critical
receiver: pagerduty
continue: true
- match:
severity: warning
receiver: slack
receivers:
- name: default
slack_configs:
- api_url: https://hooks.slack.com/services/XXX/YYY/ZZZ
channel: '#alerts'
- name: slack
slack_configs:
- api_url: https://hooks.slack.com/services/XXX/YYY/ZZZ
channel: '#alerts-warning'
- name: pagerduty
pagerduty_configs:
- service_key: your-pagerduty-integration-keyUpload it:
mimirtool alertmanager load alertmanager.yaml \
--address=http://localhost:9009 \
--id=team-platformThe built-in Alertmanager UI is now reachable at http://localhost:9009/alertmanager/ (it will prompt for the X-Scope-OrgID header if you are using multitenancy_enabled: true).
Fire a test alert from the Mimir ruler logs to confirm end-to-end delivery:
sudo journalctl -u mimir | grep -i "ruler"Step 10: Set Long-Term Retention Policies
Retention in Mimir is controlled by the compactor, not by the ingester. Two settings drive it:
Global default (in /etc/mimir/config.yaml):
limits:
compactor_blocks_retention_period: 2yPer-tenant override (in /etc/mimir/runtime.yaml):
overrides:
team-platform:
compactor_blocks_retention_period: 3y # production: 3 years
team-data:
compactor_blocks_retention_period: 90d # dev: 90 daysThe compactor scans blocks, deletes any older than the policy, and merges small blocks into larger ones. You can force a compaction cycle for testing:
curl -X POST http://localhost:9009/compactor/ringCheck what blocks are currently stored per tenant:
curl -s "http://localhost:9009/api/v1/user_stats" -H "X-Scope-OrgID: team-platform"Choosing a retention period
A useful heuristic:
- 7–30 days for dev/test environments.
- 90–180 days for non-critical production (sufficient to debug most incidents, see quarterly trends).
- 1–2 years for capacity planning, executive reporting, and SLO tracking.
- 3+ years for regulated industries, capacity models with multi-year seasonality, or when metrics double as audit evidence.
Step 11: Put Nginx in Front with TLS
Exposing Mimir's raw HTTP port on the internet is a bad idea. Use Nginx as a TLS-terminating reverse proxy with rate limiting.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site:
sudo tee /etc/nginx/sites-available/mimir > /dev/null <<'EOF'Rate-limit writes separately from reads — writes are much higher volume.
limit_req_zone $binary_remote_addr zone=mimir_write:10m rate=200r/s; limit_req_zone $binary_remote_addr zone=mimir_read:10m rate=50r/s;server { listen 80; server_name mimir.example.com; return 301 https://$host$request_uri; }
server { listen 443 ssl http2; server_name mimir.example.com;
ssl_certificate /etc/letsencrypt/live/mimir.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mimir.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
# Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY;
client_max_body_size 32m;
# Prometheus remote_write endpoint location /api/v1/push { limit_req zone=mimir_write burst=500 nodelay; proxy_pass http://127.0.0.1:9009; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_request_buffering off; }
# Grafana query endpoint + everything else location / { limit_req zone=mimir_read burst=100 nodelay; proxy_pass http://127.0.0.1:9009; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_read_timeout 300s; proxy_send_timeout 300s; } } EOF
sudo ln -s /etc/nginx/sites-available/mimir /etc/nginx/sites-enabled/ sudo nginx -t sudo certbot --nginx -d mimir.example.com sudo systemctl reload nginx
Now update your Prometheus remote_write URL and Grafana data source to use https://mimir.example.com/..., and harden the firewall so only 80/443 are exposed:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 9009/tcp
sudo ufw enableAuthentication
The configuration above trusts the X-Scope-OrgID header from any caller that reaches Nginx. For real multi-tenant isolation, add a layer of auth in front. Two common patterns:
Basic auth per tenant (simplest):
location /api/v1/push {
auth_basic "Mimir Push";
auth_basic_user_file /etc/nginx/.mimir-htpasswd;
# ...
}Auth proxy that rewrites the tenant header (more robust): run something like Grafana's auth gateway or a small custom service that validates an API token and sets X-Scope-OrgID based on the token's identity. This way clients never send the tenant header themselves — they present a token and the proxy decides who they are.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Failed to create compactor: unable to create bucket client at startup | S3 credentials wrong or bucket does not exist | Check common.storage.s3 values. Test with mc ls local/mimir-blocks or aws s3 ls s3://mimir-blocks. Create missing buckets. |
Prometheus logs remote_write: Forbidden | multitenancy_enabled: true but no X-Scope-OrgID header | Add the header block in remote_write config, reload Prometheus. |
| Grafana shows "no data" but Prometheus shows data | Grafana querying Prometheus directly instead of Mimir, or wrong tenant header | Check data source URL and X-Scope-OrgID custom header. Remember writes and queries must use the same tenant. |
| Mimir RAM usage keeps climbing | Cardinality explosion — too many unique label combinations | Query cortex_ingester_memory_series per tenant. Find the offender via /api/v1/label/__name__/values. Drop bad labels in write_relabel_configs. |
ingester: rpc error: max series limit errors | Tenant hit max_global_series_per_user | Raise the limit in /etc/mimir/runtime.yaml or reduce cardinality at the source. |
| Alertmanager not firing | Rules loaded but Alertmanager config missing for that tenant | mimirtool alertmanager get --id=team-platform to confirm config exists. Check /alertmanager/#/status UI. |
| Queries are slow on long time ranges | Store-gateway cold cache | Pre-warm by opening the dashboard once; tune store_gateway.bucket_store.chunks_cache and index_cache. |
open file descriptor errors | Default 1024 limit too low | LimitNOFILE=65536 in the systemd unit — restart the service. |
Useful debug endpoints
# Service status
curl http://localhost:9009/ready
curl http://localhost:9009/servicesConfig currently in effect
curl http://localhost:9009/configPer-tenant stats
curl -H "X-Scope-OrgID: team-platform" \
http://localhost:9009/api/v1/user_statsRing state (who owns what)
curl http://localhost:9009/ingester/ring
curl http://localhost:9009/compactor/ringLogs
sudo journalctl -u mimir -f
sudo journalctl -u mimir -n 200 --no-pager | grep -i errorFAQ
Is monolithic mode production-ready, or do I need microservices mode?
Monolithic mode is absolutely production-ready and is the right choice up to several million active series on a single beefy VPS. The Mimir code path is identical — it is the same binary, the same modules, just co-located in one process. You switch to microservices mode when you need horizontal scaling of specific components (typically ingesters and store-gateways) or want independent failure domains. For most self-hosted setups that point is tens of millions of series, well above what a single Prometheus ever reached.
How does Mimir compare to Thanos and VictoriaMetrics?
Mimir is a descendant of Cortex, actively maintained by Grafana Labs. It has the most polished multi-tenant model, a built-in Alertmanager and Ruler, and mimirtool for ops. It is the right choice if you value feature completeness and a managed-like experience on self-hosted infrastructure.
Thanos is an older sidecar-plus-components architecture that augments existing Prometheus instances. It is excellent if you want to keep Prometheus as the system of record and add long-term storage and global query around it with minimal changes. The trade-off is more moving parts and less polished multi-tenancy.
VictoriaMetrics is a ground-up rewrite in Go that prioritizes raw performance and storage efficiency. Benchmarks often show it using significantly less CPU and disk than Mimir/Thanos. The trade-off is a smaller ecosystem, fewer built-ins (no integrated Alertmanager), and a different operational model. It is a great choice if you care primarily about resource efficiency and are willing to assemble the surrounding pieces yourself.
For a stack that already uses Grafana and Prometheus and wants the closest thing to Grafana Cloud on your own hardware, Mimir is the natural fit.
Can I migrate my existing Prometheus data into Mimir?
Yes. Use mimirtool to backfill historical TSDB blocks:
mimirtool backfill \
--address=http://localhost:9009 \
--id=team-platform \
/var/lib/prometheus/dataThis reads Prometheus's TSDB blocks and uploads them directly into Mimir's object storage. It is safe to run while Mimir is serving live traffic.
Do I still need Prometheus if I have Mimir?
Yes — Mimir does not scrape targets. You still run one or more Prometheus instances (or the Grafana Agent) for scraping, and they remote-write to Mimir. Some teams run an entire fleet of tiny Prometheus agents (one per Kubernetes cluster, one per VPS) and let Mimir be the global store. A nice side effect: individual Prometheus instances become stateless and disposable.
How expensive is S3 for Mimir storage?
Very cheap compared to managed metrics services. Rough numbers for AWS S3 Standard in us-east-1:
- 1M active series at 15s scrape interval for 1 year ≈ 50 GB compressed ≈ $1.15/month in storage.
- 10M active series for 2 years ≈ 1 TB ≈ $23/month.
- Request costs are negligible (Mimir batches everything).
What happens if the Mimir VPS dies?
Because all long-term data lives in S3, you lose only the in-flight samples in the ingester's WAL — typically the last ~2 hours. To recover, spin up a new VPS, install Mimir, point it at the same bucket, and it will pick up serving historical queries immediately. For higher durability, run two or three Mimir nodes with replication_factor: 3 so the WAL is itself replicated.
Next Steps
Now that Mimir is running, a few directions to take it further:
- Install the Grafana Mimir data source plugin for a richer query experience that understands tenants natively.
- Deploy a second Mimir node pointed at the same object storage and raise
replication_factorto 3 for true HA ingestion. - Ship the Mimir-mixin dashboards — Grafana publishes a curated pack of dashboards and alert rules that monitor Mimir itself. Import them via
mimirtool rules load mimir-mixin-rules.yaml. - Add Grafana Loki and Grafana Tempo for logs and traces backed by the same S3 bucket — the three together form a unified Grafana Labs observability stack on your own VPS.
- Review our companion guides: How to Install Prometheus on Ubuntu, How to Install Grafana on Ubuntu, and How to Install MinIO on Ubuntu.
- Read the official Mimir documentation for advanced topics like microservices mode, shuffle-sharding, and query federation.
Build your metrics platform on infrastructure you control. The CloudCore Business VPS gives you the CPU, RAM, and NVMe storage Mimir needs, at a flat monthly price with no per-series or per-sample billing surprises.