# What the setup script does This is a line-by-line account of everything `install.sh` (the curl/wget-able entrypoint; `setup.sh` is an alias for it, for an existing checkout) and the wizard it launches (`scripts/setup-wizard.js`) install, configure, write to disk, or send over the network — for anyone who wants to know exactly what they're running before they run it, or is troubleshooting a step that failed partway through. For the *usage* walkthrough (what the form asks and what to pick), see the [Installation Guide](Installation-Guide.md). For what a later `./update.sh` run does differently, see [Updating](Updating.md). Nothing here runs with elevated privileges until explicitly noted, and every `sudo` prompt is called out below — the script never silently escalates. > **Testing scope.** The Linux path (`install.sh`/`setup.sh`) has only actually been run on **Rocky Linux 9** and > **Ubuntu 24.04/26** — Ubuntu tested as a VM and on bare metal, *not* as an LXC container (see > [Ubuntu LXC is not recommended](Installation-Guide.md#docker-deployment-in-full) in the Installation > Guide) — other distros follow the same logic but haven't been as thoroughly > exercised. This script covers a lot > of environment variation (package managers, > service managers, Docker installers) automatically, and won't be perfect on every system. If > something breaks, please report it — [GitHub > issues](https://github.com/j5guy/fondwaypoints/issues) — with your OS/version and what you ran, so it > can get fixed for the next person. In the meantime, [Manual / non-Docker > deployment](Installation-Guide.md#manual--non-docker-deployment-in-full) or the manual `docker > compose` commands are always a fallback if the script itself doesn't work on your system. > > All of the above is x86_64 only — **ARM (e.g. Raspberry Pi) is untested**, but should mostly work: > Node and Docker Engine both officially support Debian/Ubuntu on arm64/armhf. The guided setup always > builds the image locally — this project doesn't publish a pre-built one itself (see [CI/CD and > Releases](CI-CD-and-Releases.md)) — so there's no architecture-mismatch pitfall to worry about here. > Building locally compiles `bcrypt` from source as a fallback if no prebuilt binary exists for your > platform (see [the Dockerfile step below](#if-you-picked-docker)) — slower, but should still succeed. ## The phases Running `curl ... | bash` (or `./install.sh`/`./setup.sh` from an existing checkout) does the following in order: 0. **Clone the repo into a scratch directory** (skipped when run from an existing checkout — that checkout is used in place instead) — `git clone --depth 1`, after installing `git`/`curl` first if either is missing. 1. **Install Node.js**, if it isn't already on `PATH`. 2. **Run `npm install`**, if `node_modules/` doesn't exist yet — installs this project's dependencies (Express, Mongoose, bcrypt, etc.) from the public npm registry into `node_modules/` inside the repo. Nothing outside the repo is touched by this step. 3. **Launch the setup wizard** (`node scripts/setup-wizard.js`) — a small local web server that collects your `.env` values, then brings the app up. This is where the interesting stuff happens; see below — including the "Installation footprint" choice (only offered when a scratch clone from step 0 is actually in play) that decides whether the checkout stays, or gets trimmed down and deleted once the stack is up (see [Updating](Updating.md) for the equivalent split on a later update). Re-running `install.sh`/`setup.sh` always reopens the wizard, prefilled with whatever's already in `.env`/`.deploy-state.json`. To redeploy/restart with the existing settings unchanged instead — e.g. after a code update — use `./update.sh` (see [Updating](Updating.md)), not the wizard. ## Step 1: installing Node.js Only runs if `node` isn't found on `PATH`. Exact commands, per platform: - **Linux** (`setup.sh`): NodeSource's install script, via whichever package manager is present — `curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -` then `sudo apt-get install -y nodejs` (Debian/Ubuntu), or the `dnf` equivalent (Fedora/RHEL/Rocky). If neither `apt-get` nor `dnf` is found, the script stops and asks you to install Node manually. If `curl` itself isn't present (some minimal images, recent Ubuntu included, don't ship it) it's installed first via the same package manager — the Docker install path (below) does the same check before its own `curl` use. ## Step 2: installing npm dependencies Only runs if `node_modules/` doesn't already exist (so a re-run doesn't redo it every time). Plain `npm install` from the project root, pulling this project's dependencies (Express, Mongoose, bcrypt, etc. — see `package.json`) from the public npm registry into `node_modules/`. Nothing outside the repo is touched — no system-wide packages, no config written anywhere else. ## Step 3: the setup wizard `scripts/setup-wizard.js` starts a plain Express server, **bound to your machine** (not the public internet) on the first free port from `5599` up, and opens it in your default browser automatically. If you're on a headless server with no GUI, it also prints a LAN URL (`http://:/`) you can open from another computer — anyone else on that network can reach this page, unauthenticated, until you submit the form, so keep that window short on a shared network. When rendering the form, it also checks whether this CPU can actually run current MongoDB versions (reading `/proc/cpuinfo` for the `atomics` feature flag — see [MongoDB on ARM](Installation-Guide.md#docker-deployment-in-full)) and shows a plain read-only notice next to the MongoDB choice saying which image it'll use and why — there's no field to fill in. If **Internal MongoDB** is submitted, `MONGO_IMAGE` in `.env` is set to match (`mongo:4.4.18` if the CPU check failed, otherwise left blank so `docker-compose.mongo.yml`'s own default, `mongo:7`, applies). The wizard also checks whether nginx is already installed on this host (`nginx --version`, and `systemctl is-active nginx` for whether it's actually running) and, if so, scans `/etc/nginx/sites-enabled/` and `/etc/nginx/conf.d/` for every distinct `ssl_certificate`/ `ssl_certificate_key` pair already referenced there — offered in the form as picks instead of generating a new certificate. This is shown in an **Existing nginx** section: an INFO note if nginx is detected (with a checkbox to add a site for it, described further below), or a PASS note if it isn't ("the bundled Docker nginx container will be deployed instead" in Docker mode; a note pointing at the local-mode reverse-proxy warning otherwise). The **TLS certificate** choice is shown right after this, since which options actually apply depends on it. While the form is open, the wizard also does several small live-check requests as you type, all entirely local: - **"generate" buttons** next to `sessionSecret`/`PRELOGIN_SECRET` call back to the wizard itself, which returns `crypto.randomBytes(64).toString('hex')` — generated locally, never sent anywhere. - **`SSL_CERT_FILE`/`SSL_KEY_FILE` live check** — as you type a path, the wizard calls `fs.statSync()` on *its own* host (the machine running the wizard, which is also the machine Docker will read the bind-mounted cert from) to show a ✓/✗ next to the field. - **`WEB_FQDN` DNS check** — as you type, the wizard calls `dns.lookup()` on its own host and shows a green "DNS resolves for this address" note if it resolves, or a yellow note if it doesn't, saying setup can still proceed but it's recommended to have the name in DNS (or a hosts file entry) before relying on it day-to-day. This check is informational only and never blocks saving — but the field itself is required and can't be left blank (it starts empty, with a suggested-format note next to it, rather than prefilled with a placeholder domain). - **`NGINX_HTTPS_PORT`/`HOST_NGINX_IP_PORT` live port check** — as you type either port field (the bundled Docker nginx's HTTPS port, or an existing host nginx's LAN-IP port — see below), the wizard attempts to actually bind that port on its own host and reports whether it's free, suggesting the next free port after it if not. `HOST_NGINX_IP_PORT` additionally can't match the app's own fixed port (5560) — that number is always already taken on this host in "Existing nginx" mode. - **"Test connection" button** next to MongoDB — attempts an authenticated connection with whatever's currently typed into the form (host/port/user/pass/db name), without saving anything yet. Required to pass before submitting whenever MongoDB isn't the bundled internal container (i.e. local mode, or Docker + External MongoDB) — the internal container doesn't exist yet at this point in the flow, so there's nothing reachable to test there. Every field marked "Required" (`sessionSecret`, `PRELOGIN_SECRET`, `ADMIN_EMAIL`, `WEB_FQDN`, and `SSL_CERT_FILE`/`SSL_KEY_FILE` whenever "I'll provide my own certificate files" applies) swaps that red note for a green "PASS: filled in." one live, the moment it actually has something in it — no server round-trip, just checking the field isn't blank. Submitting the form (`POST /save`) does the following, in order: 1. **(Only when a certificate is actually needed — Docker mode always, or local mode with "Existing nginx" enabled — and "Generate one" is picked) Generates a local TLS certificate.** Entirely local via the `openssl` CLI — nothing is sent over the network. Creates (or reuses, if already present) a Certificate Authority at `certs/ca.key` / `certs/ca.pem`, then issues a leaf certificate at `certs/cert.key` / `certs/cert.pem` for `WEB_FQDN`, with SAN entries covering `WEB_FQDN` and this machine's LAN IP address(es) plus `127.0.0.1`. The CA's private key never leaves this step — it stays in `certs/`. **This certificate isn't trusted by anything yet** — see [Import the certificate first](Installation-Guide.md#guided-setup-in-full) — you (and anyone else who needs to reach the site) still need to install the downloaded CA certificate into each device's OS/browser trust store before visiting the site there. If instead one of the detected "Use the certificate detected at..." options was picked, that cert/key path is used as-is — nothing is generated. 2. **Writes `.env`** (file mode `0600`, project root) with everything you filled in. 3. **Writes `.deploy-state.json`** — non-secret bookkeeping (deploy mode, service name/user, Mongo mode, cert mode, image source, whether "Existing nginx" was enabled) so a later re-run of `setup.sh` without reopening the wizard still knows how to redeploy. 4. **Brings the app up**, one of two ways depending on what you picked: ### If you picked Docker - **Installs Docker, only if it's missing, and only now that you've actually chosen Docker** — never preemptively. Exact commands: - **Linux, Rocky specifically**: `sudo dnf install dnf-plugins-core`, then registers Docker's RHEL repo directly (`download.docker.com/linux/rhel/docker-ce.repo` — Docker's own Rocky repo is missing packages, so this sidesteps that) and installs `docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin` via `sudo dnf`. - **Linux, everything else**: Docker's official convenience script — `curl -fsSL https://get.docker.com | sudo sh`. - **Linux only, after either path**: `sudo systemctl enable --now docker` (starts Docker and sets it to start on boot) and `sudo usermod -aG docker ` (lets you run `docker` without `sudo` — needs a fresh login/`newgrp docker` to take effect). - **Runs `docker compose up -d --build`** — always builds the image from this checkout (see [Why build locally instead of pulling an image?](https://github.com/j5guy/fondwaypoints#why-build-locally-instead-of-pulling-an-image) in the README) — using whichever compose files apply: `docker-compose.yml` always, plus either `docker-compose.nginx.yml` (the bundled nginx sidecar — the default) or `docker-compose.host- nginx.yml` (exposes the app container's port directly to the host instead, skipping the bundled nginx entirely) if "Existing nginx" was enabled, plus `docker-compose.mongo.yml` if you picked internal MongoDB. This is what actually builds and starts the containers: `app` (this application), `nginx` (TLS termination — reads the cert from step 1 or your own files — only when not using an existing host nginx instead), and `mongo` (only if internal MongoDB was picked). - **(If "Existing nginx" was enabled) Removes any previous version of this site, then writes and enables a new one.** Detects the `sites-available`/`sites-enabled` (Debian/Ubuntu) or `conf.d` (RHEL/Rocky) layout, deletes the old site file (and its `sites-enabled` symlink, if any) so nothing stale can linger, then writes three server blocks: `:80` redirects to HTTPS for `WEB_FQDN` only, `:443` serves `WEB_FQDN` over TLS, and a third listener on `HOST_NGINX_IP_PORT` (the port you picked, live-checked above) serves this host's LAN IP address(es) — all three proxy to the app's fixed port (5560). Runs `nginx -t`, then `sudo systemctl reload nginx`. The `:80`/`:443` FQDN blocks deliberately don't also answer to the bare IP, since this host's nginx may front other sites too — claiming the IP on the same port they use could steal it from them; the dedicated `HOST_NGINX_IP_PORT` sidesteps that entirely. This is why the bundled nginx sidecar above is skipped in this case — running both would fight over host port 80/443. - Inside the `app` image build itself (see `Dockerfile`), `npm install --omit=dev` and the SCSS build (`sass` → `public/css/main.css`) both happen — you don't need Node installed on the Docker host for this path beyond what the wizard itself needed to run. That `npm install` temporarily installs `build-essential`/`python3` first and purges them again afterward, in the same image layer — a fallback so `bcrypt`'s native module compiles from source on platforms without a prebuilt binary for it (e.g. possibly ARM — see the testing-scope note above) instead of failing the build outright. ### If you picked "Local Node.js process" No Docker involved at all. Runs `npm install` and `npm run build-css` (compiles `public/scss/main.scss` → `public/css/main.css`), then registers `node server.js` as a background service so it survives reboots and restarts on crash: Writes a systemd unit to `/etc/systemd/system/.service` (via `sudo tee` — you'll be prompted for a sudo password), then `sudo systemctl daemon-reload`, `enable`, and `restart`. Runs as whatever user you specified in the form (defaults to whoever ran the wizard). This mode does **not** set up MongoDB, TLS, or a reverse proxy for you — point `mongoHost` in `.env` at an existing MongoDB server, and put your own TLS-terminating reverse proxy in front of the app yourself. See [Local Node.js process, without the wizard](Installation-Guide.md#manual--non-docker-deployment-in-full) for the manual equivalent of everything above. ### Finally: prints where the app is reachable Once Docker (or the local service) is actually up, the last thing printed is the URL(s) the app is reachable at — `WEB_FQDN` plus this machine's LAN IP address(es), scheme and port included — highlighted (bold cyan) in the terminal so it stands out from the rest of the setup output, via `scripts/lib/network.js`'s `highlight()` (skipped automatically when stdout isn't a real terminal, or `NO_COLOR` is set). In Docker mode (without "Existing nginx") that's `NGINX_HTTPS_PORT`, the bundled nginx's own port (e.g. `https://fondwaypoints.example.com:5560/` — 5560 is just the default; you may have picked a different free port in the form). In local mode (without "Existing nginx") it's the app's own fixed port, 5560 — whatever reverse proxy you're running yourself is assumed to be terminating TLS there. `127.0.0.1` is deliberately never listed — nothing outside this machine can reach the site there, so printing it would be misleading. `./update.sh` (see [Updating](Updating.md)) prints the same highlighted URL(s) again once it finishes. If "Existing nginx" was enabled instead, this changes: the app's own fixed port is plain HTTP at that point (the host nginx is doing TLS, not that port directly), which isn't a secure context, so the app's client-side WebCrypto wouldn't actually work there — it's never printed. Instead you get `https://WEB_FQDN/` (through the host nginx, once its site is confirmed live) plus `https://:HOST_NGINX_IP_PORT/` for each LAN IP address — the dedicated port that same host nginx site also listens on for direct IP access (see above). ## Everything written to disk, in one place | Path | What | Committed to git? | | --- | --- | --- | | `node_modules/` | npm dependencies | No (`.gitignore`) | | `.env` | Your secrets/config, mode `0600` | No (`.gitignore`) | | `.deploy-state.json` | Non-secret deploy bookkeeping, so re-runs know what to do | No (`.gitignore`) | | `certs/ca.key`, `certs/ca.pem` | Local CA, only if you picked "Generate one" | No (`.gitignore`) | | `certs/cert.key`, `certs/cert.pem`, `certs/cert.csr`, `certs/cert.ext` | Leaf cert issued by the above | No (`.gitignore`) | | `public/css/main.css` | Compiled from `public/scss/` | No (`.gitignore`) | | `logs/` | App logs (Winston); also service stdout/stderr | No (`.gitignore`) | | `/etc/systemd/system/.service` (local mode) | systemd unit | Outside the repo entirely | | `/etc/nginx/sites-available/.conf` (+ a `sites-enabled` symlink on Debian/Ubuntu) or `/etc/nginx/conf.d/.conf` (RHEL/Rocky) | Host nginx site, only if "Existing nginx" was enabled | Outside the repo entirely | ## Everything that touches the network - **Package managers/installers**, only for whatever's actually missing: NodeSource/`apt`/`dnf` (Node on Linux), Docker's `get.docker.com` script or RHEL repo (Docker on Linux). - **The npm registry**, for `npm install` (this project's dependencies) — both directly (local/Docker build) and inside the CI-published image build. - **Debian's package mirrors**, inside the Docker image build only, to install (and then remove again) `build-essential`/`python3` — the `bcrypt` native-module fallback mentioned above. - **Docker Hub**, to pull the `node:20-bookworm-slim` base image the first time you build locally (or whenever it's changed) — cached after that. The guided setup never contacts any other registry — see [Why build locally instead of pulling an image?](https://github.com/j5guy/fondwaypoints#why-build-locally-instead-of-pulling-an-image) in the README. - **Whatever you configure**: the MongoDB host and SMTP relay you enter into the form — the app talks to those once it's running, same as any deployment. - **Nothing else.** The wizard's own web server is local/LAN-only and never phones home; there's no telemetry or analytics anywhere in this project. ## Privilege escalation, called out explicitly `sudo` is used only for: - Installing Node.js or Docker system packages (Steps 1 and 4). - `sudo systemctl enable --now docker` and `sudo usermod -aG docker ` (Linux Docker install). - Writing the systemd unit and enabling/starting the service (local deploy mode). - (If "Existing nginx" was enabled) Writing the host nginx site config (`sudo tee`), symlinking it into `sites-enabled` on Debian/Ubuntu layouts (`sudo ln -sf`), testing it (`sudo nginx -t`), and reloading nginx (`sudo systemctl reload nginx`). If `sudo` isn't available at all (and you're not already root), the relevant step fails with a message telling you so rather than silently skipping it. ## Re-running later Re-running `install.sh`/`setup.sh` after `.env` already exists always reopens the wizard, prefilled with your current values (including `.deploy-state.json`'s record of which mode you picked last time) — use this to change a setting. Local-mode service installs use `restart` (not a fresh `enable`), and the Docker CA is reused rather than regenerated, so re-running this won't invalidate CA trust you've already installed on other devices. To instead just redeploy/restart with what's already there — most commonly, to pick up a newer release — use `./update.sh` (see [Updating](Updating.md)) rather than reopening the wizard.