1 ---
2 name: smbcloud-deploy-nextjs
3 description: Use when deploying or debugging Next.js apps on smbCloud, especially the dedicated `nextjs-ssr` flow that builds locally, uploads `.next/standalone` via rsync, restarts PM2 over SSH, and serves traffic behind Nginx.
4 ---
5
6 # smbCloud Deploy Next.js
7
8 Use this skill when work touches Next.js deployment on smbCloud.
9
10 Applies to:
11
12 - `kind = "nextjs-ssr"` projects
13 - `.smb/config.toml` deploy configuration
14 - `process_deploy_nextjs_ssr.rs`
15 - PM2 process management for standalone builds
16 - Nginx proxying for SSR apps
17 - production-only issues caused by CORS, ports, or runtime mismatch
18
19 ## Deployment model
20
21 For `nextjs-ssr`, smbCloud does not use git-push deploy.
22
23 The CLI route in `process_deploy.rs` short-circuits on:
24
25 - `kind = "nextjs-ssr"`
26
27 That path builds locally, then:
28
29 - rsyncs `.next/standalone/` into the remote app directory root
30 - rsyncs `.next/static/` into `remote/.next/static/`
31 - rsyncs `public/` into `remote/public/`
32 - runs `ssh` and restarts the app with PM2
33
34 Do not describe this flow as git based unless the project is actually using the generic git deploy path.
35
36 ## Required app configuration
37
38 The Next.js app must produce a standalone server bundle.
39
40 Create `next.config.mjs` or equivalent with:
41
42 ```js
43 /** @type {import('next').NextConfig} */
44 const nextConfig = {
45 output: "standalone",
46 };
47
48 export default nextConfig;
49 ```
50
51 Without `output: "standalone"`, the deploy code fails because `.next/standalone/` is missing.
52
53 The app package scripts should support:
54
55 - `build`: `next build`
56
57 `pnpm` is the safe package manager for the current CLI implementation.
58
59 ## Required smbCloud config
60
61 Minimum `.smb/config.toml` for SSR:
62
63 ```toml
64 [project]
65 deployment_method = 1
66 kind = "nextjs-ssr"
67 source = "."
68 package_manager = "pnpm"
69 pm2_app = "my-app"
70 path = "apps/web/my-app"
71 ```
72
73 Important rules:
74
75 - `kind = "nextjs-ssr"` is what activates the SSR deploy path
76 - `source` is the local Next.js app directory
77 - `path` is the remote destination directory on the server
78 - `pm2_app` is mandatory because the CLI restarts PM2 by name
79 - `deployment_method = 1` may still be present, but the `kind` routing is what matters most
80
81 ## Expected remote layout
82
83 After a successful deploy, the remote directory should look like:
84
85 - `/home/git/apps/web/<app>/server.js`
86 - `/home/git/apps/web/<app>/.next/static/...`
87 - `/home/git/apps/web/<app>/public/...`
88
89 Note that `server.js` is expected at the remote root because the CLI uploads the contents of `.next/standalone/`, not the folder itself.
90
91 ## PM2 behavior
92
93 The CLI's deploy path runs logic equivalent to:
94
95 ```sh
96 cd "$APP_PATH"
97
98 if pm2 describe "$PM2_APP" > /dev/null 2>&1; then
99 pm2 restart "$PM2_APP"
100 else
101 PORT=3010 HOSTNAME=127.0.0.1 pm2 start node --name "$PM2_APP" -- server.js
102 fi
103
104 pm2 save
105 ```
106
107 This has two consequences:
108
109 - old git-based `post-receive` hooks are irrelevant for the SSR path
110 - the port is currently hardcoded to `3010` in the CLI for fresh starts unless patched
111
112 ### Environment variables — ecosystem file (standard pattern)
113
114 The server runs in a **multi-tenant setup**: multiple Next.js apps share one server, each managed as a separate PM2 process. The standard way to manage per-app environment variables is an `ecosystem.config.js` file kept **on the server only** (never committed to the app repo).
115
116 The flow works like this:
117
118 1. On first setup, SSH in, create the ecosystem file, start with it, and save:
119
120 ```sh
121 ssh git@<server>
122 cd /home/git/apps/web/<app>
123
124 cat > ecosystem.config.js << 'EOF'
125 module.exports = {
126 apps: [{
127 name: "<pm2_app>",
128 script: "server.js",
129 cwd: "/home/git/apps/web/<app>",
130 env_production: {
131 NODE_ENV: "production",
132 PORT: 3026,
133 HOSTNAME: "127.0.0.1",
134 MY_SECRET: "...",
135 }
136 }]
137 }
138 EOF
139
140 pm2 start ecosystem.config.js --env production
141 pm2 save
142 ```
143
144 2. Every subsequent CLI deploy runs `pm2 restart <pm2_app>`. Because `pm2 save` already persisted the env, PM2 restarts with the same environment — **the CLI does not need to know about the ecosystem file at all**.
145
146 3. To change or add env vars, SSH in, edit `ecosystem.config.js`, then:
147
148 ```sh
149 pm2 restart <pm2_app> --update-env
150 pm2 save
151 ```
152
153 The ecosystem file is the server-side source of truth for each app’s runtime config. Future versions of the smbCloud web console will provide a UI to manage these vars per app, which will write to this file and trigger the restart remotely.
154
155 ### Manual PM2 operations (without ecosystem file)
156
157 If the app is already deployed and `server.js` exists:
158
159 ```sh
160 cd /home/git/apps/web/<app>
161 pm2 restart <pm2_app> --update-env
162 pm2 save
163 ```
164
165 If starting from scratch without an ecosystem file:
166
167 ```sh
168 cd /home/git/apps/web/<app>
169 PORT=<port> HOSTNAME=127.0.0.1 pm2 start node --name <pm2_app> -- server.js
170 pm2 save
171 ```
172
173 ## Port management
174
175 Use one source of truth for the runtime port.
176
177 If an older `ecosystem.config.js` exists and contains both:
178
179 - `args: "start -p 3026"`
180 - `env_production.PORT = 3025`
181
182 that is a broken configuration. Standardize on one port value.
183
184 For the current SSR deploy code, the real runtime is controlled by the environment passed to `node server.js`, not by `next start` or an ecosystem file.
185
186 If the app must run on a port other than `3010`, patch the CLI or restart PM2 manually with the correct port.
187
188 ## Nginx for SSR
189
190 Do not use a static-site Nginx config like:
191
192 ```nginx
193 root /home/git/apps/web/my-app;
194 location / {
195 try_files $uri $uri/ =404;
196 }
197 ```
198
199 That is for static files, not Next.js SSR.
200
201 Use reverse proxying to the local Node process instead:
202
203 ```nginx
204 location / {
205 proxy_pass http://127.0.0.1:3026;
206 proxy_http_version 1.1;
207
208 proxy_set_header Host $host;
209 proxy_set_header X-Real-IP $remote_addr;
210 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
211 proxy_set_header X-Forwarded-Proto $scheme;
212
213 proxy_set_header Upgrade $http_upgrade;
214 proxy_set_header Connection "upgrade";
215
216 proxy_cache_bypass $http_upgrade;
217 }
218 ```
219
220 Nginx must proxy to the actual PM2 port.
221
222 ## Common failure modes
223
224 ### `.next/standalone` missing
225
226 Cause:
227
228 - no `output: "standalone"` in Next config
229
230 Fix:
231
232 - add standalone output
233 - rebuild locally
234
235 ### `server.js` missing on the server
236
237 Check:
238
239 - whether the SSR deploy path actually ran
240 - whether `path` points to the directory you inspected
241 - whether `rsync` uploaded to a different location
242
243 ### Missing `public/`
244
245 The current deploy code tries to rsync `public/` unconditionally.
246
247 If the project has no `public/` directory, deploy may fail. Either:
248
249 - create an empty `public/`
250 - or patch the CLI to skip missing directories
251
252 ### Wrong port after deploy
253
254 Cause:
255
256 - CLI hardcodes `PORT=3010` for fresh starts
257
258 Fix:
259
260 - manually restart with the correct port
261 - or patch the CLI to read a configurable port from `.smb/config.toml`
262
263 ### Misleading deploy output
264
265 The CLI may print generic deploy messages such as:
266
267 - `Deploying > Building the app`
268 - `App is running`
269
270 These messages do not mean git push happened. Confirm behavior from the code path, not the spinner text.
271
272 ## Auth and production-only web issues
273
274 If the Next.js app uses the wasm auth client from `smbcloud-auth-sdk-wasm`, production failures that show:
275
276 - `Network error. Please check your internet connection and try again.`
277
278 are often not actual connectivity failures.
279
280 Check these first:
281
282 - Rails CORS allows the production origin
283 - `Authorization` response header is exposed over CORS
284 - backend auth status codes match the Rust parser logic
285
286 ### Rails requirement
287
288 For browser clients using `POST /v1/client/users/sign_in`, if the token is returned in the `Authorization` response header, CORS must include:
289
290 - production origins such as `https://ondeinference.com`
291 - `expose: %w[... Authorization]`
292
293 Without that, browser fetch in wasm can surface as a fake network error.
294
295 ### Rust requirement
296
297 The shared Rust networking layer must not collapse all non-200 auth responses into `NetworkError`.
298
299 If backend returns meaningful auth errors such as `401` invalid password, update the parser in `request_login` to preserve the real failure.
300
301 ## Recommended validation
302
303 Use the smallest checks that prove the contract.
304
305 ### Next.js app
306
307 - `pnpm build`
308 - verify `.next/standalone/server.js` exists
309
310 ### smbcloud-cli
311
312 - `cargo check -p smbcloud-network`
313 - `cargo check -p cli` when deploy code changed
314
315 ### Server
316
317 - `pm2 list`
318 - `pm2 logs <pm2_app>`
319 - `ls -la /home/git/apps/web/<app>`
320 - `sudo nginx -t`
321
322 ## Common mistakes
323
324 - assuming `deployment_method = 1` alone activates SSR deploy
325 - forgetting `kind = "nextjs-ssr"`
326 - keeping an old git `post-receive` hook and expecting it to manage SSR deploys
327 - using static-file Nginx config for an SSR app
328 - editing `ecosystem.config.js` but forgetting `pm2 restart <app> --update-env && pm2 save` — PM2 keeps the old env until explicitly refreshed
329 - leaving port values inconsistent across PM2, Nginx, and CLI
330 - treating browser CORS failures as real internet outages