This playbook deploys a MERN or MEAN application (MongoDB, Express, React or Angular, Node.js) on a fresh Ubuntu VPS with no hosting control panel. Nginx serves your site with a free Let's Encrypt certificate, PM2 keeps the Node.js processes running, and MongoDB listens only on the server itself. It applies to your own VPS, where you have root access, not to shared hosting.
On a clean Ubuntu 24.04 LTS VPS, create an app user, install the current Node.js LTS with nvm and PM2, run MongoDB bound to 127.0.0.1 with authentication, and put Nginx in front: static React or Angular builds served directly, /api/ proxied to Express. Get the certificate with certbot --nginx, make PM2 start on boot, and allow only ports 22, 80 and 443 in the firewall. Most "Cannot GET /api" problems are a trailing slash in proxy_pass.
1. What we're building
| Part | Listens on | Role |
|---|---|---|
| Nginx | Public ports 80 and 443 | TLS, static files, reverse proxy |
| Frontend | Files on disk, or 127.0.0.1:3000 | React or Angular build; a Node server only if you use SSR (Next.js, Angular SSR) |
| API | 127.0.0.1:4000 | Express on Node.js, managed by PM2 |
| MongoDB | 127.0.0.1:27017 | Database, never exposed to the internet |
The site answers at https://example.com/ and the API at https://example.com/api/....
2. Why no control panel
Hosting panels are built around Apache, PHP and MySQL. A Node.js stack needs long-running processes and an Nginx reverse proxy, and a panel's own web, mail and DNS services add ports, conflicts and things to patch. A clean server keeps the attack surface small and makes the routing yours to control. For the longer argument, see why run MERN, MEAN or Node.js without a control panel.
3. Prerequisites and first steps
- A VPS running Ubuntu 24.04 LTS, ordered with no control panel, and root or sudo access.
- A domain with an A record pointing to the VPS IP (for AAAA, only if the VPS has IPv6 configured).
- SSH hardened first: key login, no root password login. Follow the SSH security hardening checklist.
Create a user for the app and open only the ports you need:
sudo adduser --disabled-password --gecos "" appuser
sudo -u appuser mkdir -p /home/appuser/apps/example/{api,web}
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable4. Install Node.js LTS and PM2 for the app user
nvm keeps Node.js inside the app user's home, so upgrades don't touch the system. Use the install command from the nvm README (the version in the URL changes; v0.40.3 is shown here), then install the current LTS line. In 2026 that is Node.js 24; Node.js 20 reached end of life in April 2026.
sudo -iu appuser bash -lc '
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
. "$HOME/.nvm/nvm.sh"
nvm install --lts
npm install -g pm2
node -v && pm2 -v
'5. MongoDB, bound to localhost with authentication
The simplest current setup is the official image in Docker, published on 127.0.0.1 only. Use a current MongoDB release (8.x in 2026); the 4.x and 5.x series are end of life.
sudo apt update && sudo apt install -y docker.io
grep -qw avx /proc/cpuinfo && echo "AVX OK" || echo "No AVX: MongoDB 5+ will not start"
sudo docker run -d --name mongo --restart unless-stopped \
-p 127.0.0.1:27017:27017 \
-v /var/lib/mongo:/data/db \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD='use-a-long-random-password' \
mongo:8.0Then create a user for the app with rights on its own database only, and use it in the connection string:
sudo docker exec -it mongo mongosh -u admin -p --authenticationDatabase admin --eval \
'db.getSiblingDB("appdb").createUser({user: "appdbuser", pwd: "app-password", roles: [{role: "readWrite", db: "appdb"}]})'Docker publishes ports past ufw, which is exactly why the 127.0.0.1: prefix matters: without it MongoDB would be open to the internet.
If you prefer a native install, follow MongoDB's official repository instructions for Ubuntu 24.04, keep bindIp: 127.0.0.1 in /etc/mongod.conf and turn on security.authorization.
6. The API: a minimal Express app with a health check
Node.js 24 reads .env files natively, so no extra package is needed for that.
sudo -iu appuser bash -lc '
cd ~/apps/example/api
npm init -y >/dev/null
npm pkg set type=module scripts.start="node server.js"
npm install express mongodb
cat > .env <<EOF
NODE_ENV=production
PORT=4000
MONGO_URI=mongodb://appdbuser:[email protected]:27017/appdb?authSource=appdb
EOF
chmod 600 .env
cat > server.js <<"JS"
import express from "express";
import { MongoClient } from "mongodb";
process.loadEnvFile(".env");
const client = new MongoClient(process.env.MONGO_URI);
await client.connect();
const app = express();
app.use(express.json());
app.get("/api/health", async (_req, res) => {
try {
await client.db().command({ ping: 1 });
res.json({ ok: 1 });
} catch (e) {
res.status(500).json({ ok: 0 });
}
});
app.listen(process.env.PORT, "127.0.0.1", () => console.log("API ready"));
JS
pm2 start server.js --name example-api --time
'
curl -s http://127.0.0.1:4000/api/health # {"ok":1}The API binds to 127.0.0.1, so only Nginx can reach it. It connects to MongoDB once at start-up and reuses the connection, instead of opening one per request.
7. The frontend: static build or SSR
- React (Vite) or Angular without SSR: run
npm run buildand copy the output (dist/, ordist/your-project/browserfor Angular) to/var/www/example. Nginx serves the files directly; no Node process is needed. - Next.js, or Angular with SSR: run the production server under PM2 on 127.0.0.1:3000, for example
pm2 start npm --name example-web -- start.
8. Nginx and the free certificate
Install Nginx and Certbot, and write a plain-HTTP site first. Certbot then adds the HTTPS server block and the redirect itself, so you never reference a certificate file that doesn't exist yet.
sudo apt install -y nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'NGX'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example; # static React or Angular build
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:4000; # NO trailing slash: keeps /api/ in the path
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
try_files $uri /index.html; # single-page app routing
# For an SSR frontend, replace the line above with:
# proxy_pass http://127.0.0.1:3000;
}
}
NGX
sudo ln -sf /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com -d www.example.com --redirect \
-m [email protected] --agree-tos -nCheck through the domain:
curl -sI https://example.com/ | head -n1
curl -s https://example.com/api/health
# Before DNS has propagated, point curl at the IP yourself:
curl -s --resolve example.com:443:203.0.113.10 https://example.com/api/health9. Start everything on boot
sudo -iu appuser bash -lc 'pm2 save && pm2 startup systemd'pm2 startup prints one sudo env PATH=... pm2 startup systemd -u appuser --hp /home/appuser command. Copy it and run it as root. After you upgrade Node.js with nvm, run pm2 unstartup, then repeat both steps, because the path to Node changes.
10. Verify, troubleshoot and maintain
- Frontend.
https://example.com/loads, and deep links such as/aboutdon't give a 404. - API.
https://example.com/api/healthreturns{"ok":1}. - Processes.
pm2 statusshows your apps online;systemctl status nginxis active. - Certificate. `systemctl list-timersgrep certbot` shows the renewal timer.
- Exposure.
ss -tlnpshows only Nginx and SSH on public addresses; Node and MongoDB on 127.0.0.1.
"Cannot GET /api/..." usually means a trailing slash in proxy_pass: proxy_pass http://127.0.0.1:4000/; strips /api/ before Express sees the path. Leave the slash off, or change your Express routes to match.
Port 80 already in use. Another web server is running. Find it with sudo ss -ltnp 'sport = :80', then stop and disable that service properly rather than killing processes.
MongoDB authentication failed. Check the user, password, database name and authSource in MONGO_URI. Special characters in the password must be URL-encoded.
The default Nginx page still appears. Remove /etc/nginx/sites-enabled/default, run nginx -t and reload.
For upkeep, apply security updates regularly (unattended-upgrades helps), watch logs with pm2 logs and set up pm2 install pm2-logrotate, and back up the database with mongodump to storage off the server. Code belongs in Git. See PM2 process management for more.
11. Where Domain India fits
A Domain India VPS is self-managed, with full root access, KVM virtualisation and a choice of Linux including Ubuntu 24.04 (per the VPS page); choose no control panel at checkout for this playbook. Prices on the cards are live and exclude 18% GST.
- 1 vCPU
- 2 GB DDR4 RAM
- 64 GB NVMe SSD Storage
- 2 TB Monthly Bandwidth
If you would rather not manage a server, the App Platform detects Node.js apps automatically and runs them in containers with SSL. Its included database is PostgreSQL, so a MongoDB app needs an external MongoDB service. See App Platform: getting started.
- 512 MB RAM per app
- 1 vCPU
- 5 GB NVMe SSD
- PostgreSQL Database
Simple Node.js apps can also run on cPanel or DirectAdmin shared hosting through the control panel's Node.js tool; see deploying a Node.js app on shared hosting.
Which Node.js version should I use for a MERN app in 2026?
The current LTS release, Node.js 24. Node.js 22 is still in maintenance until April 2027, and Node.js 20 reached end of life in April 2026. Install it with nvm so you can upgrade per user.
Why does Nginx return "Cannot GET /api/"?
Usually a trailing slash in proxy_pass. With location /api/ and proxy_pass http://127.0.0.1:4000/ Nginx strips /api/ before passing the request, so Express routes defined under /api don't match. Remove the trailing slash.
Should MongoDB be reachable from the internet?
No. Bind it to 127.0.0.1, enable authentication, and give the app a user limited to its own database. With Docker, publish the port as 127.0.0.1:27017:27017, because Docker's port rules bypass ufw.
Do I need a Node process for the React frontend?
Not for a normal React or Angular build. Nginx serves the built files directly. You need a Node process only for server-side rendering, such as Next.js or Angular SSR.
How do I keep my Node.js apps running after a reboot?
Start them with PM2, run pm2 save, then run pm2 startup systemd and execute the command it prints as root. Repeat after upgrading Node.js, because the path changes.
Can I run a MERN stack on Domain India shared hosting?
Shared hosting has no MongoDB server and doesn't allow long-running processes outside the control panel's Node.js tool. For a full MERN stack with its own database, use a VPS.
Ready to deploy? Compare VPS plans, look at the App Platform if you prefer not to manage a server, or open a ticket if you are not sure which fits your app.
A self-managed KVM VPS with full root access and your choice of Linux, ready for Node.js, MongoDB and Nginx.
See VPS plans