Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Mimir Ubuntu
GUIDEInstall Guides

How to Install Grafana Mimir on Ubuntu 24.04 VPS: Self-Hosted Horizontally-Scalable Prometheus

29 min read

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?
  • Why Self-Host Mimir vs Grafana Cloud Metrics?
  • Prerequisites
  • Step 1: Update the System and Create a Service User
  • Step 2: Download and Install the Mimir Binary
  • Step 3: Prepare Object Storage (MinIO or S3)
  • Step 4: Create the Mimir Configuration File
  • Step 5: Create the systemd Service
  • Step 6: Configure Prometheus Remote Write
  • Step 7: Add Mimir as a Grafana Data Source
  • Step 8: Enable Multi-Tenancy (X-Scope-OrgID)
  • Step 9: Configure the Ruler and Alertmanager
  • Step 10: Set Long-Term Retention Policies
  • Step 11: Put Nginx in Front with TLS
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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.
    The trade-off is that you take on the operational burden — upgrades, backups, capacity planning, and cardinality hygiene. This guide is designed to make that burden small.

    Cost Comparison

    ScenarioGrafana 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 retention13 months (cap)Years (you choose)
    Queried-sample chargesYesNo
    Data residency controlLimited regionsFull (any region)
    Custom limits per tenantLimitedFull

    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:

    bash
    ssh root@your-server-ip

    Step 1: Update the System and Create a Service User

    Start by updating packages and installing a few utilities:

    bash
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y curl wget tar ca-certificates

    Create a dedicated unprivileged user and the directory layout Mimir will use:

    bash
    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:

    bash
    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/mimir

    If you are on ARM64 (for example a VPS using an Ampere-based instance), swap amd64 for arm64 in the URL.

    Verify the binary:

    bash
    mimir --version

    Expected output:

    text
    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/amd64

    Step 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:

    bash
    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-alertmanager

    Mimir 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:

    bash
    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:

    bash
    sudo tee /etc/mimir/config.yaml > /dev/null <<'EOF'
    

    -----------------------------------------------------------------------------

    Grafana Mimir monolithic-mode configuration

    -----------------------------------------------------------------------------

    multitenancy_enabled: true

    The "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 MinIO

    Run every Mimir microservice inside one process.

    target: all,alertmanager,overrides-exporter

    Blocks 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-sync

    Ingester: 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: memberlist

    Distributor: 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: true

    Ruler: 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: memberlist

    ruler_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: 1

    alertmanager_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: memberlist

    Store-gateway: serves historical queries from S3.

    store_gateway: sharding_ring: replication_factor: 1 kvstore: store: memberlist

    Memberlist: 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: 100

    HTTP + gRPC server ports.

    server: http_listen_port: 9009 grpc_listen_port: 9095 log_level: info

    Point Mimir at a runtime config file so you can change per-tenant limits

    without restarting the binary.

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

    Create an empty runtime overrides file (you will populate it as you onboard tenants):

    bash
    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: true turns on the X-Scope-OrgID header requirement. Every write and every query must carry it. If you want single-tenant behavior for now, set this to false and Mimir uses the tenant ID anonymous automatically.
    • replication_factor: 1 is correct for a single-node install. When you add more ingesters for HA, bump this to 3.
    • compactor_blocks_retention_period: 2y is where long-term retention is actually enforced. Blocks older than this are deleted by the compactor.
    • common.storage is inherited by blocks_storage, ruler_storage, and alertmanager_storage — that is why each of those only needs to override bucket_name.

    Step 5: Create the systemd Service

    A systemd unit ensures Mimir starts on boot, restarts on crash, and runs under the mimir user:

    bash
    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:

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

    Expected output:

    text
    ● 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.1s

    Test the HTTP endpoint:

    bash
    curl http://localhost:9009/ready

    Expected output: ready

    Check the metrics endpoint (Mimir self-monitors):

    bash
    curl -s http://localhost:9009/metrics | head -20

    You 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:

    yaml
    global:
      scrape_interval: 15s
      external_labels:
        cluster: prod-eu
        replica: prom-01

    remote_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-OrgID is 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_labels should be unique per Prometheus replica. Mimir uses them to deduplicate when you run HA Prometheus pairs.
    • write_relabel_configs is 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.
    Reload Prometheus:

    bash
    sudo systemctl reload prometheus

    Confirm writes are flowing by querying Mimir's ingester metrics:

    bash
    curl -s http://localhost:9009/metrics | grep cortex_ingester_ingested_samples_total

    The counter should be increasing. You can also check Mimir's structured logs:

    bash
    sudo journalctl -u mimir -f

    You 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 (or https://mimir.example.com/prometheus once Nginx is in front)
    • HTTP Custom Headers: add one named X-Scope-OrgID with the value matching the tenant you want to query — e.g. team-platform
    Click Save & test. Grafana should respond: "Successfully queried the Prometheus API."

    Open the Explore tab and run a test query:

    promql
    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:

    yaml
    # /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-platform

    Then 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:

    yaml
    remote_write:
      - url: http://mimir.example.com:9009/api/v1/push
        headers:
          X-Scope-OrgID: team-data

    In 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:

    yaml
    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: 14d

    Mimir reloads this file automatically every 10 seconds. Watch the logs to confirm:

    bash
    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:

    bash
    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/mimirtool

    Create a rule file, rules.yaml:

    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:

    bash
    mimirtool rules load rules.yaml \
      --address=http://localhost:9009 \
      --id=team-platform

    List the rules to confirm:

    bash
    mimirtool rules list \
      --address=http://localhost:9009 \
      --id=team-platform

    Configure Alertmanager

    Each tenant has its own Alertmanager configuration, also stored in S3. Create alertmanager.yaml:

    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-key

    Upload it:

    bash
    mimirtool alertmanager load alertmanager.yaml \
      --address=http://localhost:9009 \
      --id=team-platform

    The 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:

    bash
    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):

    yaml
    limits:
      compactor_blocks_retention_period: 2y

    Per-tenant override (in /etc/mimir/runtime.yaml):

    yaml
    overrides:
      team-platform:
        compactor_blocks_retention_period: 3y    # production: 3 years
      team-data:
        compactor_blocks_retention_period: 90d   # dev: 90 days

    The compactor scans blocks, deletes any older than the policy, and merges small blocks into larger ones. You can force a compaction cycle for testing:

    bash
    curl -X POST http://localhost:9009/compactor/ring

    Check what blocks are currently stored per tenant:

    bash
    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.
    Remember that 2 years of 1M active series at a 15s scrape interval is roughly 100 GB compressed in S3 — cheap, but not free. Set retention based on the questions you will actually answer with the data.

    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:

    bash
    sudo apt install -y nginx certbot python3-certbot-nginx

    Create the site:

    bash
    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:

    bash
    sudo ufw allow 22/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw deny 9009/tcp
    sudo ufw enable

    Authentication

    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):

    nginx
    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

    ProblemCauseSolution
    Failed to create compactor: unable to create bucket client at startupS3 credentials wrong or bucket does not existCheck 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: Forbiddenmultitenancy_enabled: true but no X-Scope-OrgID headerAdd the header block in remote_write config, reload Prometheus.
    Grafana shows "no data" but Prometheus shows dataGrafana querying Prometheus directly instead of Mimir, or wrong tenant headerCheck data source URL and X-Scope-OrgID custom header. Remember writes and queries must use the same tenant.
    Mimir RAM usage keeps climbingCardinality explosion — too many unique label combinationsQuery 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 errorsTenant hit max_global_series_per_userRaise the limit in /etc/mimir/runtime.yaml or reduce cardinality at the source.
    Alertmanager not firingRules loaded but Alertmanager config missing for that tenantmimirtool alertmanager get --id=team-platform to confirm config exists. Check /alertmanager/#/status UI.
    Queries are slow on long time rangesStore-gateway cold cachePre-warm by opening the dashboard once; tune store_gateway.bucket_store.chunks_cache and index_cache.
    open file descriptor errorsDefault 1024 limit too lowLimitNOFILE=65536 in the systemd unit — restart the service.

    Useful debug endpoints

    bash
    # Service status
    curl http://localhost:9009/ready
    curl http://localhost:9009/services

    Config currently in effect

    curl http://localhost:9009/config

    Per-tenant stats

    curl -H "X-Scope-OrgID: team-platform" \ http://localhost:9009/api/v1/user_stats

    Ring state (who owns what)

    curl http://localhost:9009/ingester/ring curl http://localhost:9009/compactor/ring

    Logs

    bash
    sudo journalctl -u mimir -f
    sudo journalctl -u mimir -n 200 --no-pager | grep -i error

    FAQ

    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:

    bash
    mimirtool backfill \
      --address=http://localhost:9009 \
      --id=team-platform \
      /var/lib/prometheus/data

    This 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).
    Self-hosted MinIO costs you only the disk space on the VPS. For most teams storage is a rounding error compared to the VPS itself.

    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_factor to 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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket