Type something to search...
Deploying MongoDB on Kubernetes with the MongoDB Operator

Deploying MongoDB on Kubernetes with the MongoDB Operator

Kubernetes is great at running stateless services: if a pod dies, start another one anywhere. Databases don't work that way. A MongoDB replica set member has a persistent identity, its own data volume, and a place in the replica set configuration. Members need to find each other by stable hostnames, authenticate with a shared key, and be reconfigured carefully when you scale. Upgrades have to roll through members in the right order. A plain StatefulSet gives you stable names and volumes, but it knows nothing about any of the rest.

That's what a Kubernetes operator is for. An operator is a controller that understands a specific application. You declare what you want ("a three-member MongoDB 8.0 replica set with these users and TLS enabled") as a custom resource, and the operator creates the StatefulSet, services, secrets, and replica set configuration, then keeps reconciling reality toward that declaration.

This guide covers MongoDB's official operator, MongoDB Controllers for Kubernetes (MCK): installing it with Helm, deploying a replica set, creating users and connecting from an application, configuring storage and scheduling, enabling TLS, and handling scaling, upgrades, and backups.

One Operator, Two Flavors

MongoDB used to ship two separate operators: the open-source Community Kubernetes Operator and the Enterprise Kubernetes Operator. In 2025, they were consolidated into a single project, MongoDB Controllers for Kubernetes (MCK). If you find older guides referencing mongodb-kubernetes-operator or enterprise-operator, they're describing the predecessors.

MCK manages two families of resources:

ResourceForRequires
MongoDBCommunityMongoDB Community replica setsNothing extra
MongoDBEnterprise replica sets and sharded clustersMongoDB Enterprise, plus Ops Manager or Cloud Manager
MongoDBOpsManagerRunning Ops Manager itself in KubernetesEnterprise subscription

This guide focuses on MongoDBCommunity, which you can run for free. The Enterprise resources follow similar patterns and are covered briefly at the end.

If you're migrating from the old Community Operator, MCK is designed to take over existing MongoDBCommunity resources. Read the migration guide in the MCK documentation before switching, and try it on a non-production cluster first.

Prerequisites

You'll need:

  • A Kubernetes cluster with a StorageClass that provisions persistent volumes (any managed cluster like EKS, GKE, or AKS has one; for local testing, kind or minikube works).
  • kubectl and helm installed.
  • At least three schedulable nodes if you want members spread across nodes, which you do for anything beyond testing.

Check your storage classes:

kubectl get storageclass
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION
standard-rwo (default) pd.csi.storage.gke.io  Delete          WaitForFirstConsumer   true
premium-rwo          pd.csi.storage.gke.io   Delete          WaitForFirstConsumer   true

WaitForFirstConsumer is what you want: volumes are created in the same zone as the pod that uses them.

Step 1: Install the Operator

MCK is distributed as a Helm chart from MongoDB's Helm repository:

helm repo add mongodb https://mongodb.github.io/helm-charts
helm repo update

helm install mongodb-kubernetes mongodb/mongodb-kubernetes \
  --namespace mongodb \
  --create-namespace

This installs the custom resource definitions (CRDs), the operator deployment, and the RBAC roles it needs. By default, the operator watches the namespace it's installed in. Chart values let you watch other namespaces or the whole cluster; check helm show values mongodb/mongodb-kubernetes for the options in your chart version.

Confirm it's running:

kubectl get pods -n mongodb
NAME                                           READY   STATUS    RESTARTS   AGE
mongodb-kubernetes-operator-6c9f7d8b5d-x2kqp   1/1     Running   0          40s

Step 2: Create a User Password Secret

The operator creates MongoDB users from the resource spec, reading each password from a Kubernetes Secret. Create the secret first:

kubectl create secret generic shop-app-password \
  --namespace mongodb \
  --from-literal=password='use-a-long-random-password'

In real deployments, generate passwords randomly and manage them with your usual secrets tooling (External Secrets Operator, Sealed Secrets, or your cloud provider's secret manager) rather than typing them on the command line.

Step 3: Deploy a Replica Set

Here's a complete MongoDBCommunity resource for a three-member replica set:

# shop-mongo.yaml
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: shop-mongo
  namespace: mongodb
spec:
  type: ReplicaSet
  members: 3
  version: "8.0.12" # pin to a current 8.0 patch release
  security:
    authentication:
      modes: ["SCRAM"]
  users:
    - name: shop-app
      db: admin
      passwordSecretRef:
        name: shop-app-password
      roles:
        - name: readWrite
          db: shop
      scramCredentialsSecretName: shop-app-scram
  additionalMongodConfig:
    storage.wiredTiger.engineConfig.journalCompressor: zlib
    operationProfiling.slowOpThresholdMs: 200
  statefulSet:
    spec:
      volumeClaimTemplates:
        - metadata:
            name: data-volume
          spec:
            storageClassName: premium-rwo
            accessModes: ["ReadWriteOnce"]
            resources:
              requests:
                storage: 100Gi
        - metadata:
            name: logs-volume
          spec:
            storageClassName: standard-rwo
            accessModes: ["ReadWriteOnce"]
            resources:
              requests:
                storage: 10Gi

Walking through the important parts:

  • members and version are the core of the declaration. Change them later and the operator reconciles: adding members, or performing a rolling upgrade.
  • security.authentication.modes enables SCRAM authentication. The operator generates the internal keyfile that members use to authenticate to each other.
  • users declares database users. The operator creates them and keeps them in sync. scramCredentialsSecretName names a secret where the operator stores the derived SCRAM credentials.
  • additionalMongodConfig passes settings into mongod.conf using dotted paths.
  • statefulSet.spec is merged into the StatefulSet the operator generates. Here it overrides the default volume claims (named data-volume and logs-volume) with explicit storage classes and sizes.

Apply it and watch the rollout:

kubectl apply -f shop-mongo.yaml
kubectl get mongodbcommunity -n mongodb -w
NAME         PHASE     VERSION
shop-mongo   Pending
shop-mongo   Running   8.0.12

The pods come up one at a time, as a StatefulSet does:

kubectl get pods -n mongodb -l app=shop-mongo-svc
NAME           READY   STATUS    RESTARTS   AGE
shop-mongo-0   2/2     Running   0          3m
shop-mongo-1   2/2     Running   0          2m
shop-mongo-2   2/2     Running   0          1m

Each pod runs two containers: mongod and the MongoDB agent, which the operator uses to apply configuration and coordinate changes like upgrades. The exact labels on pods can differ between operator versions, so if the label selector returns nothing, list all pods in the namespace instead.

Step 4: Connect from an Application

The operator creates a headless service (shop-mongo-svc) giving each member a stable DNS name, such as shop-mongo-0.shop-mongo-svc.mongodb.svc.cluster.local. It also creates a connection string secret for every user, named <resource-name>-<user-db>-<username>:

kubectl get secret shop-mongo-admin-shop-app -n mongodb \
  -o jsonpath='{.data.connectionString\.standardSrv}' | base64 -d
mongodb+srv://shop-app:...@shop-mongo-svc.mongodb.svc.cluster.local/admin?replicaSet=shop-mongo&ssl=false

Mount it into your application as an environment variable rather than copying the value anywhere:

# In your application Deployment
spec:
  template:
    spec:
      containers:
        - name: api
          image: registry.example.com/shop-api:1.14.0
          env:
            - name: MONGODB_URI
              valueFrom:
                secretKeyRef:
                  name: shop-mongo-admin-shop-app
                  key: connectionString.standardSrv

The secret lives in the database's namespace. If your application runs in a different namespace, the operator can be configured to create connection string secrets in other namespaces (check the connectionStringSecretNamespace option on the user in your version), or you can sync the secret with your secrets tooling.

The generated string authenticates against the admin database, where the user was created, so your application only needs to choose the database it works with, such as client.db("shop") in Node.js. For advice on sizing client pools across many pods, see MongoDB Connection Pooling: Avoiding Too Many Connections.

For a quick interactive check:

kubectl exec -it shop-mongo-0 -n mongodb -c mongod -- \
  mongosh "mongodb://shop-app:use-a-long-random-password@localhost:27017/shop?authSource=admin" \
  --quiet --eval "rs.status().members.map(m => m.name + ' ' + m.stateStr)"

Step 5: Resources, Scheduling, and Disruptions

The default pod spec is fine for trying things out, not for production. Override it under statefulSet.spec.template:

spec:
  statefulSet:
    spec:
      template:
        spec:
          containers:
            - name: mongod
              resources:
                requests:
                  cpu: "2"
                  memory: 8Gi
                limits:
                  memory: 8Gi
          affinity:
            podAntiAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector:
                    matchLabels:
                      app: shop-mongo-svc
                  topologyKey: kubernetes.io/hostname
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
              labelSelector:
                matchLabels:
                  app: shop-mongo-svc

Three things matter here:

Memory requests equal to limits. mongod sizes its WiredTiger cache from the container's memory limit (roughly half of it, minus a margin). Setting requests equal to limits gives the pod a predictable amount of memory and avoids it being evicted first under node pressure.

Pod anti-affinity. Three members on the same node survive nothing. The requiredDuringScheduling rule puts each member on a different node. Topology spread across zones adds protection against a zone outage.

A PodDisruptionBudget. Node drains during cluster upgrades evict pods. A PDB keeps Kubernetes from evicting more than one member at a time:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: shop-mongo-pdb
  namespace: mongodb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: shop-mongo-svc

Verify the labels your operator version puts on pods (kubectl get pods --show-labels) and match them in the selectors.

Step 6: Enable TLS

For production, encrypt traffic between clients and members. The operator reads a certificate from a Kubernetes TLS secret and a CA from a ConfigMap. cert-manager is the easiest way to produce them. With a cert-manager Issuer in place, request a certificate covering every member's hostname:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: shop-mongo-cert
  namespace: mongodb
spec:
  secretName: shop-mongo-tls
  issuerRef:
    name: mongodb-ca-issuer
    kind: Issuer
  commonName: "*.shop-mongo-svc.mongodb.svc.cluster.local"
  dnsNames:
    - "*.shop-mongo-svc.mongodb.svc.cluster.local"
    - "shop-mongo-svc.mongodb.svc.cluster.local"

Then enable TLS on the resource:

spec:
  security:
    tls:
      enabled: true
      certificateKeySecretRef:
        name: shop-mongo-tls
      caConfigMapRef:
        name: shop-mongo-ca

The CA ConfigMap must contain the CA certificate under the key ca.crt. When enabling TLS on an existing replica set, check the operator's documentation for the recommended transition (it can temporarily allow both TLS and non-TLS connections so clients can switch without downtime).

Scaling and Upgrades

Both are edits to the resource.

Scaling changes members. Going from 3 to 5 adds two pods; the operator adds them to the replica set configuration as they become ready, and they initial-sync from existing members. Keep an odd number of voting members.

kubectl patch mongodbcommunity shop-mongo -n mongodb \
  --type merge -p '{"spec":{"members":5}}'

Upgrading changes version. The operator performs a rolling upgrade, updating secondaries first and stepping down the primary before upgrading it, exactly the order you'd follow by hand. The same rules apply as outside Kubernetes: upgrade one major version at a time, and raise the feature compatibility version only after you're confident. The resource has a featureCompatibilityVersion field so you can keep FCV pinned while the new binaries run:

spec:
  version: "8.0.12"
  featureCompatibilityVersion: "7.0"

After observing the new version for a while, update featureCompatibilityVersion to "8.0". How to Upgrade MongoDB Versions Safely in Production explains why this two-step approach matters.

Storage expansion depends on your StorageClass allowing volume expansion. StatefulSet volume claim templates can't be edited in place, so growing disks usually means resizing each PVC directly and letting the storage driver expand the volume. Test the procedure for your operator and storage driver before you need it urgently.

Backups and Monitoring

The Community resource doesn't include a managed backup service. Options for MongoDBCommunity deployments include:

  • Scheduled mongodump runs from a Kubernetes CronJob, writing archives to object storage. Simple and effective for smaller datasets.
  • Volume snapshots through the CSI snapshot API, taken from a secondary.
  • Percona Backup for MongoDB, which supports consistent backups and point-in-time recovery to S3-compatible storage.

Here's a minimal CronJob that dumps nightly using the connection string secret of a dedicated backup-user declared in the resource's users list with the backup role:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: shop-mongo-dump
  namespace: mongodb
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: dump
              image: mongo:8.0
              command: ["/bin/sh", "-c"]
              args:
                - mongodump --uri="$MONGODB_URI" --readPreference=secondary --archive=/backup/shop-$(date +%F).archive.gz --gzip
              env:
                - name: MONGODB_URI
                  valueFrom:
                    secretKeyRef:
                      name: shop-mongo-admin-backup-user
                      key: connectionString.standardSrv
              volumeMounts:
                - name: backup
                  mountPath: /backup
          volumes:
            - name: backup
              persistentVolumeClaim:
                claimName: mongo-backups

Ship the archives off the cluster, and test restores regularly. A backup living on a PVC in the same cluster is lost if the cluster is.

For monitoring, the Community resource can expose Prometheus metrics from the agent when you configure the prometheus section of the spec. Alternatively, run the Percona MongoDB exporter as a sidecar or separate deployment. Monitoring MongoDB with Prometheus and Grafana covers which metrics to watch.

When to Use the Enterprise Resources

The MongoDB resource (with MongoDBOpsManager or Cloud Manager) adds capabilities that MongoDBCommunity doesn't have: sharded clusters, managed backups with point-in-time restore through Ops Manager, multi-cluster deployments spanning several Kubernetes clusters, and Enterprise features like LDAP and auditing. It requires an Enterprise Advanced subscription (or Cloud Manager). If you need sharding on Kubernetes with MongoDB's official tooling, this is the path.

It's also worth asking whether you want to run MongoDB on Kubernetes at all. Operators remove a lot of toil, but you're still responsible for storage performance, backups, upgrades, and incident response. For many teams, MongoDB Atlas (reached from Kubernetes workloads over private networking) is less work for the same result.

Common Pitfalls

No anti-affinity. By default, nothing stops the scheduler from placing all members on one node. One node failure takes down the whole replica set. Always set pod anti-affinity.

Memory limits that are too tight. The WiredTiger cache is sized from the limit, but mongod also needs memory for connections, aggregations, and index builds. Leave headroom, or the kernel OOM-kills mongod under load.

Slow storage classes. The default storage class on many clusters is a general-purpose tier. Database volumes usually deserve an SSD class with adequate IOPS.

Editing the StatefulSet directly. The operator owns it and will revert manual changes. Make changes through the custom resource.

Deleting the resource without thinking about PVCs. Depending on your reclaim policy and operator settings, deleting a resource may leave volumes behind (good for safety, bad for cost) or remove them (bad if unintentional). Know which applies before cleaning up.

Skipping major versions. The operator won't protect you from an invalid upgrade path. Go one major version at a time.

Conclusion

MongoDB Controllers for Kubernetes turn a MongoDB replica set into a declarative resource. Install the operator with Helm, create password secrets, and apply a MongoDBCommunity spec with members, version, users, and storage. The operator builds the StatefulSet, services, keyfile, users, and connection string secrets, then handles scaling and rolling upgrades as you edit the spec. Production readiness comes from what you add around it: resource requests and limits, anti-affinity, a PodDisruptionBudget, TLS, monitoring, and tested off-cluster backups.

Your next step: install MCK on a local kind or minikube cluster, apply the replica set manifest from Step 3 with members: 3, then delete the primary pod with kubectl delete pod and watch the replica set elect a new primary and the operator bring the member back.

Tags :
Share :

Related Posts

A Complete Guide to MongoDB Query Operators

A Complete Guide to MongoDB Query Operators

Your first MongoDB queries are usually simple equality filters: find the user with this email, find orders with this status. That covers a surprising

Continue Reading
Async MongoDB in Python with Motor and FastAPI

Async MongoDB in Python with Motor and FastAPI

FastAPI runs your endpoints on an event loop. That's what lets a single worker juggle hundreds of concurrent requests: while one request waits on the

Continue Reading
Atlas Online Archive: Tiering Cold Data to Cut Costs

Atlas Online Archive: Tiering Cold Data to Cut Costs

Look at almost any production database and you'll find the same shape. A small slice of recent data gets nearly all the reads and writes: this week's

Continue Reading