DEV Community

Daniel Ioni
Daniel Ioni

Posted on

From Zero to Production: Deploying a Node.js App with Nginx, Cloudflare, systemd, and Tor

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 exact path I followed to deploy MyZubsterGateway – a skills and services exchange platform – 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 for automatic startup and crash recovery

Adding a Tor onion service for privacy

Solving the most common issues (DNS, proxy_pass, SSL, and GitHub authentication)

Deploying with Git and SSH
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

Your Node.js application ready (with server.js and package.json)

A Cloudflare account with the domain added

Basic familiarity with the Linux command line
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:
bash

nano .env

Example .env:
text

PORT=3000
NODE_ENV=production
MONGODB_URI=mongodb://localhost:27017/myzubster
JWT_SECRET=your-secret-key

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.

Check the status:
bash

systemctl status myzubster-gateway

Step 5: Add a Tor Onion Service (Optional)

If you want to offer anonymous access to your application, add a Tor onion service.

Install Tor:
bash

sudo apt-get install tor -y

Edit the Tor configuration file:
bash

nano /etc/tor/torrc

Add these lines:
text

HiddenServiceDir /var/lib/tor/myzubster/
HiddenServicePort 80 127.0.0.1:3000

Restart Tor:
bash

systemctl restart tor

Get your onion address:
bash

cat /var/lib/tor/myzubster/hostname

This will output something like:
text

olqcnbdlt35k2stmmwvzhvuetu2fc4us2jnn5wg6y6wlcddihfmdomid.onion

Now your application is also accessible via Tor!
Step 6: Issues I Encountered (and How I Fixed Them)

  1. DNS not updating

Problem: The domain resolved to a Cloudflare IP, but records on the registrar weren't being read.

Solution: I discovered the nameservers were pointing to Cloudflare, so I had to modify the DNS records directly in Cloudflare instead of the registrar's panel.

Check your nameservers:
bash

dig NS myzubster.com

  1. 502 Bad Gateway from Nginx

Problem: Nginx was configured to proxy to 127.0.0.1:4000, but my Node.js app was listening on 3000.

Solution: I corrected the proxy_pass in the Nginx configuration:
bash

sed -i 's/proxy_pass http:\/\/127.0.0.1:4000;/proxy_pass http:\/\/127.0.0.1:3000;/' /etc/nginx/sites-available/myzubster
systemctl reload nginx

  1. Missing root route in Express

Problem: Express was returning 404 on /.

Solution: I added a root route in server.js:
javascript

app.get('/', (req, res) => {
res.send('Welcome to MyZubsterGateway API. Go to /api/health for status.');
});

  1. GitHub authentication failed

Problem: git push failed with Invalid username or token. Password authentication is not supported for Git operations.

Solution: I switched the remote URL from HTTPS to SSH:
bash

git remote set-url origin git@github.com:username/repo.git

Then verified SSH authentication:
bash

ssh -T git@github.com

  1. Cloudflare Tunnel not forwarding to the right port

Problem: The tunnel was active but not forwarding traffic to localhost:3000.

Solution: I created a config.yml file for the tunnel:
yaml

tunnel: myzubster-tunnel
credentials-file: /root/.cloudflared/your-credentials.json

ingress:

Then restarted the tunnel:
bash

systemctl restart cloudflared

Step 7: 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

You should see HTTP/1.1 200 OK.
Full Tech Stack Summary
Layer Technology
Backend Node.js + Express
Database MongoDB
Reverse Proxy Nginx (SSL via Let's Encrypt)
DNS Cloudflare
Process Management systemd
Privacy Tor onion service
Payments Monero (XMR)
Version Control Git + GitHub (SSH)
Hosting Ubuntu VPS
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

systemd for process management

Tor for anonymous access

Git with SSH for version control
Enter fullscreen mode Exit fullscreen mode

The result is a stable, scalable, and secure application, accessible via HTTPS and Tor, with automatic restart on crash.

Every step has its pitfalls, but with patience and the right commands, everything can be solved.
🔗 My Project

If you want to see the final result:

Live site: https://myzubster.com

Tor onion: http://olqcnbdlt35k2stmmwvzhvuetu2fc4us2jnn5wg6y6wlcddihfmdomid.onion

GitHub: https://github.com/DanielIoni-creator/MyZubsterGateway
Enter fullscreen mode Exit fullscreen mode

📝 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 title: From Zero to Production: Deploying a Node.js App with Nginx, Cloudflare, systemd, and Tor

Add tags: nodejs, nginx, cloudflare, systemd, linux, devops, production, ubuntu, tor, mongodb

Format code blocks with bash, nginx, 
Enter fullscreen mode Exit fullscreen mode

javascript, etc.

    Preview and publish.

If you have questions or want to dive deeper, feel free to leave a comment! 🚀
Enter fullscreen mode Exit fullscreen mode

Top comments (0)