shiphound 1.0.1
dotnet tool install --global shiphound --version 1.0.1
dotnet new tool-manifest
dotnet tool install --local shiphound --version 1.0.1
#tool dotnet:?package=shiphound&version=1.0.1
nuke :add-package shiphound --version 1.0.1
ShipHound
Deploy full-stack apps to your VPS in one command.
ShipHound is a free, open-source CLI tool that automates the entire deployment pipeline: build locally, upload via SSH, configure Caddy reverse proxy with auto-HTTPS, set up systemd services, and go live.
Supports .NET and Node.js/Express backends with multi-service deployments. No Docker. No cloud vendor lock-in.
shiphound deploy
Features
- One-Command Deploy -
shiphound deployhandles everything - Multi-Service Support - Deploy multiple backend services (API + microservices) in one config
- Multi-Backend Support - .NET and Node.js/Express backends (or mix both)
- Zero-Downtime Deployments - Symlink-based switching for instant updates
- Auto-HTTPS - Caddy handles SSL certificates automatically via Let's Encrypt
- Automatic VPS Setup -
shiphound setupinstalls Caddy, .NET, Node.js as needed - Auto-Restart - Systemd ensures your app restarts on crash or server reboot
- Automatic Rollback - Failed deployments restore the previous version
- Health Checks - Verifies your app responds after deployment
- Pre-flight Checks - Validates SSH, disk space, and required software before deploying
- Path-Based Routing - Multiple services sharing a domain with URL path routing
- Per-Service Environment Variables - Global env vars with per-service overrides
- Additional Paths - Deploy extra folders alongside your app (templates, seed data, etc.)
- Deployment History - Local and remote deployment logging with git commit tracking
Prerequisites
- .NET 8 SDK or later
- A VPS running Ubuntu 22.04+ or Debian 11+ with SSH key access
Installation
dotnet tool install -g shiphound
Verify the installation:
shiphound --help
Update
dotnet tool update -g shiphound
Uninstall
dotnet tool uninstall -g shiphound
Quick Start
1. Initialize Your Project
cd /path/to/your/project
shiphound init
This creates a shiphound.yaml config file in your project root.
2. Edit shiphound.yaml
project: my-app
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./frontend
domain: myapp.com
build: npm run build
output: dist
backend:
path: ./backend
port: 5000
domain: api.myapp.com
env:
DATABASE_URL: "postgres://localhost/mydb"
3. Setup Your VPS (First Time Only)
shiphound setup
This automatically installs Caddy, .NET runtime, Node.js, and configures directories and permissions.
4. Deploy
shiphound deploy
Your app is now live at https://myapp.com with automatic HTTPS.
Commands
| Command | Description |
|---|---|
shiphound init |
Create a new shiphound.yaml config file |
shiphound validate |
Validate your configuration |
shiphound setup |
Set up VPS (installs Caddy, .NET/Node.js as needed) |
shiphound setup --dry-run |
Preview what setup would install |
shiphound deploy |
Deploy your app to VPS |
shiphound deploy --dry-run |
Preview deployment without executing |
shiphound deploy --skip-health-check |
Deploy without HTTP health checks |
shiphound status |
Check app health and URLs |
shiphound ps |
List all deployed apps on VPS |
shiphound logs |
View application logs |
shiphound logs -f |
Follow logs in real-time |
shiphound logs --service api |
View logs for a specific service |
shiphound logs --service caddy |
View Caddy access logs |
shiphound history |
View deployment history |
shiphound diagnose |
Run diagnostic checks on VPS |
shiphound debug |
Debug deployment file structure and config |
shiphound rollback |
Roll back to a previous deployment |
Configuration Examples
Frontend Only (Static Site)
project: my-portfolio
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./
domain: portfolio.com
build: npm run build
output: dist
env: {}
.NET Backend Only
project: my-api
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
backend:
path: ./
port: 5000
domain: api.myapp.com
env:
DATABASE_URL: "postgres://localhost/db"
Node.js/Express Backend Only
project: my-express-api
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
backend:
type: node
path: ./
port: 3000
domain: api.myapp.com
entry: index.js
env:
DATABASE_URL: "postgres://localhost/db"
Node.js with TypeScript
project: my-ts-api
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
backend:
type: node
path: ./
port: 3000
domain: api.myapp.com
build: "npm run build"
output: dist
entry: index.js
env:
DATABASE_URL: "postgres://localhost/db"
Full-Stack (React + .NET)
project: my-fullstack
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./frontend
domain: myapp.com
build: npm run build
output: dist
backend:
path: ./backend
port: 5000
domain: api.myapp.com
env:
DATABASE_URL: "postgres://localhost/db"
Full-Stack (React + Node.js)
project: my-fullstack
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./frontend
domain: myapp.com
build: npm run build
output: dist
backend:
type: node
path: ./backend
port: 3000
domain: api.myapp.com
build: "npm run build"
output: dist
entry: index.js
env:
DATABASE_URL: "postgres://localhost/db"
Multi-Service Deployment
Deploy multiple backend services with separate domains:
project: my-platform
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./frontend
domain: myapp.com
build: npm run build
output: dist
services:
- name: api
path: ./backend
port: 5000
domain: api.myapp.com
type: dotnet
healthCheckPath: /health
- name: notifications
path: ./notification-service
port: 5001
domain: notifications.myapp.com
type: node
entry: index.js
env:
SMTP_HOST: "smtp.example.com"
env:
DATABASE_URL: "postgres://localhost/db"
Multi-Service with Path-Based Routing
Multiple services sharing the same domain:
project: my-platform
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./frontend
domain: myapp.com
build: npm run build
output: dist
services:
- name: api
path: ./backend
port: 5000
domain: api.myapp.com
type: dotnet
- name: notifications
path: ./notification-service
port: 5001
domain: api.myapp.com
domainPath: /notifications/*
type: node
entry: index.js
env:
DATABASE_URL: "postgres://localhost/db"
Additional Paths
Deploy extra files/folders alongside your app:
project: my-app
vps:
host: 123.45.67.89
user: deploy
sshKey: ~/.ssh/id_rsa
frontend:
path: ./frontend
domain: myapp.com
build: npm run build
output: dist
backend:
type: node
path: ./backend
port: 3000
domain: api.myapp.com
entry: index.js
additionalPaths:
- source: ./templates
destination: templates
exclude:
- node_modules
- .git
- source: ./seed-data.json
destination: data/seed.json
env:
DATABASE_URL: "postgres://localhost/db"
Configuration Reference
# Project name (used for service names and directories)
project: my-app
# VPS connection settings
vps:
host: 123.45.67.89 # VPS IP or hostname
user: deploy # SSH user with sudo access
sshKey: ~/.ssh/id_rsa # Path to SSH private key
# Runtime versions (installed by 'shiphound setup')
dotnetVersion: "8.0" # .NET runtime version (e.g., "8.0", "9.0")
nodeVersion: "20" # Node.js major version (e.g., "20", "22")
# Frontend configuration (optional)
frontend:
path: ./frontend # Path to frontend directory
domain: myapp.com # Domain for frontend
build: npm run build # Build command
output: dist # Build output directory
healthCheckPath: / # Health check path (default: /)
# Backend configuration (optional, use this OR services, not both)
backend:
type: dotnet # "dotnet" (default) or "node"
path: ./backend # Path to backend directory
port: 5000 # Port your app listens on
domain: api.myapp.com # Domain for backend API
healthCheckPath: /health # Health check endpoint
# Node.js only options:
build: "npm run build" # Build command (optional)
output: dist # Output directory (optional)
entry: index.js # Entry point file (required for Node.js)
# Multi-service configuration (optional, use this OR backend, not both)
services:
- name: api # Unique service name (used in systemd unit name)
type: dotnet # "dotnet" (default) or "node"
path: ./backend # Path to service source
port: 5000 # Port the service listens on
domain: api.myapp.com # Domain (optional, omit for internal-only services)
domainPath: /api/* # Path prefix for shared-domain routing (optional)
healthCheckPath: /health
build: "dotnet publish -c Release -o ./publish" # Custom build command (optional)
entry: index.js # Node.js entry point (required for node type)
output: dist # Build output directory (optional)
env: # Per-service env vars (merged with global, per-service wins)
SERVICE_KEY: "value"
# Additional paths to deploy (optional)
additionalPaths:
- source: ./templates # Local path (file or directory)
destination: templates # Remote path relative to deploy directory
exclude: # Directories to skip (optional)
- node_modules
- .git
# Environment variables (injected into all systemd services)
# Use single quotes for values containing special characters
# Example: ConnectionStrings__Default: 'Host=localhost;Password="secret"'
env:
DATABASE_URL: "postgres://localhost/db"
NODE_ENV: "production"
How It Works
1. PRE-FLIGHT CHECKS
- Test SSH connection
- Check disk space
- Verify required runtimes (.NET/Node.js)
- Validate DNS records
2. BUILD (Local)
- npm run build (frontend)
- dotnet publish / npm build (each service)
3. BACKUP
- Create timestamped backup of current deployment
4. UPLOAD
- Upload files via SFTP to new deploy directory
- Upload additional paths (with exclude support)
- For Node.js: runs npm install on VPS
5. CONFIGURE
- Generate Caddy config (reverse proxy + HTTPS)
- Generate systemd service per backend service
6. ACTIVATE
- Update current symlink to new deploy
- Restart services
- Verify services stay running (3s stability check)
- Reload Caddy
7. HEALTH CHECK
- Verify each service responds via localhost
- Rollback automatically if any check fails
VPS Directory Structure
After deployment, your VPS has this structure:
/var/www/{project}/
├── current/ -> symlink to deploys/{timestamp}
├── deploys/
│ └── 20240115-143052/
│ ├── frontend/
│ ├── api/
│ └── notifications/
├── backups/
│ └── 20240115-140000/
└── .shiphound/
Health Checks
After deployment, ShipHound verifies your app is running by sending HTTP requests to each service via SSH (curl on localhost). A response with status code 200-399 is considered healthy. If health checks fail, ShipHound automatically rolls back to the previous deployment.
By default, ShipHound checks the root path (/). Most APIs don't serve anything at /, which causes a false failure. You should either:
- Set a
healthCheckPaththat returns a 200 response:
backend:
path: ./backend
port: 5000
domain: api.myapp.com
healthCheckPath: /health
- Skip health checks if your app has no health endpoint:
shiphound deploy --skip-health-check
The service stability check (verifies the process stays running via systemd) always runs regardless of the --skip-health-check flag.
Health check behavior:
- Waits 2 seconds after service start before first check
- Retries up to 3 times with exponential backoff
- Shows which URL is being checked and the attempt number
- On failure, shows the HTTP status code returned
- Automatically rolls back on failure
VPS Requirements
- Ubuntu 22.04+ or Debian 11+
- SSH access with key authentication
- Passwordless sudo configured
To enable passwordless sudo:
ssh your-user@your-vps
echo "$USER ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/$USER
Everything else (Caddy, .NET, Node.js) is installed automatically by shiphound setup.
Troubleshooting
SSH Connection Failed
# Test SSH manually
ssh -i ~/.ssh/id_rsa deploy@your-vps-ip
# Check SSH key permissions (Linux/Mac)
chmod 600 ~/.ssh/id_rsa
Backend Won't Start
# Check service logs
shiphound logs --service api
# Or SSH and check directly
ssh deploy@your-vps
sudo journalctl -u my-app-api -f
Caddy/HTTPS Issues
# Check Caddy logs
shiphound logs --service caddy
# Verify DNS points to your VPS
nslookup yourdomain.com
Deployment Failed
ShipHound automatically rolls back on failure. Check the error message and logs:
shiphound diagnose
shiphound logs
shiphound debug
Service Crash-Looping
If a service starts but crashes immediately, ShipHound detects this during the stability check and shows the last 15 lines of logs. Common causes:
- Missing environment variables
- Wrong port configuration
- Database connection issues
- Missing dependencies
Wrong .NET / Node.js Version
If your app targets a different runtime version than what's installed on the VPS:
dotnetVersion: "9.0"
nodeVersion: "22"
Then run shiphound setup again to install the correct version. Preflight checks will block deploys if the required version isn't found.
Health Check Fails But App Is Running
If the stability check passes but health check fails, your app is running but not responding on the checked path. By default ShipHound checks / which many APIs don't serve.
Fix: Set healthCheckPath to an endpoint that returns HTTP 200, or use --skip-health-check.
Environment Variables Not Working
YAML has special characters that can break parsing. If your env values contain quotes, colons, or special characters, use single quotes:
env:
# BAD - nested double quotes break YAML:
ConnectionStrings__Default: "Host=localhost;Password="secret""
# GOOD - single quotes handle inner quotes:
ConnectionStrings__Default: 'Host=localhost;Password="secret"'
Check the generated service file on your VPS:
cat /etc/systemd/system/{project}-{service}.service
FAQ
Q: What backend frameworks are supported?
A: .NET and Node.js/Express. Set type: node for Node.js backends.
Q: Can I deploy multiple services?
A: Yes. Use the services: config instead of backend: to define multiple named services, each with their own port, domain, and runtime type.
Q: Do I need Docker? A: No. ShipHound deploys apps natively on Linux using systemd and Caddy.
Q: How does Node.js deployment work?
A: ShipHound uploads your code (excluding node_modules), runs npm install --production on the VPS, and creates a systemd service.
Q: Will my app restart if it crashes?
A: Yes. The systemd service is configured with Restart=always and starts on boot.
Q: What happens if deployment fails? A: ShipHound automatically restores your previous version from the backup directory.
Q: Can services share a domain?
A: Yes. Use domainPath on services to route different URL paths to different services on the same domain.
Q: Can I deploy extra files like templates or seed data?
A: Yes. Use additionalPaths to deploy any extra files or directories alongside your app, with optional exclude patterns.
Q: What VPS providers work with ShipHound? A: Any provider that gives you a Linux VPS with SSH access: DigitalOcean, Hetzner, Linode, Vultr, AWS Lightsail, etc.
Q: Can I use ShipHound with a monorepo?
A: Yes. Point frontend.path and each service path to the correct subdirectory within your repo.
Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
git clone https://github.com/CydoEntis/ShipHound.git
cd ShipHound
dotnet build
dotnet test
License
Deploy with confidence. Ship with ShipHound.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.