Browse Source

Rework deploy docs for FTP sync instead of git pull

The server gets code by FTP, not `git pull`, and the process is managed over SSH
(pm2/systemd). That flips the build story: build LOCALLY (your machine has the
devDeps), then upload the result. Because app/ is gitignored, it only reaches
the server if you build first and include it in the upload -- forget, and the
server serves stale files with no error.

  - README: rewritten around FTP. What to upload (app/ + app.js + package files),
    what the server needs (npm ci --omit=dev for the 5 runtime deps only, no
    build tooling), and the redeploy loop (build -> FTP -> restart).
  - Flagged the FTP-doesn't-delete trap: removed files (gulpfile, webrtc bundle,
    vendored libs) linger on the server; inside app/ a stale bundle could be
    served, so clear the server's app/ before uploading a fresh one.
  - ecosystem.config.cjs / radio.service.example: same correction, git-pull
    lines replaced with the local-build + FTP + restart flow.
windhamdavid 7 hours ago
parent
commit
27e2aecc80
3 changed files with 88 additions and 46 deletions
  1. 70 32
      deploy/README.md
  2. 7 5
      deploy/radio.service.example
  3. 11 9
      ecosystem.config.cjs

+ 70 - 32
deploy/README.md

@@ -4,37 +4,63 @@ The app lives at **davidwindham.com/radio** — a long-running Node process that
 Apache proxies to, same shape as `/chess` and `/ask`. It is *not* served from the
 web docroot.
 
-## The one thing that's different from chess
+Code is synced to the server by **FTP**, and the process is managed over SSH
+(pm2 or systemd), same as the other node apps.
 
-**This app has a build step.** Express serves `app/`, which is compiled from
-`src/` by esbuild and is **gitignored** — it is not in the repo. So production
-must build it, and building needs the devDependencies (esbuild, bootstrap,
-handlebars). That means a full `npm install`, not `npm ci --omit=dev`.
+## Build locally — this is the part that bites
 
-Every deploy is therefore: **pull → install → build → restart.**
+Express serves `app/`, which is compiled from `src/` by esbuild and is
+**gitignored**. FTP uploads files from your disk, so:
+
+> **Run `npm run build` locally before every upload, and make sure `app/` is
+> included in what you FTP up.**
+
+If you forget, the server keeps serving the *previous* `app/` (or none), and your
+changes silently don't appear — no error, just stale files. `src/` and the build
+tooling (esbuild, bootstrap, handlebars) never need to go to the server at all;
+they only exist to produce `app/` on your machine.
+
+## What the server actually needs
+
+Only four things, at runtime:
+
+| On the server | Where it comes from |
+| --- | --- |
+| `app/` | built locally, uploaded by FTP |
+| `app.js` | uploaded by FTP |
+| `package.json` (+ `package-lock.json`) | uploaded by FTP |
+| `node_modules/` (prod only — 5 packages) | `npm ci --omit=dev` on the server |
+
+Not needed on the server: `src/`, `build.mjs`, `esbuild`, `bootstrap`,
+`handlebars`. Those are build-time only.
 
 ## First-time setup
 
-1. **Check out the repo** on the server (e.g. `/var/www/radio`).
+1. **Build locally.**
+   ```sh
+   npm run build          # produces app/
+   ```
 
-   The default branch tracks `master`. If you deployed the `modernize` branch,
-   check that out instead.
+2. **FTP up** the project folder, including `app/`. At minimum:
+   `app/`, `app.js`, `package.json`, `package-lock.json`,
+   `ecosystem.config.cjs`, `deploy/`. Skip `node_modules/`, `.env`, `src/`.
 
-2. **Build.**
+3. **Install runtime deps on the server** (SSH, once — and again only when
+   `package.json` changes):
    ```sh
-   npm install          # full — the build needs the devDeps
-   npm run build        # src/ -> app/
+   cd /path/to/radio
+   npm ci --omit=dev      # installs ONLY the 5 runtime deps, no build tooling
    ```
+   *(No shell npm? Then also build `node_modules` locally with
+   `npm ci --omit=dev` and FTP it up. Slower — thousands of files — but works.)*
 
-3. **Configure secrets.** All operational config (PORT, BASE_PATH, TRUST_PROXY,
-   stream URLs) is set by the process manager below. The only secret is the
-   Last.fm API key. Put it in a gitignored `.env` in the repo root:
+4. **Set the secret** — a gitignored `.env` in the app root on the server:
    ```sh
    echo 'LASTFM_API_KEY=your-key-here' > .env
    ```
-   Unset is fine too — the sidebar lists just stay empty and the app still runs.
+   Unset is fine — the sidebar lists just stay empty and the app still runs.
 
-4. **Start it — pick one process manager:**
+5. **Start it — pick one process manager:**
 
    **pm2** (matches chess-io):
    ```sh
@@ -52,7 +78,7 @@ Every deploy is therefore: **pull → install → build → restart.**
    sudo systemctl enable --now radio
    ```
 
-5. **Add the Apache proxy.** Paste the block from
+6. **Add the Apache proxy.** Paste the block from
    `deploy/apache-radio.conf.example` into the **existing** davidwindham.com
    vhost (both `:80` and `:443`) — not a new vhost. Check the Apache version
    first (`apachectl -v`); the file has Option A for 2.4.47+ and Option B for
@@ -61,29 +87,41 @@ Every deploy is therefore: **pull → install → build → restart.**
    apachectl configtest && sudo systemctl reload apache2   # or: apachectl -k graceful
    ```
 
-6. **Verify:**
+7. **Verify:**
    ```sh
-   curl -s https://davidwindham.com/radio/health           # {"ok":true,...}
-   curl -s https://davidwindham.com/radio/api/status        # {"online":...}
+   curl -s https://davidwindham.com/radio/health            # {"ok":true,...}
+   curl -s https://davidwindham.com/radio/api/status         # {"online":...}
    ```
-   Then load `https://davidwindham.com/radio/` in a browser and confirm the
-   chat connects (network tab: a `101 Switching Protocols` on
-   `/radio/socket.io/…` means the websocket upgraded; HTTP 200 long-polls mean
-   the Apache upgrade rule isn't matching).
+   Then load `https://davidwindham.com/radio/` and confirm the chat connects
+   (network tab: a `101 Switching Protocols` on `/radio/socket.io/…` means the
+   websocket upgraded; HTTP-200 long-polls mean the Apache upgrade rule isn't
+   matching).
 
 ## Redeploying new code
 
 ```sh
-git pull && npm install && npm run build && pm2 restart radio
-#                                            (or: sudo systemctl restart radio)
-```
+# locally
+npm run build
 
-Optional, to slim `node_modules` after building — the devDeps are only needed
-at build time, since their output is baked into `app/`:
-```sh
-npm prune --omit=dev     # run AFTER npm run build, never before
+# FTP: upload the changed files — and app/ whenever you rebuilt
+
+# on the server (SSH)
+pm2 restart radio          # or: sudo systemctl restart radio
 ```
 
+Run `npm ci --omit=dev` on the server again **only** if `package.json` changed.
+The node process serves the new `app/` the moment it restarts — but it must
+restart; it won't pick up files live.
+
+**FTP uploads but never deletes.** Files removed from the project (this cleanup
+dropped `gulpfile.js`, the whole `webrtc-*` bundle, and vendored libs) will
+linger on the server after a plain upload — harmless clutter, except inside
+`app/`, where a stale bundle could actually be served. `npm run build` wipes
+`app/` locally each time, so the safe move is to **delete the server's `app/`
+before uploading the fresh one** (or use an FTP "mirror / delete orphans" mode
+scoped to `app/`). For a first deploy there's nothing there yet, so it doesn't
+matter.
+
 ## Prerequisites on the server
 
 - **Node 20+** (22.9+ if you use the `--env-file-if-exists` line in

+ 7 - 5
deploy/radio.service.example

@@ -7,10 +7,12 @@
 # apache-radio.conf.example). Adjust User/Group and WorkingDirectory to match
 # where the repo is checked out on the server.
 #
-# BUILD FIRST — this app serves the built bundle in app/, which is gitignored,
-# and systemd does NOT build. Before enabling the service, and after every pull:
+# BUILD LOCALLY — this app serves the built bundle in app/, which is gitignored,
+# and systemd does NOT build. Code is synced by FTP, so build on your machine
+# (`npm run build`) and include app/ in the upload. The server only needs the
+# runtime deps:
 #
-#     npm install && npm run build      # devDeps needed to build; app/ is output
+#     npm ci --omit=dev                  # 5 runtime deps only, no build tooling
 #
 # Then:
 #     sudo cp deploy/radio.service.example /etc/systemd/system/radio.service
@@ -19,8 +21,8 @@
 #     systemctl status radio
 #     journalctl -u radio -f             # logs (startup + connection events)
 #
-# On deploy of new code:
-#     git pull && npm install && npm run build && sudo systemctl restart radio
+# Redeploy: `npm run build` locally -> FTP up app/ + changed files ->
+#     sudo systemctl restart radio       (npm ci again only if package.json changed)
 
 [Unit]
 Description=daveo-radio (Icecast player + socket.io chat, node)

+ 11 - 9
ecosystem.config.cjs

@@ -4,20 +4,22 @@
 // ecosystem.config.js would be parsed as ESM and `module.exports` would throw.
 // pm2 reads .cjs fine.
 //
-// IMPORTANT — build first. Unlike chess-io (which renders templates at
-// runtime), this app serves the built bundle in app/, which is gitignored.
-// pm2 does NOT build. So on the server, before starting or after any pull:
+// IMPORTANT — build LOCALLY, upload the result. Unlike chess-io (which renders
+// templates at runtime), this app serves the built bundle in app/, which is
+// gitignored. Code is synced by FTP, so app/ only reaches the server if you
+// build it first and include it in the upload. pm2 does NOT build.
 //
-//     npm install          # full install -- the build needs the devDeps
-//                          # (esbuild, bootstrap, handlebars)
-//     npm run build        # src/ -> app/
-//     pm2 start ecosystem.config.cjs      # first time
+// On the server (SSH), first time:
+//     npm ci --omit=dev                   # 5 runtime deps only, no build tooling
+//     pm2 start ecosystem.config.cjs
 //     pm2 logs radio --lines 5            # confirm  basePath":"/radio"
 //     pm2 save                            # remember across restarts
 //     pm2 startup                         # prints a sudo command to enable on boot
 //
-// Redeploy of new code:
-//     git pull && npm install && npm run build && pm2 restart radio
+// Redeploy: `npm run build` locally -> FTP up app/ + changed files ->
+//     pm2 restart radio        (npm ci again only if package.json changed)
+//
+// Full runbook: deploy/README.md
 //
 module.exports = {
   apps: [