CI / ci (push) Canceled after 0s
Fix notification-delivery cron docs: cron may not be installed, /var/log/ needs root
283 lines
11 KiB
Markdown
283 lines
11 KiB
Markdown
# NextChair
|
|
|
|
Booking app as an alternative to Booksy
|
|
|
|
## VPS deployment (multi-business)
|
|
|
|
Everything below runs on a fresh VPS with Docker and the Compose plugin
|
|
already installed — no other prerequisite (no host Node.js/npm needed;
|
|
every command either uses Docker itself or plain shell tools). Every code
|
|
block is meant to be pasted as-is, in order.
|
|
|
|
### 0. One-time: clone the repo and generate your secrets
|
|
|
|
```bash
|
|
# STACKS_DIR later on must be this exact absolute path — every stack
|
|
# shells `docker compose` back out to the HOST's dockerd via a mounted
|
|
# socket, so the path only resolves to the same thing on both sides if
|
|
# it's cloned here and never moved.
|
|
git clone <your-repo-url> /opt/nextchair
|
|
cd /opt/nextchair
|
|
```
|
|
|
|
Generate the secrets you'll paste into the `.env` files in the steps
|
|
below. Run these now and keep the output somewhere safe (a password
|
|
manager) — you'll need each value again in a moment.
|
|
|
|
```bash
|
|
# MYSQL_ROOT_PASSWORD — goes into BOTH stacks/infra/.env and manager/.env
|
|
# (the same value in both places).
|
|
echo "MYSQL_ROOT_PASSWORD=$(openssl rand -hex 24)"
|
|
|
|
# MANAGER_SESSION_SECRET — manager/.env only.
|
|
echo "MANAGER_SESSION_SECRET=$(openssl rand -hex 32)"
|
|
|
|
# MANAGER_SETTINGS_ENCRYPTION_KEY — manager/.env only. Encrypts the
|
|
# SMTP/VAPID secrets you'll enter in the Manager's Settings page later.
|
|
# Losing/rotating this makes any already-saved secret there undecryptable,
|
|
# so store it as carefully as the other secrets on this page.
|
|
echo "MANAGER_SETTINGS_ENCRYPTION_KEY=$(openssl rand -hex 32)"
|
|
```
|
|
|
|
```bash
|
|
# MANAGER_PASSWORD_HASH — manager/.env only. Replace "your-password" with
|
|
# your chosen Manager login password first. Run via a throwaway container
|
|
# so this works with nothing but Docker installed on the host.
|
|
docker run --rm node:22-alpine sh -c \
|
|
"mkdir -p /tmp/gen && cd /tmp/gen && npm install bcryptjs --no-audit --no-fund >/dev/null 2>&1 && \
|
|
node -e \"const h=require('/tmp/gen/node_modules/bcryptjs').hashSync('your-password',12); console.log('MANAGER_PASSWORD_HASH=\\\"'+h.replace(/\\$/g,'\\$\\$')+'\\\"')\""
|
|
```
|
|
|
|
### 1. Shared infrastructure — network, MySQL and Caddy
|
|
|
|
```bash
|
|
docker network create next-chair-edge
|
|
|
|
cp .env.infra.example stacks/infra/.env
|
|
```
|
|
|
|
Edit `stacks/infra/.env`:
|
|
- `MYSQL_ROOT_PASSWORD` — the first secret you generated above.
|
|
- `MYSQL_HOST_PORT` — set this to `127.0.0.1:3306` so MySQL is only exposed on the VPS loopback interface, not publicly. You can still connect to it remotely through an SSH tunnel.
|
|
- `ACME_EMAIL` — a real address you check (Let's Encrypt renewal notices).
|
|
|
|
```bash
|
|
docker compose -f stacks/infra/compose.yaml --env-file stacks/infra/.env up -d
|
|
```
|
|
|
|
### 2. Build the shared application image
|
|
|
|
Every business (and no other step in this guide) runs this same image —
|
|
you only rebuild it here, once, and again whenever you ship a code
|
|
update (see "Updating" below).
|
|
|
|
```bash
|
|
docker build -t nextchair-app:latest .
|
|
```
|
|
|
|
### 3. The management UI (Manager)
|
|
|
|
This is what you'll use to add every business from here on.
|
|
|
|
```bash
|
|
cp manager/.env.example stacks/manager/.env
|
|
```
|
|
|
|
Edit `stacks/manager/.env`:
|
|
- `MANAGER_EMAIL` — the email you'll log in with.
|
|
- `MANAGER_PASSWORD_HASH` — the bcrypt hash generated in step 0.
|
|
- `MANAGER_SESSION_SECRET` and `MANAGER_SETTINGS_ENCRYPTION_KEY` — the two
|
|
other values generated in step 0.
|
|
- `MANAGER_PUBLIC_URL` — `https://` + the domain you'll point at this app
|
|
in the next step.
|
|
- `MYSQL_ROOT_PASSWORD` — same value as `stacks/infra/.env`.
|
|
- `STACKS_DIR` — same absolute path as step 1.
|
|
|
|
```bash
|
|
docker compose -f stacks/manager/compose.yaml --env-file stacks/manager/.env up -d --build
|
|
```
|
|
|
|
Point your chosen Manager domain's DNS (A/AAAA record) at the VPS, then
|
|
give it a Caddy site and reload:
|
|
|
|
```bash
|
|
cat > stacks/infra/sites/manager.caddy << 'EOF'
|
|
manager.example.com {
|
|
reverse_proxy next-chair-manager:3000
|
|
}
|
|
EOF
|
|
docker exec next-chair-caddy caddy reload --config /etc/caddy/Caddyfile
|
|
```
|
|
|
|
(Replace `manager.example.com` with your real domain — Caddy issues its
|
|
TLS certificate automatically on first request.)
|
|
|
|
### 4. Configure platform-wide settings (once)
|
|
|
|
Log into the Manager at `https://<your-manager-domain>` and open
|
|
**Settings**. Both sections are optional to fill in now, but doing it
|
|
before adding your first business means that business never needs its
|
|
own SMTP/push setup:
|
|
|
|
- **Email / SMTP** — your SMTP relay's host/port/username/password and a
|
|
from-address. Use **Send Test Email** to confirm it before saving for
|
|
real, then **Save Changes** to apply it to every business.
|
|
- **Push Notifications** — click **Generate VAPID Keys** once (a contact
|
|
email is enough to start). This is a one-time, platform-wide key pair —
|
|
see that section's own copy for what happens if you ever regenerate it.
|
|
|
|
### 5. Add your first business
|
|
|
|
Still in the Manager: **Add Business** → name, slug, domain. This creates
|
|
its database, generates its secrets, starts its container, and runs its
|
|
migrations for you — no manual per-business `.env` editing needed.
|
|
|
|
Point that business's own domain's DNS at the VPS (the Manager doesn't do
|
|
this for you), then finish its setup by visiting
|
|
`https://<business-domain>/install`.
|
|
|
|
Repeat this step for each additional business, whenever you want one.
|
|
|
|
### 6. Notification delivery (required once, covers every business)
|
|
|
|
Staff invitation emails, appointment reminders, loyalty reward emails,
|
|
and Google Reviews sync are **not sent automatically** — the app only
|
|
queues them; something has to actually trigger delivery on a schedule by
|
|
calling two `CRON_SECRET`-gated endpoints
|
|
(`/api/cron/process-notifications`, `/api/cron/sync-google-reviews` — see
|
|
their own route files for the full reasoning). Skip this step and every
|
|
business's queued notifications just sit as `PENDING` forever — the UI
|
|
still says "an invitation email has been sent" the moment it's queued,
|
|
regardless of whether anything has actually delivered it yet.
|
|
|
|
[stacks/infra/scripts/process-cron.sh](stacks/infra/scripts/process-cron.sh)
|
|
calls both endpoints for **every** business under `STACKS_DIR` in one
|
|
run, so a single crontab entry covers businesses added later too — no
|
|
per-business cron line to maintain.
|
|
|
|
Many minimal cloud VPS images (including some Debian ones) don't ship
|
|
`cron` at all — `crontab -e` failing with `command not found` means this
|
|
first:
|
|
|
|
```bash
|
|
sudo apt update && sudo apt install -y cron
|
|
sudo systemctl enable --now cron
|
|
```
|
|
|
|
Then add this to the VPS's crontab (`crontab -e`) once — **not** `sudo
|
|
crontab -e`: run it as the same non-root user that already owns
|
|
`/opt/nextchair` (the one you've been running `git pull`/`docker build`
|
|
as), so the log file below is writable without needing root:
|
|
|
|
```bash
|
|
# Deliver queued notifications every 2 minutes.
|
|
*/2 * * * * STACKS_DIR=/opt/nextchair/stacks /opt/nextchair/stacks/infra/scripts/process-cron.sh notifications >> /opt/nextchair/nextchair-cron.log 2>&1
|
|
|
|
# Refresh Google Reviews once a day.
|
|
0 3 * * * STACKS_DIR=/opt/nextchair/stacks /opt/nextchair/stacks/infra/scripts/process-cron.sh reviews >> /opt/nextchair/nextchair-cron.log 2>&1
|
|
```
|
|
|
|
(Adjust `STACKS_DIR`/the log path if you cloned this repo somewhere other
|
|
than `/opt/nextchair`, or don't have write access to it — `/var/log/`
|
|
itself is **not** a safe default here: it needs root, and a crontab
|
|
entry that can't write its own log file just silently produces no file
|
|
at all, which looks identical to "the job never ran.") To confirm it's
|
|
working, or to flush a notification that's stuck pending right now, run
|
|
either line's command by hand and check
|
|
`/opt/nextchair/nextchair-cron.log` — a healthy run logs `<slug>
|
|
process-notifications ok` for every business.
|
|
|
|
## Updating after a code change
|
|
|
|
For an existing NextChair server, **do not re-run the fresh VPS setup above** and do not overwrite your production `.env` files.
|
|
|
|
First pull the latest code:
|
|
|
|
```bash
|
|
cd /opt/nextchair
|
|
git pull
|
|
```
|
|
|
|
Before rebuilding, check whether either example environment file has gained new settings since your last deployment:
|
|
|
|
```bash
|
|
# Review only — do not copy these files over your existing production .env files.
|
|
diff -u stacks/infra/.env .env.infra.example || true
|
|
diff -u stacks/manager/.env manager/.env.example || true
|
|
```
|
|
|
|
If a new variable has been added, copy **only that variable** into the corresponding production `.env` and give it an appropriate value. Keep all existing passwords, secrets and generated values unchanged.
|
|
|
|
Then rebuild the one shared image every business uses:
|
|
|
|
```bash
|
|
cd /opt/nextchair
|
|
docker build -t nextchair-app:latest .
|
|
```
|
|
|
|
Then, for **each** business: use its **Update** button in the Manager UI,
|
|
or run this directly:
|
|
|
|
```bash
|
|
cd /opt/nextchair/stacks/<slug>
|
|
docker compose --env-file .env up -d --force-recreate
|
|
```
|
|
|
|
Either way this recreates that business's container against the freshly
|
|
built image — and its own startup automatically runs
|
|
`prisma migrate deploy`, so any new database migration is applied on the
|
|
same step. No separate migration command needed.
|
|
|
|
If the code change touched the **Manager app itself** (anything under
|
|
`manager/`), rebuild/recreate the Manager too:
|
|
|
|
```bash
|
|
cd /opt/nextchair
|
|
docker compose -f stacks/manager/compose.yaml --env-file stacks/manager/.env up -d --build
|
|
```
|
|
|
|
If both the business app and Manager changed, do both update steps.
|
|
|
|
## Upgrading from a version that included Dockge (one-time)
|
|
|
|
Dockge used to run alongside NextChair Manager as a separate stack
|
|
management UI. It's no longer part of the shared infrastructure stack —
|
|
Manager is the only tool needed to provision and manage business stacks,
|
|
so the two were redundant. If your server was set up before this change,
|
|
existing tenant databases, tenant containers, business Compose files, and
|
|
the global SMTP/VAPID settings are all unaffected — this only removes the
|
|
old Dockge container.
|
|
|
|
Pull the change, then re-apply the infra stack with `--remove-orphans` so
|
|
Docker removes the container that's no longer defined in
|
|
`stacks/infra/compose.yaml`:
|
|
|
|
```bash
|
|
cd /opt/nextchair
|
|
git pull
|
|
|
|
docker compose \
|
|
-f stacks/infra/compose.yaml \
|
|
--env-file stacks/infra/.env \
|
|
up -d --remove-orphans
|
|
```
|
|
|
|
`--remove-orphans` only removes containers Compose no longer recognizes
|
|
(the old `next-chair-dockge` container) — it does not touch Caddy, MySQL,
|
|
or their volumes.
|
|
|
|
This does **not** delete Dockge's own data volume
|
|
(`next-chair-dockge-data`) — Docker never removes volumes on its own, and
|
|
this command has no reason to either. Once you've confirmed Caddy, MySQL
|
|
and every business are still healthy, you can remove that now-unused
|
|
volume by hand:
|
|
|
|
```bash
|
|
docker volume rm next-chair-dockge-data
|
|
```
|
|
|
|
Finally, `STACKS_DIR` and `DOCKGE_HOST_PORT` in `stacks/infra/.env` were
|
|
only ever used by Dockge and are safe to delete from that file — nothing
|
|
else in the infra stack reads them. (Manager keeps its own separate
|
|
`STACKS_DIR` in `stacks/manager/.env`; leave that one alone.)
|