Skip to content

Commit bf76230

Browse files
committed
docs: sync Shipnode v3 agent skills
1 parent e99ecdb commit bf76230

6 files changed

Lines changed: 985 additions & 151 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# shipnode 2.x Reference
2+
3+
For projects still on `@devalade/shipnode@2` (`latest` on npm as of 2.5.x).
4+
For v3, use [REFERENCE.md](REFERENCE.md). Migration: `docs/migrations/2.x-to-3.0.md`.
5+
6+
## Mental model
7+
8+
- **One app per config file.** Multi-app = multiple `shipnode.*.config.ts` files + multiple deploys.
9+
- **Flat config.** Domain, pm2, healthCheck, envFile, hooks live on the root builder / root `ShipnodeConfig` (not `apps[]`).
10+
- **Disk layout:** `<remotePath>/releases/<ts>/` + `<remotePath>/current` (no per-app subdirectory).
11+
- **Releases are always on** (since 2.0.13). There is no `.legacy()` in-place mode. Keep N releases with `.keepReleases(n)` (default 5).
12+
- Note: early 2.0 docs mentioned `.legacy()` / `.zeroDowntime()` for release vs in-place — those methods were **removed** in 2.0.13. Ignore them on 2.5.x.
13+
14+
## Quick config (2.x)
15+
16+
```ts
17+
import { shipnode } from '@devalade/shipnode';
18+
19+
export default shipnode
20+
.backend()
21+
.ssh({ host: '1.2.3.4', user: 'deploy' })
22+
.deployTo('/var/www/myapp')
23+
.pm2('myapp', { instances: 2 })
24+
.port(3000)
25+
.worker({ name: 'worker', command: 'node dist/worker.js' }) // 2.3+
26+
.domain('api.example.com')
27+
.healthCheck('/health')
28+
.keepReleases(5)
29+
.envFile('.env')
30+
.nodeVersion('22')
31+
.pkgManager('pnpm')
32+
.preDeploy(async ({ exec }) => {
33+
await exec('npx prisma migrate deploy');
34+
})
35+
.aliases({ migrate: 'pnpm db:apply' })
36+
.build();
37+
```
38+
39+
Frontend: `.frontend()`, `.buildDir('dist')`, drop `.pm2()` / `.port()`.
40+
41+
Monorepo: either `.appRoot('apps/backend')` in one config, or separate config files + `--config`.
42+
43+
## Commands (2.x)
44+
45+
No `--app` flag. Target a different app by `--config path/to/other.config.ts`.
46+
47+
| Command | Description |
48+
|---|---|
49+
| `init` | Generate shipnode.config.ts |
50+
| `setup` | Install Node, PM2, Caddy, mise |
51+
| `deploy [--dry-run] [--skip-build]` | Deploy |
52+
| `doctor [--security]` | Config / security check |
53+
| `status` | PM2 status |
54+
| `rollback [--steps n]` | Roll back n releases (default 1) |
55+
| `migrate` | In-place → release structure |
56+
| `env [--file path]` | Upload .env to `shared/` |
57+
| `run [cmd] [--tty]` | Remote command / alias |
58+
| `logs [--lines n]` | PM2 logs |
59+
| `restart` / `stop` | Process control |
60+
| `metrics` | PM2 monit |
61+
| `unlock` | Clear `<remotePath>/.shipnode/deploy.lock` (file in 2.x) |
62+
| `harden` | SSH / UFW / fail2ban |
63+
| `user sync` / `list` / `remove` | Users from `.shipnode/users.yml` |
64+
| `backup setup` / `run` / `status` / `list` | Snapshot-style S3 backups |
65+
| `cloudflare init` / `audit` / `status` | Tunnel (uses `appHostname` in older configs) |
66+
| `ci github` / `ci env-sync` | GitHub Actions |
67+
| `config show` / `validate` / `path` | Config helpers |
68+
| `eject` / `upgrade` | Templates / self-upgrade |
69+
70+
## Builder DSL (2.x)
71+
72+
| Method | Notes |
73+
|---|---|
74+
| `.backend()` / `.frontend()` | App type |
75+
| `.ssh({ host, user, port?, identityFile? })` | SSH |
76+
| `.deployTo(path)` | Remote path (releases live directly under it) |
77+
| `.pm2(name, opts?)` | Primary process |
78+
| `.port(n)` | Web port → `PORT` env; marks web app |
79+
| `.worker(opts)` | Extra PM2 process (2.3+) |
80+
| `.domain(d)` | Caddy site |
81+
| `.keepReleases(n)` | Retention (default 5) |
82+
| `.healthCheck(path, opts?)` / `.noHealthCheck()` | Probe |
83+
| `.envFile(f)` | Shared env |
84+
| `.sharedDirs` / `.sharedFiles` | Persist across releases |
85+
| `.appRoot(dir)` | Monorepo subdir |
86+
| `.buildDir(dir)` | Frontend output |
87+
| `.nodeVersion` / `.pkgManager` / `.installCommand` | Runtime |
88+
| `.database` / `.redis` / `.backup` / `.cloudflare` | Infra |
89+
| `.preDeploy` / `.postDeploy` | Hooks |
90+
| `.aliases(map)` | `shipnode run <name>` |
91+
92+
**Not in 2.x:** `.apps([...])`, `.app()`, `.zeroDowntime()` (v3 blue-green), `.accessories()`, `.registry()`, `.servers()` / `.on()`, `monitor`, `backup restore`, `user add`, `accessory *`, `secret *`, `registry login`.
93+
94+
## Paths (2.x)
95+
96+
| Path | Purpose |
97+
|---|---|
98+
| `<remotePath>/releases/<ts>/` | Release |
99+
| `<remotePath>/current` | Active symlink |
100+
| `<remotePath>/shared/` | Env + shared files |
101+
| `<remotePath>/.shipnode/releases.json` | History |
102+
| `<remotePath>/.shipnode/deploy.lock` | Lock **file** |
103+
104+
## Cloudflare (2.x)
105+
106+
Older configs used `cloudflare: { zone, appHostname }`. Ingress was single-hostname. In 3.x, `appHostname` is removed — use per-app `.domain()` instead.
107+
108+
## Reading config programmatically (2.x)
109+
110+
```ts
111+
config.app // 'backend' | 'frontend'
112+
config.domain
113+
config.pm2
114+
config.healthCheck
115+
config.envFile
116+
// ...
117+
```
118+
119+
In 3.x these root mirrors are gone — use `config.apps[0].…`.
120+
121+
## Migrating to 3.x
122+
123+
See [REFERENCE.md](REFERENCE.md) and repo `docs/migrations/2.x-to-3.0.md`.
124+
125+
Short version:
126+
127+
1. Builder-only single-app configs usually keep working (synthesized to `apps[0]`).
128+
2. Prefer `.apps([api, web])` for monorepos.
129+
3. Disk moves to `<remotePath>/<app-name>/…` — clean up old root `releases/` / `current` after first 3.x deploy.
130+
4. Use `--app` on CLI; `rollback` requires it when multiple apps exist.
131+
5. Install: `npm i -D @devalade/shipnode@3` or `@alpha` while v3 is pre-release.
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# shipnode v3 Reference
2+
3+
For the 2.x single-app API (npm `latest` / `@devalade/shipnode@2`), see [REFERENCE-v2.md](REFERENCE-v2.md).
4+
5+
## All commands
6+
7+
Most commands accept `--config <path>` and `--app <name>` (multi-app). `rollback` requires `--app` when more than one app exists.
8+
9+
### Core
10+
| Command | Description |
11+
|---|---|
12+
| `init [--non-interactive] [--print]` | Generate shipnode.config.ts |
13+
| `setup [--no-deploy-user]` | Install Node, PM2, Caddy, mise; bootstrap `deploy` user |
14+
| `deploy [--dry-run] [--skip-build] [--app name]` | Deploy all apps or one |
15+
| `doctor [--security]` | Local + remote config / security audit |
16+
| `status [--app name]` | PM2 process status |
17+
18+
### Release management
19+
| Command | Description |
20+
|---|---|
21+
| `rollback --app name [--steps n]` | Roll back n releases (default 1). Blue-green: one-step Caddy flip |
22+
| `migrate` | Migrate existing deploy to release/symlink structure |
23+
24+
### Environment & process
25+
| Command | Description |
26+
|---|---|
27+
| `env [--file path] [--app name]` | Upload .env → `<appPath>/shared/` |
28+
| `run [cmd] [--tty] [--app name]` | One-off remote command (or alias) |
29+
| `logs [--lines n] [--app name]` | Tail PM2 logs |
30+
| `restart [--app name]` | Reload PM2 with `--update-env` |
31+
| `stop [--app name]` | Stop the application |
32+
| `metrics` | PM2 monitoring dashboard |
33+
| `monitor [--app name]` | Live TUI (PM2, system, health, logs) |
34+
| `unlock` | Clear stuck deploy lock (`.shipnode/deploy.lock/`) |
35+
36+
### Accessories & secrets
37+
| Command | Description |
38+
|---|---|
39+
| `accessory status [name]` | Docker accessory status |
40+
| `accessory logs <name>` | Accessory logs |
41+
| `accessory restart <name>` | Restart accessory |
42+
| `accessory stop <name>` | Stop accessory |
43+
| `accessory health <name>` | Run accessory health check |
44+
| `secret set <name> [value]` | Set remote secret (value or local env) |
45+
| `registry login` | Store registry token + `docker login` on target |
46+
47+
### Security & maintenance
48+
| Command | Description |
49+
|---|---|
50+
| `harden` | SSH hardening, UFW, fail2ban, PM2 boot check |
51+
| `doctor --security` | Security audit |
52+
53+
### Users
54+
| Command | Description |
55+
|---|---|
56+
| `user add <name> [--key path] [--sudo] [--no-sync]` | Write `.shipnode/users.yml` + sync |
57+
| `user sync` | Sync users from `.shipnode/users.yml` |
58+
| `user list` | List non-system users |
59+
| `user remove <username>` | Remove user |
60+
61+
### Backup
62+
| Command | Description |
63+
|---|---|
64+
| `backup setup` | Install script + systemd timer |
65+
| `backup run` | Run immediately |
66+
| `backup status` | Timer + last run |
67+
| `backup list` | List S3 / restic snapshots |
68+
| `backup restore [snapshot] --target <path>` | Extract restic snapshot (default `latest`) |
69+
70+
### Cloudflare
71+
| Command | Description |
72+
|---|---|
73+
| `cloudflare init` | Tunnel + DNS + ingress for every app domain |
74+
| `cloudflare audit` | Verify DNS + tunnel |
75+
| `cloudflare status` | cloudflared service status |
76+
77+
### CI/CD
78+
| Command | Description |
79+
|---|---|
80+
| `ci github` | Write `.github/workflows/shipnode-deploy.yml` |
81+
| `ci env-sync [--all]` | Push .env → GitHub repo secrets |
82+
83+
### Config
84+
| Command | Description |
85+
|---|---|
86+
| `config show [--app name]` | Print resolved config |
87+
| `config validate` | Validate config file |
88+
| `config path` | Print config file path |
89+
90+
### Misc
91+
| Command | Description |
92+
|---|---|
93+
| `eject [pm2\|caddy\|all]` | Eject templates to `.shipnode/templates/` |
94+
| `upgrade` | Upgrade shipnode CLI |
95+
96+
---
97+
98+
## Workspace builder (root)
99+
100+
| Method | Default | Description |
101+
|---|---|---|
102+
| `.ssh({ host, user, port?, identityFile? })` | port 22 | Default SSH connection |
103+
| `.servers({ name: SshConfig })` || Named server targets |
104+
| `.cloudflareAccess(proxyCommand?)` || SSH via Cloudflare Access |
105+
| `.deployTo(path)` | `/var/www/app` | Workspace remote base path |
106+
| `.nodeVersion(v)` | `lts` | Node via mise |
107+
| `.pkgManager(pm, { installCommand? })` | auto | `npm` \| `yarn` \| `pnpm` \| `bun` |
108+
| `.installCommand(cmd)` | pkg default | Override remote install |
109+
| `.database(opts)` || Postgres/MySQL/SQLite provisioning hints |
110+
| `.redis(opts)` || Redis connection |
111+
| `.backup(opts)` || S3 / restic backup config |
112+
| `.cloudflare(opts)` || Tunnel zone / name / lockdown |
113+
| `.aliases(map)` || Named `shipnode run` shortcuts |
114+
| `.registry({ server, username, passwordEnv })` || Default Docker registry |
115+
| `.accessories(map)` || Docker sidecar definitions |
116+
| `.app()` || Start a per-app sub-builder |
117+
| `.apps([appBuilders])` || Compose multi-app workspace (wins over top-level per-app methods) |
118+
| `.backend()` / `.frontend()`|| Legacy single-app shortcuts → `apps[0]` |
119+
| `.build()` || Assemble + validate → `ShipnodeConfig` |
120+
121+
### Backup options
122+
```ts
123+
.backup({
124+
s3Bucket: string,
125+
s3Prefix?: string,
126+
s3Endpoint?: string, // e.g. R2
127+
schedule?: 'hourly' | 'daily' | 'weekly',
128+
retentionDays?: number, // snapshot strategy
129+
strategy?: 'snapshot' | 'restic',
130+
keepDaily?: number, // restic (default 7)
131+
keepWeekly?: number, // default 4
132+
keepMonthly?: number, // default 6
133+
})
134+
```
135+
136+
### Accessory shape
137+
```ts
138+
{
139+
image: string,
140+
on?: string, // server target name
141+
port?: string | string[],
142+
volumes?: string[],
143+
networks?: string[],
144+
command?: string | string[],
145+
env?: Record<string, string>,
146+
restart?: 'no' | 'always' | 'unless-stopped' | 'on-failure',
147+
registry?: { server, username, passwordEnv },
148+
healthCheck?: { command: string },
149+
}
150+
```
151+
152+
---
153+
154+
## Per-app builder (`app()` / `shipnode.app()`)
155+
156+
| Method | Default | Description |
157+
|---|---|---|
158+
| `.backend()` / `.frontend()` || App type (required) |
159+
| `.name(n)` | inferred | App id → release dir + CLI `--app` |
160+
| `.on(serverName)` | default ssh | Target from `.servers()` |
161+
| `.appRoot(dir)` | `.` | Monorepo package path |
162+
| `.pm2(name, { instances?, maxMemory? })` || Primary PM2 process |
163+
| `.port(n)` || Web port (marks the HTTP app; sets `PORT`) |
164+
| `.worker({ name, command, … })` || Extra PM2 process (no port) |
165+
| `.domain(d)` || Caddy + Cloudflare ingress |
166+
| `.caddy({ append })` || Extra Caddy directives |
167+
| `.buildDir(dir)` | auto | Frontend output dir |
168+
| `.envFile(f)` | `.env` | Uploaded to `shared/`, symlinked as `.env` |
169+
| `.keepReleases(n)` | `5` | Releases to retain |
170+
| `.zeroDowntime(altPort?)` | automatic when eligible | Force blue-green or choose the alternate port; default offset is 10,000 |
171+
| `.noZeroDowntime()` || Opt out of automatic blue-green |
172+
| `.sharedDirs(dirs)` / `.sharedFiles(files)` || Persist across releases |
173+
| `.healthCheck(path, opts?)` | `/health`, 30s, 3 retries, 3s delay | Post-deploy probe |
174+
| `.noHealthCheck()` || Skip health check |
175+
| `.preDeploy(fn)` / `.postDeploy(fn)` || Deploy hooks |
176+
| `.dependsOn(['accessory'])` || Ensure accessories before deploy |
177+
178+
---
179+
180+
## Deploy lifecycle
181+
182+
```
183+
lock → accessories → per app:
184+
release dir → stage → setup (install/build) → preDeploy
185+
→ symlink current → start (web colour if blue-green)
186+
→ health → afterHealthy (workers) → Caddy flip (blue-green)
187+
→ record success → postDeploy → cleanup
188+
→ caddy.configureAll → unlock
189+
```
190+
191+
On failure after symlink: revert `current` (if previous exists), record `failed`, unlock.
192+
193+
### Blue-green notes
194+
- Requires backend + one PM2 app with `.port()`.
195+
- Web process duplicated (`name-blue` / `name-green`); workers single set, reloaded only after health.
196+
- State: `<appPath>/.shipnode/deploy-state.json` (port pair frozen after first deploy).
197+
- Health failure: old colour keeps traffic; workers untouched; `current` reverted.
198+
199+
---
200+
201+
## Deploy hook API
202+
203+
```ts
204+
.preDeploy(async ({ exec, config }) => { ... })
205+
.postDeploy(async ({ exec, config }) => { ... })
206+
```
207+
208+
`exec(cmd)` runs on the remote in the new release dir (or `<release>/<appRoot>`). Env file sourced when configured. Throws on non-zero exit.
209+
210+
---
211+
212+
## Run aliases
213+
214+
```ts
215+
.aliases({
216+
migrate: 'pnpm db:apply',
217+
seed: 'pnpm db:seed',
218+
})
219+
```
220+
221+
```bash
222+
shipnode run migrate
223+
shipnode run migrate --step 2
224+
shipnode run "echo hello"
225+
```
226+
227+
---
228+
229+
## Paths (per app)
230+
231+
| Path | Purpose |
232+
|---|---|
233+
| `<remotePath>/<app>/releases/<ts>/` | Release snapshot |
234+
| `<remotePath>/<app>/current` | Symlink to active release |
235+
| `<remotePath>/<app>/shared/` | Env + shared dirs/files |
236+
| `<remotePath>/<app>/.shipnode/releases.json` | Release history |
237+
| `<remotePath>/<app>/.shipnode/deploy-state.json` | Blue-green colour/ports |
238+
| `<remotePath>/.shipnode/deploy.lock/` | Workspace deploy lock (mkdir) |

0 commit comments

Comments
 (0)