-
-
Notifications
You must be signed in to change notification settings - Fork 0
Installation Guide
For the quick-start commands, see Installation in the README. This page covers the full guided-setup walkthrough and doing it by hand instead. For a step-by-step account of exactly what the setup script installs, writes to disk, and sends over the network — useful if you want to know before running it, or something failed partway through — see What the Setup Script Does.
The guided setup below is the quickest and easiest way to get running. Prefer to skip the repo checkout entirely and run just the published Docker image yourself instead? Skip ahead to Just the image, no repo checkout.
The one-click setup script installs Node if needed and walks you through .env in a browser form
(with a "generate" button for the two secrets):
- Linux:
./setup.sh
Testing scope:
setup.shhas only actually been run on Rocky Linux 9 and Ubuntu 24.04/26 (x86_64) — Ubuntu tested as a VM and on bare metal, not as an LXC container (see Ubuntu LXC is not recommended below). It handles a lot of environment variation automatically and won't be perfect everywhere — please report anything that breaks with your OS/version and what happened. See What the Setup Script Does for exactly what it does, and Manual / non-Docker deployment or the manualdocker composecommands below as a fallback.ARM / Raspberry Pi: untested, but should mostly work — Node and Docker Engine both officially support Debian/Ubuntu on arm64/armhf. Pick "Build locally", not "pull a pre-built image" (the published image at
ghcr.io/j5guy/fondwaypointsis amd64-only — see CI/CD and Releases). The image build compilesbcryptfrom source as a fallback if no prebuilt binary exists for your platform (seeDockerfile) — slower, but should still succeed.
The script installs Node if missing (apt/dnf + NodeSource on Linux). Docker
is deliberately not installed at this stage — only once you actually pick Docker deployment in the
form below does the wizard attempt to install it (Docker Engine on Linux via get.docker.com,
best-effort and non-fatal). You may need to log
out/in for new group membership (docker without sudo) to take effect. If auto-install fails or
isn't available, the wizard prints a manual install link and exits — fix the issue and re-run the
script.
Partway through the form it asks how to run the app:
-
Docker (recommended) — runs a bundled nginx alongside the app that terminates TLS itself. See
Docker deployment below for what that needs from you. It also asks:
-
Docker image source: Build locally (recommended) runs
docker compose buildfrom this checkout, so it always matches the code you have — this never references or contacts a container registry. Pull a pre-built image instead runsdocker compose pull, usingFONDWAYPOINTS_IMAGE— defaults toghcr.io/j5guy/fondwaypoints:latest, published on every push tomaster(see CI/CD and Releases). -
MongoDB: Internal (recommended) brings up a bundled
mongocontainer and pointsmongoHostat it automatically; External skips that container — fill inmongoHost/mongoUser/mongoPassfor your own server further down the form. -
TLS certificate: Generate one (recommended) creates a local Certificate Authority the
first time you use it, then issues a certificate for
WEB_FQDNsigned by that CA — no real domain or public CA needed. After saving you get a download link for the CA certificate; install it into your OS/browser trust store on any device that needs to reach the site without a warning (the CA's private key never leaves the server, and re-running the wizard reuses the same CA rather than generating a new one each time). I'll provide my own uses whatever you fill intoSSL_CERT_FILE/SSL_KEY_FILE— as you type either path, the form checks live whether that file actually exists on the host running the wizard.
-
Docker image source: Build locally (recommended) runs
-
Local Node.js process — runs
node server.jsdirectly on this machine, registered as asystemdbackground service so it survives reboots. There's no bundled nginx in this mode — pointmongoHostat your own MongoDB and put your own TLS-terminating reverse proxy in front of it (see Manual / non-Docker deployment below). This shells out tosudoto write the unit file and enable/restart the service — expect a sudo password prompt.
If you choose "Local Node.js process," you must run a reverse proxy — like nginx — yourself to handle TLS. This mode does not terminate TLS for you the way Docker mode does, and TLS is mandatory (see above); without your own reverse proxy in front of it, the app won't be reachable securely at all.
Either way the wizard brings the app itself up before exiting — there's no separate "now run
docker compose" step afterward. Re-running the script later detects an existing .env and asks
whether to open the wizard again (prefilled with your current values, including which deploy mode
you picked last time) or leave it alone and just redeploy/restart with the existing settings.
The setup wizard normally only listens on 127.0.0.1 and opens your browser automatically; if
you're running it on a headless server it also prints a LAN URL you can open from another computer
on the network (anyone on that network can reach the page, unauthenticated, until you submit the
form — keep that window short).
Once the app itself is up (Docker stack running, or the local service started), the script prints the
URL(s) it's reachable at — WEB_FQDN plus this machine's LAN IP address(es) (loopback isn't listed,
since nothing outside this machine can reach it there). This also happens on a later re-run that skips
reopening the wizard (redeploying from the existing .env).
Import the certificate first. Unless
SSL_CERT_FILE/SSL_KEY_FILEpoint at a certificate from a public CA (e.g. Let's Encrypt), your browser won't trust it yet — whether it's the one the wizard generated (see TLS certificate above) or one you supplied yourself. Install the CA certificate (downloaded at the end of setup, for a generated one) — or your own self-signed/ private CA — into the OS/browser trust store of every device that needs to reach the site, before visiting it there. Skipping this gets a certificate warning on every visit, and some browsers or managed devices refuse to proceed past it at all.
Then visit /auth/signup on one of the printed URLs to create the first account. There is no
CLI-based account bootstrap — account creation generates your encryption keys in the browser, which a
server-side script can't do without seeing your password.
Running inside a Proxmox LXC container? Ubuntu LXC is not recommended and not supported for Docker deployment — running Docker nested inside an Ubuntu LXC guest reliably hits AppArmor confinement failures (see below) that range from awkward to unfixable depending on the exact kernel/runc combination, and we can't chase every version-specific variant of this. If you're deploying into an LXC container, use a Rocky Linux (or other RHEL-based) image instead — RHEL-family distros use SELinux rather than AppArmor and don't have this nested-confinement fragility; Rocky 9 is one of this project's two actually-tested platforms (see the testing scope note above) and is the recommended choice specifically for LXC deployment. Plain (non-LXC) Ubuntu — a real VM or bare metal — is unaffected by any of this and works fine.
If you still want to try Docker in an Ubuntu LXC anyway: Nesting alone isn't enough to run Docker inside an unprivileged LXC — you'll also need Keyctl, and AppArmor confinement relaxed for the container. Without all of this, containers fail to start with an
apparmor failed to apply profileerror — often confusingly mid-docker build, on whicheverRUNstep happens to be the first one that needs to actually start a fresh container on this host (previous steps can look fine if they hit Docker's build cache).
- In the Proxmox web UI: select the container → Options → Features → Edit → check both Nesting and Keyctl → OK. (Or on the Proxmox host:
pct set <CTID> --features nesting=1,keyctl=1.)- On the Proxmox host itself (not inside the container), edit
/etc/pve/lxc/<CTID>.confand add:lxc.apparmor.profile: unconfined- Restart the container:
pct restart <CTID>.If you'd rather not loosen an unprivileged container's confinement this much, the simpler alternative is to make the container privileged instead (usually easiest done by recreating it) — that avoids these nesting/AppArmor issues entirely, at the cost of weaker isolation between the container and the Proxmox host.
Still failing even on a privileged container? You may be hitting a runc/AppArmor version incompatibility on newer Ubuntu (24.04+) kernels — Docker (inside the container) still tries to apply its own
docker-defaultprofile to every container/build step it creates, independent of the outer LXC's own confinement, and that can fail withapparmor failed to apply profile: ... no such file or directoryif the installed runc doesn't yet support this kernel's AppArmor interface. See Disabling AppArmor on Ubuntu 24+ below.
Ubuntu 24.04+ enabled AppArmor more aggressively than earlier releases, which is a common source of
friction for Docker running inside a container (nested AppArmor confinement is fragile and very
version-dependent — see the Proxmox LXC note above). If you're hitting apparmor failed to apply profile errors even after the LXC-level fixes, or you'd simply rather avoid this class of problem
before it starts, AppArmor can be disabled outright:
On a real Ubuntu 24+ VM or bare-metal install — this fully disables AppArmor at the kernel level, which is what actually matters (stopping the userspace service alone is not enough; the LSM itself initializes at boot from the kernel command line, before systemd runs):
sudo systemctl disable --now apparmor.service
sudo systemctl mask apparmor.service
sudo sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="/GRUB_CMDLINE_LINUX_DEFAULT="apparmor=0 /' /etc/default/grub
sudo update-grub
sudo rebootAfter rebooting, confirm it took effect:
cat /sys/module/apparmor/parameters/enabled # should print NInside an LXC container (Proxmox, or similar): the guest doesn't have its own kernel — it shares
the host's — so the GRUB/reboot step above does nothing from inside the container; the kernel-level
AppArmor flag is controlled by the Proxmox host, not the guest. From inside the container you can
still stop and mask the userspace service:
sudo systemctl disable --now apparmor.service
sudo systemctl mask apparmor.serviceThis removes the container's own AppArmor tooling/enforcement, but since the LSM is still active at the shared host kernel level, it may not resolve every variant of this error on its own. If it doesn't, the remaining option is disabling AppArmor the same way (kernel command line + reboot) on the Proxmox host itself — which affects every VM/container on that host, not just this one, so only do this if you're comfortable with that trade-off (or the host isn't running anything that depends on AppArmor confinement).
TLS is handled by the nginx container bundled in docker-compose.yml — no separate reverse proxy
needed in front of it. Point SSL_CERT_FILE / SSL_KEY_FILE in .env at a cert/key pair on the
Docker host; the guided setup wizard can generate one for you (a local CA
plus a certificate for WEB_FQDN), or you can supply your own.
MongoDB is not bundled by docker-compose.yml itself — docker-compose.mongo.yml is an overlay
that adds it. Include it only if you want the internal/bundled option. Similarly, the image published
to a registry isn't referenced by docker-compose.yml either — that lives in the
docker-compose.pull.yml overlay (see Pulling a pre-built image
below), so a plain build never contacts a registry:
- Internal MongoDB:
docker compose -f docker-compose.yml -f docker-compose.mongo.yml up -d --build, withmongoHost=mongoin.env. - External MongoDB:
docker compose up -d --build(base file only), withmongoHostpointed at your server.
- Copy
docker-compose.yml,docker-compose.mongo.yml(if using internal MongoDB),docker-compose.pull.yml(if pulling a pre-built image — see below), and.env.exampleto the deploy host,cp .env.example .env, and fill it in:-
sessionSecret/PRELOGIN_SECRET— generate each withnode -e "console.log(require('crypto').randomBytes(64).toString('hex'))" -
mongoHost—mongoif using the bundled container (see above), otherwise your external server's host/IP. -
SMTP_*— an SMTP relay for invite/access-request emails. For Gmail, create an App Password (requires 2-Step Verification) and use that, not your normal password. If left blank, the app still works — emails are just logged instead of sent. -
ADMIN_EMAIL— the email that should get admin on signup (optional, but needed to reach/admin). -
SSL_CERT_FILE/SSL_KEY_FILE— your own cert/key pair, or generate a quick self-signed one:then installopenssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ -keyout certs/ca.key -out certs/ca.pem -subj "/CN=Local CA" openssl req -newkey rsa:2048 -nodes -keyout certs/cert.key -out certs/cert.csr \ -subj "/CN=<your WEB_FQDN>" openssl x509 -req -in certs/cert.csr -CA certs/ca.pem -CAkey certs/ca.key -CAcreateserial \ -out certs/cert.pem -days 825 -sha256 \ -extfile <(echo "subjectAltName=DNS:<your WEB_FQDN>,IP:<your server's LAN IP>,IP:127.0.0.1")certs/ca.peminto your OS/browser trust store on any device that needs to reach the site without a warning. Include anIP:SAN entry for every address (LAN IP,127.0.0.1, etc.) you might open the site by directly — a hostname-only cert throws a mismatch warning the moment someone visits by IP instead. This is exactly what the guided setup wizard automates.
-
-
docker compose up -d --build(add-f docker-compose.yml -f docker-compose.mongo.ymlfor internal MongoDB).
This brings up the app and, if you included the MongoDB overlay, a bundled MongoDB container (data
persisted in a Docker volume), plus an uploads-data volume for encrypted file attachments.
docker compose up -d --build above always builds from this checkout, and never references a
registry image — docker-compose.yml has no image: key of its own. To pull a pre-built image
instead, add the docker-compose.pull.yml overlay (which supplies that key) and drop --build:
docker compose -f docker-compose.yml -f docker-compose.pull.yml pull
docker compose -f docker-compose.yml -f docker-compose.pull.yml up -d
# add -f docker-compose.mongo.yml too if you also want internal MongoDB
FONDWAYPOINTS_IMAGE in .env defaults to ghcr.io/j5guy/fondwaypoints:latest, published on every
push to master (see CI/CD and Releases); set it to a different path to pull
from elsewhere. This is exactly what the guided setup wizard's "Pull a pre-built image" option does.
Note that this still uses docker-compose.yml/docker-compose.pull.yml from a checkout — it just
skips the build step by pulling the image instead. If you don't want a checkout at all, see
Just the image, no repo checkout below.
Guided setup, in full above is the quickest and easiest way to get
running — use this section instead only if you specifically want to skip the repo checkout.
Everything above — the guided setup wizard, this repo's own docker-compose.yml, the bundled nginx
container — is a convenience that comes with a checkout, not a prerequisite, but this path is more
manual since none of that automation is there to do it for you. On any machine that already has
Docker installed, running the app is three things: pull the image, write a .env, write a small
docker-compose.yml of your own to bring up the stack. No git clone, no build.
TLS is mandatory (see above) but doesn't require the repo either — nginx:alpine is just a public
image, so a TLS-terminating reverse proxy can ride in the same self-authored docker-compose.yml.
docker pull ghcr.io/j5guy/fondwaypoints:latestGenerate a self-signed cert/key (or drop in your own), and a browser/OS will only trust it once you
install cert.pem into its trust store — or use a real cert from a public CA instead and skip this:
openssl req -x509 -newkey rsa:4096 -sha256 -days 825 -nodes \
-keyout cert.key -out cert.pem -subj "/CN=fondwaypoints.example.com" \
-addext "subjectAltName=DNS:fondwaypoints.example.com,IP:127.0.0.1"Write an nginx.conf next to it:
server {
listen 80;
server_name _;
return 301 https://$host:5560$request_uri;
}
server {
listen 5560 ssl;
server_name _;
client_max_body_size 150M;
ssl_certificate /etc/nginx/certs/server.pem;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app:5560;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
}
A .env — generate sessionSecret/PRELOGIN_SECRET with openssl rand -hex 64 (the same command
the setup wizard's "generate" buttons run):
PORT=5560
WEB_FQDN=fondwaypoints.example.com
sessionSecret=<64 random hex bytes>
PRELOGIN_SECRET=<64 random hex bytes>
mongoHost=mongo
mongoUser=
mongoPass=
mongoPort=27017
mongoDBName=fondwaypoints
ADMIN_EMAIL=
SIGNUP_ALLOWLIST=
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_USER=
SMTP_PASS=
SMTP_FROM=Fond Waypoints <your-address@gmail.com>
DEFAULT_WAIT_DAYS=14
And a docker-compose.yml tying it all together:
services:
app:
image: ghcr.io/j5guy/fondwaypoints:latest
restart: unless-stopped
env_file:
- .env
volumes:
- uploads-data:/app/public/uploads
- logs-data:/app/logs
depends_on:
mongo:
condition: service_healthy
nginx:
image: nginx:alpine
restart: unless-stopped
depends_on:
- app
ports:
- "80:80"
- "5560:5560"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./cert.pem:/etc/nginx/certs/server.pem:ro
- ./cert.key:/etc/nginx/certs/server.key:ro
mongo:
image: mongo:7
restart: unless-stopped
volumes:
- mongo-data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 5s
timeout: 5s
retries: 20
volumes:
uploads-data:
logs-data:
mongo-data:Then docker compose up -d. Note that app has no ports: of its own — nginx is the only thing
reachable from outside, at 80/5560 on whichever host runs this. Only bind those to 127.0.0.1
instead if you specifically want to restrict them to local-host-only reachability (e.g. because
something else in front of nginx handles the actual public-facing side).
Already have something else on port 80 or 5560?
nginxhere will fail to start with a "port is already allocated" error — nothing else breaks, but this stack's proxy won't come up until it's resolved. Either change the host-side port inports:(e.g."8443:5560"— only the number before the colon needs to change, the container-side5560must stay put to matchnginx.conf), or, if the other container is itself a reverse proxy, skip publishing a port here entirely and join thisnginxservice to that proxy's Docker network instead so it can route tofondwaypoints-nginx:5560directly.
Already have a MongoDB server elsewhere? Delete the mongo service and the depends_on block on
app, and point mongoHost (plus mongoUser/mongoPass if it needs auth) at that server instead.
Already have your own reverse proxy (or don't want nginx bundled in)? Delete the nginx service, add
ports: ["5560:5560"] to app instead, and point your proxy's TLS termination at that.
Then visit /auth/signup once it's reachable to create the first account. See
.env.example
on GitHub for the full list of environment variables and what each does — ADMIN_EMAIL/
SIGNUP_ALLOWLIST (who's allowed to sign up) and SMTP_* (invite/access-request emails; the app
still works with these blank, it just logs emails instead of sending them) are the ones most worth
reading before you go live.
The guided setup wizard's "Local Node.js process" option automates the steps
below (npm install/build, .env, and the systemd service) — this section is for setting it up
by hand instead, or for understanding what the wizard did.
MongoDB isn't bundled this way — point mongoHost in .env at an existing server, or run a
standalone container for it:
docker run -d --name fondwaypoints-mongo --restart unless-stopped \
-p 127.0.0.1:27017:27017 -v fondwaypoints-mongo-data:/data/db mongo:7
Then:
npm install
npm run build-css
npm start
See .env.example for the full list of environment variables.