Let me tell you a story about taming the beast that is MongoDB sharding. After spending countless hours deploying MongoDB clusters in production, I’ve learned that while the official docs are great, they often skip the nitty-gritty details that can make or break your setup. This guide is everything I wish I had when I first started working with MongoDB sharded clusters.
Why Sharding? Why Docker? Link to heading
Before we dive in, let’s talk about why you might need this setup. If you’re reading this, you’ve probably hit one of these scenarios:
- Your data doesn’t fit on a single server anymore
- Your application is crying for better read/write performance
- You need geographic distribution of your data
- You’re tired of late-night calls about database performance
Docker makes this whole process much more manageable. Trust me, I’ve done it both ways, and containerization saves you from a world of pain when it comes to dependency management and deployment consistency.
What We’re Building Link to heading
We’ll set up a production-grade MongoDB sharded cluster that includes:
- 2 mongos routers (because one is never enough in production)
- 3 config servers in a replica set (the brain of our operation)
- 3 shards, each with its own replica set (where the actual data lives)
I’ve picked this setup based on real-world experience - it’s the sweet spot between reliability and manageability for most applications.
The Setup Process Link to heading
Step 1: The Authentication Keyfile Link to heading
First things first - security. Here’s something that bit me once: MongoDB needs a keyfile for internal authentication, and it’s super picky about permissions.
# Generate a proper keyfile
openssl rand -base64 756 > mongo-keyfile
chmod 400 mongo-keyfile
chown 999:999 mongo-keyfile # MongoDB runs as user 999 in the container
Pro tip: Keep this file safe and secure - it’s like the master key to your MongoDB kingdom.
Step 2: Config Server Setup Link to heading
Let’s start with the config servers. Here’s the real deal - I’ve tweaked these settings after many late-night debugging sessions:
# docker-compose.yml for config servers
services:
config-server-1:
container_name: config-server-1
image: mongo:7.0
command: >
mongod --configsvr
--replSet configReplSet
--port 27019
--dbpath /data/db
--bind_ip 0.0.0.0
--wiredTigerCacheSizeGB 16
--oplogSize 8192
--syncdelay 30
--auth
--keyFile /data/mongo-keyfile
ports:
- "27019:27019"
volumes:
- ./data/config1:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 32G
cpus: '16.0'
reservations:
memory: 16G
cpus: '8.0'
restart: always
config-server-2:
container_name: config-server-2
image: mongo:7.0
command: >
mongod --configsvr
--replSet configReplSet
--port 27020
--dbpath /data/db
--bind_ip 0.0.0.0
--wiredTigerCacheSizeGB 16
--oplogSize 8192
--syncdelay 30
--auth
--keyFile /data/mongo-keyfile
ports:
- "27020:27020"
volumes:
- ./data/config2:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 32G
cpus: '16.0'
reservations:
memory: 16G
cpus: '8.0'
restart: always
config-server-3:
container_name: config-server-3
image: mongo:7.0
command: >
mongod --configsvr
--replSet configReplSet
--port 27021
--dbpath /data/db
--bind_ip 0.0.0.0
--wiredTigerCacheSizeGB 16
--oplogSize 8192
--syncdelay 30
--auth
--keyFile /data/mongo-keyfile
ports:
- "27021:27021"
volumes:
- ./data/config3:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 32G
cpus: '16.0'
reservations:
memory: 16G
cpus: '8.0'
restart: always
After running docker compose docker-compose.yml up -d
, you should see your config servers up and running:
root@Ubuntu-2404-noble-arm64-base /mongodb # ls
data docker-compose.yml mongo-keyfile
root@Ubuntu-2404-noble-arm64-base /mongodb # docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
631e097e5963 mongo:7.0 "docker-entrypoint.s…" 7 weeks ago Up 2 weeks 27017/tcp, 0.0.0.0:27019->27019/tcp, :::27019->27019/tcp config-server-1
fe20605feb3e mongo:7.0 "docker-entrypoint.s…" 7 weeks ago Up 2 weeks 27017/tcp, 0.0.0.0:27021->27021/tcp, :::27021->27021/tcp config-server-3
cd761cbe06c0 mongo:7.0 "docker-entrypoint.s…" 7 weeks ago Up 2 weeks 27017/tcp, 0.0.0.0:27020->27020/tcp, :::27020->27020/tcp config-server-2
A word about those resource limits - they might look high, but config servers need enough juice to keep your metadata snappy. I’ve seen clusters struggle with lower limits.
After starting your config servers, you’ll need to initialize the replica set. Here’s the command that won’t let you down:
rs.initiate({
_id: "configReplSet",
configsvr: true,
members: [
{ _id: 0, host: "config-server-1-ip:27019" },
{ _id: 1, host: "config-server-2-ip:27020" },
{ _id: 2, host: "config-server-3-ip:27021" }
]
})
Step 3: Mongos Router Setup Link to heading
Now for the mongos routers - your application’s gateway to the sharded cluster. Here’s a battle-tested configuration:
# docker-compose.yml for mongos routers
services:
mongos1:
container_name: mongos1
image: mongo:7.0
command: >
mongos
--configdb configReplSet/config-server-1-ip:27019,config-server-2-ip:27020,config-server-3-ip:27021
--bind_ip 0.0.0.0
--port 27017
--keyFile /data/mongo-keyfile
--maxConns 50000
ports:
- "27017:27017"
volumes:
- ./data/mongos1:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 48G
cpus: '32.0'
reservations:
memory: 32G
cpus: '16.0'
restart: always
mongos2:
container_name: mongos2
image: mongo:7.0
command: >
mongos
--configdb configReplSet/config-server-1-ip:27019,config-server-2-ip:27020,config-server-3-ip:27021
--bind_ip 0.0.0.0
--port 27018
--keyFile /data/mongo-keyfile
--maxConns 50000
ports:
- "27018:27018"
volumes:
- ./data/mongos2:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 48G
cpus: '32.0'
reservations:
memory: 32G
cpus: '16.0'
restart: always
Pro tip: While mongos routers don’t need to know about each other, your application should be aware of all available routers for proper load balancing. I typically use a load balancer like HAProxy or an internal DNS record to distribute traffic across the mongos instances.
After running docker compose docker-compose.yml up -d
, you should see your mongos routers up and running:
It’s better to have separate servers for mongos routers to avoid a single point of failure.
root@Ubuntu-2404-noble-arm64-base /mongodb # ls
backup data docker-compose.yml mongo-keyfile
root@Ubuntu-2404-noble-arm64-base /mongodb # docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2574754babee mongo:7.0 "docker-entrypoint.s…" 7 weeks ago Up 2 weeks 0.0.0.0:27017->27017/tcp, :::27017->27017/tcp mongos1
ebc19417465c mongo:7.0 "docker-entrypoint.s…" 7 weeks ago Up 2 weeks 27017/tcp, 0.0.0.0:27018->27018/tcp, :::27018->27018/tcp mongos2
That maxConns setting? Yeah, it’s high, but you’ll thank me when your application scales up.
Step 4: Shard Setup Link to heading
Each shard needs its own replica set. Here’s a configuration I’ve refined over multiple deployments:
services:
shard1:
container_name: shard1
image: mongo:7.0
command: >
mongod
--shardsvr
--replSet shard1RepSet
--port 27018
--dbpath /data/db
--bind_ip 0.0.0.0
--wiredTigerCacheSizeGB 32
--oplogSize 8192
--syncdelay 30
--auth
--keyFile /data/mongo-keyfile
ports:
- "27018:27018"
volumes:
- ./data/shard1:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 96G
cpus: '64.0'
reservations:
memory: 64G
cpus: '32.0'
restart: always
Initialize each shard’s replica set like this:
rs.initiate({
_id: "shard1RepSet",
members: [
{ _id: 0, host: "shard1-server-ip:27018" },
{ _id: 1, host: "shard1-secondary1-ip:27018" },
{ _id: 2, host: "shard1-secondary2-ip:27018" }
]
})
Step 5: Putting It All Together Link to heading
Now comes the fun part - adding shards to your cluster. Connect to mongos and run:
sh.addShard("shard1RepSet/shard1-server-ip:27018")
sh.addShard("shard2RepSet/shard2-server-ip:27018")
sh.addShard("shard3RepSet/shard3-server-ip:27018")
Here’s a gotcha that cost me hours of debugging: always verify your shard was added successfully with sh.status()
. Sometimes network hiccups can cause silent failures.
Real-World Tips and Tricks Link to heading
Choosing Shard Keys Link to heading
This is where many people stumble. Your shard key choice can make or break your cluster’s performance. Here’s what I’ve learned:
-
Hashed shard keys are great for:
- Even data distribution
- Write-heavy workloads
- When you don’t need range-based queries
-
Ranged shard keys work better for:
- Read-heavy workloads with range queries
- Time-series data
- Geospatial data
Monitoring That Actually Matters Link to heading
Forget about watching every metric - here’s what you really need to keep an eye on:
- Chunk distribution imbalance
- Write performance on primaries
- Replication lag on secondaries
- Connection counts on mongos routers
I use this quick check command regularly:
// Quick health check
db.printShardingStatus({ verbose: false })
When Things Go Wrong Link to heading
Because they will. Here’s your survival guide:
-
Jumbo Chunks: If you see these, your shard key might need rethinking. I’ve had to reshard entire collections because of this.
-
Split Storms: Watch out for these during heavy write loads. Adjust your chunk size if needed:
use config
db.settings.updateOne(
{ _id: "chunksize" },
{ $set: { value: 64 } } // Default is 64MB
)
- Config Server Issues: These are rare but serious. Always keep backups of your config database:
mongodump --db config --out /backup/config
Maintenance Without Tears Link to heading
Here’s my monthly maintenance checklist:
- Check for balanced chunks:
db.chunks.aggregate([
{ $group: { _id: "$shard", count: { $sum: 1 } } }
])
- Look for slow queries:
db.currentOp(
{ "secs_running": { $gt: 10 },
"op": { $ne: "command" } }
)
- Verify replica set health:
rs.status()
Setting Up MongoDB Authentication Link to heading
1. Create the Admin User Link to heading
First, connect to mongos without authentication (this only works before setting up auth):
docker exec -it mongos1 mongosh
Create the admin user:
use admin
db.createUser({
user: "root",
pwd: "your_secure_password", // Replace with a strong password
roles: [
{ role: "root", db: "admin" },
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "clusterAdmin", db: "admin" }
]
})
2. Connection String Format Link to heading
For applications to connect:
mongodb://root:password@mongos-router1-ip:27017,mongos-router2-ip:27017/?authSource=admin&retryWrites=true&w=1&readPreference=nearest&readConcernLevel=local
3. Update Mongos Configuration Link to heading
Update your mongos docker-compose.yml to include authentication:
services:
mongos1:
container_name: mongos1
image: mongo:7.0
command: >
mongos
--configdb configReplSet/config-server-1-ip:27019,config-server-2-ip:27020,config-server-3-ip:27021
--bind_ip 0.0.0.0
--port 27017
--keyFile /data/mongo-keyfile
--auth # Enable authentication
--maxConns 50000
ports:
- "27017:27017"
volumes:
- ./data/mongos1:/data/db
- ./mongo-keyfile:/data/mongo-keyfile:ro
user: "999:999"
ulimits:
nofile:
soft: 100000
hard: 100000
deploy:
resources:
limits:
memory: 48G
cpus: '32.0'
reservations:
memory: 32G
cpus: '16.0'
restart: always
4. Test Authentication Link to heading
Test the connection with the new user:
mongosh "mongodb://root:password@mongos-router1-ip:27017/admin?retryWrites=true&w=1"
5. Connection String Parameters Explained Link to heading
mongodb://
: Protocol identifierconfigAdmin:password
: Username and passwordmongos-router1-ip:27017,mongos-router2-ip:27017
: Multiple mongos routers for high availability/?
: Start of optionsauthSource=admin
: Database containing the user credentialsretryWrites=true
: Automatically retry write operationsw=1
: Write concern (1 = primary acknowledgment)readPreference=nearest
: Read from nearest node for better performancereadConcernLevel=local
: Consistency level for reads
6. Additional Security Parameters Link to heading
You can add these parameters to your connection string for enhanced security:
mongodb://root:password@mongos-router1-ip:27017,mongos-router2-ip:27017/?authSource=admin&retryWrites=true&w=1&readPreference=nearest&readConcernLevel=local&ssl=false&connectTimeoutMS=5000&maxPoolSize=50
Parameters explained:
ssl=true
: Enable SSL/TLS connectionconnectTimeoutMS=5000
: Connection timeout in millisecondsmaxPoolSize=50
: Maximum connection pool size
7. Environment Variables Example Link to heading
Store connection string in environment variable (recommended):
# .env file
MONGODB_URI="mongodb://root:password@mongos-router1-ip:27017,mongos-router2-ip:27017/?authSource=admin&retryWrites=true&w=1"
9. Node.js Connection Example Link to heading
const { MongoClient } = require('mongodb');
const uri = process.env.MONGODB_URI;
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 50,
connectTimeoutMS: 5000,
serverSelectionTimeoutMS: 5000
});
async function connect() {
try {
await client.connect();
console.log('Connected to MongoDB');
} catch (err) {
console.error('Connection error:', err);
}
}
connect();
Useful Resources Link to heading
- MongoDB University - Worth every minute
- MongoDB Jira - For when you hit those weird edge cases
- MongoDB Community Forums - Saved my bacon more than once