back to home

Beginner Guide: How to Deploy a Node.js App with ShipNode

##Introduction

You just vibecoded your Next.js app or Express API. It’s 2AM. You want it live on the internet — with your domain, HTTPS, zero downtime — right now.

Traditional deployment is painful. Docker takes hours to learn. Kubernetes is overkill for a single app. Heroku and Vercel lock you in. Every other tool assumes you already know how servers work.

ShipNode was built for exactly this moment.

ShipNode deploys your Node.js app to your own server over SSH. No containers. No complex config. No vendor lock-in. Just your code, running on a server you own, accessible to the world.

Here’s what ShipNode sets up on your server automatically:

  • Node.js — runs your app
  • PM2 — keeps your app alive even after crashes or server reboots
  • Caddy — handles your domain AND automatic HTTPS via Let’s Encrypt

One command to remember:

shipnode init && shipnode deploy

That’s it. Let’s walk through every step.


##What ShipNode Sets Up on Your Server

Before we touch anything, let’s understand what you’re building. Here’s the full architecture:

Three layers, explained simply:

  1. PM2 is your app’s bodyguard. It starts your app, restarts it if it crashes, and wakes it up if your server reboots.
  2. Caddy is the receptionist. It receives all incoming traffic, handles HTTPS certificates automatically, and forwards requests to PM2.
  3. Node.js runs your actual code.

Why no Docker? ShipNode connects directly to your server over SSH and runs your code natively. No layers to manage, no images to build.


##What You Need Before Starting

You need three things:

  1. A Node.js app — Express, NestJS, Next.js, Hono, Fastify, AdonisJS, whatever you built
  2. A domain name — pointed to your server (A record). You can deploy without one first, but HTTPS only works when you have a domain. Point your domain early to avoid reconfiguration later.
  3. A server — Ubuntu or Debian, with SSH access. Any provider works: DigitalOcean, Hetzner, Vultr, Linode. A basic VPS ($6/month) is more than enough.

###Pointing Your Domain to Your Server

If you bought a domain from Namecheap, GoDaddy, or Cloudflare, you need to create an A record that points to your server’s IP address:

FieldValue
TypeA
Name@ (or your subdomain, like api)
ValueYOUR_SERVER_IP
TTLAuto

Propagation takes a few minutes to a few hours. You can continue with the setup while waiting.

###Setting Up SSH Keys (One Time)

If you haven’t connected to your server via SSH before, you need an SSH key:

ssh-keygen -t ed25519 -C "your_email@example.com"
ssh-copy-id root@YOUR_SERVER_IP

This creates a key, then copies it to your server. After this, you can connect without typing a password.


##Install ShipNode

ShipNode is a single binary installed on your laptop. No npm, no Docker, no Ruby.

Open your terminal and run:

curl -fsSL https://github.com/devalade/shipnode/releases/latest/download/shipnode-installer.sh | bash

That’s it. Verify it installed:

shipnode --version

You’ll see something like ShipNode v1.2.3.

To uninstall later: rm -rf ~/.shipnode


##Initialize Your Project

Go into your Node.js project and run the init wizard:

cd your-node-app
shipnode init

The wizard asks you a few questions. Press Enter to accept the defaults — ShipNode auto-detects your framework and suggests sensible settings.

Here’s what the wizard asks:

Application type:
  1) Backend (Node.js API with PM2)
  2) Frontend (Static site)
  [detected: backend]

SSH user [root]:
SSH host (IP or hostname): 203.0.113.10
SSH port [22]:

Remote deployment path [/var/www/myapp]:
PM2 process name [myapp]:
Application port [3000]:

Domain (optional): api.yourdomain.com

After you confirm, ShipNode creates shipnode.conf in your project root. It looks like this:

APP_TYPE=backend
SSH_USER=root
SSH_HOST=203.0.113.10
SSH_PORT=22
REMOTE_PATH=/var/www/myapp
PM2_APP_NAME=myapp
BACKEND_PORT=3000
DOMAIN=api.yourdomain.com
HEALTH_CHECK_ENABLED=true
HEALTH_CHECK_PATH=/health
ZERO_DOWNTIME=true

Pro tip: If your server uses a different port for SSH, change SSH_PORT from 22 to your port.


##Prepare Your Node.js App

ShipNode needs a /health endpoint on your app. This is how it knows your app is running correctly after each deployment.

Add this to your Express app:

// app.js
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

For NestJS, add a health controller:

// health.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('health')
export class HealthController {
  @Get()
  check() {
    return { status: 'ok' };
  }
}

That’s it. If your app already has a health endpoint at a different path, update shipnode.conf:

HEALTH_CHECK_PATH=/api/health

##Set Up Your Server

Now you install ShipNode’s stack on your server. This runs once per server:

shipnode setup

What happens during setup:

  1. Node.js LTS — installed via NodeSource
  2. PM2 — installed globally
  3. Caddy — installed and configured as a systemd service

The process takes 2 to 5 minutes depending on your server’s speed.

Pro tip: If you see any errors during setup, run shipnode doctor for diagnostics.


##Deploy

Everything is configured. Time to ship:

shipnode deploy

Here’s what happens step by step:

What gets synced to your server:

  • Your source code
  • package.json and lock file
  • Build output (if applicable)

What stays local and gets excluded:

  • node_modules — rebuilt on the server
  • .env and .env.* — managed separately for security
  • .git/ — not needed in production
  • shipnode.conf — contains server credentials

If everything goes well, you’ll see:

✓ Deployed to production
→ Health check passed (1 attempt, 23ms)
→ App live at https://api.yourdomain.com

Your app is now live on the internet, with HTTPS automatically handled by Caddy and Let’s Encrypt.

###Interactive: Deployment Flow

Watch exactly what happens when you run shipnode deploy. Each step is visualized below — click “Run deployment” to see it in action, or try “Health check fails” to see the rollback mechanism in the event of a failure.

What happens when you run shipnode deploy

1
Acquire lockPrevents concurrent deploys
2
Sync filesRsync to timestamped release folder
3
Install depsnpm install on server
4
Build appRun build script if present
5
Switch symlinkZero-downtime atomic switch
6
Reload PM2Graceful reload, zero requests dropped
7
Health checkVerify /health endpoint

##Check It’s Live

Use ShipNode’s status command to verify your app is healthy:

shipnode status

You’ll see a dashboard showing:

  • App name and status
  • Uptime
  • Memory usage
  • CPU usage
  • Current deployment timestamp

To stream live logs:

shipnode logs

To check real-time CPU and memory:

shipnode metrics

##Zero-Downtime Magic

When ShipNode deploys, it never replaces your live app. It uses atomic symlink switching:

Releases
v1.2
Apr 02
active
v1.1
Mar 22
v1.0
Mar 15
Users
happy
Click deploy to create a new release
  1. New code lands in a timestamped folder: releases/20260408143022/
  2. current symlink atomically switches to the new folder
  3. PM2 reloads without dropping any requests
  4. If health check fails within 30 seconds, ShipNode reverts the symlink to the previous release

Zero downtime. Zero dropped requests. You can deploy at midnight without anyone noticing.


##Rollback (Just in Case)

Something went wrong? Rollback to the previous release instantly:

shipnode rollback

To see all deployments with their timestamps:

shipnode releases

To rollback two releases back:

shipnode rollback 2

Rollback never fails. ShipNode keeps the previous release intact until the next successful deployment.


##Next Steps

You’ve deployed to production. Here’s where to go from here:

###CI/CD with GitHub Actions

Automate your deployments so every git push triggers a deploy:

shipnode ci github

This generates a GitHub Actions workflow file. You’ll need to add your server’s SSH key as a GitHub secret.

###Multi-Environment Deployments

Deploy to a staging environment alongside production:

shipnode deploy --profile staging

This uses shipnode.staging.conf instead of shipnode.conf. Separate config for separate servers.

###Customizing PM2 and Caddy

Eject the auto-generated templates and customize them:

shipnode eject

This creates editable templates in .shipnode/templates/:

  • ecosystem.config.cjs — PM2 config (cluster mode, memory limits)
  • Caddyfile.caddy — Caddy config (rate limiting, custom headers)

Ejected templates are preserved across deploys.


##Conclusion

You deployed to production in under 5 minutes. No Docker. No Kubernetes. No vendor lock-in. Your app is live at https://api.yourdomain.com with HTTPS automatically enabled.

Here’s the full workflow recap:

# 1. Install ShipNode (once)
curl -fsSL https://github.com/devalade/shipnode/releases/latest/download/shipnode-installer.sh | bash

# 2. Go into your project
cd your-node-app

# 3. Initialize (creates shipnode.conf)
shipnode init

# 4. Set up server (once per server)
shipnode setup

# 5. Deploy
shipnode deploy

That’s it. ShipNode handles the rest — keeping your app running, renewing HTTPS certificates, and rolling back automatically if something breaks.

For more details on configuration, hooks, and advanced features, check out the ShipNode documentation.