I self-host MongoDB on a $12 DigitalOcean droplet for every product I ship — vCardly, Himaal Pure, Kurthi. Atlas is great for getting started, but once you understand the moving parts, a self-managed MongoDB on a small VPS gives you predictable cost, full control, and is honestly faster for sites with users in one region.
This is the production-grade MongoDB setup I run in 2026: install MongoDB 7 on Ubuntu, lock it down with authentication and a firewall, connect from Node.js with Mongoose, optimize with indexes and lean queries, and automate daily backups.
Prerequisites
- An Ubuntu 22.04 LTS VPS (DigitalOcean, Hetzner, AWS Lightsail — any of them).
- Root or sudo SSH access.
- A domain or subdomain pointed at the server (optional but recommended).
- At least 2 GB RAM — MongoDB will technically run on 1 GB, but the WiredTiger cache starves and your queries get slow.
Step 1 — Install MongoDB 7 from the official repo
Do NOT install MongoDB from Ubuntu's default apt repo — it ships a years-old version. Use the official MongoDB repository:
sudo apt update
sudo apt install -y gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl enable mongod
sudo systemctl start mongod
Verify it's running:
sudo systemctl status mongod
mongosh --eval "db.runCommand({ ping: 1 })"
You should see { ok: 1 }.
Step 2 — Create an admin user
MongoDB has authentication disabled by default. This is the single most common security mistake — exposed MongoDB instances get scanned and ransomed within hours.
Open mongosh and create an admin:
mongosh
use admin
db.createUser({
user: "admin",
pwd: passwordPrompt(),
roles: [{ role: "userAdminAnyDatabase", db: "admin" }]
})
exit
Use a long, random password. Save it to a password manager — you'll need it once.
Step 3 — Enable authentication
Edit /etc/mongod.conf:
sudo nano /etc/mongod.conf
Find the security section (uncomment it) and set:
security:
authorization: enabled
net:
port: 27017
bindIp: 127.0.0.1 # ONLY listen on localhost
That bindIp: 127.0.0.1 is critical. It means MongoDB is only reachable from this same server. Your Node.js app on the same droplet can connect; the internet cannot. This is the single most important line in this whole guide.
Restart:
sudo systemctl restart mongod
Step 4 — Create an app-specific database user
Never use the admin user from your Node.js app. Create a least-privilege user for each database:
mongosh -u admin -p --authenticationDatabase admin
use vcardly_prod
db.createUser({
user: "vcardly_app",
pwd: passwordPrompt(),
roles: [{ role: "readWrite", db: "vcardly_prod" }]
})
exit
Step 5 — Add a firewall (defense in depth)
Even though bindIp blocks external connections, defense in depth is cheap. Enable UFW:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Port 27017 is intentionally NOT opened. MongoDB stays internal.
Step 6 — Connect from Node.js with Mongoose
Install Mongoose:
npm install mongoose dotenv
Store the URI in .env (and add .env to your .gitignore):
MONGODB_URI=mongodb://vcardly_app:YOUR_PASSWORD@127.0.0.1:27017/vcardly_prod?authSource=vcardly_prod
Connect once at startup:
import mongoose from "mongoose";
export async function connectDB() {
try {
await mongoose.connect(process.env.MONGODB_URI!, {
maxPoolSize: 20,
serverSelectionTimeoutMS: 5000,
});
console.log("MongoDB connected");
} catch (err) {
console.error("MongoDB connection failed", err);
process.exit(1);
}
}
Why maxPoolSize: 20? The default is 100. Most small apps don't need 100 concurrent DB connections; you waste RAM. Tune it to your real traffic.
Step 7 — Define schemas with indexes
This is where most apps die in production — slow queries because nobody added indexes. Define them in the schema so they exist on day one:
import { Schema, model } from "mongoose";
const UserSchema = new Schema({
email: { type: String, required: true, unique: true, lowercase: true, index: true },
name: { type: String, required: true },
createdAt: { type: Date, default: Date.now, index: true },
}, { timestamps: true });
UserSchema.index({ email: 1, createdAt: -1 });
export const User = model("User", UserSchema);
Step 8 — Performance: lean, projection, pagination
Three patterns that give you 80% of the win:
- Use
.lean()when you don't need Mongoose methods. Returns plain JS objects, 5–10x faster:const users = await User.find({ active: true }).lean(); - Project only the fields you need. Don't pull the whole document:
const users = await User.find({}, "email name createdAt").lean(); - Paginate everything. Never
.find()without a limit:const page = Number(req.query.page) || 1; const users = await User.find() .sort({ createdAt: -1 }) .skip((page - 1) * 20) .limit(20) .lean();
Step 9 — Automate backups
If you don't back it up, it doesn't exist. Create /usr/local/bin/mongo-backup.sh:
#!/bin/bash
BACKUP_DIR="/var/backups/mongo"
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"
mongodump --uri="$MONGODB_URI" --gzip --archive="$BACKUP_DIR/dump-$DATE.gz"
# Keep last 14 days
find "$BACKUP_DIR" -type f -mtime +14 -delete
Make it executable and add a cron job:
sudo chmod +x /usr/local/bin/mongo-backup.sh
sudo crontab -e
# Add:
0 3 * * * /usr/local/bin/mongo-backup.sh
Backs up every night at 3 a.m. and prunes anything older than 14 days. For real safety, rclone the dumps off-box to S3 or Backblaze B2 — a backup on the same machine as the database is not really a backup.
Frequently asked questions
Should I use MongoDB Atlas or self-host?
Atlas if you want zero ops and don't mind paying. Self-host if you want predictable cost and full control. Either is a fine choice.
Is MongoDB safe to expose publicly?
Only behind a VPN or with TLS plus client certs. For 99% of apps, keep bindIp: 127.0.0.1 and connect from the same machine.
How much RAM does MongoDB need?
2 GB minimum for serious work. WiredTiger uses ~50% of free RAM for its cache by default; if you have less, queries slow down dramatically.
SQL vs MongoDB — which should I pick?
If your data has clear relations and transactions matter (a ledger, an order system), PostgreSQL. If you're modeling documents and want flexibility (user profiles, product catalogs, content), MongoDB. Use the right tool — I use both on different products.
Conclusion
That's the production-grade MongoDB setup I use on every droplet. The whole thing takes about 30 minutes the first time and 10 minutes once you know it. Combined with my Node.js REST API guide and DigitalOcean deploy guide, you have the full stack.
Need help setting this up for your project? Get in touch.