This guide walks you through Host Node.js on an Ubuntu from start to finish — installing Nginx and Node, deploying your project, keeping it alive with PM2, routing traffic through Nginx, and locking it all down with a free SSL certificate.
By the end, you’ll have a Node.js app running on your own domain over HTTPS, staying online through crashes and reboots.
We set up Nginx and the firewall first so the server has a working, secure foundation before anything else goes on it.
Update your package list, then install Nginx:
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx
Confirm it’s running:
sudo systemctl status nginx
Now configure UFW (Ubuntu’s firewall). Always allow SSH first so you don’t lock yourself out:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
“Nginx Full” opens both port 80 (HTTP) and 443 (HTTPS), so you’re already prepared for SSL later.
Visit your server’s IP address in a browser — you should see the default “Welcome to nginx!” page, confirming everything’s working before we add anything else.
We recommend installing Node with “nvm” rather than a fixed version number — it lets you install and switch Node versions easily and won’t go stale as new LTS releases ship.
Install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
Install the latest LTS release of Node.js:
nvm install --lts
nvm use --lts
Verify:
node -v
npm -v
Alternative: if you’d rather install Node as a system-wide package, use the NodeSource repository instead:
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
We’ll deploy the project into /var/www/my-node-app — the standard location Nginx configs typically expect. Since /var/www is owned by root by default, create the project folder and hand ownership to your user first so you don’t need sudo for every upload or git pull:
sudo mkdir -p /var/www/my-node-app
sudo chown -R $USER:$USER /var/www/my-node-app
Using an SFTP client like FileZilla or WinSCP, connect to your server with your SSH credentials, navigate to /var/www/my-node-app, and upload your project’s files there.
If you prefer the command line, scp handles this in one step (the trailing /. copies the folder’s contents rather than the folder itself):
scp -r /path/to/local/my-node-app/. your_user@your_server_ip:/var/www/my-node-app/
If your project is in a Git repository, clone it directly into the folder you just created:
git clone https://github.com/your-username/your-repo.git /var/www/my-node-app
(Cloning into an existing empty directory works fine — Git will populate it in place.)
For ongoing deployments, many teams set up a GitHub Action that SSHes into the server and runs “git pull” (plus a PM2 restart) on every push to main. That’s a deeper topic worth its own guide — for now, a manual “git pull” after each update works fine:
cd /var/www/my-node-app
git pull origin main
Move into your project folder and install dependencies:
cd /var/www/my-node-app
npm install
Start the app to confirm it runs cleanly:
node app.js
(Replace app.js with your actual entry file — check package.json for the main field or start script if you’re unsure.)
In a separate terminal session, test it locally:
curl http://localhost:3000
Replace 3000 with whatever port your app listens on. If you get a response back, your app is working.
Stop the app with Ctrl+C — next we’ll hand it off to PM2 so it doesn’t depend on this terminal session staying open.
Install PM2 globally:
npm install -g pm2
Start your app under PM2 instead of running it directly with node:
pm2 start app.js --name my-node-app
Check that it’s running:
pm2 status
Useful commands going forward:
pm2 logs my-node-app # view real-time logs
pm2 restart my-node-app # restart after code changes
pm2 stop my-node-app # stop the app
Auto-Start PM2 on Reboot
pm2 startup
Run the command it prints out exactly as shown, then save the current process list:
pm2 save
Your app will now survive both crashes and full server reboots.
Now we connect the dots: Nginx will listen on port 80/443 and forward requests to your app running on its internal port (e.g. 3000).
Create a new site config:
sudo nano /etc/nginx/sites-available/my-node-app
Paste in the following, replacing your_domain.com with your actual domain and 3000 with your app’s port:
server {
listen 80;
server_name your_domain.com www.your_domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable the config and remove the default one so it doesn’t conflict:
sudo ln -s /etc/nginx/sites-available/my-node-app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
Test the config, then reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Visit http://your_domain.com in a browser — your app should now load on your actual domain, no port number needed.
With DNS already pointed at your server (from the prerequisites) and Nginx correctly serving your domain, adding HTTPS takes just a couple of commands via Certbot.
Install Certbot and its Nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
Request and install the certificate:
sudo certbot --nginx -d your_domain.com -d www.your_domain.com
Certbot will ask for an email (for renewal notices) and whether to redirect HTTP traffic to HTTPS — choose yes, it’s the recommended default. It automatically edits your Nginx config to add the SSL block.
Let’s Encrypt certificates expire every 90 days, but Certbot sets up automatic renewal for you. Confirm it’s working with a dry run:
sudo certbot renew --dry-run

© 2026 Stackstroke. All rights reserved.