Installing MongoDB on Ubuntu 20.04, 22.04 and 24.04, and actually understanding WiredTiger
- ubuntu
- mongodb
- wiredtiger
- databases
- linux
MongoDB is not in Ubuntu’s repositories in any form you should use. On 20.04 there is an ancient
mongodb package from the 3.6 era; from 22.04 it was dropped entirely after MongoDB changed to the
SSPL licence. Either way, you add the official repository.
Picking a version
MongoDB supports a specific set of Ubuntu releases per version, and it moves. At the time of writing:
| Ubuntu release | Codename | Use |
|---|---|---|
| 20.04 LTS | focal |
MongoDB 7.0 |
| 22.04 LTS | jammy |
MongoDB 8.0 (7.0 also supported) |
| 24.04 LTS | noble |
MongoDB 8.0 |
MongoDB 8.0 dropped 20.04. If you are on focal and want 8.0, the upgrade you need is the operating system, not the database. Before you commit, check the current platform support matrix — this table ages faster than the rest of this guide.
The commands below use 8.0 and noble. Substitute your codename and version in both the key URL
and the repository line, and keep the two consistent.
Installing
1. Prerequisites
sudo apt update
sudo apt install -y gnupg curl
2. Import the signing key
curl -fsSL https://pgp.mongodb.com/server-8.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
3. Add the repository
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
For the other releases, swap the codename:
# 22.04
... https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse
# 20.04 (7.0 — focal is not supported by 8.0)
... https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/7.0 multiverse
4. Install
sudo apt update
sudo apt install -y mongodb-org
This pulls in the server, mongosh, and the database tools. Then:
sudo systemctl start mongod
sudo systemctl enable mongod
sudo systemctl status mongod
Check it is alive:
mongosh --eval 'db.runCommand({ ping: 1 })'
5. Pin the version
MongoDB does not support skipping major versions, and an unattended apt upgrade that moves you
from 8.0 to 9.0 is a genuinely bad day. Hold the packages:
sudo apt-mark hold mongodb-org mongodb-org-database mongodb-org-server \
mongodb-mongosh mongodb-org-mongos mongodb-org-tools
Where things live once installed:
| Path | What |
|---|---|
/etc/mongod.conf |
Configuration |
/var/lib/mongodb |
Data files |
/var/log/mongodb/mongod.log |
Logs |
mongodb |
The system user it runs as |
WiredTiger: what it is and why you don’t choose it
WiredTiger became the default storage engine in MongoDB 3.2, and since 4.2 it is the only one — MMAPv1 was removed outright. So the honest framing is not “why pick WiredTiger” but “what you got, and how to tune it”.
It is still worth understanding what changed, because the differences explain most of MongoDB’s behaviour under load.
Document-level concurrency
MMAPv1 locked an entire collection for a write. One slow update blocked every other write to that collection, and throughput collapsed under concurrent load.
WiredTiger locks at the document level. Two writes to different documents in the same collection proceed in parallel. On a write-heavy workload this is not a small improvement — it is the difference between a database that scales with your cores and one that does not.
Where two writers do collide on the same document, WiredTiger detects the conflict and retries
internally. You may see this in the logs as WriteConflict, which is normal at low rates and a
sign of a hot document at high ones.
MVCC and snapshots
Every operation sees a consistent point-in-time snapshot of the data. Readers never block writers and writers never block readers — a reader continues against its snapshot while a writer moves on. This is what makes multi-document transactions possible at all.
Compression, on by default
Data is compressed on disk out of the box:
- Collections — snappy by default. Typically 3–5× smaller than raw BSON.
- Indexes — prefix compression, which is very effective on the sorted, repetitive keys indexes are made of.
- Journal — snappy.
The compressed form is what sits in the filesystem cache; the uncompressed form is what sits in WiredTiger’s own cache. That two-tier arrangement matters for the sizing decision below.
Checkpoints and the journal
WiredTiger writes a checkpoint — a consistent on-disk image — every 60 seconds. Between checkpoints, durability comes from the journal, a write-ahead log flushed every 100 ms or on demand.
Lose power between checkpoints and MongoDB replays the journal on restart, recovering to the last journalled write. Journalling cannot be disabled in current versions, and that is the right default — the option existed, people turned it off for benchmarks, and then ran production that way.
Configuration
The stock /etc/mongod.conf is deliberately minimal. Here is an annotated version covering what
actually matters:
storage:
dbPath: /var/lib/mongodb
wiredTiger:
engineConfig:
# See the sizing section below. Leave unset to accept the default.
cacheSizeGB: 2
journalCompressor: snappy
# Put indexes in their own directory — useful when you want them
# on a separate, faster volume. Must be set before first start.
directoryForIndexes: false
collectionConfig:
# snappy (default) | zstd | zlib | none
blockCompressor: snappy
indexConfig:
prefixCompression: true
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
net:
port: 27017
# NEVER 0.0.0.0 on a public box. See the security section.
bindIp: 127.0.0.1
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
authorization: enabled
Apply changes with sudo systemctl restart mongod.
Sizing the cache
By default WiredTiger takes 50% of (RAM − 1 GB), or 256 MB, whichever is larger. On a 4 GB machine that is roughly 1.5 GB.
The default is reasonable and most people should leave it alone. Raise or lower it when:
- Other services share the box. A 4 GB VPS also running an app server and nginx should get an
explicit, smaller
cacheSizeGB— otherwise MongoDB and your app fight over RAM and the OOM killer settles it. - The machine is dedicated to MongoDB. The default is already close to right. Do not push it to 80% of RAM: the filesystem cache holds the compressed pages, and starving it means every miss becomes a disk read.
The metric to watch, rather than guessing:
db.serverStatus().wiredTiger.cache
Two numbers matter. "bytes currently in the cache" against "maximum bytes configured" tells you
how full it is. "pages read into cache" climbing steadily means your working set does not fit and
you are going to disk — that is when more cache, or more RAM, actually helps.
Choosing a compressor
| Compressor | Ratio | CPU | Use when |
|---|---|---|---|
snappy |
Good | Low | Default. Correct for almost everyone |
zstd |
Better | Moderate | Storage-bound, CPU to spare |
zlib |
Similar to zstd | High | Legacy. Prefer zstd |
none |
— | None | Data already compressed, e.g. stored media |
zstd (MongoDB 4.2+) is the one worth considering: noticeably smaller than snappy for a modest CPU
cost. On a storage-constrained VPS it can pay for itself.
The compressor applies at collection creation. Changing it in the config affects only new collections — existing ones keep whatever they were made with. To convert, dump and restore.
Securing the installation
A MongoDB reachable from the internet without authentication gets found and wiped, usually within hours. This has been an automated attack for years. Two settings prevent it.
1. Create an admin user
With security.authorization still disabled, connect and create the user:
mongosh
use admin
db.createUser({
user: "admin",
pwd: passwordPrompt(),
roles: [
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" }
]
})
passwordPrompt() keeps the password out of your shell history. Use it.
2. Turn authorization on
Set security.authorization: enabled in /etc/mongod.conf, then:
sudo systemctl restart mongod
mongosh -u admin -p --authenticationDatabase admin
3. One user per application
Give each app the narrowest role that works — readWrite on its own database, nothing else:
use myapp
db.createUser({
user: "myapp",
pwd: passwordPrompt(),
roles: [{ role: "readWrite", db: "myapp" }]
})
Connection string:
mongodb://myapp:password@127.0.0.1:27017/myapp?authSource=myapp
4. Keep it off the network
bindIp: 127.0.0.1 is the default and should stay that way unless something genuinely remote needs
access. If it does, bind to the private interface and firewall the port:
sudo ufw allow from 10.0.0.0/8 to any port 27017
Never sudo ufw allow 27017. A database on a public port is a matter of time.
OS tuning that matters
MongoDB logs warnings about these at startup, and they are worth acting on.
Transparent Huge Pages
THP hurts database workloads: MongoDB allocates in small chunks, and THP’s 2 MB pages cause allocation stalls and wasted memory. Disable it with a systemd unit so it survives reboots:
sudo tee /etc/systemd/system/disable-thp.service > /dev/null <<'EOF'
[Unit]
Description=Disable Transparent Huge Pages
Before=mongod.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled"
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/defrag"
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now disable-thp
sudo systemctl restart mongod
Verify:
cat /sys/kernel/mm/transparent_hugepage/enabled # [never] expected
File descriptor limits
The default 1024 open files is far too low for a busy database:
sudo mkdir -p /etc/systemd/system/mongod.service.d
sudo tee /etc/systemd/system/mongod.service.d/limits.conf > /dev/null <<'EOF'
[Service]
LimitNOFILE=64000
LimitNPROC=64000
EOF
sudo systemctl daemon-reload
sudo systemctl restart mongod
Filesystem
MongoDB recommends XFS over ext4 for WiredTiger — better allocation behaviour under the write pattern WiredTiger produces. ext4 works and is what you will have by default; if you are provisioning a dedicated data volume, make it XFS.
Mount the data volume with noatime either way, so reads stop generating metadata writes:
/dev/sdb1 /var/lib/mongodb xfs defaults,noatime 0 2
Enabling transactions and change streams
Multi-document transactions and change streams both require a replica set. A single-node replica set gives you them on one machine, and is the right development setup.
With authentication enabled, a replica set also needs a keyfile for internal member authentication — even with one member:
openssl rand -base64 756 | sudo tee /etc/mongodb-keyfile > /dev/null
sudo chown mongodb:mongodb /etc/mongodb-keyfile
sudo chmod 400 /etc/mongodb-keyfile
Add to /etc/mongod.conf:
replication:
replSetName: rs0
security:
authorization: enabled
keyFile: /etc/mongodb-keyfile
Restart and initiate:
sudo systemctl restart mongod
mongosh -u admin -p --authenticationDatabase admin
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "127.0.0.1:27017" }]
})
rs.status()
The prompt changes to rs0 [direct: primary] once it is up. Add ?replicaSet=rs0 to your
connection string.
One caveat: a single-node replica set gives you the features, not the redundancy. There is no failover with one member — it is a development convenience, not high availability.
Backups
The straightforward route:
# Dump
mongodump --uri="mongodb://admin:password@127.0.0.1:27017/?authSource=admin" \
--out=/var/backups/mongo/$(date +%F)
# Restore
mongorestore --uri="mongodb://admin:password@127.0.0.1:27017/?authSource=admin" \
/var/backups/mongo/2026-08-02
mongodump is fine up to tens of gigabytes. Past that it gets slow and puts real load on the
server — move to filesystem snapshots (LVM, or your provider’s volume snapshots), which for
WiredTiger must capture the data directory and the journal atomically to be consistent.
Whichever you pick: test the restore. An untested backup is a hypothesis.
Verifying and inspecting
// Which engine, and its configuration
db.serverStatus().storageEngine
db.serverStatus().wiredTiger.cache
// Per-collection: compression, size on disk, index sizes
db.mycollection.stats()
// Confirm the compressor a collection was created with
db.mycollection.stats().wiredTiger.creationString
Useful shell checks:
sudo systemctl status mongod
sudo tail -f /var/log/mongodb/mongod.log
mongosh --eval 'db.adminCommand({ getCmdLineOpts: 1 })'
Troubleshooting
mongod will not start after a config change. YAML is whitespace-sensitive and MongoDB is
unforgiving about it. Read the actual error:
sudo journalctl -u mongod -n 50 --no-pager
Permission errors on /var/lib/mongodb. Usually follows a manual file move or a restore run as
root:
sudo chown -R mongodb:mongodb /var/lib/mongodb /var/log/mongodb
Authentication failed with correct credentials. Almost always the wrong authSource. Users
live in the database that created them — an admin user needs --authenticationDatabase admin, an
app user needs ?authSource=myapp.
Killed by the OOM killer. The cache is too large for the box:
dmesg | grep -i "killed process"
Set an explicit cacheSizeGB that leaves room for the OS, the filesystem cache and everything else
on the machine.
connect ECONNREFUSED 127.0.0.1:27017 from an application in Docker. 127.0.0.1 inside a
container is the container. Use host.docker.internal, the host’s bridge address, or put MongoDB
on the same Docker network — and remember bindIp has to allow whatever address you end up using.
Comments
Corrections, additions and "this broke on my machine" reports are all welcome. You can post anonymously — no account needed.