@jai47/hj2
v0.3.3
Published
Hyperion Just-in-time Journey — CLI-first VPS helper: nginx, Certbot, and Node/static site publishing on Ubuntu/Debian
Maintainers
Readme
hj2
CLI-first VPS deployment engine for Ubuntu/Debian.
Deploy Node.js and static applications to any VPS with automatic Nginx configuration, Let's Encrypt TLS, systemd process management, versioned releases, blue-green deployments, and instant rollback — all from a single CLI.
npm i -g @jai47/hj2
cd my-project
hj2 init --domain app.example.com
sudo hj2 deploy --email [email protected]Your app is now live at https://app.example.com with HTTPS, reverse proxy, systemd supervision, and rollback support.
Table of Contents
- Install
- Quick Start
- How It Works
- Commands
- Configuration (
hj2.yml) - Production Features
- Deployment Strategies
- Server Filesystem Layout
- Requirements
- License
Install
npm i -g @jai47/hj2This installs the hj2 binary globally. Requires Node.js ≥ 18.
Quick Start
Static site (Vite, Next.js export, plain HTML)
cd my-vite-app
hj2 init --domain www.example.com
sudo hj2 deploy --email [email protected]Node.js app (Express, Fastify, Next.js server, etc.)
cd my-api
hj2 init --domain api.example.com
sudo hj2 deploy --email [email protected]One-shot proxy or static publish (no hj2.yml needed)
# Reverse proxy to a running app on port 3000
sudo hj2 publish --domain api.example.com --port 3000 --email [email protected]
# Serve a static directory
sudo hj2 publish --domain www.example.com --static ./dist --email [email protected]How It Works
When you run hj2 deploy, the engine follows this pipeline:
Source → Build → Release → Start → Health Check → Proxy → TLS → Activate- Detect — reads
hj2.ymlfor runtime, build, and start commands. - Copy — rsyncs project files into a versioned release directory.
- Install — runs
npm ci --omit=dev(ornpm install) for Node.js projects. - Build — executes the configured build command.
- Release — creates a timestamped release under
/var/lib/hj2/apps/<name>/releases/. - Start — writes a hardened systemd unit (dedicated system user, sandboxing) and starts the process via a wrapper script (Node.js apps) or copies built output to a web root (static apps).
- Health check — polls the configured health endpoint until it responds HTTP 200.
- Proxy — writes an Nginx server block with reverse proxy (or static file serving).
- TLS — requests a Let's Encrypt certificate via Certbot.
- Activate — symlinks
current → releases/<id>and records the deployment.
If any step fails and autoRollback is enabled (default), the previous release is automatically restored.
Commands
Setup & Publish (Phase 1)
Quick commands for simple Nginx + Certbot site management without a full deployment pipeline.
hj2 setup
Install and enable Nginx, Certbot, and the Certbot Nginx plugin. Run once on a fresh server.
sudo hj2 setuphj2 publish
Publish a domain as a reverse proxy or static site in a single command.
# Reverse proxy
sudo hj2 publish --domain api.example.com --port 3000 --email [email protected]
# Static files
sudo hj2 publish --domain www.example.com --static ./dist --email [email protected]
# HTTP only (skip TLS)
sudo hj2 publish --domain dev.example.com --port 4000 --no-ssl
# Overwrite existing site
sudo hj2 publish --domain api.example.com --port 3001 --email [email protected] --force| Flag | Description |
|---|---|
| -d, --domain <host> | Domain name (required) |
| -p, --port <n> | Local port to reverse-proxy |
| -s, --static <path> | Directory of static files to serve |
| -e, --email <addr> | Email for Let's Encrypt (cached after first use) |
| --no-ssl | Skip Certbot; serve HTTP only |
| -f, --force | Overwrite an existing site for this domain |
hj2 list
List all sites managed by hj2.
hj2 listhj2 remove
Remove a managed site and its Nginx config.
sudo hj2 remove --domain api.example.com
# Keep the TLS certificate
sudo hj2 remove --domain api.example.com --keep-certInit & Deploy
hj2 init
Detect the current project and generate an hj2.yml configuration file.
cd my-project
hj2 initThe init command inspects package.json and project structure to auto-detect:
- Runtime —
nodeorstatic - Build command — from
scripts.build - Start command — from
scripts.start,scripts.start:prod, or falls back tonode server.js/node index.js - Output directory —
dist(Vite) orout(Next.js export) - Port — defaults to
3000for Node.js apps
# Set a domain during init
hj2 init --domain app.example.com
# Overwrite existing hj2.yml
hj2 init --force
# Init a different directory
hj2 init --cwd /path/to/projecthj2 deploy
Build and deploy the app according to hj2.yml.
sudo hj2 deploy
# Override domain
sudo hj2 deploy --domain staging.example.com
# Provide Let's Encrypt email
sudo hj2 deploy --email [email protected]
# Skip TLS
sudo hj2 deploy --no-ssl
# Force blue-green deployment
sudo hj2 deploy --blue-green
# Disable blue-green (stop-then-start)
sudo hj2 deploy --no-blue-green
# Deploy from a different directory
sudo hj2 deploy --cwd /path/to/project| Flag | Description |
|---|---|
| -d, --domain <host> | Override the domain in hj2.yml |
| -e, --email <addr> | Let's Encrypt email |
| --no-ssl | Skip TLS certificate |
| --blue-green | Force blue-green deployment |
| --no-blue-green | Disable blue-green (restart in place) |
| --cwd <path> | Project directory (default: current directory) |
hj2 rollback
Roll back to the previous release. No rebuild required — the previous release directory is reactivated instantly.
sudo hj2 rollback # rollback current project (reads hj2.yml)
sudo hj2 rollback myapp # rollback a specific app by nameApp Lifecycle
hj2 status [app]
Show the current status of an app.
hj2 status
hj2 status myappOutput:
app: myapp
domain: app.example.com
runtime: node
release: 20260827-183200
port: 3000
systemd: activehj2 logs [app]
View journal logs for an app's systemd unit.
hj2 logs myapp
hj2 logs myapp -n 50 # last 50 lineshj2 restart [app]
Restart the app's systemd service.
sudo hj2 restart myapphj2 start [app] / hj2 stop [app]
Start or stop the app's systemd service.
sudo hj2 start myapp
sudo hj2 stop myapphj2 app remove [app]
Remove an app entirely — stops the systemd unit, deletes all releases, and unregisters the app.
sudo hj2 app remove myapphj2 deployment list [app]
List all releases for an app.
hj2 deployment list myappDomains
hj2 domain add <domain>
Add or bind a domain to an app, port, or static directory.
# Bind to a registered app
sudo hj2 domain add app.example.com --app myapp
# Bind to a port
sudo hj2 domain add api.example.com --port 3000 --email [email protected]
# Bind to static files
sudo hj2 domain add docs.example.com --static ./docs --email [email protected]
# Dry run — show what would happen without making changes
sudo hj2 domain add app.example.com --app myapp --dry-run| Flag | Description |
|---|---|
| --app <name> | Bind to a registered app |
| -p, --port <n> | Proxy port |
| -s, --static <path> | Static files directory |
| -e, --email <addr> | Let's Encrypt email |
| --no-ssl | Skip TLS |
| -f, --force | Overwrite existing site |
| --dry-run | Print actions without changing the system |
hj2 domain remove <domain>
Remove a managed domain.
sudo hj2 domain remove app.example.com
# Keep the Nginx site files
sudo hj2 domain remove app.example.com --keep-nginxhj2 domain list
List all managed domains.
hj2 domain listhj2 domain verify <domain>
Check DNS resolution and warn on mismatches.
hj2 domain verify app.example.comEnvironment Variables
Manage per-app environment variables.
# Set a variable
sudo hj2 env set myapp NODE_ENV production
# List all variables
hj2 env list myapp
# Remove a variable
sudo hj2 env remove myapp NODE_ENVSecrets
Secrets are stored base64-encoded with restrictive file permissions (0600), separate from application metadata.
# Set a secret
sudo hj2 secret set myapp STRIPE_SECRET_KEY sk_live_xxx
# List secret keys (values are never printed)
hj2 secret list myapp
# Remove a secret
sudo hj2 secret remove myapp STRIPE_SECRET_KEYServer Management
hj2 server provision
SSH into a remote server and install Nginx, Certbot, and the hj2 directory structure.
hj2 server provision --host 1.2.3.4
hj2 server provision --host 1.2.3.4 --user deploy
# Dry run — print the remote provisioning script without executing
hj2 server provision --host 1.2.3.4 --dry-runhj2 server harden
Enable UFW firewall and allow ports 22 (SSH), 80 (HTTP), and 443 (HTTPS).
sudo hj2 server harden
# Dry run
sudo hj2 server harden --dry-runhj2 server update
Run apt-get upgrade on the server.
sudo hj2 server updatehj2 server status
Show Nginx systemd status.
hj2 server statushj2 server doctor
Run host health checks (OS, resources, network, proxy, TLS).
hj2 server doctorServices
Install and manage database and cache services via apt.
# Install a service
sudo hj2 service install postgres
sudo hj2 service install redis
sudo hj2 service install mysql
sudo hj2 service install mongodb
# List tracked services
hj2 service list
# Start / stop / remove
sudo hj2 service start postgres
sudo hj2 service stop postgres
sudo hj2 service remove postgresSupported services: postgres, redis, mysql, mongodb.
Backups
Create and manage tar.gz backups of app releases.
# Create a backup
sudo hj2 backup create myapp
# Create and upload to S3
sudo hj2 backup create myapp --s3
# List local backups
hj2 backup list
# Restore a backup
sudo hj2 backup restore <id>
# Configure backup destination
sudo hj2 backup configure --destination /mnt/backups
sudo hj2 backup configure --s3-bucket my-backupsGit Deploy
Clone (or pull) a Git repository and deploy in one step.
sudo hj2 git-deploy [email protected]:user/project.git
# Specify branch, domain, and email
sudo hj2 git-deploy [email protected]:user/project.git \
--branch main \
--domain app.example.com \
--email [email protected]
# Use an existing checkout
sudo hj2 git-deploy [email protected]:user/project.git --cwd /opt/projectGitHub Integration
# Store GitHub token and repo
sudo hj2 connect github --token ghp_xxx --repo user/project
# Print the webhook URL for an app
hj2 connect webhook myappMulti-Server
Manage multiple server profiles for staging/production deployments.
# Add server profiles
hj2 servers add --name production --host 1.2.3.4
hj2 servers add --name staging --host 5.6.7.8 --user deploy --environment staging
# List profiles
hj2 servers list
# Deploy to a specific server (rsyncs project and runs hj2 deploy remotely)
hj2 servers deploy --server production --domain app.example.com
# Remove a profile
hj2 servers remove stagingAPI
Start a local HTTP control API (and optional UI).
hj2 api start # http://127.0.0.1:7422/
hj2 api start --host 0.0.0.0 --port 8080Doctor
Run comprehensive health checks across the host and all managed apps.
hj2 doctorChecks include OS support, CPU/memory/disk, DNS, ports, Nginx status, TLS certificates, runtime availability, process health, and backup recency.
Configuration (hj2.yml)
hj2 init generates this file automatically. Edit it to customize your deployment.
# Generated by hj2 init — edit as needed
name: myapp
runtime: node
domain: app.example.com
build: "npm run build"
start: "npm start"
port: 3000
healthPath: /health
shared:
- database/sqlite
- public/uploads
deploy:
autoRollback: true
env:
NODE_ENV: "production"Fields
| Field | Type | Default | Description |
|---|---|---|---|
| name | string | from package.json | App identifier (used for systemd unit, release dirs) |
| runtime | node | static | auto-detected | Deployment strategy |
| domain | string | — | Domain to serve (can be overridden with --domain) |
| build | string | — | Build command (e.g. npm run build) |
| start | string | npm start | Start command (Node.js apps only) |
| output | string | dist | Static output directory (static apps only) |
| port | number | 3000 | Port for the Node.js process |
| healthPath | string | / | Health check endpoint |
| shared | string[] | — | Relative paths persisted across releases (see Shared persistent volumes) |
| deploy.autoRollback | boolean | true | Automatically rollback on deploy failure |
| env | object | — | Environment variables injected into the systemd unit |
Static site example
name: docs
runtime: static
domain: docs.example.com
build: "npm run build"
output: dist
deploy:
autoRollback: trueNode.js API example
name: api
runtime: node
domain: api.example.com
build: "npm run build"
start: "npm start"
port: 4000
healthPath: /health
deploy:
autoRollback: true
env:
NODE_ENV: "production"Production Features
hj2 includes three production-oriented capabilities for Node.js apps: per-app process isolation, pinned Node versions via fnm, and persistent shared directories that survive deploys and rollbacks.
Process isolation
Every Node.js app runs under its own dedicated system user (hj2-<appname>), not as root. On first deploy, hj2 creates the user and group, assigns ownership of release and shared directories, and writes a hardened systemd unit.
Systemd hardening applied to Node apps:
| Option | Purpose |
|---|---|
| User / Group | Run as hj2-<appname> |
| ProtectSystem=full | Read-only mount for most of /usr, /boot, /etc |
| PrivateTmp=true | Private /tmp for the process |
| NoNewPrivileges=true | Block privilege escalation |
| ReadWritePaths | Allow writes only under the app's release, shared, and metadata trees |
Static sites are served directly by Nginx and do not use systemd isolation.
Inspect the generated unit after deploy:
systemctl cat hj2-myappNode version management
If your project includes a .nvmrc or .node-version file, hj2 uses fnm (Fast Node Manager) to install and activate that version for dependency install, build, and runtime.
Install fnm on the server (once per host):
curl -fsSL https://fnm.vercel.app/install | bash
# reload shell or source ~/.bashrc
fnm --versionPin a version in your repo:
echo "20.11.0" > .nvmrc
# or
echo "20.11.0" > .node-versionDuring hj2 deploy:
- hj2 reads
.nvmrcor.node-versionfrom your project root. - fnm installs the version if missing.
npm ci/npm installand the build command run under that Node version.- A start wrapper (
.hj2-start.sh) sets up fnm before executing yourstartcommand. - The Node version is recorded in release metadata (
release.json).
If no version file is present, hj2 uses the system node on PATH (same as before).
Release metadata and CLI:
hj2 deployment show myapp 20260831-071215
# includes nodeVersion when pinnedShared persistent volumes
Use shared in hj2.yml to keep data across deploys — SQLite databases, user uploads, local caches, etc. Paths are relative to your project root.
name: myapp
runtime: node
domain: app.example.com
start: "node server.js"
port: 3000
shared:
- database/sqlite
- public/uploadsHow it works:
- hj2 creates
/var/lib/hj2/apps/<name>/shared/<path>on the server (once). - Before starting a release, hj2 replaces matching paths inside the release directory with symlinks into
shared/. - Your app reads and writes the same files on every deploy.
- Rollback re-links symlinks so the previous release uses the same persistent data.
Example layout after deploy:
/var/lib/hj2/apps/myapp/
├── shared/
│ ├── database/sqlite/app.db ← persistent
│ └── public/uploads/photo.jpg ← persistent
├── releases/20260831-120000/
│ ├── database/sqlite → ../../shared/database/sqlite
│ └── public/uploads → ../../shared/public/uploads
└── current → releases/20260831-120000Rules:
- Paths must be relative (no
..or absolute paths). - Empty directories in a new release are replaced by symlinks; existing files in
shared/are preserved. - Ownership is set to the app's system user so the process can read and write.
Full Node.js example with all production features:
name: api
runtime: node
domain: api.example.com
build: "npm run build"
start: "node dist/server.js"
port: 4000
healthPath: /health
shared:
- data/sqlite
- storage/uploads
deploy:
autoRollback: true
env:
NODE_ENV: "production"Add .nvmrc in the repo root, install fnm on the server, then:
sudo hj2 deploy --email [email protected]Deployment Strategies
Standard Deploy
For first-time deployments or when blue-green is disabled. The app is stopped, the new release is activated, and the app is restarted.
sudo hj2 deploy
sudo hj2 deploy --no-blue-greenBlue-Green Deploy
When a previous release exists, hj2 automatically uses blue-green deployment:
- The current release stays live on its assigned port.
- The new release starts on an alternate port.
- Health checks run against the new release.
- If healthy, the proxy switches traffic to the new release.
- The old release is stopped.
- The previous release is kept for instant rollback.
sudo hj2 deploy # auto blue-green when a previous release exists
sudo hj2 deploy --blue-green # force blue-greenAutomatic Rollback
If a deployment fails at any step (build, health check, proxy), hj2 automatically:
- Stops the failed release.
- Reactivates the previous release.
- Restores proxy traffic.
No rebuild is needed — the previous release directory is used as-is.
# Manual rollback
sudo hj2 rollback myappDisable auto-rollback in hj2.yml:
deploy:
autoRollback: falseServer Filesystem Layout
/etc/hj2/
├── config.json # Global config (Let's Encrypt email, etc.)
├── sites.json # Managed Nginx sites registry
├── apps.json # Registered app metadata
├── ports.json # Port allocations
├── servers.json # Multi-server profiles
├── connect.json # GitHub / webhook config
├── services.json # Tracked database/cache services
├── backup.json # Backup configuration
└── secrets/ # Encrypted secrets (mode 0600)
/var/lib/hj2/apps/<name>/
├── releases/
│ ├── 20260827-173000/
│ ├── 20260827-180500/
│ └── 20260827-183200/
├── current -> releases/20260827-183200
├── shared/ # Persistent data (see shared: in hj2.yml)
│ ├── database/sqlite/
│ └── public/uploads/
└── metadata/
└── history.json
/var/www/hj2/<domain>/ # Static site web roots
/var/log/hj2/ # Audit logEach release is a self-contained directory. The current symlink points to the active release, enabling instant rollback by re-pointing the symlink.
Requirements
| Requirement | Details |
|---|---|
| OS | Ubuntu or Debian with systemd |
| Node.js | ≥ 18 on the server (system Node, or pinned via fnm — see Node version management) |
| fnm | Required only when using .nvmrc / .node-version |
| Root | Required for mutating commands (deploy, publish, setup, server harden, etc.) |
| DNS | A/AAAA records pointing to the server for TLS |
| Ports | 80 and 443 open for Let's Encrypt and HTTPS |
Non-mutating commands (list, status, logs, domain list, doctor, etc.) do not require root.
License
MIT
