Introduction
You have a Node.js application ready, a VPS, and a domain. Now how do you put it all together in production?
In this post, I'll walk you through the path I followed to deploy my application MyZubsterGateway on an Ubuntu VPS. We'll cover:
Configuring Nginx as a reverse proxy.
Managing the domain with Cloudflare (DNS and Tunnels).
Creating a systemd service to keep the app always running.
Solving the most common issues (DNS, proxy_pass, SSL certificates).
By the end, you'll have a working Node.js application, reachable via HTTPS, that restarts automatically on crash or system reboot.
Prerequisites
Before we begin, make sure you have:
A domain (e.g., myzubster.com).
A VPS with Ubuntu (20.04 or 22.04) and root access.
The Node.js application ready (with server.js and package.json).
A Cloudflare account with the domain added.
Step 1: Prepare the Server and the Application
Connect to your VPS via SSH and install Node.js and npm:
bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
Clone or upload your application code, for example in /root/MyZubsterGateway, and install dependencies:
bash
cd /root/MyZubsterGateway
npm install
Create a .env file with the necessary environment variables (port, MongoDB, JWT, etc.). For now, make sure the port is 3000.
Step 2: Configure Nginx as a Reverse Proxy
Install Nginx:
bash
sudo apt-get install nginx -y
Create a configuration file for your domain in /etc/nginx/sites-available/myzubster:
nginx
server {
server_name myzubster.com www.myzubster.com;
location / {
proxy_pass http://127.0.0.1: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;
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;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/myzubster.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myzubster.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = www.myzubster.com) {
return 301 https://$host$request_uri;
}
if ($host = myzubster.com) {
return 301 https://$host$request_uri;
}
listen 80;
server_name myzubster.com www.myzubster.com;
return 404;
}
Enable the site and reload Nginx:
bash
ln -s /etc/nginx/sites-available/myzubster /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
Note: The SSL configuration requires a Let's Encrypt certificate. If you don't have one, you can obtain it with Certbot:
bash
sudo apt-get install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myzubster.com -d www.myzubster.com
Step 3: Manage the Domain with Cloudflare
Add your domain to Cloudflare and set the nameservers. In the DNS settings, create an A record pointing to your VPS IP (e.g., 188.213.161.186). If you want to use the proxy (orange cloud), enable it; otherwise leave it gray (DNS only).
For a more advanced setup, you can use Cloudflare Tunnel (cloudflared) to expose the service without opening public ports. In that case, the DNS record will be of type Tunnel and point to the tunnel you created.
Step 4: Start the App with systemd (Automatic Startup)
To avoid having to manually run node server.js every time, we'll create a systemd service.
Create the file /etc/systemd/system/myzubster-gateway.service:
ini
[Unit]
Description=MyZubster Gateway
After=network.target mongod.service
Wants=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/MyZubsterGateway
EnvironmentFile=/root/MyZubsterGateway/.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Then start and enable the service:
bash
systemctl daemon-reload
systemctl enable myzubster-gateway
systemctl start myzubster-gateway
Now the app will start automatically at boot and restart on crash.
Step 5: Issues I Encountered (and How I Fixed Them)
DNS not updating
The domain resolved to a Cloudflare IP, but records on Aruba weren't being read. I discovered the nameservers were pointing to Cloudflare, so I had to modify the records directly there.502 Bad Gateway from Nginx
Nginx was configured to proxy to 127.0.0.1:4000, but my Node.js app was listening on 3000. I corrected the proxy_pass and the issue disappeared.GitHub authentication failed
The git push failed because I was using HTTPS. I configured SSH access and fixed it.Missing root route
Express was returning 404 on /. I added an app.get('/') route to respond with a welcome message.
Step 6: Final Verification
At this point, open your browser and go to https://myzubster.com. You should see your application running. To check the service status:
bash
systemctl status myzubster-gateway
curl -I https://myzubster.com
Conclusion
We've seen how to deploy a Node.js app in production on an Ubuntu VPS, using Nginx as a reverse proxy, Cloudflare for DNS, and systemd for process management. The result is a stable application, accessible via HTTPS, and ready to handle real traffic.
Every step has its pitfalls, but with patience and the right commands, everything can be solved. If you have questions or want to dive deeper, feel free to leave a comment!
How to Publish on Dev.to
Go to dev.to and log in.
Click the "Write a Post" button.
Copy and paste the content above.
Add the suggested title and tags.
Use the Dev.to editor to format the code blocks (they should be bash or nginx).
Preview the post and publish.
Let me know if you need any changes or adjustments! 🚀
📖 Blog & Articoli: DEV.to - Daniel Ioni
🐦 X (Twitter): @myzubster
💼 LinkedIn: Daniel Ioni
🐙 GitHub: DanielIoni-creator
🎵 TikTok: @h4x0r_23
http://olqcnbdlt35k2stmmwvzhvuetu2fc4us2jnn5wg6y6wlcddihfmdomid.onion
Top comments (0)