
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:
| Resource | For | Requires |
|---|---|---|
MongoDBCommunity | MongoDB Community replica sets | Nothing extra |
MongoDB | Enterprise replica sets and sharded clusters | MongoDB Enterprise, plus Ops Manager or Cloud Manager |
MongoDBOpsManager | Running Ops Manager itself in Kubernetes | Enterprise 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).
kubectlandhelminstalled.- 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:
membersandversionare the core of the declaration. Change them later and the operator reconciles: adding members, or performing a rolling upgrade.security.authentication.modesenables SCRAM authentication. The operator generates the internal keyfile that members use to authenticate to each other.usersdeclares database users. The operator creates them and keeps them in sync.scramCredentialsSecretNamenames a secret where the operator stores the derived SCRAM credentials.additionalMongodConfigpasses settings intomongod.confusing dotted paths.statefulSet.specis merged into the StatefulSet the operator generates. Here it overrides the default volume claims (nameddata-volumeandlogs-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
mongodumpruns from a KubernetesCronJob, 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.


