{
  "schema_version": 2,
  "id": "operate/radar/install",
  "title": "Install self-managed Radar",
  "url": "https://redis.io/docs/latest/operate/radar/install/",
  "summary": "Install Radar on RHEL, Kubernetes, or Docker Compose, then make it reachable from your network.",
  "tags": [
    "docs",
    "operate",
    "radar"
  ],
  "last_updated": "2026-09-06T07:25:30-05:00",
  "page_type": "content",
  "content_hash": "41147bf84ed60025a8d02ec14ca816b9dc797981980b455982bc093b80b246da",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This page covers self-managed Radar. If you're using Redis Cloud's hosted Radar, see [Redis Radar on Redis Cloud](https://redis.io/docs/latest/operate/rc/radar) instead.\n\nRadar runs as two services backed by a PostgreSQL database that you provide:\n\n- API server: serves the REST API and the web UI.\n- Worker: connects to each cluster on a schedule, collects its state, and stores it in the database.\n\nBoth services read the same database and the same encryption key. You supply both, no matter which install method you choose.\n\nPlan for remote access before you start. Set up:\n\n- TLS certificates\n- A proxy or load balancer\n- Firewall rules\n- DNS\n- Network access to your PostgreSQL database\n\nWhat you have to do differs by method, so each install method below ends with its own remote-access step."
    },
    {
      "id": "choose-an-install-method",
      "title": "Choose an install method",
      "role": "content",
      "text": "| Method | Use it when | Requires | Containers |\n|---|---|---|---|\n| [RPM on RHEL](#install-on-rhel-with-the-rpm) | You run RHEL 9 and cannot or do not want to run containers. | RHEL 9 on x86_64 | No |\n| [Kubernetes with Helm](#install-on-kubernetes-with-helm) | You already run Kubernetes or OpenShift. | Kubernetes 1.23 or later, and Helm 3.x. Validated on OpenShift 4.x | Yes |\n| [Docker Compose](#install-with-docker-compose) | You want a single host and already run Docker. | A Docker engine with the `docker compose` plugin | Yes |\n\nAll three are supported and built from the same release. You can install any of them on a host with no internet access. See [Install on an air-gapped host](#install-on-an-air-gapped-host).\n\nGet the RPM from the [Redis Download Center](https://cloud.redis.io/#/rlec-downloads), under **Modules, tools and integrations**. Get the container images from Docker Hub, and the Helm chart."
    },
    {
      "id": "before-you-start",
      "title": "Before you start",
      "role": "content",
      "text": "Before you install:\n\n- Set up an external, managed PostgreSQL database for production (evaluation can use a bundled container instead). See [PostgreSQL](#postgresql).\n- Generate a 32-byte credential encryption key. See [The credential encryption key](#the-credential-encryption-key).\n- Decide if you need FIPS, since it's a separate build, not a setting you can change later. See [FIPS mode](#fips-mode)."
    },
    {
      "id": "postgresql",
      "title": "PostgreSQL",
      "role": "content",
      "text": "For production, set up your own external, managed [PostgreSQL](https://www.postgresql.org/docs/) database before you install Radar. You need to provision, back up, and tune it yourself, since Radar only connects to it and creates the roles and schema it needs on startup.\n\nFor evaluation or testing, you can skip that step: the Helm chart and the Compose bundle can each start a PostgreSQL container for you, though neither is hardened for production use.\n\nThe connection string needs privileges for both normal runtime work and schema migration, including `CREATEROLE`. On startup, the API server creates the roles it needs before it begins serving traffic.\n\nUse `sslmode=require` or stricter to encrypt the connection. Radar passes your connection string through unchanged."
    },
    {
      "id": "the-credential-encryption-key",
      "title": "The credential encryption key",
      "role": "content",
      "text": "Radar encrypts the cluster credentials you supply. Each tenant gets its own data key, and all of those keys are wrapped by one key-encryption key (KEK) that you supply. The key must be **32 raw bytes**, not base64 or hex.\n\n\nBack up the KEK alongside the database and store the two backups separately. Neither is usable without the other. Radar cannot decrypt stored credentials if the API server and the worker read different keys, or if a restored database is paired with the wrong key. It fails closed rather than losing them silently."
    },
    {
      "id": "fips-mode",
      "title": "FIPS mode",
      "role": "content",
      "text": "[FIPS 140-3](https://csrc.nist.gov/pubs/fips/140-3/final) validated cryptography comes as a separate build of Radar, not a setting you turn on later, so decide before you install. Contact your Redis account team for the FIPS variant.\n\nSet the `MCM_REQUIRE_FIPS=true` environment variable to make FIPS mandatory. Radar then refuses to start unless FIPS is active, and it checks before it touches the database or opens a port. A misconfigured deployment fails immediately rather than running with cryptography you did not approve.\n\nEach service logs its FIPS state once at startup, so you can confirm what is running. In production, Radar writes this log line as structured JSON rather than flat text, and includes additional fields such as the Go runtime's FIPS build mode. The core fields look like this:\n\n[code example]\n\nIn that line, both `enabled` and `required` should read `true`. Search your logs by field name rather than this literal line, since the exact format depends on your deployment method."
    },
    {
      "id": "package-and-service-names",
      "title": "Package and service names",
      "role": "content",
      "text": "Radar's packages, services, and paths use an `mcm` prefix. The RPM is named `mcm`, its services are `mcm-api` and `mcm-worker`, and its configuration lives in `/etc/mcm/`. The Docker Compose bundle's container images are `mcm-app`, `mcm-worker`, and `mcm-migrate`; the Helm chart's default image repositories use a `radar-` prefix instead."
    },
    {
      "id": "install-on-rhel-with-the-rpm",
      "title": "Install on RHEL with the RPM",
      "role": "setup",
      "text": "The RPM installs native binaries and needs no container runtime. It also installs the `mcmctl` diagnostics command.\n\n\nThe RPM listens only on loopback by default. A successful RPM install is not yet reachable from any other machine until you put a proxy in front of it.\n\n\n1. Install the package.\n\n   [code example]\n\n   The package depends on RHEL's `postgresql-server`, so `dnf` installs PostgreSQL software if it is absent. It does not create or start a database.\n\n   <br>\n\n2. Confirm the services are inactive.\n\n   The package deliberately installs them without starting or enabling them.\n\n   [code example]\n\n   <br>\n\n3. Configure the package. Edit `/etc/mcm/mcm.env` and set the two required values.\n\n   [code example]\n\n   | Setting | Description |\n   |---|---|\n   | `DATABASE_URL` | Connection string for your PostgreSQL database, with runtime and migration privileges. |\n   | `CREDENTIAL_ENCRYPTION_KEY` | The credential encryption key. |\n\n   Radar refuses to start while the placeholder values are still in place. The file is owned by `root:mcm`, redacted from logs and diagnostics, and kept across upgrades and removal. Include it in your backup plan.\n\n   You may want to change these defaults.\n\n   | Setting | Default |\n   |---|---|\n   | `HTTP_ADDR` | `127.0.0.1:8080` |\n   | `CREDENTIAL_KEK_PATH` | `/var/lib/mcm/kek` |\n   | `SESSION_COOKIE_SECURE` | `true` |\n\n   Restart `mcm-api.service` after changing API, UI, CORS, LDAP, session, or HTTP settings. Restart `mcm-worker.service` after changing worker, database, encryption, or collection settings. Restart both after changing `DATABASE_URL` or `CREDENTIAL_ENCRYPTION_KEY`.\n\n   <br>\n\n4. Check the configuration.\n\n   [code example]\n\n   Before the first start, `mcmctl doctor` reports that schema migration has not run yet. That is expected; the configuration and database connectivity checks should still pass.\n\n   <br>\n\n5. Start the services.\n\n   [code example]\n\n   The API server applies the database migrations as it starts.\n\n   <br>\n\n6. Verify the services are running.\n\n   [code example]\n\n   <br>\n\n7. Enable the services only after the health checks pass, so a reboot cannot start a half-configured deployment.\n\n   [code example]\n\n   <br>\n\n8. Create the first administrator.\n\n   Radar does not ship a default account or a default password. Open the UI once the API server is healthy and complete the one-time first-administrator flow. It is available only while the database has no users; after that, it closes and normal sign-in applies.\n\n   <br>\n\n9. Provide remote access.\n\n   At this point Radar is reachable only from the host itself.\n\n   Run a reverse proxy that terminates TLS and forwards to the loopback address. Keep `HTTP_ADDR=127.0.0.1:8080` when the proxy runs on the same host. That is the safest arrangement, because nothing but the proxy can reach the API.\n\n   If the proxy runs on a different host, set `HTTP_ADDR` to the private interface it should reach, then restrict access with your own firewall rules. Restart the API server.\n\n   [code example]\n\n   `mcmctl doctor` checks runtime health through the configured address. If it reports a runtime-health failure after you change the listen address, confirm the service bound to the interface you expected and that the proxy forwards to the same address.\n\n   \n   Do not expose Radar directly on a public interface. Terminate TLS and apply access controls at the edge.\n   \n\n   <br>"
    },
    {
      "id": "install-on-kubernetes-with-helm",
      "title": "Install on Kubernetes with Helm",
      "role": "setup",
      "text": "A production install has four parts you supply: the PostgreSQL connection, the credential encryption key, image pull access, and an external access path.\n\n1. Create the database secret. Store the database connection string in a secret.\n\n   [code example]\n\n   <br>\n\n2. Create the credentials secret. Generate the credential encryption key as a file and load it with `--from-file`.\n\n   [code example]\n\n   \n   Write the key to a file rather than using `--from-literal=\"$(head -c 32 /dev/urandom)\"`. If the random key contains a zero byte, command substitution truncates it there, so the key would be shorter than 32 bytes.\n   \n\n   The secret must contain a key named `CREDENTIAL_KEK`. Without it, the pods stay in `ContainerCreating` rather than starting with no encryption key.\n\n   <br>\n\n3. Install the chart.\n\n   [code example]\n\n   With an external database, as configured here, the chart runs schema migration as a Kubernetes job before the API server and worker start. If you use the chart's bundled PostgreSQL container instead, migration instead runs after the API and worker pods start, so expect them to restart briefly until the migration job completes. Migrations apply forward only; there is no automated rollback.\n\n   **For a private or air-gapped registry**, override the image source.\n\n   [code example]\n\n   **For OpenShift**, use the OpenShift values file instead, which lets OpenShift assign namespace-scoped user IDs and switches the external access path from an ingress to a route.\n\n   [code example]\n\n   The chart does not need an `anyuid` policy, privileged security context, host paths, or `cluster-admin` permissions.\n\n   <br>\n\n4. Verify the install.\n\n   [code example]\n\n   Expect a running API pod, a running worker pod, and a completed migration job. To check health without an external access path, use the following commands.\n\n   [code example]\n\n   <br>\n\n5. Provide remote access. \n   \n   The API server and UI are served on port 80 of an in-cluster service. Expose it with an ingress, an OpenShift route, or a `LoadBalancer` service, and terminate TLS there.\n\n   [code example]\n\n   Radar marks the browser session cookie as secure by default, so serve Radar over HTTPS. Over plain HTTP the browser rejects the cookie and sign-in fails.\n\n   <br>"
    },
    {
      "id": "install-with-docker-compose",
      "title": "Install with Docker Compose",
      "role": "setup",
      "text": "The Compose bundle runs Radar on a single host. It ships the container images, the Compose files, and an environment template.\n\n1. Load the images.\n\n   [code example]\n\n   <br>\n\n2. Configure the environment. \n   \n   Copy `.env.production.example` to `.env.production` and replace every placeholder, including the PostgreSQL credentials and the credential encryption key.\n\n   \nConfirm you've replaced every sample value, especially the credential encryption key, before you start the services. Unlike the RPM, Compose does not detect leftover sample values: if you start them before replacing the credential encryption key, Radar runs with the published example key rather than refusing to start.\n   \n\n   <br>\n\n3. Start the services.\n\n   [code example]\n\n   The production Compose file pins the image tags and never pulls, so the stack runs fully offline once the images are loaded. A migration service runs once, before the API server and worker start.\n\n   <br>"
    },
    {
      "id": "install-on-an-air-gapped-host",
      "title": "Install on an air-gapped host",
      "role": "setup",
      "text": "Air-gapped installation uses the same three methods.\n\nTransfer the release artifacts to the target host or to an offline repository it can reach, then verify them:\n\n[code example]\n\n| Method | What to transfer | How it installs |\n|---|---|---|\n| RPM | The `.rpm`, `SHA256SUMS`, and the dependency closure, including `postgresql-server` if the host has no offline PostgreSQL | `dnf install` from the local file |\n| Helm | `images.tar.gz`, the packaged chart, and the bundled values file | `docker load` the images onto the nodes, then install the chart |\n| Docker Compose | `images.tar.gz` and the Compose files | `docker load`, then `docker compose up` |\n\nYour PostgreSQL database and the clusters you plan to monitor still need to be reachable from the Radar host over the network."
    },
    {
      "id": "next-steps",
      "title": "Next steps",
      "role": "content",
      "text": "Radar is installed but has nothing to show yet. Continue to [Connect clusters](https://redis.io/docs/latest/operate/radar/connect) to add your first cluster."
    }
  ],
  "examples": [
    {
      "id": "fips-mode-ex0",
      "language": "text",
      "code": "fips state service=mcm-api category=startup enabled=true required=true",
      "section_id": "fips-mode"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex0",
      "language": "bash",
      "code": "sha256sum -c SHA256SUMS\n   sudo dnf install -y ./mcm-<version>-<release>.x86_64.rpm",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex1",
      "language": "bash",
      "code": "systemctl is-active mcm-api.service || true\n   systemctl is-active mcm-worker.service || true",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex2",
      "language": "bash",
      "code": "sudoedit /etc/mcm/mcm.env",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex3",
      "language": "bash",
      "code": "sudo mcmctl doctor",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex4",
      "language": "bash",
      "code": "sudo systemctl start mcm-api.service\n   sudo systemctl start mcm-worker.service",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex5",
      "language": "bash",
      "code": "curl -fsS http://127.0.0.1:8080/healthz/ready\n   sudo mcmctl doctor",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex6",
      "language": "bash",
      "code": "sudo systemctl enable mcm-api.service\n   sudo systemctl enable mcm-worker.service",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-rhel-with-the-rpm-ex7",
      "language": "bash",
      "code": "sudo systemctl restart mcm-api.service\n   sudo mcmctl doctor",
      "section_id": "install-on-rhel-with-the-rpm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex0",
      "language": "bash",
      "code": "kubectl create secret generic radar-db \\\n     --namespace radar \\\n     --from-literal=DATABASE_URL='postgres://radar:secret@postgres.example.com:5432/radar?sslmode=require'",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex1",
      "language": "bash",
      "code": "head -c 32 /dev/urandom > kek.bin\n\n   kubectl create secret generic radar-credentials \\\n     --namespace radar \\\n     --from-file=CREDENTIAL_KEK=./kek.bin\n\n   shred -u kek.bin",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex2",
      "language": "bash",
      "code": "helm install radar ./helm/radar \\\n     --namespace radar \\\n     --create-namespace \\\n     --set database.existingSecret=radar-db \\\n     --set credentials.existingSecret=radar-credentials \\\n     --set ingress.enabled=true \\\n     --set ingress.className=nginx \\\n     --set ingress.hosts[0].host=radar.example.com \\\n     --set ingress.hosts[0].paths[0].path=/ \\\n     --set ingress.hosts[0].paths[0].pathType=Prefix",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex3",
      "language": "yaml",
      "code": "global:\n     imageRegistry: registry.example.com/redislabs\n     imagePullSecrets:\n       - name: registry-creds",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex4",
      "language": "bash",
      "code": "helm install radar ./helm/radar \\\n     --namespace radar \\\n     --create-namespace \\\n     -f ./helm/radar/values-openshift.yaml \\\n     --set database.existingSecret=radar-db \\\n     --set credentials.existingSecret=radar-credentials \\\n     --set route.host=radar.apps.example.com",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex5",
      "language": "bash",
      "code": "kubectl get pods -n radar\n   kubectl get jobs -n radar -l app.kubernetes.io/component=migrate\n   helm test radar --namespace radar",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex6",
      "language": "bash",
      "code": "kubectl port-forward -n radar svc/radar 8080:80\n   curl http://localhost:8080/healthz/ready",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-on-kubernetes-with-helm-ex7",
      "language": "yaml",
      "code": "ingress:\n     enabled: true\n     className: nginx\n     annotations:\n       cert-manager.io/cluster-issuer: letsencrypt-prod\n     hosts:\n       - host: radar.example.com\n         paths:\n           - path: /\n             pathType: Prefix\n     tls:\n       - secretName: radar-tls\n         hosts:\n           - radar.example.com",
      "section_id": "install-on-kubernetes-with-helm"
    },
    {
      "id": "install-with-docker-compose-ex0",
      "language": "bash",
      "code": "sha256sum -c SHA256SUMS\n   docker load -i images.tar.gz",
      "section_id": "install-with-docker-compose"
    },
    {
      "id": "install-with-docker-compose-ex1",
      "language": "bash",
      "code": "docker compose -f compose.yaml -f compose.prod.yaml --env-file .env.production up -d",
      "section_id": "install-with-docker-compose"
    },
    {
      "id": "install-on-an-air-gapped-host-ex0",
      "language": "bash",
      "code": "sha256sum -c SHA256SUMS",
      "section_id": "install-on-an-air-gapped-host"
    }
  ]
}
