This guide walks through hosting a Node.js service (Express, Fastify, NestJS, Koa — whichever framework you're running) on a DeployHQ Managed VPS. End to end: provision the VPS, install Node, configure Nginx as a reverse proxy, run the service under systemd, set up the DeployHQ build pipeline, and ship the first deploy.
If you're considering DeployHQ Managed VPS against PaaS alternatives — Render, Railway, Fly.io — our Heroku alternatives roundup covers the trade-offs. This post is the implementation path for the DeployHQ Managed VPS choice. If you'd rather run your service in a container than directly on the box, Dockerize a Node.js app walks the Docker path instead.
What you'll build
- A DeployHQ Managed VPS running Ubuntu with Node.js installed
- Your Node service running under systemd with auto-restart
- Nginx reverse-proxying HTTP/HTTPS to the Node process
- DeployHQ build pipeline running
npm ciand any compile step - Custom domain over HTTPS
- Atomic zero-downtime deploys with one-click rollback
Expected time: 30-45 minutes for a fresh project.
Prerequisites
- A Node.js service in a Git repository (Express, Fastify, NestJS, Koa, or any HTTP framework)
- A DeployHQ account with beta features enabled (Settings > Beta Features)
- A database if your app needs one (install Postgres on the VPS or use an external managed DB)
- Local Node.js to verify the build before pushing
If you don't have a DeployHQ account yet, you can start a free trial — the trial includes one Managed VPS at the smallest size at no charge.
Step 1: Provision the Managed VPS
In your DeployHQ project, click New Server:
- Enter a name (your reference only)
- Select Managed VPS under the Hosting section
- Pick a region and server size. For a small Express API: 1 vCPU / 1 GB RAM is workable; bump to 2 GB if you're running a database alongside.
- SSH keys: blank for auto-generated, or paste your own
- Set the deployment path to
/var/www/node-app(or your preferred root) - Click Create Server
Provisioning takes about a minute. The server appears in Settings > Hosted Resources with IP, region, size, and sync status.
Step 2: Install Node.js and supporting packages
SSH in. Install Node via NodeSource (gives you a current LTS release with apt-managed updates):
sudo apt update
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs nginx
Adjust the major version (22.x, 20.x) to match what your project requires. Verify:
node --version
npm --version
Set up the deployment directory:
sudo mkdir -p /var/www/node-app
sudo chown -R $USER:www-data /var/www/node-app
Step 3: Configure Nginx as a reverse proxy
Create /etc/nginx/sites-available/node-app:
upstream node_app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
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;
proxy_redirect off;
proxy_read_timeout 60s;
}
client_max_body_size 10M;
}
The Upgrade and Connection headers enable WebSocket support if your Node service uses them (Socket.IO, ws, NestJS websockets).
Enable the site:
sudo ln -s /etc/nginx/sites-available/node-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 4: Environment variables
Create /var/www/node-app/shared/.env with your environment variables (database URL, API keys, secrets, port):
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/dbname
JWT_SECRET=...
This file lives in shared/ so it persists across deploys. The systemd service reads from it.
Step 5: Create a systemd service
Create /etc/systemd/system/node-app.service:
[Unit]
Description=Node.js application
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/node-app/current
EnvironmentFile=/var/www/node-app/shared/.env
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=5
StandardOutput=append:/var/log/node-app.log
StandardError=append:/var/log/node-app.log
SyslogIdentifier=node-app
[Install]
WantedBy=multi-user.target
(Adjust the ExecStart path to wherever your built entry point lives — dist/server.js for TypeScript projects, src/index.js for plain JS, etc.)
Enable but don't start yet — wait for the first deploy:
sudo systemctl daemon-reload
sudo systemctl enable node-app
Step 6: Configure the build pipeline in DeployHQ
In project build settings, add the build pipeline commands. These run on DeployHQ's build infrastructure before transferring to the VPS:
npm ci --omit=dev
npm run build
(Skip npm run build if your project is plain JavaScript with no compile step. For TypeScript projects, npm run build typically runs tsc or your bundler.)
For deeper Node build configuration — dependency caching, pnpm and Bun, pinning the Node version — see using Node.js and npm with the DeployHQ build pipeline.
In the environment variables UI, set any vars the build step needs at build time (not runtime — runtime vars live in shared/.env).
In deployment hooks, add a post-deploy hook to restart the service:
sudo systemctl restart node-app
For the sudo to work without a password prompt, configure passwordless sudo for systemctl restart node-app via /etc/sudoers.d/:
# /etc/sudoers.d/deployhq-node-app
deployhq ALL=(ALL) NOPASSWD: /bin/systemctl restart node-app
(Replace deployhq with whichever user DeployHQ deploys as.)
In server settings, ensure shared files includes .env and shared folders includes any persistent directories your app uses (uploaded files, generated artifacts, etc.). Enable atomic transfers — see one-click rollback for what the atomic mechanism enables.
Step 7: First deploy
Push to the repository or trigger a manual deploy. DeployHQ:
- Clones the repo at HEAD
- Runs the build pipeline (
npm ci+ your build step) - Transfers the built artifact to the VPS via SSH
- Symlinks
current/to the new release - Runs the post-deploy hook (
systemctl restart node-app) - Old releases kept for rollback
Verify the service is up:
sudo systemctl status node-app
sudo journalctl -u node-app -n 50 # check recent logs
Then curl http://localhost:3000/ (or your service's health endpoint) to confirm the Node process responds before exposing externally.
Common first-deploy failures:
- Module not found —
npm ci --omit=devskips devDependencies. If your runtime code imports something from devDeps, install it as a regular dep. - Build artifacts missing — confirm
npm run buildactually produces the entry point at the path systemd'sExecStartreferences. - Permission denied on systemd restart — the sudoers line for passwordless restart isn't set up. Either set it up, or run the restart as root via a different mechanism.
Step 8: Custom domain + HTTPS
Point your domain at the server's IP via A record. Then either:
Cloudflare: add domain, set DNS to Proxied, HTTPS terminated at the edge. Free and zero config on the VPS.
Let's Encrypt directly on the VPS:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot configures Nginx for HTTPS and auto-renews.
Common gotchas
Service crashes on startup and systemd restarts it forever. systemd's Restart=on-failure keeps restarting failing services indefinitely. If your app crashes due to a config error, you'll see this in journalctl -u node-app. Fix the underlying error, then systemctl restart node-app once the fix is in place.
Environment variables don't load. systemd doesn't expand shell variables. If your .env file uses ${VAR} syntax, systemd won't expand them — use literal values. Or use a tool like dotenv-cli inside ExecStart to load and run.
WebSockets disconnect after 60 seconds. Nginx's default proxy_read_timeout is 60s. For long-lived WebSocket connections, increase it: proxy_read_timeout 86400s;.
TypeScript compile fails on the build worker due to memory. TypeScript can be memory-hungry on large codebases. Either run tsc --incremental to use a build cache, or compile to swc or esbuild which use much less memory.
Process running as root because of missing User directive. systemd defaults to running as root if User= isn't set. Add User=www-data to the unit file and make sure the deployment user owns the current/ and shared/ directories.
No graceful shutdown handling. A Node service should listen for SIGTERM and finish in-flight requests before exiting. Without that, systemctl restart kills mid-request connections. Add a SIGTERM handler in your server startup that closes the HTTP server, waits for in-flight requests, then exits.
What you've shipped
You now have:
- A Node.js service running on a DeployHQ Managed VPS
- Nginx + systemd + the runtime on a real Ubuntu box you can SSH into
- Atomic zero-downtime deploys with one-click rollback via the DeployHQ dashboard
- HTTPS via Cloudflare or Let's Encrypt
For the broader picture, the hosting hub catalogs all five DeployHQ hosting types. The Managed VPS pillar guide covers the provisioning surface and includes comparisons against DigitalOcean App Platform, Linode, and Render. For the category-wide reference, best software deployment tools in 2026 puts DeployHQ alongside the rest, and your universal deployment and hosting platform is the cross-cluster bridge.
What's next
If you also have a static frontend — a React/Vue SPA, a Next.js static export, a documentation site — pair this Managed VPS Node backend with DeployHQ Static Hosting in the same DeployHQ project. The frontend rides on Cloudflare's edge, the API runs on the VPS, one project ships both with one billing relationship.
For Express-specific patterns (Express deployment guide) or Fastify, NestJS, or Koa specifics, the canonical guides cover the framework-specific build steps. For broader background on where Node deployment sits among hosting alternatives, the DeployHQ ecosystem catalog is the deeper reference.
Start a free trial if you don't have an account yet. For the full Managed VPS product reference, see the Managed VPS support library.
Questions or feedback on Node.js + Managed VPS? Email support@deployhq.com or follow @deployhq on X for product updates.