-
-
Notifications
You must be signed in to change notification settings - Fork 0
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 manualdocker composecommands 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 compilesbcryptfrom source as a fallback if no prebuilt binary exists for your platform (see the Dockerfile step below) — slower, but should still succeed.
Running ./setup.sh does three things in order:
-
Install Node.js, if it isn't already on
PATH. -
Run
npm install, ifnode_modules/doesn't exist yet — installs this project's dependencies (Express, Mongoose, bcrypt, etc.) from the public npm registry intonode_modules/inside the repo. Nothing outside the repo is touched by this step. -
Launch the setup wizard (
node scripts/setup-wizard.js) — a small local web server that collects your.envvalues, 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).
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 -thensudo apt-get install -y nodejs(Debian/Ubuntu), or thednfequivalent (Fedora/RHEL/Rocky). If neitherapt-getnordnfis found, the script stops and asks you to install Node manually. Ifcurlitself 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 owncurluse.
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.
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.
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) 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_SECRETcall back to the wizard itself, which returnscrypto.randomBytes(64).toString('hex')— generated locally, never sent anywhere. -
SSL_CERT_FILE/SSL_KEY_FILElive check — as you type a path, the wizard callsfs.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_FQDNDNS check — as you type, the wizard callsdns.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_PORTlive 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_PORTadditionally 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:
-
(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
opensslCLI — nothing is sent over the network. Creates (or reuses, if already present) a Certificate Authority atcerts/ca.key/certs/ca.pem, then issues a leaf certificate atcerts/cert.key/certs/cert.pemforWEB_FQDN, with SAN entries coveringWEB_FQDNand this machine's LAN IP address(es) plus127.0.0.1. The CA's private key never leaves this step — it stays incerts/. 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. 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. -
Writes
.env(file mode0600, project root) with everything you filled in. -
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 ofsetup.shwithout reopening the wizard still knows how to redeploy. - Brings the app up, one of two ways depending on what you picked:
-
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 installsdocker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-pluginviasudo 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) andsudo usermod -aG docker <your username>(lets you rundockerwithoutsudo— needs a fresh login/newgrp dockerto take effect).
-
Linux, Rocky specifically:
-
Runs
docker compose up -d(with--buildunless you picked "pull a pre-built image", in which case it'sdocker compose pullfirst) using whichever compose files apply:docker-compose.ymlalways, plus eitherdocker-compose.nginx.yml(the bundled nginx sidecar — the default) ordocker-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, plusdocker-compose.mongo.ymlif you picked internal MongoDB, plusdocker-compose.pull.ymlif 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 — only when not using an existing host nginx instead), andmongo(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) orconf.d(RHEL/Rocky) layout, deletes the old site file (and itssites-enabledsymlink, if any) so nothing stale can linger, then writes three server blocks::80redirects to HTTPS forWEB_FQDNonly,:443servesWEB_FQDNover TLS, and a third listener onHOST_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). Runsnginx -t, thensudo systemctl reload nginx. The:80/:443FQDN 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 dedicatedHOST_NGINX_IP_PORTsidesteps 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
appimage build itself (seeDockerfile),npm install --omit=devand 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. Thatnpm installtemporarily installsbuild-essential/python3first and purges them again afterward, in the same image layer — a fallback sobcrypt'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.
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-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.
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. 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. 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/NGINX_HTTPS_PORT/HOST_NGINX_IP_PORT from it directly).
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://<lan-ip>: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).
| 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 |
/etc/nginx/sites-available/<name>.conf (+ a sites-enabled symlink on Debian/Ubuntu) or /etc/nginx/conf.d/<name>.conf (RHEL/Rocky) |
Host nginx site, only if "Existing nginx" was enabled | Outside the repo entirely |
-
Package managers/installers, only for whatever's actually missing: NodeSource/
apt/dnf(Node on Linux), Docker'sget.docker.comscript 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— thebcryptnative-module fallback mentioned above. -
Docker Hub, to pull the
node:20-bookworm-slimbase 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 fromFONDWAYPOINTS_IMAGE(defaults toghcr.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.
sudo is used only for:
- Installing Node.js or Docker system packages (Steps 1 and 4).
-
sudo systemctl enable --now dockerandsudo usermod -aG docker <user>(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 intosites-enabledon 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 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.