Server Playbook

Ubuntu setup for real project deployments.

Phase 1

Prepare the server

Start with the basics: patch packages, verify your deployment bundle, and make sure the machine is ready before any app-specific setup begins.

Base update

1. Refresh Ubuntu packages

Run the update and upgrade first so the server gets the latest package metadata and security fixes.

sudo apt update
sudo apt upgrade -y
Why Prevents package conflicts and avoids starting from stale repositories.
When Immediately after login to a fresh server.
Check Look for errors before continuing to app installs.
Deploy bundle

2. Upload the project files you need

Keep the server clean by sending only what production needs.

  • dist
  • package.json
  • package-lock.json
  • .env
If your app builds on the server, also upload the source files. If it only runs compiled output, sending just the runtime files is faster and safer.
Phase 2

Install the app runtime

Set up Node.js first, then install PM2 so the app can stay alive, restart on boot, and expose useful logs.

Node.js

3. Install Node.js LTS

This uses NodeSource to install the current LTS stream, which is usually the best default for production Node apps.

sudo apt install -y curl ca-certificates gnupg
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v
PM2

4. Install PM2 globally

PM2 keeps your process running in the background and can restore it automatically after reboots.

sudo npm install -g pm2
pm2 -v
  • Use one PM2 process name per app or per app-port pair.
  • Later you will run pm2 startup and pm2 save to persist the process list.
Logs

5. Enable PM2 log rotation

Rotating logs is a small step that saves a lot of pain once a service runs for weeks or months.

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 14
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'
pm2 set pm2-logrotate:workerInterval 30
Phase 3

Expose the app with Nginx

Nginx receives public traffic on port 80 or 443 and forwards it to your Node app running locally on a private port.

Nginx

6. Install and start Nginx

sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx
SSL

7. Install Certbot for HTTPS

Once DNS points to the server and the Nginx site is active, Certbot can request and install the certificate.

sudo apt install -y certbot python3-certbot-nginx
certbot --version
sudo certbot --nginx
Site config

8. Create the reverse proxy config

Replace the placeholders with your domain, app port, and chosen config filename.

sudo nano /etc/nginx/sites-available/your-app
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
sudo ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Run sudo nginx -t every time before reload. It catches syntax mistakes before they take your site down.
Phase 4

Install and secure MongoDB

MongoDB 8

9. Add the MongoDB repository

sudo apt update
sudo apt install -y gnupg curl

curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor

echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME)/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list

sudo apt update
sudo apt install -y mongodb-org
mongod --version
mongosh --version
Config

10. Update mongod.conf

Edit the config to define a replica set and bind MongoDB only to addresses you actually need.

sudo nano /etc/mongod.conf
replication:
  replSetName: "rs0"
net:
  port: 27017
  bindIp: 127.0.0.1
  • Use only 127.0.0.1 if the app is on the same machine.
  • Add a private IP only when another server must connect directly.
Service

11. Start MongoDB and initialize the replica set

sudo systemctl enable mongod
sudo systemctl restart mongod
sudo systemctl status mongod

mongosh
rs.initiate()
rs.status()
Admin user

12. Create the first admin account

use admin
db.createUser({
  user: "neung",
  pwd: "change-this-password",
  roles: [ { role: "root", db: "admin" } ]
})
Do not reuse the sample password from the old note in production. Replace it with a strong unique secret.
Authorization

13. Turn on authentication

security:
  authorization: enabled

Add that block inside /etc/mongod.conf, then restart MongoDB and reconnect with credentials.

mongosh "mongodb://neung:your-password@127.0.0.1:27017/admin?replicaSet=rs0"
Keyfile

14. Add an internal keyfile if you need replica auth

This is useful when replica set members need to trust each other securely.

sudo mkdir -p /etc/mongodb
openssl rand -base64 756 | sudo tee /etc/mongodb/keyfile > /dev/null
sudo chmod 400 /etc/mongodb/keyfile
sudo chown mongodb:mongodb /etc/mongodb/keyfile
security:
  keyFile: /etc/mongodb/keyfile
  authorization: enabled
Phase 5

Start the application

Once packages, proxy, and database are ready, start the app under PM2 and persist that process list for server reboot recovery.

Launch

15. Run the production process with PM2

cd /path/to/your/project
pm2 start dist/index.js --name your-app-3000
pm2 startup
pm2 save
  • If your entry file is server.js, replace dist/index.js with that file.
  • Use a clear PM2 process name so logs and restarts are easy to identify later.
Frontend

16. Serve static frontend files with PM2

For HTML, Vite, or other frontend build output, PM2 can serve the built folder directly on an internal port.

pm2 serve /var/www/my-html 3000 --name my-static-site
pm2 startup
pm2 save
  • Point Nginx proxy_pass to the same internal port, such as http://127.0.0.1:3000.
  • For Vite or React builds, upload the contents of the build folder such as dist.
Must know

17. Important notes for frontend hosting with PM2

  • pm2 serve is good for static sites, dashboards, admin panels, and simple single-page apps.
  • If the frontend uses client-side routing, refreshes on nested URLs may need fallback handling through Nginx or a dedicated static server setup.
  • Always serve the built production files, not the local dev server from npm run dev.
  • Keep the frontend on a private local port and let Nginx handle the public domain, SSL, and redirects.
  • Useful checks: pm2 list, pm2 logs my-static-site, and curl http://127.0.0.1:3000.
Helpful additions

Small extras worth adding

These are not strictly required for every project, but they are common quality-of-life and safety wins for production servers.

Firewall

Allow only what you need

sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Health

Keep quick debugging commands nearby

pm2 list
pm2 logs your-app-3000
sudo systemctl status nginx
sudo tail -f /var/log/nginx/error.log
Swap

Add swap on small servers

Helpful for tiny VPS instances that can crash under memory pressure.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
swapon --show
Install

Install production dependencies

npm ci --omit=dev

If the server has a full source checkout and package-lock.json, this is the cleanest way to install production packages.