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:
- PM2 is your app’s bodyguard. It starts your app, restarts it if it crashes, and wakes it up if your server reboots.
- Caddy is the receptionist. It receives all incoming traffic, handles HTTPS certificates automatically, and forwards requests to PM2.
- 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:
- A Node.js app — Express, NestJS, Next.js, Hono, Fastify, AdonisJS, whatever you built
- 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.
- 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:
| Field | Value |
|---|---|
| Type | A |
| Name | @ (or your subdomain, like api) |
| Value | YOUR_SERVER_IP |
| TTL | Auto |
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:
- Node.js LTS — installed via NodeSource
- PM2 — installed globally
- 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.jsonand lock file- Build output (if applicable)
What stays local and gets excluded:
node_modules— rebuilt on the server.envand.env.*— managed separately for security.git/— not needed in productionshipnode.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
##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:
- New code lands in a timestamped folder:
releases/20260408143022/ currentsymlink atomically switches to the new folder- PM2 reloads without dropping any requests
- 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.