# Host a MERN App for the Price of One Small VPS (2026)

URL: https://www.savemores.store/vps/host-mern-stack
Author: Mr. Ahmed · Published 2026-09-10 · Updated 2026-09-10 · VPS & deployment

## Short answer

- The cheapest MERN setup with room to grow is one VPS running the React build, the Express API and MongoDB in Docker, so the bill stays flat as you add projects.
- MongoDB Atlas M0 is free but caps at 512 MB of storage and around 100 operations per second, which suits a portfolio or MVP, not a real product.
- Publish MongoDB's port as `127.0.0.1:27017:27017`; Docker writes its own iptables rules and a plain `-p 27017:27017` can be reachable from the internet even with UFW enabled.
- Serve React and proxy `/api/` from one Nginx server block to remove CORS issues, and use `try_files $uri $uri/ /index.html` so client-side routes survive a refresh.
- Self-hosting Mongo means owning backups: a nightly `mongodump`, two weeks of retention, and at least one test restore.

## Stop thinking of it as one app

MERN is three deployables wearing one name. The React side compiles to static files. Express is a process that must stay alive. MongoDB is a database that needs persistent disk. Once you price them separately, the decisions get easy.

Static files cost almost nothing anywhere. The API needs a host that keeps a process running. The database is where both the money and the risk sit, and it is the part people most often get wrong.

## Three realistic setups, priced

Figures checked in September 2026; they will move. The trade-offs column is the part that ages well.

| Setup | Approx. monthly cost | What you trade |
| --- | --- | --- |
| One VPS + MongoDB Atlas M0 | The VPS only; the database is free | M0 is free with no end date, but stops at 512 MB of storage and roughly 100 ops/sec. Fine for an MVP or portfolio; it will not carry a product. |
| One VPS running everything, Mongo in Docker | The VPS only | No database ceiling and no second invoice. Backups, upgrades and security are all on you. Cheapest path that still leaves room to grow. |
| Managed PaaS + Atlas Flex | ~$7–37 for the app, up to $30 for the database | Least operational work. Highest bill, and the least predictable, because usage-based pricing can surprise you. |

> From the bench: Atlas M0 is one free cluster per project at roughly 100 operations per second. When you hit that ceiling, requests queue instead of erroring, so it shows up as unexplained latency. Know that before you spend an evening profiling the wrong layer.

## My pick: everything on one 4 GB box

If I were starting a side project today, I would put all three pieces on a single entry-tier VPS with 4 GB of RAM, such as [KVM 1](https://www.savemores.store/hostinger-deals/kvm-1). Express, the React build and MongoDB run together without strain, and the next project you add costs nothing.

Install Node 24 LTS (the Active LTS line as of September 2026) and Docker. MongoDB goes in a container so you are not chasing apt repository changes every time Mongo ships a major version. If you have not hardened the server yet, do the [security checklist](https://www.savemores.store/vps/secure-new-vps) first.

Fresh Ubuntu 24.04 or 26.04, as a sudo user:

```bash
# Node 24 LTS
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs nginx git

# Docker, for MongoDB
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
# log out and back in so the group takes effect
```

## MongoDB, kept off the public internet

This is the step that matters most. Exposed, unauthenticated MongoDB instances are behind a long list of data breaches, and the Docker command in most tutorials publishes the port to everyone.

Two settings fix it. Publish the port on `127.0.0.1` so only local processes can connect, and set root credentials on first start. Be aware that Docker manages its own iptables rules and can bypass UFW entirely: a bare `-p 27017:27017` is reachable from outside even when you believe the firewall is shut.

The 127.0.0.1 prefix on the port mapping is the whole security story:

```bash
docker volume create mongo-data

docker run -d \
  --name mongo \
  --restart unless-stopped \
  -p 127.0.0.1:27017:27017 \
  -v mongo-data:/data/db \
  -e MONGO_INITDB_ROOT_USERNAME=appuser \
  -e MONGO_INITDB_ROOT_PASSWORD='use-a-long-random-password' \
  mongo:8
```

## Express under PM2, listening on loopback

Your connection string stays on loopback too: `mongodb://appuser:password@127.0.0.1:27017/myapp?authSource=admin`. Keep it in an env file that never goes into Git.

PM2 restarts the API on a crash and after a reboot. Bind Express to `127.0.0.1` as well; Nginx should be the only thing that can reach it.

```bash
sudo npm install -g pm2

cd /var/www/api
npm ci --omit=dev

pm2 start server.js --name api
pm2 save
pm2 startup systemd
# run the sudo command PM2 prints, then 'pm2 save' again
```

server.js: listen on loopback only

```js
const PORT = process.env.PORT || 5000;

app.listen(PORT, "127.0.0.1", () => {
  console.log("API listening on 127.0.0.1:" + PORT);
});
```

## One domain for the React build and the API

Serve the front end and the API from the same origin and CORS stops being a problem, because the browser only sees one domain. Nginx serves the build from disk and forwards anything under `/api/` to Express.

The `try_files` line is what keeps client-side routing alive. Without it, refreshing `/dashboard` gives a 404, because no such file exists on disk. Point the domain at the server first ([DNS guide](https://www.savemores.store/vps/point-domain-dns)) so Certbot can issue the certificate.

Build the client where Nginx expects it:

```bash
cd /var/www/client
npm ci
npm run build
# Vite outputs to dist/, Create React App to build/
```

/etc/nginx/sites-available/mern

```nginx
server {
    listen 80;
    server_name example.com;

    root /var/www/client/dist;
    index index.html;

    location /api/ {
        proxy_pass http://127.0.0.1:5000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Client-side routing: unknown paths fall back to index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}
```

Enable it and add HTTPS:

```bash
sudo ln -s /etc/nginx/sites-available/mern /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
```

## Nightly backups, because no one else is taking them

This is the price of running Mongo yourself, and it is small. A nightly `mongodump` into a dated folder, pruned after two weeks, covers the failures that actually happen: a bad migration, a dropped collection, a corrupted write.

Your host's weekly server backups protect against losing the whole machine. They do nothing for the collection you deleted on a Tuesday afternoon. You need both layers.

/home/deploy/backup-mongo.sh

```bash
#!/usr/bin/env bash
set -euo pipefail

STAMP=$(date +%F)
OUT=/home/deploy/backups/$STAMP
mkdir -p "$OUT"

docker exec mongo mongodump \
  --username appuser \
  --password "$MONGO_PASSWORD" \
  --authenticationDatabase admin \
  --archive > "$OUT/myapp.archive"

# Keep two weeks
find /home/deploy/backups -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
```

Schedule it with crontab -e:

```bash
0 3 * * * MONGO_PASSWORD='...' /home/deploy/backup-mongo.sh >> /home/deploy/backup.log 2>&1
```

> From the bench: A backup you have never restored is a hypothesis. Restore one into a throwaway container, look at the data, then delete the container. It takes ten minutes and it is the only proof you have.

## Verify the stack, layer by layer

Run these after the first deploy and after any change to Docker or Nginx.

- **502 on `/api/` routes**: Express is down or not on port 5000. Check `pm2 logs api`.
- **Authentication failed from the API**: the connection string is missing `?authSource=admin`.
- **Slow requests on Atlas M0 with no errors**: you are likely at the ~100 ops/sec ceiling and requests are queueing.

1. Confirm MongoDB is bound to loopback only. `127.0.0.1:27017` is correct; `0.0.0.0:27017` means it is exposed.
2. Confirm the API answers locally on port 5000 before blaming Nginx.
3. Load a nested client route over HTTPS and refresh it. A 200 means `try_files` is doing its job.
4. Test a restore from last night's archive into a scratch container.

```bash
sudo ss -tlnp | grep 27017
# Want:  127.0.0.1:27017
# Bad:   0.0.0.0:27017  <- reachable from the internet

docker ps --filter name=mongo        # STATUS should be 'Up'
pm2 status                           # api should be 'online'
curl -I https://example.com/dashboard  # 200, not 404
```

Restore test into a disposable container:

```bash
docker run -d --name mongo-restore-test \
  -e MONGO_INITDB_ROOT_USERNAME=appuser \
  -e MONGO_INITDB_ROOT_PASSWORD='test-password' \
  mongo:8

docker exec -i mongo-restore-test mongorestore \
  --username appuser --password 'test-password' \
  --authenticationDatabase admin \
  --archive < /home/deploy/backups/$(date +%F)/myapp.archive

docker rm -f mongo-restore-test
```

## When the database should move to managed hosting

Self-hosted Mongo is the right call while the data fits comfortably on one disk and a restore-length outage is survivable. Switch when any of these become true. Notice that none of them is about size; they are all about consequences.

- A few hours of downtime during a restore would genuinely hurt the business.
- You need point-in-time recovery, not 'yesterday at 3 a.m.'.
- You want replica-set failover, and building it yourself has stopped being an interesting afternoon.

## The short version

For a side project or MVP, React, Express and Mongo on one VPS is the cheapest arrangement that does not trap you, and the price holds as you add projects. If you would rather not own a database yet, start on Atlas M0 and move Mongo onto the box when 512 MB runs out.

For the server, the links here apply coupon **WEBHOST2026** automatically. The [Hostinger VPS overview](https://www.savemores.store/hostinger/vps-hosting) lists the plan tiers.

## FAQ

### What is the cheapest way to host a MERN app?

One VPS running the React build, the Express API and MongoDB in Docker. You pay for the server only, there is no database ceiling, and additional projects on the same box cost nothing extra.

### Is MongoDB Atlas M0 good enough for production?

Not for a real product. It is free, but it is limited to 512 MB of storage and about 100 operations per second, which suits a portfolio or an MVP.

### Does UFW protect a MongoDB container?

Not reliably. Docker writes its own iptables rules, so a port published as 27017:27017 can be reachable from the internet despite UFW. Publish it as 127.0.0.1:27017:27017 instead.

### How do I stop 404 errors when refreshing a React route?

Add try_files $uri $uri/ /index.html to the Nginx location block so unknown paths fall back to index.html and the client-side router handles them.
