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
Start with the basics: patch packages, verify your deployment bundle, and make sure the machine is ready before any app-specific setup begins.
Run the update and upgrade first so the server gets the latest package metadata and security fixes.
sudo apt update sudo apt upgrade -y
Keep the server clean by sending only what production needs.
distpackage.jsonpackage-lock.json.envSet up Node.js first, then install PM2 so the app can stay alive, restart on boot, and expose useful logs.
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 keeps your process running in the background and can restore it automatically after reboots.
sudo npm install -g pm2 pm2 -v
pm2 startup and pm2 save to persist the process list.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
Nginx receives public traffic on port 80 or 443 and forwards it to your Node app running locally on a private port.
sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx sudo systemctl status nginx
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
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
sudo nginx -t every time before reload. It catches syntax mistakes before they take your site down.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
mongod.confEdit 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
127.0.0.1 if the app is on the same machine.sudo systemctl enable mongod sudo systemctl restart mongod sudo systemctl status mongod mongosh rs.initiate() rs.status()
use admin
db.createUser({
user: "neung",
pwd: "change-this-password",
roles: [ { role: "root", db: "admin" } ]
})
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"
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
Once packages, proxy, and database are ready, start the app under PM2 and persist that process list for server reboot recovery.
cd /path/to/your/project pm2 start dist/index.js --name your-app-3000 pm2 startup pm2 save
server.js, replace dist/index.js with that file.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
proxy_pass to the same internal port, such as http://127.0.0.1:3000.dist.pm2 serve is good for static sites, dashboards, admin panels, and simple single-page apps.npm run dev.pm2 list, pm2 logs my-static-site, and curl http://127.0.0.1:3000.These are not strictly required for every project, but they are common quality-of-life and safety wins for production servers.
sudo apt install -y ufw sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable sudo ufw status
pm2 list pm2 logs your-app-3000 sudo systemctl status nginx sudo tail -f /var/log/nginx/error.log
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
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.