New to KubeDB? Please start here.
MongoDB Database Migration
This guide will show you how to use KubeDB Migration to migrate an existing MongoDB database — such as one running on DigitalOcean Managed MongoDB or any external instance — entirely into a KubeDB-managed MongoDB with minimal downtime.
Before You Begin
At first, you need to have a Kubernetes cluster, and the
kubectlcommand-line tool must be configured to communicate with your cluster.Install
KubeDBoperator with the kubedb-courier operator enabled in your cluster following the steps here.The source
MongoDBinstance must be network-reachable from within your Kubernetes cluster.The source
MongoDBinstance must be part of a replica set with the oplog enabled. The database user provided for migration must have appropriate read privileges on all databases.You should be familiar with the following
KubeDBconcepts:
To keep everything isolated, we are going to use a separate namespace called demo throughout this tutorial.
$ kubectl create ns demo
namespace/demo created
Prepare Source Database
We will use a DigitalOcean Managed MongoDB cluster as the source. Connect to it to verify the prerequisites, set up the migration user, and insert test data.
Configuring your source instance.
Self-hosted MongoDB
A standalone mongod must first be converted to a single-node replica set. Once running as a replica set, the oplog is enabled automatically. Then create the migration user:
use admin
db.createUser({
user: "migrator",
pwd: "yourStrongPassword",
roles: [
{ role: "readAnyDatabase", db: "admin" },
{ role: "clusterMonitor", db: "admin" }
]
})
MongoDB Atlas
Atlas clusters run as replica sets by default — no extra configuration needed. Create a database user with Read Any Database and Cluster Monitor built-in roles in Database Access settings.
DigitalOcean Managed MongoDB
DigitalOcean managed MongoDB clusters run as replica sets by default. Create a database user with readAnyDatabase and clusterMonitor roles under the Users & Databases section of your cluster dashboard.
See the official MongoDB Replica Set docs for more details.
Verify prerequisites
Connect to the source instance and verify that the oplog is available:
$ mongosh "mongodb+srv://<digitalocean-host>.mongo.ondigitalocean.com" -u admin -p
use local
switched to db local
show collections
clustermanager
oplog.rs
replset.election
replset.initialSyncId
replset.minvalid
replset.oplogTruncateAfterPoint
startup_log
system.replset
system.rollback.id
system.tenantMigration.oplogView
system.views
db.oplog.rs.findOne()
{
op: 'n',
ns: '',
o: { msg: 'initiating set' },
ts: Timestamp({ t: 1782795685, i: 1 }),
v: Long('2'),
wall: ISODate('2026-06-30T05:01:25.530Z')
}
The oplog.rs collection must exist and contain entries — this confirms the source is running as a replica set with the oplog enabled.
Create a dedicated migration user
Create a dedicated user with the minimum required privileges:
use admin
db.createUser({
user: "migrator",
pwd: "<password>",
roles: [
{ role: "readAnyDatabase", db: "admin" },
{ role: "clusterMonitor", db: "admin" }
]
})
The migrator user is referenced in the Kubernetes secret and AppBinding for the rest of this guide.
Create collection and seed data
use shop
db.orders.insertMany([
{ customer_name: 'Alice', product: 'Laptop', quantity: 1, status: 'shipped', created_at: new Date('2026-06-29T08:00:00Z') },
{ customer_name: 'Bob', product: 'Headphones', quantity: 2, status: 'pending', created_at: new Date('2026-06-29T08:00:01Z') },
{ customer_name: 'Carol', product: 'Keyboard', quantity: 3, status: 'delivered', created_at: new Date('2026-06-29T08:00:02Z') }
])
db.orders.find().pretty()
[
{
_id: ObjectId("..."),
customer_name: 'Alice',
product: 'Laptop',
quantity: 1,
status: 'shipped',
created_at: ISODate('2026-06-29T08:00:00.000Z')
},
{
_id: ObjectId("..."),
customer_name: 'Bob',
product: 'Headphones',
quantity: 2,
status: 'pending',
created_at: ISODate('2026-06-29T08:00:01.000Z')
},
{
_id: ObjectId("..."),
customer_name: 'Carol',
product: 'Keyboard',
quantity: 3,
status: 'delivered',
created_at: ISODate('2026-06-29T08:00:02.000Z')
}
]
Prepare Source Connection Information
First, create an authentication secret using the migrator user credentials:
$ kubectl create secret generic source-mongodb-auth -n demo \
--type=kubernetes.io/basic-auth \
--from-literal=username=migrator \
--from-literal=password=<password>
If your database has TLS enabled, create a secret with the CA certificate:
kubectl create secret generic ca-secret \
--from-file=ca.crt=$CERT_PATH/ca.crt \
--namespace=demo
Note: For mTLS, also include the client certificate and key by appending
--from-file=tls.crt=$CERT_PATH/tls.crt--from-file=tls.key=$CERT_PATH/tls.key
to the command above.
Now create an AppBinding with the necessary information. The kubedb-courier operator reads the source MongoDB connection information from this AppBinding CR. Use the following YAML to create your AppBinding:
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
metadata:
name: mgo-source
namespace: demo
spec:
type: mongodb
version: "4.4.26"
clientConfig:
url: "mongodb+srv://<digitalocean-host>.mongo.ondigitalocean.com"
secret:
name: source-mongodb-auth
tlsSecret: # omit if TLS is disabled
name: ca-secret
Here,
spec.clientConfig.urlis the connection URL of the source MongoDB instance.spec.secret.nameis the reference to the secret we created earlier, containing the MongoDB authentication information.
For a
KubeDB-managed database, anAppBindingis created by default. So there is no need to create one for the target database.
Create Target MongoDB Database
KubeDB implements a MongoDB CRD to define the specification of a MongoDB database. Use the following MongoDB object to create the target database.
apiVersion: kubedb.com/v1
kind: MongoDB
metadata:
name: mgo-destination
namespace: demo
spec:
version: "4.4.26"
replicaSet:
name: "rs1"
replicas: 2
storageType: Durable
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 15Gi
serviceTemplates:
- alias: primary
metadata:
annotations:
passMe: ToService
spec:
type: NodePort
ports:
- name: http
port: 27017
$ kubectl create -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mongodb/migration/mgo-destination.yaml
mongodb.kubedb.com/mgo-destination created
Note: Adjust the
resources.requests.storagebased on the source database size.
Wait until mgo-destination has status Ready.
Apply Migration CR
To migrate the database we have to create a Migration CR. KubeDB uses mongoshake to perform the migration. Below is the YAML of the Migration CR that we are going to create:
apiVersion: courier.kubedb.com/v1alpha1
kind: Migration
metadata:
name: mongodb-migrate
namespace: demo
spec:
source:
mongodb:
connectionInfo:
appBinding:
name: mgo-source
namespace: demo
mongoshake:
syncMode: all
extraConfiguration:
full_sync.executor.insert_on_dup_update: "true"
target:
mongodb:
connectionInfo:
appBinding:
name: mgo-destination
namespace: demo
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mongodb/migration/mongodb-migrate.yaml
migration.courier.kubedb.com/mongodb-migrate created
Here,
spec.source.mongodb / spec.target.mongodb — connectionInfo:
appBinding.name/appBinding.namespace— references theAppBindingfor the source or target MongoDB instance.
spec.source.mongodb.mongoshake — migration configuration:
syncMode: all— performs a full data sync (snapshot + incremental oplog replay).extraConfiguration— additionalmongoshakeconfiguration parameters. For example:full_sync.executor.insert_on_dup_update: "true"— uses upsert instead of insert during full sync to handle duplicate key errors gracefully.
For a full description of every field, see the Migration CRD reference.
Watch Migration Progress
Let’s wait for the Migration to finish the full sync and enter the incremental sync. Run the following command to watch Migration CR:
Every 2.0s: kubectl get migration -n demo
During the full stage, you’ll see progress advancing to 100%:
NAME PHASE DBTYPE STAGE LAG PROGRESS AGE
mongodb-migrate Running mongodb full 100.00% 22s
When the LAG drops to near zero, both databases are fully in sync:
NAME PHASE DBTYPE STAGE LAG PROGRESS AGE
mongodb-migrate Running mongodb incr 0 17h
View detailed progress via pod logs
You can also see collection-wise progress, detailed checkpoints, and sync metrics by checking the migration pod logs:
$ kubectl logs -n demo migration-<migration-pod-name>
Example output during the full sync stage — showing per-collection progress, total/finished/processing/waiting collections:
2026-06-30T12:14:30.632Z INFO mongodb server/utils.go:51 Transferring initial data (Full Sync) {"Stage": "full", "Progress": "100.00%", "TotalCollections": 1, "FinishedCollections": 1, "ProcessingCollections": 0, "WaitingCollections": 0, "CollectionMetric": {"shop.orders":"100.00% (3/3)"}}
Example output during incremental sync — showing LAG, checkpoint timestamps, and LSN details:
2026-06-30T12:58:30.631Z INFO mongodb server/utils.go:51 Incremental replication running {"Stage": "incr", "Lag": 187, "LSNTime": "2026-06-30 12:58:25", "LSNAckTime": "2026-06-30 12:58:25", "LSNCheckpoint": "2026-06-30 12:55:18"}
Verify initial snapshot on target
Once the migration reaches the incr stage (continuous oplog tailing), exec into the KubeDB target pod and confirm all seed documents were copied over:
$ kubectl exec -it -n demo mgo-destination-0 -- mongosh -u root -p<root-password>
use shop
db.orders.find().pretty()
[
{
_id: ObjectId("..."),
customer_name: 'Alice',
product: 'Laptop',
quantity: 1,
status: 'shipped',
created_at: ISODate('2026-06-29T08:00:00.000Z')
},
{
_id: ObjectId("..."),
customer_name: 'Bob',
product: 'Headphones',
quantity: 2,
status: 'pending',
created_at: ISODate('2026-06-29T08:00:01.000Z')
},
{
_id: ObjectId("..."),
customer_name: 'Carol',
product: 'Keyboard',
quantity: 3,
status: 'delivered',
created_at: ISODate('2026-06-29T08:00:02.000Z')
}
]
Test live CDC streaming
With the migration still running, connect to the source DigitalOcean instance and run some DML:
$ mongosh "mongodb+srv://<digitalocean-host>.mongo.ondigitalocean.com" -u migrator -p
use shop
// Insert a new order
db.orders.insertOne({
customer_name: 'Dave', product: 'Mouse', quantity: 1, status: 'pending', created_at: new Date()
})
// Mark Bob's headphones as delivered
db.orders.updateOne(
{ customer_name: 'Bob' },
{ $set: { status: 'delivered' } }
)
// Remove the already-shipped laptop order
db.orders.deleteOne({ customer_name: 'Alice' })
Wait a few seconds for the oplog events to propagate, then re-query the target:
use shop
db.orders.find().pretty()
[
{
_id: ObjectId("..."),
customer_name: 'Bob',
product: 'Headphones',
quantity: 2,
status: 'delivered',
created_at: ISODate('2026-06-29T08:00:01.000Z')
},
{
_id: ObjectId("..."),
customer_name: 'Carol',
product: 'Keyboard',
quantity: 3,
status: 'delivered',
created_at: ISODate('2026-06-29T08:00:02.000Z')
},
{
_id: ObjectId("..."),
customer_name: 'Dave',
product: 'Mouse',
quantity: 1,
status: 'pending',
created_at: ISODate('2026-06-29T08:10:00.000Z')
}
]
The INSERT, UPDATE, and DELETE are all reflected on the target — CDC streaming is working correctly.
Cutover
Once the LAG drops to near zero, stop all writes to the source database. Wait until the LAG reaches exactly zero — at that point both databases are fully in sync.
Now delete the Migration CR to stop the migration process:
$ kubectl delete migration -n demo mongodb-migrate
migration.courier.kubedb.com "mongodb-migrate" deleted
Finally, update your application’s connection string to point to the target KubeDB-managed MongoDB database. The migration is complete.































