shipnode.config.ts
Every ShipNode project has a shipnode.config.ts at the root. shipnode init generates it interactively; you can also write it by hand.
Minimal backend
Section titled “Minimal backend”import { shipnode } from '@devalade/shipnode';
export default shipnode .backend() .ssh({ host: '203.0.113.10', user: 'deploy' }) .deployTo('/var/www/api') .pm2('api', { instances: 2 }) .port(3000) .domain('api.example.com') .healthCheck('/health') .nodeVersion('22') .pkgManager('pnpm') .build();Static frontend
Section titled “Static frontend”import { shipnode } from '@devalade/shipnode';
export default shipnode .frontend() .ssh({ host: '203.0.113.10', user: 'deploy' }) .deployTo('/var/www/web') .domain('example.com') .buildOutput('dist') .build();Method reference
Section titled “Method reference”| Method | Purpose |
|---|---|
.backend() / .frontend() | App type. Backend uses PM2; frontend is served as static files by Caddy. |
.ssh({ host, user, port? }) | SSH target. Port defaults to 22. |
.deployTo(path) | Absolute path on the server (e.g. /var/www/api). |
.pm2(name, opts?) | PM2 app name + options ({ instances, exec_mode }). Backend only. |
.port(n) | App’s listening port. Caddy reverse-proxies to it. |
.domain(host) | Public hostname. Caddy issues + renews certs. |
.healthCheck(path, opts?) | GET path the deploy must hit after reload. |
.nodeVersion(v) | Node major version pinned via mise. |
.pkgManager('npm' | 'pnpm' | 'yarn' | 'bun') | How to install + build. |
.worker({ name, command, env? }) | Extra long-running PM2 process. Can be repeated. |
.env(path) | Path to a .env file uploaded as shared/.env. |
.keepReleases(n) | How many old releases to keep on disk (default 5). |
.zeroDowntime(bool) | Toggle the symlink-flip release strategy. |
.aliases({ name: cmd }) | Define short names for shipnode run (see Aliases). |
.preDeploy(fn) | Run a function on the server before the health check (see Hooks). |
.postDeploy(fn) | Run a function on the server after a healthy release. |
.build() | Required terminal call. Returns the resolved config. |
Aliases
Section titled “Aliases”shipnode run matches its first argument against the alias map and expands it. Useful for one-off operational commands you don’t want to memorize.
export default shipnode .backend() .ssh({ host: '203.0.113.10', user: 'root' }) .deployTo('/var/www/api') .pm2('api') .port(3000) .aliases({ migrate: 'pnpm prisma migrate deploy', seed: 'pnpm tsx scripts/seed.ts', repl: 'node --experimental-repl-await', }) .build();Now on the server:
npx shipnode run "migrate"# -> runs `pnpm prisma migrate deploy` inside the current releasenpx shipnode run "seed"Anything after the alias name is appended to the expanded command, so shipnode run "migrate --create-only" works.
Hooks let you run logic on the server at well-defined points in the deploy. They receive a typed context with the resolved config and a server-side exec helper that runs inside the release directory with the project’s environment loaded.
export default shipnode .backend() .ssh({ host: '203.0.113.10', user: 'root' }) .deployTo('/var/www/api') .pm2('api') .port(3000) .preDeploy(async (ctx) => { // Runs inside the new release, before PM2 reload + health check. await ctx.exec('pnpm prisma migrate deploy'); }) .postDeploy(async (ctx) => { // Runs after the release is healthy. await ctx.exec('curl -X POST $SLACK_WEBHOOK -d \'{"text":"deployed"}\''); }) .build();When each hook runs
Section titled “When each hook runs”| Hook | Timing |
|---|---|
preDeploy | After install + build + symlink flip, before PM2 reload and the health check. Throw to abort the deploy — the symlink rolls back. |
postDeploy | After the health check passes. Failures here log but do not roll back the release. |
The context
Section titled “The context”type HookFn = (ctx: HookContext) => Promise<void> | void;
interface HookContext { config: ShipnodeConfig; // the resolved config for this deploy release?: string; // the new release directory (absolute path on the server) env: string; // 'production' for now exec(cmd: string, options?: { cwd?: string; env?: Record<string, string>; timeout?: number } ): Promise<{ stdout: string; stderr: string; exitCode: number }>;}ctx.exec runs the command on the remote server, in the new release directory, with mise in PATH (so node, pnpm, etc. are available). Stdout streams to your terminal prefixed with │. Non-zero exit throws — that’s enough to fail the deploy from a preDeploy hook.
See Recipes for ready-to-paste hook patterns (Prisma migrations, Slack notifications, Sentry releases, CDN purge, cache priming).
Where the config can live
Section titled “Where the config can live”Default: ./shipnode.config.ts. Override with --config <path> on any command — useful for multi-environment setups (see Multi-environment).