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
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
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;
}
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.
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)
- 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
- 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
- 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.');
});
- 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
- 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:
- hostname: myzubster.com service: http://localhost:3000
- hostname: www.myzubster.com service: http://localhost:3000
- service: http_status:404
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
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
📝 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,
javascript, etc.
Preview and publish.
If you have questions or want to dive deeper, feel free to leave a comment! 🚀
Top comments (0)