Skip to article
Open SourceDevOps & Infrastructure17 min read

Deploy Node.js Apps Behind Nginx in One Command — Meet Depsite, the CLI That Replaces Your 30-Step Deployment Checklist

Depsite is the open-source CLI that automates the entire Node.js + nginx + Let's Encrypt deployment in under 12 seconds.

WA
Deploy Node.js Apps Behind Nginx in One Command — Meet Depsite, the CLI That Replaces Your 30-Step Deployment Checklist

The Pain of Deploying a Node.js App to a VPS in 2026

If you've ever Googled "how to deploy a Node.js app with nginx and SSL," you know the drill. The top results all tell you the same 12-step story:

  1. Install nginx
  2. Create a config file in /etc/nginx/sites-available/
  3. Symlink it to /etc/nginx/sites-enabled/
  4. Run nginx -t to test the config
  5. Reload nginx
  6. Install certbot
  7. Provision a Let's Encrypt SSL certificate
  8. Set up auto-renewal
  9. Configure proxy headers correctly
  10. Add WebSocket upgrade headers if you need them
  11. Set the right proxy_read_timeout so long-running requests don't drop
  12. Hope you didn't make a typo somewhere

Every single tutorial about how to set up nginx as a reverse proxy for Node.js walks through this manually. Every. Single. One. The DigitalOcean tutorial, the LogRocket guide, the SitePoint article — they're all variations of "open this config file, paste this snippet, run these commands, pray." And when something goes wrong — wrong proxy headers, missing trailing slash, certbot rate-limited because your DNS wasn't ready — you get to debug it at midnight while your client is asking why staging is down.

This is the problem depsite solves. It is a single-command CLI that takes you from "I have a Node.js app running on port 3000" to "my domain is live with HTTPS and a hardened nginx config" in about 12 seconds. No copy-pasting. No editing files. No debugging certbot.

This article walks through what depsite does, why it exists, and how to use it for a real Node.js + nginx deployment on a Linux VPS.

§ 01

#What Depsite Actually Does

Depsite is an open-source command-line tool for automating Node.js deployments behind an nginx reverse proxy on Linux servers. Published as the depsite package on npm, it handles every step of the manual deployment checklist — and adds production-grade safety nets that handwritten setups don't have.

The core promise: one command, full deployment.

bash
depsite

That's it. The CLI walks you through four interactive prompts (project name, domain, port, SSL yes/no), then executes six discrete steps:

plain
◇  Writing nginx configuration       ✓
◇  Enabling site                     ✓
◇  Testing nginx configuration       ✓
◇  Reloading nginx                   ✓
◇  Provisioning SSL                  ✓
◇  Recording site in registry        ✓

If any step fails, depsite rolls back every prior step. If everything succeeds, you get a working HTTPS deployment with a hardened security-headers config, a registered site you can manage with depsite list / status / remove, and an auto-renewing Let's Encrypt certificate.

Here is what each part of that pipeline does:

  • Nginx configuration. Generates a snapshot-tested nginx server block tailored to your project. Includes WebSocket support (optional), security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy), proxy header forwarding, and a /healthz endpoint.
  • Site enabling. Creates the symlink from sites-available to sites-enabled.
  • Config testing. Runs nginx -t to validate the config before any reload.
  • Reload. Reloads nginx with zero downtime.
  • SSL provisioning. Calls certbot --nginx to provision and install a Let's Encrypt cert. Certbot handles the HTTPS server block — depsite stays out of certbot's lane to avoid drift.
  • Registry. Records the deployment in ~/.depsite/sites.json so you have a real inventory to query later.
§ 02

#Installing Depsite on Your Linux Server

Depsite ships in two flavors: a single-file precompiled binary (recommended for production servers), and an npm package (for developer machines or if you already have Node.js installed).

bash
curl -fsSL https://raw.githubusercontent.com/kemora13conf/depsite/master/scripts/install.sh | bash

The installer detects your CPU architecture (x86_64 or aarch64), downloads the matching binary from GitHub Releases, verifies its SHA-256 checksum, and drops it at /usr/local/bin/depsite. No Node.js or Bun runtime required on the server.

This matters because most production servers don't have Node.js installed for system tools — and even when they do, you don't want a deployment CLI that breaks every time you upgrade your Node version.

#The npm Install

bash
# Global
npm install -g depsite

# Or with bun
bun install -g depsite

# One-shot, no install
npx depsite
bunx depsite

#Requirements

  • Linux (x86_64 or aarch64) — Ubuntu, Debian, AlmaLinux, Rocky, Fedora all work
  • nginx version 1.28.2 or newer (depsite warns on older versions due to CVE-2026-1642)
  • certbot installed (only needed if you want SSL — and you do)
  • Passwordless sudo for the user running depsite

Run depsite doctor after installation to verify your environment is ready:

bash
depsite doctor

This checks for nginx, certbot, sudo, disk space, and other prerequisites. If anything is missing or misconfigured, doctor tells you exactly what to fix.

§ 03

#Your First Deployment: From Zero to HTTPS in One Command

Let's walk through a real-world scenario: you have an Express API running on port 3000, you've pointed api.example.com at your VPS in DNS, and you want it live with HTTPS.

#Interactive Mode

bash
depsite

Depsite prompts you through four questions:

plain
┌  depsite v2.0.0

◆  Project name
│  api

◆  Domain
│  api.example.com

◆  Port your Node.js app listens on
│  3000

◆  Set up Let's Encrypt SSL?
│  Yes

After confirming, the deployment runs. About 12 seconds later:

plain
└  Done.

✓ Deployment complete

  URL     https://api.example.com
  Site    api
  Port    3000
  SSL     enabled
  Time    11.4s

Your API is live, behind nginx, with a valid HTTPS certificate, security headers applied, and the deployment recorded in your local registry.

#Non-Interactive Mode (CI/CD-Friendly)

For scripted deployments — GitHub Actions, GitLab CI, Ansible, anything that runs without a TTY — depsite accepts every option as a flag:

bash
depsite deploy \
  --name api \
  --domain api.example.com \
  --port 3000 \
  --ssl --email me@example.com \
  --non-interactive --yes

Add --json to get machine-parseable output for CI pipelines:

bash
depsite deploy --name api --domain api.example.com --port 3000 \
  --ssl --email me@example.com --non-interactive --yes --json

#Config-File Driven Deployments

If you prefer to declare deployments in version control, generate a starter config:

bash
depsite init

This scaffolds a depsite.config.json you can commit to your repo. Then deploy from it:

bash
depsite deploy --config depsite.config.json --non-interactive --yes

Caption: Config files are particularly useful for monorepos where multiple services deploy to the same server. Each service gets its own config, and your CI pipeline can deploy all of them in parallel.

§ 04

#What Gets Generated Under the Hood

A common question for any deployment tool is "what is it actually writing to my server?" Depsite is fully transparent about this — the generated nginx config is committed to the repo as a snapshot test, and you can read it directly:

nginx
# Managed by depsite — do not edit by hand.
upstream api_prod {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    listen [::]:80;
    server_name api.example.com;

    client_max_body_size 20M;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    location / {
        proxy_pass http://api_prod;
        proxy_http_version 1.1;
        proxy_set_header Host $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_set_header X-Forwarded-Host $host;
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 8 16k;
    }

    location = /healthz { return 200 "ok\n"; }
    location ~ /\.(?!well-known) { deny all; }
}

A few things worth noting about this config:

Upstream block with keepalive. Modern nginx setups should use named upstream blocks with connection keepalive — it dramatically reduces TCP overhead on high-traffic apps. Most copy-paste tutorials skip this. Depsite includes it by default.

Security headers, applied with always. The always flag ensures headers are sent even on error responses (4xx, 5xx). Without it, an error page is missing your security headers, which is one of the most common Mozilla Observatory failures on hand-rolled nginx setups.

Hidden file protection. The location ~ /\.(?!well-known) block denies access to any path starting with a dot — .env, .git, .ssh — except for /.well-known/ which Let's Encrypt needs for cert validation. This single line prevents an entire class of accidental file-exposure bugs.

Health check endpoint. A /healthz endpoint that returns 200 "ok\n" is included automatically. This is essential for load balancers, uptime monitors, and Kubernetes liveness probes.

HTTPS handling delegated to certbot. Depsite intentionally does not write the HTTPS server block. When SSL is enabled, certbot's --nginx plugin owns the HTTPS config. This avoids drift between depsite's view of the world and certbot's view, which used to be a bug source in v1.

§ 05

#The Full Command Reference

Depsite ships with eight subcommands, each handling a specific lifecycle step:

Command

What it does

depsite deploy

Interactive (or flag-driven) deployment

depsite remove <name>

Disable + delete a site, reload nginx

depsite list

List all sites recorded in the registry

depsite status <name>

Show enable state, SSL state, paths

depsite renew [name]

Run certbot renew, then reload nginx

depsite doctor

Diagnose host readiness (deps, sudo, disk, etc.)

depsite init

Scaffold a depsite.config.json template

Run depsite <command> --help for the full flag set.

#Global Flags

Available on every command:

  • --json — machine-parseable output for scripts
  • --quiet — suppress non-essential output
  • --verbose — show every step in detail
  • --no-color — disable terminal colors
  • -y / --yes — skip all confirmation prompts

#Listing All Deployed Sites

bash
depsite list

This reads from ~/.depsite/sites.json and shows every site you've deployed: name, domain, port, SSL status, deploy date. Unlike grepping through /etc/nginx/sites-enabled/, this gives you a real inventory.

#Inspecting a Single Site

bash
depsite status api

Shows the site's full state: domain, upstream port, enabled status, SSL state, paths to config files, last renewal date.

#Removing a Site

bash
depsite remove api

Disables the site (removes the symlink from sites-enabled), deletes the config file, reloads nginx, and removes the entry from the registry. The Let's Encrypt cert is left in place so you don't accidentally lose it.

#Renewing SSL Certificates

bash
depsite renew api      # Renew a specific site
depsite renew          # Renew all sites

Wraps certbot renew with a forced nginx reload after, so renewed certs take effect immediately rather than at the next reload.

§ 06

#Why a State Machine With Rollback Matters

This is the part of depsite that separates it from a shell script with the same goal.

If you write a Bash script that does the same six steps — write config, symlink, test, reload, certbot, register — and step 4 fails, you are now in a broken state. Nginx might have a config file pointing to a port that doesn't exist. The symlink is in place but pointing at invalid config. The next person who reloads nginx (or the next time the server reboots) takes the entire web stack down.

Depsite handles this with an explicit state machine and a rollback log. Every step that mutates state (writing a file, creating a symlink, running a command) is recorded. If a later step fails, depsite walks the rollback log backwards, undoing each previous mutation in reverse order. The result: nginx is restored to a known-good state.

This also means partial failures are recoverable. If certbot fails because your DNS hasn't propagated yet — a very common issue for new deployments — depsite removes the nginx config, removes the symlink, leaves nginx in its original state, and tells you exactly what went wrong. You fix the DNS, rerun the deployment, and you're back in business.

§ 07

#Snapshot-Tested Nginx Configs (And Why You Should Care)

Most "deploy script" tools have a fundamental quality problem: their config templates are tested by the developer running the tool, observing the result, and going "looks fine to me." The next time someone tweaks the template — adds a flag, changes a default — the test is "did the tool not crash?"

Depsite locks down every nginx config it generates with snapshot tests. Every flag combination — with SSL, without SSL, with WebSockets, without WebSockets, custom body size, custom timeouts — is tested by generating the config and comparing it byte-for-byte against a committed snapshot.

bash
bun test --coverage

The repo runs over 100 tests on every commit. If a refactor accidentally changes the rendered nginx output, the snapshot test fails immediately. A regression cannot ship without a developer explicitly approving the change.

For users this means: the nginx config you got from depsite v2.0.1 is the exact same nginx config every other user on v2.0.1 got, and it is the exact same config that was reviewed and approved when v2.0.1 was released.

§ 08

#Comparing Depsite to Manual Nginx Deployment

Here is the side-by-side comparison most developers searching for "how to deploy Node.js with nginx" will appreciate:

Step

Manual Approach

Depsite

Write nginx config

Copy from a tutorial, edit by hand

Generated automatically

Choose security headers

Read multiple guides, decide

Hardened defaults included

Set proxy headers correctly

Get them wrong twice

Correct on first try

Symlink to sites-enabled

sudo ln -sf ...

Automatic

Validate config

sudo nginx -t

Automatic

Reload nginx

sudo systemctl reload nginx

Automatic

Install certbot

sudo apt install certbot python3-certbot-nginx

Detected, you install

Provision certificate

Run certbot, answer prompts

Automatic

Set up auto-renewal

Configure cron / systemd timer

Auto-handled by certbot

Track what's deployed

ls /etc/nginx/sites-enabled/

depsite list

Recover from failure

Manually undo every step

Automatic rollback

Total time

20–45 minutes

~12 seconds

Total mistakes

1–3 per deployment

0

The most underrated benefit is the last row. A manual deployment introduces opportunities for typos, missed steps, and inconsistent configs across servers. A scripted deployment eliminates these as a category.

§ 09

#Comparing Depsite to Other Deployment Tools

Depsite is not the only tool in the Node.js deployment space, but it occupies a specific niche. Here is how it compares to the most common alternatives:

Versus PM2. PM2 is a process manager — it keeps your Node.js process running, restarts on crash, manages logs. Depsite is a deployment tool — it puts nginx in front of your already-running process. They complement each other: most production setups use PM2 to run the app and depsite to expose it via nginx + SSL.

Versus Docker / Docker Compose. Docker is a containerization platform. If you're running containerized apps with Traefik or nginx-proxy, depsite is not what you need. If you're running plain Node.js processes (with PM2, systemd, or forever) on a VPS, depsite is exactly what you need.

Versus a hand-rolled shell script. This is depsite's most direct competitor — many teams have a deploy.sh that does roughly what depsite does. The differences: depsite has snapshot-tested configs, automatic rollback, a real registry, JSON output, and DNS pre-flight checks. A shell script gets you 80% of the way there and breaks in the remaining 20% at 2am.

Versus Vercel / Netlify / Railway. Hosted platforms like Vercel handle the deployment for you, but they're for specific use cases (mostly frontend / static / serverless). If you're running a stateful Node.js backend on your own VPS — for cost, control, regulatory, or technical reasons — you still need to handle nginx. Depsite is for the self-hosted-VPS path.

Versus Caddy. Caddy is a competing reverse proxy with built-in automatic HTTPS. If you're starting greenfield and have no nginx attachment, Caddy is fantastic. Depsite is for teams already invested in nginx (existing configs, ops familiarity, or because nginx is mandated by the stack). It makes the nginx path nearly as friction-free as Caddy's.

§ 10

#Troubleshooting Common Issues

The most common deployment failures and how depsite handles them:

#"Domain doesn't resolve to this server"

When you ask depsite for SSL, it does a DNS pre-flight check first. If your domain doesn't resolve to the server's IP, depsite refuses to call certbot. This avoids burning the Let's Encrypt rate limit (5 failures per hour, per domain), which can lock you out for an hour.

bash
# Skip the DNS check if you know what you're doing
depsite deploy --skip-dns ...

#"I want to test SSL setup without hitting rate limits"

Use the staging endpoint while iterating:

bash
depsite deploy --ssl --ssl-staging ...

The staging cert is not trusted by browsers, but it lets you test the full provisioning flow without touching the production rate limit. Once your config works in staging, drop --ssl-staging and rerun for a real cert.

#"My app uses WebSockets and they're not working"

bash
depsite deploy --websockets ...

This adds the WebSocket upgrade headers (Upgrade, Connection) to the nginx config. Without it, WebSocket connections get downgraded to plain HTTP and fail.

#"Certbot is hanging waiting for the app to be reachable"

By default, depsite's nginx config proxies traffic to your app on the configured port. If your app isn't running yet, certbot's HTTP-01 challenge can fail because nginx returns 502s. Use --require-running to make depsite check that something is listening on the port before deploying:

bash
depsite deploy --require-running ...

#"I want to redeploy without affecting the running cert"

Depsite v2 lets certbot own the HTTPS block. Rerunning depsite deploy for an existing site updates only the HTTP block — the cert and HTTPS server block stay intact. To force a complete regenerate:

bash
depsite deploy --force ...
§ 11

#Production-Ready Workflows

A few patterns that work well in real production setups:

#CI/CD with GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy to production
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /var/www/api
            git pull
            npm ci --production
            pm2 reload api
            depsite deploy \
              --name api \
              --domain api.example.com \
              --port 3000 \
              --ssl --email ${{ secrets.SSL_EMAIL }} \
              --non-interactive --yes --json

#Multi-Service Monorepo

bash
# In your monorepo root, deploy all services in parallel
parallel -j 4 depsite deploy --config {} --non-interactive --yes \
  ::: services/*/depsite.config.json

#Scheduled Cert Renewals (Belt-and-Suspenders)

certbot already auto-renews, but if you want explicit control:

cron
0 3 * * * /usr/local/bin/depsite renew --json --quiet >> /var/log/depsite-renew.log 2>&1

#Inventory Audits

Run on every server to see what's deployed:

bash
depsite list --json | jq '.sites[] | {name, domain, ssl}'
§ 12

#Migrating From Depsite v1 to v2

If you're on depsite v1 and considering the upgrade, here are the breaking changes:

  • Runtime change. v2 runs on Bun (or as a precompiled binary). Node.js is no longer required at runtime. The npm install still works for development, but the binary is the recommended production install.
  • Stricter site removal. depsite remove now requires the exact slug. Run depsite list first to see your sites.
  • HTTPS block ownership. v1 wrote its own HTTPS server block. v2 lets certbot own that block to avoid drift. Existing v1 sites continue to work, but rerunning depsite deploy --force will regenerate them with the v2 layout.
  • New flags. --ssl-staging, --websockets, --require-running, and --skip-dns are all new in v2.

For most users the upgrade path is:

bash
# Update
npm install -g depsite@latest
# Or replace the binary
curl -fsSL https://raw.githubusercontent.com/kemora13conf/depsite/master/scripts/install.sh | bash

# Audit existing sites
depsite list

# Optionally re-deploy each one with v2's config
depsite deploy --force --name api --domain ... --port ... --ssl --email ...
§ 13

#Contributing and Roadmap

Depsite is open-source under the MIT license. The repo lives at github.com/kemora13conf/depsite.

To contribute:

bash
git clone https://github.com/kemora13conf/depsite
cd depsite
bun install

bun run dev -- --help        # run from source
bun test --coverage          # 100+ tests, snapshot-locked nginx output
bun run typecheck
bun run lint                 # biome
bun run build                # bundle to dist/
bun run build:bin            # compile single Linux x64 binary

The codebase is intentionally structured for contributions:

plain
src/
├── bin/depsite.ts          Commander entry
├── commands/               One file per subcommand
├── core/                   Pure logic — template, workflow, errors, schemas
├── services/               I/O — shell, nginx, certbot, dns, registry, fs
├── ui/                     log, prompts, spinner, summary, banner
├── utils/                  sanitize, Result<T, E>
└── config/                 constants

tests/                      bun:test, mirroring src/

Pull requests welcome. The maintainer is particularly interested in: support for additional reverse proxies (Caddy, Traefik), Windows compatibility (Windows Server scenarios), more granular cert management, and integration recipes for popular CI providers.

§ 14

#References

[1] depsite on npm. (2026). https://www.npmjs.com/package/depsite

[2] depsite on GitHub. (2026). https://github.com/kemora13conf/depsite

[3] DigitalOcean. NGINX as Reverse Proxy for Node or Angular Application. https://www.digitalocean.com/community/tutorials/nginx-reverse-proxy-node-angular

[4] LogRocket Blog. How to Use Nginx as a Reverse Proxy for a Node.js Server. https://blog.logrocket.com/how-to-run-node-js-server-nginx/

[5] Better Stack Community. How to Configure Nginx as a Reverse Proxy for Node.js Applications. https://betterstack.com/community/guides/scaling-nodejs/nodejs-reverse-proxy-nginx/

[6] Let's Encrypt. Rate Limits Documentation. https://letsencrypt.org/docs/rate-limits/

[7] Certbot User Guide. certbot --nginx Plugin. https://eff-certbot.readthedocs.io/en/stable/using.html#nginx

[8] nginx Documentation. Module ngx_http_proxy_module. https://nginx.org/en/docs/http/ngx_http_proxy_module.html

[9] Mozilla Web Security. HTTP Strict Transport Security. https://infosec.mozilla.org/guidelines/web_security#http-strict-transport-security

[10] @clack/prompts. Beautiful CLI prompts library. https://www.npmjs.com/package/@clack/prompts

Enjoyed this piece?

Be the first to comment

No comments yet. Start the conversation.