DEV Community

Daniel Ioni
Daniel Ioni

Posted on

From Domain to Deploy: A Practical Guide to Deploying a Node.js App with Nginx, Cloudflare, and systemd

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).
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

}

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
Enter fullscreen mode Exit fullscreen mode

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)

  1. 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.

  2. 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.

  3. GitHub authentication failed
    The git push failed because I was using HTTPS. I configured SSH access and fixed it.

  4. 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.
Enter fullscreen mode Exit fullscreen mode

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

www.myzubster.com

Top comments (0)