1 ---
2 name: smbcloud-deploy-monorepo
3 description: Use when deploying from a monorepo with smbCloud CLI, especially repositories that contain multiple deployable sub-projects with different strategies (Next.js SSR, Vite SPA, Rails, static) behind a single `.smb/config.toml` with `runner = 255` and `[[projects]]` entries.
4 ---
5
6 # smbCloud Deploy Monorepo
7
8 Use this skill when work touches monorepo deployment on smbCloud.
9
10 Applies to:
11
12 - repositories with `runner = 255` in `[project]`
13 - `.smb/config.toml` files with `[[projects]]` arrays
14 - `process_deploy.rs` monorepo resolution logic
15 - sub-project routing to `nextjs-ssr`, `vite-spa`, `rails`, rsync, or git deploy paths
16 - multi-tenant PM2 and Nginx server configuration
17 - CI workflows that deploy individual sub-projects from a monorepo
18
19 ## What makes a monorepo
20
21 A monorepo is a repository where `[project]` has `runner = 255` (the `Monorepo` variant) and deployable targets are listed as `[[projects]]` entries. The root project is never deployed directly — it is a container.
22
23 Keep the deploy hierarchy straight:
24
25 - root `[project]` = umbrella workspace in smbCloud (not deployable)
26 - repo = the monorepo itself, represented by a `DeployRepo` record
27 - each `[[projects]]` entry = one deployable `FrontendApp` inside that repo, identified by `frontend_app_id`
28 - each deployment record = one release of one `FrontendApp`, linked via `frontend_app_id`
29
30 The canonical chain is: Deployment → FrontendApp → DeployRepo → Project
31
32 The config format still uses `[[projects]]` for historical reasons, but conceptually those entries are app targets, not separate umbrella workspaces. Each entry should include `frontend_app_id` and `deploy_repo_id` so deployments are attributed to the correct app.
33
34 The CLI detects this in `process_deploy.rs`:
35
36 ```rust
37 let resolved_name = match project_name {
38 Some(name) => Some(name),
39 None if config.project.runner == Runner::Monorepo => Some(prompt_select_project(&config)?),
40 None => None,
41 };
42 ```
43
44 When `runner = 255` and no `--project` flag is passed, the CLI shows an interactive selection prompt. When `--project <name>` is passed, the prompt is skipped.
45
46 After selection, `resolve_sub_project()` swaps `config.project` with the matching `[[projects]]` entry. From that point, all downstream deploy code operates on a single `Project` — it does not know or care that it came from a monorepo.
47
48 ## Config structure
49
50 A monorepo `.smb/config.toml` has two layers:
51
52 ### Root project (container)
53
54 ```toml
55 name = "splitfire"
56 description = "SplitFire AI monorepo."
57
58 [project]
59 id = 38
60 name = "splitfire"
61 repository = "splitfire"
62 runner = 255
63 description = "SplitFire AI monorepo."
64 created_at = "2025-09-20T16:33:05.154Z"
65 updated_at = "2025-09-20T16:33:05.154Z"
66 ```
67
68 The root `id` is the workspace ID. It is not a deployable app ID.
69
70 Key rules:
71
72 - `runner = 255` is mandatory — this is what activates monorepo behavior
73 - `id` must be a valid smbCloud project ID (created via `smb init` or the web console)
74 - the root project has no `kind`, `source`, `path`, or `pm2_app` — it is not deployable
75
76 ### Sub-projects (deployable units)
77
78 Each `[[projects]]` entry is a self-contained deployable app target with its own `kind`, `source`, `path`, `runner`, and strategy-specific fields.
79
80 ```toml
81 [[projects]]
82 id = 38
83 frontend_app_id = "a7f6d5d4-..."
84 deploy_repo_id = 12
85 name = "splitfireweb"
86 repository = "splitfireweb"
87 description = "SplitFire AI website."
88 source = "frontend/splitfire-web"
89 path = "apps/web/splitfireweb"
90 runner = 0
91 kind = "nextjs-ssr"
92 package_manager = "pnpm"
93 pm2_app = "splitfire-web"
94 port = 3010
95 created_at = "2025-09-20T16:33:05.154Z"
96 updated_at = "2025-09-20T16:33:05.154Z"
97
98 [[projects]]
99 id = 98
100 name = "musik88web"
101 repository = "musik88-production"
102 description = "Musik88 web or SplitFire AI API."
103 source = "backend/musik88-web"
104 runner = 2
105 kind = "rails"
106 shared_lib = "lib"
107 compile_cmd = "cd ~/lib/gems/gem_error_codes && rbenv local 3.4.2 && bundle install && bundle exec rake compile"
108 created_at = "2025-09-20T16:33:05.154Z"
109 updated_at = "2025-09-20T16:33:05.154Z"
110
111 [[projects]]
112 id = 53
113 name = "connecteddevices"
114 repository = "connecteddevices"
115 description = "KaroKowe connected devices TV web app."
116 source = "frontend/karokowe-connected-devices"
117 path = "apps/web/connecteddeviceskarokowe"
118 runner = 0
119 kind = "vite-spa"
120 output = "dist"
121 package_manager = "pnpm"
122 created_at = "2025-09-20T16:33:05.154Z"
123 updated_at = "2025-09-20T16:33:05.154Z"
124 ```
125
126 ## Sub-project field reference
127
128 | Field | Required | Description |
129 | ------------------- | ----------- | ------------------------------------------------------------------------------------------- |
130 | `id` | yes | umbrella workspace ID in smbCloud |
131 | `frontend_app_id` | recommended | FrontendApp UUID — include so deployments are attributed to the correct app |
132 | `deploy_repo_id` | recommended | DeployRepo ID — links the deploy target to its git repository record |
133 | `name` | yes | unique identifier, used with `--project` flag and in the selection prompt |
134 | `repository` | yes | remote repository name on the smbCloud server (used for git deploy and SSH paths) |
135 | `description` | no | human-readable description |
136 | `source` | yes | local path to the sub-project directory, relative to the monorepo root |
137 | `path` | depends | remote directory on the server, relative to `~/` — required for `nextjs-ssr` and `vite-spa` |
138 | `runner` | yes | server tier: `0` (NodeJs), `1` (Static), `2` (Ruby), `3` (Swift) |
139 | `kind` | depends | deploy strategy: `"nextjs-ssr"`, `"vite-spa"`, `"rails"`, or omitted for generic deploy |
140 | `package_manager` | depends | `"pnpm"` or `"npm"` — required for `nextjs-ssr` and `vite-spa` |
141 | `pm2_app` | depends | PM2 process name — required for `nextjs-ssr` |
142 | `port` | depends | runtime port — required for `nextjs-ssr`, defaults to `3000` if omitted |
143 | `output` | depends | build output directory — required for `vite-spa`, typically `"dist"` |
144 | `shared_lib` | no | path to shared library directory to rsync before deploy — used by `rails` |
145 | `compile_cmd` | no | SSH command to run on the server after syncing shared libs — used by `rails` |
146 | `deployment_method` | no | `0` (Git) or `1` (Rsync) — only matters when `kind` is not set |
147 | `created_at` | yes | ISO 8601 timestamp |
148 | `updated_at` | yes | ISO 8601 timestamp |
149
150 ## Deploy routing
151
152 The `kind` field drives strategy selection. This happens after `resolve_sub_project()` has already swapped the active project, so routing logic is identical for standalone and monorepo projects.
153
154 ```
155 smb deploy --project <name>
156 │
157 ▼
158 resolve_sub_project()
159 │
160 ▼
161 config.project.kind?
162 │
163 ┌────┼──────────┬──────────────┐
164 ▼ ▼ ▼ ▼
165 vite-spa nextjs-ssr rails (none)
166 │ │ │ │
167 ▼ ▼ ▼ ▼
168 pnpm pnpm rsync deployment_method?
169 build install lib/ │
170 │ + build compile ┌──┴──┐
171 ▼ │ git push Git Rsync
172 rsync ▼ │ │ │
173 dist/ rsync 3 ▼ ▼ ▼
174 dirs post- git rsync
175 │ receive push -a
176 ▼ hook
177 SSH PM2
178 restart
179 ```
180
181 ### Strategy: `nextjs-ssr`
182
183 Local build, rsync three directories, then SSH delete + fresh PM2 start (prefer server `ecosystem.config.cjs` when present; falls back to legacy `.js`). The `.next/standalone/` upload should use `--copy-links` so `pnpm` symlinked dependencies land as real files in the deployed tree. If `outputFileTracingRoot` points above the sub-project directory, Next may preserve the sub-project path inside standalone output, and the deploy path must place `.next/static` and `public` under that nested runtime directory. See `smbcloud-deploy-nextjs` skill for details.
184
185 Required fields: `kind`, `source`, `path`, `pm2_app`, `package_manager`, `port`
186
187 ### Strategy: `vite-spa`
188
189 Local build, rsync the output directory. No PM2 — Nginx serves static files directly.
190
191 Required fields: `kind`, `source`, `path`, `output`, `package_manager`
192
193 ### Strategy: `rails`
194
195 Rsync shared libraries, SSH compile native extensions, git force-push the sub-project directory to the server's bare repo (triggers post-receive hook).
196
197 Required fields: `kind`, `source`, `repository`, `runner = 2`
198
199 Optional: `shared_lib`, `compile_cmd`
200
201 ### Strategy: generic git or rsync
202
203 When `kind` is omitted, the `deployment_method` field controls the path. Git push uses libgit2 to push to the server's bare repo. Rsync uses system `rsync` to sync the source tree.
204
205 ## The `source` field
206
207 The `source` field is the most important monorepo-specific field. It tells the CLI where the sub-project lives relative to the monorepo root.
208
209 For `nextjs-ssr` and `vite-spa`, the CLI changes into the `source` directory before running build commands:
210
211 - `pnpm install` runs inside `source/`
212 - `pnpm build` runs inside `source/`
213 - rsync sources are relative to `source/`
214 - for Next.js SSR, standalone output may still preserve the `source/` path inside `.next/standalone/` when tracing is rooted above the app directory
215
216 For `rails`, the CLI:
217
218 - rsyncs `shared_lib` from the monorepo root (not from `source`)
219 - runs `compile_cmd` on the server via SSH
220 - initializes a temporary git repo inside `source/` and force-pushes it
221
222 The `source` path must be a valid directory relative to where `smb deploy` is run (the monorepo root).
223
224 ## Runner types and server tiers
225
226 Each sub-project's `runner` determines which smbCloud server receives the deployment:
227
228 | Runner | Value | SSH Host | Use Case |
229 | ---------- | ----- | -------------------- | ----------------------------------------- |
230 | `NodeJs` | `0` | `api.smbcloud.xyz` | Next.js, Vite, Node.js apps |
231 | `Static` | `1` | `api.smbcloud.xyz` | Pure static sites (Nginx serves directly) |
232 | `Ruby` | `2` | `api-1.smbcloud.xyz` | Rails apps |
233 | `Swift` | `3` | `api-1.smbcloud.xyz` | Vapor apps |
234 | `Monorepo` | `255` | — | Container only, never deployed directly |
235
236 Sub-projects within the same monorepo can target different runners. The SplitFire monorepo demonstrates this: `splitfireweb` (runner 0, api.smbcloud.xyz) and `musik88web` (runner 2, api-1.smbcloud.xyz) deploy to different servers from the same repository.
237
238 ## Multi-tenant server layout
239
240 Multiple apps share the same server, each in its own directory under the deploy user's home:
241
242 ```
243 /home/git/
244 ├── apps/
245 │ └── web/
246 │ ├── splitfireweb/ # Next.js SSR (PM2: splitfire-web, port 3010)
247 │ │ ├── server.js
248 │ │ ├── .next/static/
249 │ │ └── public/
250 │ ├── connecteddeviceskarokowe/ # Vite SPA (no PM2, Nginx serves directly)
251 │ │ └── index.html
252 │ ├── ondeinference.com/ # Next.js SSR (PM2: ondeinference-web, port 3026)
253 │ │ ├── server.js
254 │ │ ├── .next/static/
255 │ │ └── public/
256 │ └── aircraftshubweb/ # Next.js SSR (PM2: aircraftshub-web, port 3022)
257 │ ├── server.js
258 │ ├── .next/static/
259 │ └── public/
260 └── musik88-production.git/ # Bare repo (Rails post-receive hook)
261 ```
262
263 Each `nextjs-ssr` app runs as a separate PM2 process on a unique port. Nginx reverse-proxies each domain to the correct port. Vite SPA and static apps have no PM2 process — Nginx serves files directly with `try_files`.
264
265 ### Port allocation
266
267 Every `nextjs-ssr` sub-project must have a unique `port` value. Ports are allocated manually and must be consistent across three places:
268
269 1. `.smb/config.toml` — the `port` field in the `[[projects]]` entry
270 2. PM2 — the `PORT` environment variable passed to `node server.js`
271 3. Nginx — the `proxy_pass` upstream address
272
273 If any of these disagree, the app will either not start, not be reachable, or serve the wrong app.
274
275 Current allocations on `api.smbcloud.xyz`:
276
277 | App | PM2 Name | Port |
278 | ----------------- | ------------------- | ---- |
279 | splitfire-web | `splitfire-web` | 3010 |
280 | aircraftshub-web | `aircraftshub-web` | 3022 |
281 | ondeinference-web | `ondeinference-web` | 3026 |
282
283 ### Nginx configuration per app type
284
285 **Next.js SSR apps** use reverse proxy:
286
287 ```nginx
288 upstream app_name {
289 server 127.0.0.1:<port>;
290 keepalive 64;
291 }
292
293 server {
294 server_name example.com;
295
296 location /_next/static/ {
297 alias /home/git/apps/web/<app>/.next/static/;
298 expires max;
299 add_header Cache-Control "public, max-age=31536000, immutable";
300 access_log off;
301 }
302
303 location / {
304 proxy_pass http://app_name;
305 proxy_http_version 1.1;
306 proxy_set_header Host $host;
307 proxy_set_header X-Real-IP $remote_addr;
308 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
309 proxy_set_header X-Forwarded-Proto $scheme;
310 proxy_set_header Upgrade $http_upgrade;
311 proxy_set_header Connection $connection_upgrade;
312 proxy_buffering off;
313 }
314 }
315 ```
316
317 **Vite SPA / static apps** use direct file serving:
318
319 ```nginx
320 server {
321 server_name example.com;
322 root /home/git/apps/web/<app>/;
323
324 location / {
325 try_files $uri $uri/ /index.html;
326 }
327 }
328 ```
329
330 Do not mix these patterns. An SSR app with `try_files` will return 404 for every server-rendered route. A static app with `proxy_pass` will fail because there is no PM2 process to proxy to.
331
332 ## CLI usage
333
334 ### Interactive mode
335
336 ```sh
337 cd my-monorepo/
338 smb deploy
339 # → "Select project to deploy"
340 # → 1. splitfireweb
341 # 2. musik88web
342 # 3. connecteddevices
343 ```
344
345 ### Direct mode (CI and scripts)
346
347 ```sh
348 smb deploy --project splitfireweb
349 smb deploy -p musik88web
350 smb deploy -p connecteddevices
351 ```
352
353 The `--project` / `-p` value must match the `name` field in a `[[projects]]` entry exactly. It is case-sensitive.
354
355 ## CI workflow parity
356
357 GitHub Actions workflows mirror the CLI deploy strategies. Each sub-project has its own workflow file triggered by a dedicated deploy branch.
358
359 ### Next.js SSR CI deploy
360
361 Trigger: push to `release/splitfire-web`
362
363 ```yaml
364 env:
365 GIT_HOST: api.smbcloud.xyz
366 GIT_USER: git
367 REMOTE_PATH: apps/web/splitfireweb
368 PM2_APP: splitfire-web
369
370 steps:
371 - name: Build
372 run: |
373 cd frontend/splitfire-web
374 pnpm install --ignore-scripts
375 pnpm build
376
377 - name: Upload
378 run: |
379 cd frontend/splitfire-web
380 rsync -az --delete --copy-links --mkpath .next/standalone/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/
381 rsync -az --delete --mkpath .next/static/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/.next/static/
382 rsync -az --delete --mkpath public/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/public/
383
384 - name: Restart
385 run: |
386 ssh $GIT_USER@$GIT_HOST bash << 'EOF'
387 set -e
388 cd ~/apps/web/splitfireweb
389 mkdir -p logs
390 if pm2 describe "splitfire-web" > /dev/null 2>&1; then
391 pm2 delete "splitfire-web"
392 fi
393 if [ -f ecosystem.config.cjs ]; then
394 pm2 start ecosystem.config.cjs --only "splitfire-web" --env production
395 elif [ -f ecosystem.config.js ]; then
396 pm2 start ecosystem.config.js --only "splitfire-web" --env production
397 else
398 PORT=3010 HOSTNAME=127.0.0.1 pm2 start node --name "splitfire-web" -- server.js
399 fi
400 pm2 save
401 EOF
402 ```
403
404 ### Rails CI deploy
405
406 Trigger: push to `release/backend-musik88-web`
407
408 ```yaml
409 steps:
410 - name: Upload shared lib
411 run: rsync -r ./lib $GIT_USER@$GIT_HOST:~/
412
413 - name: Compile native gem
414 run: ssh $GIT_USER@$GIT_HOST 'bash -s' < compile.sh ./lib/gems/gem_error_codes
415
416 - name: Git force-push
417 run: |
418 cd backend/musik88-web
419 git init
420 git add .
421 git commit -m "Deploy to production"
422 git remote add prod $GIT_USER@$GIT_HOST:musik88-production
423 git push --set-upstream prod main --force
424 ```
425
426 ### Vite SPA CI deploy
427
428 Trigger: push to the relevant deploy branch
429
430 ```yaml
431 steps:
432 - name: Build
433 run: |
434 cd frontend/karokowe-connected-devices
435 pnpm install
436 pnpm build
437
438 - name: Upload
439 run: |
440 cd frontend/karokowe-connected-devices
441 rsync -az --delete --mkpath dist/ $GIT_USER@$GIT_HOST:apps/web/connecteddeviceskarokowe/
442 ```
443
444 ### CI rsync flags
445
446 All CI rsync commands must include:
447
448 - `--mkpath` — creates remote directories if they do not exist (prevents failure on first deploy)
449 - `--delete` — removes stale files on the remote (prevents serving old assets)
450 - `-az` — archive mode with compression
451
452 For Next.js SSR standalone uploads, also include:
453
454 - `--copy-links` on `.next/standalone/` — dereferences `pnpm` symlinked dependencies so the server does not end up with broken `node_modules` links
455
456 ### CI PM2 restart
457
458 Current CI and CLI parity should use delete + fresh start, not restart-in-place:
459
460 - `pm2 delete <app>` if it exists
461 - `pm2 start ecosystem.config.cjs --only <app> --env production` when the `.cjs` file exists (preferred)
462 - fall back to `pm2 start ecosystem.config.js --only <app> --env production` if only the legacy `.js` file exists
463 - otherwise fall back to `PORT=<port> HOSTNAME=127.0.0.1 pm2 start node --name <app> -- server.js`
464 - `pm2 save`
465
466
467 When a monorepo root `package.json` has `"type": "module"`, PM2 cannot load a `.js` ecosystem config because Node treats it as ESM. The CLI auto-migrates `.js` → `.cjs` on the server during deploy and checks `.cjs` first when starting PM2.
468
469 This matches the standalone Next.js deploy model and avoids stale PM2 command state from older deploy methods.
470
471 ## Deployment tracking
472
473 Every deploy records status in the smbCloud API regardless of strategy:
474
475 1. `POST /deployments` with `status: Started` before transferring files
476 2. `PATCH /deployments/:id` with `status: Done` on success
477 3. `PATCH /deployments/:id` with `status: Failed` on failure
478
479 Current tracking model in the CLI:
480
481 - requests still route through the umbrella workspace `id`
482 - when `frontend_app_id` is present in the deploy target config, the payload also carries it so the API can attribute the deploy to the correct app inside the repo or monorepo
483
484 For non-git deploys (rsync, nextjs-ssr, vite-spa), the `commit_hash` field is a UTC timestamp (`20250920T163305Z`) since there is no git commit on the deploy path.
485
486 ## Adding a new sub-project to a monorepo
487
488 1. Create the project in smbCloud (via `smb init` in a temporary directory, or the web console) to get an `id`
489 2. Add a `[[projects]]` entry to the monorepo's `.smb/config.toml` with the correct `kind`, `source`, `path`, `runner`, and strategy-specific fields
490 3. If `kind = "nextjs-ssr"`: allocate a unique port, configure PM2 ecosystem file on the server, add Nginx reverse proxy config
491 4. If `kind = "vite-spa"`: add Nginx static file config
492 5. If `kind = "rails"`: set up a bare git repo on the server with a post-receive hook
493 6. Test with `smb deploy -p <name>`
494
495 ## Removing a sub-project
496
497 1. Remove the `[[projects]]` entry from `.smb/config.toml`
498 2. On the server: `pm2 delete <pm2_app> && pm2 save` (for SSR apps)
499 3. On the server: remove the Nginx config and `sudo nginx -t && sudo systemctl reload nginx`
500 4. On the server: remove the app directory
501
502 ## Standalone repos vs monorepo sub-projects
503
504 A standalone repo (like `aircraftshub-web` or `ondeinference.com`) has its own `.smb/config.toml` with `kind` and deploy fields directly in `[project]`. It has no `[[projects]]` array and `runner` is not `255`.
505
506 A monorepo sub-project has the same fields, but they live inside a `[[projects]]` entry. After `resolve_sub_project()`, the CLI treats them identically — all strategy code operates on `config.project` regardless of origin.
507
508 To convert a standalone repo into a monorepo sub-project:
509
510 1. Move the `[project]` fields into a `[[projects]]` entry
511 2. Create a new `[project]` with `runner = 255`
512 3. Set `source` to the sub-directory path
513 4. Adjust `path` if the remote directory structure differs
514
515 ## Debugging monorepo deploys
516
517 ### "No [[projects]] entries found"
518
519 The root `[project]` has `runner = 255` but there are no `[[projects]]` entries. Add at least one sub-project.
520
521 ### "Sub-project 'foo' not found in [[projects]]"
522
523 The `--project` name does not match any `[[projects]]` entry's `name` field. Check spelling and case.
524
525 ### Wrong sub-project deploys
526
527 If the interactive prompt selects the wrong project, verify the `[[projects]]` order in `.smb/config.toml` — the prompt lists them in file order.
528
529 ### Build runs in wrong directory
530
531 The `source` field is incorrect. It must be relative to the monorepo root where `smb deploy` is run.
532
533 ### Port conflict on multi-tenant server
534
535 Two `nextjs-ssr` apps have the same `port` value. PM2 will start both, but only one will bind the port. Check `pm2 list` and compare ports across all `.smb/config.toml` files that target the same server.
536
537 ### Stale assets after deploy
538
539 The Nginx `alias` directive for `/_next/static/` or the `root` for a Vite SPA points to the wrong directory or a stale deploy path. Verify the Nginx config matches the `path` field in `.smb/config.toml`.
540
541 ### `Cannot find module 'next'` after Next.js SSR deploy
542
543 The remote app directory still contains broken `pnpm` symlinks from `.next/standalone/`, or the standalone output itself is missing traced runtime files. Use the current CLI or CI flow with `rsync --copy-links` for the standalone upload, verify `node .next/standalone/server.js` works locally, then redeploy and inspect `/home/git/apps/web/<app>/node_modules/next` on the server.
544
545 ### PM2 not picking up new environment variables
546
547 Current CLI parity is delete + fresh start. Check these in order:
548
549 1. the server's `ecosystem.config.cjs` (or legacy `.js`) was edited, not just the repo copy
550 2. the deploy script is actually starting from `ecosystem.config.cjs` — if only a `.js` file exists and `"type": "module"` is set in `package.json`, PM2 will fail; rename to `.cjs`
551 3. `pm2 save` ran after the fresh start
552
553 If you are changing env manually outside the deploy flow, `pm2 restart <app> --update-env && pm2 save` is still a valid operator command.
554
555 ## Validation
556
557 ### Config
558
559 - every `[[projects]]` entry has a unique `name`
560 - every `[[projects]]` entry has a unique `id`
561 - every `nextjs-ssr` entry has a unique `port`
562 - `source` directories exist on disk
563 - `runner` values match the intended server tier
564
565 ### Local
566
567 - `smb deploy -p <name>` completes without error for each sub-project
568 - `cargo check -p smbcloud-cli` passes after any deploy code changes
569
570 ### Server
571
572 - `pm2 list` shows expected processes with correct names and ports
573 - `sudo nginx -t` passes
574 - each domain resolves to the correct app content
575 - `ls -la /home/git/apps/web/<app>/` confirms expected files
576
577 ## Common mistakes
578
579 - setting `runner = 255` on a standalone project that has no `[[projects]]` entries
580 - forgetting to set `runner = 255` on the root `[project]` of a monorepo — the CLI treats it as a standalone project and tries to deploy the root
581 - duplicating `port` values across sub-projects on the same server
582 - using `kind = "nextjs-ssr"` on a project without `output: "standalone"` in its Next.js config
583 - running `smb deploy` from a subdirectory instead of the monorepo root — `source` paths will not resolve
584 - forgetting `--mkpath` on rsync in CI workflows — first deploys fail because the remote directory does not exist
585 - forgetting `--copy-links` on `.next/standalone/` uploads — the server receives broken `pnpm` symlinks and PM2 crashes with `Cannot find module 'next'`
586 - keeping stale Nginx configs pointing at old deploy paths after migrating to a new directory structure
587 - mixing up `path` (remote destination) and `source` (local directory) — they serve different purposes
588 - treating the root workspace id as the deployable app id
589 - forgetting to add `frontend_app_id` on `[[projects]]` entries once the API exposes it
590 - using `pm2 restart` without `--update-env` after changing ecosystem file variables in older restart-in-place workflows
591 - assuming the CLI generates Nginx configs — it does not; Nginx is always configured manually on the server