This guide walks through hosting a Node.js service (Express, Fastify, NestJS, Koa — whichever framework you're running) on a [DeployHQ Managed VPS](https://www.deployhq.com/hosting/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](https://www.deployhq.com) build pipeline, and ship the first deploy.

If you're considering [DeployHQ](https://www.deployhq.com) Managed VPS against PaaS alternatives — Render, Railway, Fly.io — our [Heroku alternatives roundup](https://www.deployhq.com/blog/heroku-alternatives-render-railway-fly-deployhq-vps) covers the trade-offs. This post is the implementation path for the [DeployHQ](https://www.deployhq.com) Managed VPS choice. If you'd rather run your service in a container than directly on the box, [Dockerize a Node.js app](https://www.deployhq.com/blog/dockerize-nodejs-app) walks the Docker path instead.

## What you'll build

- A [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) build pipeline running `npm ci` and 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com) project, click **New Server** :

1. Enter a name (your reference only)
2. Select **Managed VPS** under the **Hosting** section
3. 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.
4. SSH keys: blank for auto-generated, or paste your own
5. Set the **deployment path** to `/var/www/node-app` (or your preferred root)
6. 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](https://www.deployhq.com/features/build-pipelines) 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](https://www.deployhq.com/blog/using-nodejs-and-npm-with-deployhq-build)[DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com/features/one-click-rollback) for what the atomic mechanism enables.

## Step 7: First deploy

Push to the repository or trigger a manual deploy. DeployHQ:

1. Clones the repo at HEAD
2. Runs the build pipeline (`npm ci` + your build step)
3. Transfers the built artifact to the VPS via SSH
4. Symlinks `current/` to the new release
5. Runs the post-deploy hook (`systemctl restart node-app`)
6. 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=dev` skips devDependencies. If your runtime code imports something from devDeps, install it as a regular dep.
- **Build artifacts missing** — confirm `npm run build` actually produces the entry point at the path systemd's `ExecStart` references.
- **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](https://www.deployhq.com) 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](https://www.deployhq.com) dashboard
- HTTPS via Cloudflare or Let's Encrypt

For the broader picture, the [hosting hub](https://www.deployhq.com/hosting) catalogs all five [DeployHQ](https://www.deployhq.com) hosting types. The [Managed VPS pillar guide](https://www.deployhq.com/blog/managed-vps-hosting-on-deployhq) 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](https://www.deployhq.com/blog/best-software-deployment-tools) puts [DeployHQ](https://www.deployhq.com) alongside the rest, and [your universal deployment and hosting platform](https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types) 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](https://www.deployhq.com/hosting/static) in the same [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com/guides/express)) 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](https://www.deployhq.com/guides) is the deeper reference.

[Start a free trial](https://www.deployhq.com/signup) if you don't have an account yet. For the full Managed VPS product reference, see the [Managed VPS support library](https://www.deployhq.com/support/servers/managed-vps-hosting).

* * *

Questions or feedback on Node.js + Managed VPS? Email [support@deployhq.com](mailto:support@deployhq.com) or follow [@deployhq](https://x.com/deployhq) on X for product updates.

