You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: support multi-process deployments (web + workers)
A backend deployment can now declare additional long-running processes
alongside the web server. PM2 supervises them all under one deployment,
with worker names namespace-prefixed to prevent collisions between
multiple shipnode deployments on the same host.
Adds .worker() to the builder, generalises pm2 config into pm2.apps[],
moves the ecosystem file into each release (ADR-0001), and enriches the
post-deploy health check with a pm2 jlist status assertion so workers
that crash on startup don't slip past the HTTP probe.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+26Lines changed: 26 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,32 @@ All notable changes to `@devalade/shipnode` will be documented here.
4
4
5
5
## [Unreleased]
6
6
7
+
### Added
8
+
-**Workers / multi-process deployments** — a backend can now declare additional long-running processes (workers, cron consumers, queues) alongside the web server. PM2 supervises all of them under one deployment.
9
+
- New `.worker({ name, command, instances?, maxMemory?, env? })` builder method appends a worker to the deployment.
10
+
- New `pm2.apps` config shape: each entry has `name`, optional `command` (custom entry like `node dist/worker.js`; defaults to `<pkgManager> start`), `port` (web app only), `instances`, `maxMemory`, `env`.
11
+
- The entry with a `port` is the web app; entries without are workers (see [ADR-0002](docs/adr/0002-port-presence-determines-web-app.md)).
12
+
- Worker-only deployments are legal — a backend with no web app skips the HTTP health check and Caddy site config.
13
+
-`restart`, `stop`, `logs` accept `--process <name>` to target a single app; without it they operate on the whole deployment via the PM2 namespace.
14
+
- Worker names are automatically prefixed with the deployment namespace when registered with PM2 (e.g. `.pm2('api')` + `.worker({name: 'mailer'})` shows up in `pm2 list` as `api` and `api-mailer`). Prevents collisions between multiple shipnode deployments on the same host. Users always refer to the short name in shipnode commands.
15
+
-**PM2 status check** — after every deploy, `pm2 jlist` is parsed and each declared app must be `online` with `restart_time === 0`. Catches workers that boot and crash before the HTTP health check would notice anything. Runs even on worker-only deployments.
16
+
-**Per-app `env`** — each `pm2.apps` entry can set its own env vars (`{ WORKER_QUEUE: 'emails' }`); PM2 loads the shared `.env` via `env_file` and applies per-app overrides on top.
17
+
18
+
### Changed
19
+
-**Ecosystem file is per-release** — `ecosystem.config.cjs` now lives inside each release directory instead of `<remotePath>/shared/`. PM2 references it via `<remotePath>/current/ecosystem.config.cjs` so rollback restores the exact process set that was active for that release (a worker added in v2 won't crash-loop after rolling back to v1). See [ADR-0001](docs/adr/0001-per-release-ecosystem-file.md).
20
+
-**Builder back-compat preserved** — `.pm2(name, opts)` and `.port(n)` still work and now act as sugar on the first app. Existing `shipnode.config.ts` files don't need changes.
21
+
-`shipnode config show` now lists each PM2 app with its full per-entry shape.
22
+
-`shipnode status` shows every declared app, not just the one matching the deployment name.
23
+
24
+
### Removed
25
+
-`ShipnodeConfig.backend` and the `BackendConfig` type — the port now lives on the web `pm2.apps` entry. Users of the builder/loader are unaffected; only direct consumers of the `ShipnodeConfig` TypeScript shape need to read `pm2.apps.find(a => a.port !== undefined)?.port` instead of `config.backend?.port`.
26
+
- The legacy `pm2: { name, instances, maxMemory }` object shape on `ShipnodeConfig` — same migration: read `pm2.apps[0]` instead. (The legacy *input* shape is still accepted by `assembleConfig` and folded onto `apps[0]`.)
27
+
28
+
### Migration
29
+
-**No config changes required** for existing deployments — your current `shipnode.config.ts` keeps working.
30
+
- The first deploy under this version silently cleans up any pre-existing PM2 process with the deployment's name (legacy single-app deploys), so the migration is invisible.
31
+
- To add a worker: chain `.worker({ name: 'mailer', command: 'node dist/worker.js' })` on your builder.
Copy file name to clipboardExpand all lines: CONTEXT.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -53,6 +53,12 @@ User-provided function that runs at a fixed point in the deployment lifecycle. `
53
53
### HealthCheck
54
54
A remote curl-based check that verifies the backend application is responding after a deployment. Only runs for backend apps in zero-downtime mode.
55
55
56
+
### Pm2Config
57
+
The process-supervision section of a `ShipnodeConfig`. Contains an `apps` list — one entry per long-running process PM2 should supervise (web server, workers, etc.). Backend-only; frontend apps don't have one.
58
+
59
+
### Pm2App
60
+
A single PM2-supervised process within a `Pm2Config`. Carries its own `name`, optional `command` (defaults to the package manager's `start` script), `instances`, `maxMemory`, `env`, and optionally a `port`. **The entry with a `port` is the web app** — it receives the HTTP health check and Caddy upstream wiring. Entries without a `port` are workers: PM2 supervises them, but shipnode doesn't curl them. At most one entry may carry a `port`; zero is legal (worker-only deployments).
61
+
56
62
### Caddy
57
63
The reverse proxy / static file server configured per-deployment. `CaddyService` writes config to `/etc/caddy/conf.d/` and reloads the service.
After deploy, `shipnode status` lists every app, and `shipnode restart` / `stop` / `logs` operate on the whole deployment by default. Use `--process <name>` to target a single one (use the short name from your config — shipnode handles the PM2-side naming):
61
+
62
+
```bash
63
+
shipnode logs --process mailer
64
+
shipnode restart --process cron
65
+
```
66
+
67
+
> **Note on PM2 naming.** PM2 has a flat global namespace, so two deployments on the same host with a `worker` entry would collide. To prevent that, shipnode prefixes worker names with the deployment namespace when registering them with PM2. A config with `.pm2('api')` + `.worker({name: 'mailer'})` shows up in `pm2 list` as `api` and `api-mailer`. You always use the short name (`mailer`) in shipnode commands.
68
+
69
+
A backend can also be **worker-only** (no web server, no domain) — useful for queue consumers or cron runners. Just omit `.port(...)` and `.domain(...)`:
The PM2 ecosystem file describes which processes constitute a deployed app (web + workers). It used to live at `/remotePath/shared/ecosystem.config.cjs` — shared across releases — which was fine when the ecosystem was effectively constant. Once apps can declare arbitrary workers, the ecosystem becomes part of the application's shape and can differ from one release to the next.
4
+
5
+
We write the ecosystem into `<releasePath>/ecosystem.config.cjs` instead, and PM2 commands target `<remotePath>/current/ecosystem.config.cjs` (stable across rollbacks because the `current` symlink always points at the active release). Rollback then restores the symlink *and*, transparently, the ecosystem that matches that release's code — a worker added in v2 won't crash-loop after rolling back to v1.
6
+
7
+
The alternative was snapshotting the shared ecosystem inside `ReleaseManager` metadata and restoring it on rollback. That works but invents a new "ecosystem snapshot" concept parallel to the existing release boundary, when the release boundary already means exactly this.
# Port presence determines the web app; no `type` discriminator on `Pm2App`
2
+
3
+
`pm2.apps` is a uniform list of processes — web servers and workers share the same shape. The web app is identified by carrying a `port`; workers omit it. There is deliberately no `type: 'web' | 'worker'` field.
4
+
5
+
The discriminator would be a second source of truth that can disagree with itself: it would still be the `port` that drives the health check and Caddy upstream, so a `type: 'web'` entry without a port (or a `type: 'worker'` entry with one) would have to be rejected anyway. Encoding the same fact twice invites bugs. `assembleConfig` enforces: at most one port-bearing entry; zero is legal (worker-only deployments); a `domain` requires a web entry.
6
+
7
+
The cost is that "which entry is the web app?" isn't visible from a glance at one entry — you have to look at the list. We accept that in exchange for not maintaining a tagged union whose tag is redundant with its data.
0 commit comments