Skip to main content
Back to Blog

How to Set Up MongoDB on Ubuntu VPS: Production Guide for 2026

May 10, 2026
Safal Bhattarai
12 min read
MongoDBMongooseNode.jsDatabaseUbuntuDevOpsVPSTutorial

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.

Safal Bhattarai

Safal Bhattarai is a Full Stack Developer · Tech Lead · Product Builder based in Pokhara, Nepal, shipping production web apps with Next.js, Node.js, MongoDB, PostgreSQL, and Redis.