Skip to content

What the Setup Script Does

j5guy edited this page Jul 22, 2026 · 4 revisions

What the setup script does

This is a line-by-line account of everything setup.sh and the wizard they launch (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.

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 (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 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 — with your OS/version and what you ran, so it can get fixed for the next person. In the meantime, Manual / non-Docker deployment 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. Pick "Build locally" — the published image (ghcr.io/j5guy/fondwaypoints) is only built for amd64 (see CI/CD and Releases), so "pull a pre-built image" won't work there. Building locally compiles bcrypt from source as a fallback if no prebuilt binary exists for your platform (see the Dockerfile step below) — slower, but should still succeed.

The three phases

Running ./setup.sh does three things in order:

  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.

If .env already exists, you're asked whether to re-run the wizard (to change settings) or skip it — skipping just redeploys with whatever's already in .env and .deploy-state.json (see Re-running later).

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://<your-lan-ip>:<port>/) 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.

While the form is open, the wizard also does two small live-check requests as you type, both 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.

Submitting the form (POST /save) does the following, in order:

  1. (Docker + "Generate one" cert mode only) 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 — 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.
  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) 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 <your username> (lets you run docker without sudo — needs a fresh login/newgrp docker to take effect).
  • Runs docker compose up -d (with --build unless you picked "pull a pre-built image", in which case it's docker compose pull first) using whichever compose files apply: docker-compose.yml always, plus docker-compose.mongo.yml if you picked internal MongoDB, plus docker-compose.pull.yml if pulling. This is what actually builds/pulls and starts the containers: app (this application), nginx (TLS termination — reads the cert from step 1 or your own files), and mongo (only if internal MongoDB was picked).
  • Inside the app image build itself (see Dockerfile), npm install --omit=dev and the SCSS build (sasspublic/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.scsspublic/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-name>.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 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 (e.g. https://fondwaypoints.example.com:5560/). 127.0.0.1 is deliberately never listed — nothing outside this machine can reach the site there, so printing it would be misleading. The same happens on a later run of setup.sh that skips reopening the wizard and just redeploys from the existing .env (via scripts/printAccessUrls.js, reading WEB_FQDN/PORT from it directly).

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/<name>.service (local mode) systemd unit 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. Other Docker registries, only if you picked "Pull a pre-built image" instead — pulls from FONDWAYPOINTS_IMAGE (defaults to ghcr.io/j5guy/fondwaypoints:latest).
  • 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 <user> (Linux Docker install).
  • Writing the systemd unit and enabling/starting the service (local deploy mode).

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 setup.sh after .env already exists asks whether to reopen the wizard (to change settings — prefilled with your current values) or leave .env alone and just redeploy/restart with what's already there, using .deploy-state.json to know which mode you're in. Local-mode service installs use restart (not a fresh enable), and the Docker CA is reused rather than regenerated, so a re-run won't invalidate CA trust you've already installed on other devices.

Clone this wiki locally