136 lines
5.7 KiB
Markdown
136 lines
5.7 KiB
Markdown
# Deploying CluPilot on a fresh server
|
|
|
|
Developed locally, deployed by pulling. Two scripts do the work:
|
|
|
|
```bash
|
|
# once, on a fresh Debian/Ubuntu server — as root, because of apt
|
|
bash deploy/install.sh
|
|
|
|
# afterwards, as the service account — never as root
|
|
sudo -u clupilot bash -c 'cd /opt/clupilot && bash deploy/update.sh'
|
|
```
|
|
|
|
The installer needs root only to install packages and open the firewall. It
|
|
creates a `clupilot` account that owns the checkout and runs Docker; the
|
|
application never runs as root, and `update.sh` refuses to start if you try.
|
|
That account has no password login — membership in the docker group is
|
|
root-equivalent on the host, which is inherent to Docker and the reason it is
|
|
reachable only by `sudo -u` or an SSH key.
|
|
|
|
## What the installer does
|
|
|
|
Docker, git and `wireguard-tools`; clone into `/opt/clupilot`; generate every
|
|
secret (`APP_KEY`, `VPN_CONFIG_KEY`, database password, Reverb keys); start the
|
|
stack; migrate; create the WireGuard hub and print its public key; create your
|
|
Owner account; close the firewall down to 22, 80, 443 and 51820/udp.
|
|
|
|
It is safe to re-run: an existing `.env` is kept and the hub key is never
|
|
regenerated — doing so would silently disconnect every onboarded host.
|
|
|
|
### Unattended, from a bare server
|
|
|
|
The only thing to install by hand is git:
|
|
|
|
```bash
|
|
apt-get update && apt-get install -y git
|
|
git clone https://git.bave.dev/boban/CluPilotCloud.git /tmp/clupilot
|
|
bash /tmp/clupilot/deploy/install.sh --env-file /root/clupilot.env
|
|
```
|
|
|
|
`deploy/clupilot.env.example` is the template. Put the real file on the server
|
|
as `/root/clupilot.env` with mode 600 **before** starting — the installer warns
|
|
if it is readable by anyone else. Keep the master copy in a password manager,
|
|
never in the repository (`clupilot.env` is gitignored for that reason).
|
|
|
|
After a successful run the working values live in `/opt/clupilot/.env`, so the
|
|
bootstrap copy can go: `shred -u /root/clupilot.env`.
|
|
|
|
The file also carries the operational secrets — Hetzner DNS, Stripe, SMTP, the
|
|
Proxmox SSH key path. Those are optional: the install succeeds without them and
|
|
the corresponding features simply stay dark until they are filled in.
|
|
|
|
## Order of the update
|
|
|
|
Migrations run **before** the new containers take traffic: code that expects a
|
|
column the database does not have yet answers with 500s. The image is rebuilt
|
|
only when `docker/`, composer or npm manifests actually changed. Queue workers
|
|
are restarted at the end — they are long-running PHP processes and keep the old
|
|
classes in memory otherwise. That one bit us during development: a fixed job
|
|
kept failing until the worker was restarted.
|
|
|
|
## What the installer deliberately leaves to you
|
|
|
|
- **DNS and TLS.** Put a reverse proxy in front (Zoraxy, Caddy, nginx). The
|
|
console hostname must not be publicly resolvable: `/admin` answers only on
|
|
the hostnames in `ADMIN_HOSTS` and 404s everywhere else, but keeping it out
|
|
of public DNS is the layer above that.
|
|
- **`STRIPE_WEBHOOK_SECRET`.** Stripe dashboard → Developers → Webhooks →
|
|
`https://<app-domain>/webhooks/stripe`. The secret starts with `whsec_` and
|
|
differs between test and live mode. Subscribe all six events the application
|
|
handles — the first two open a contract, the rest are the billing cycle,
|
|
which is Stripe's:
|
|
|
|
```
|
|
checkout.session.completed
|
|
checkout.session.async_payment_succeeded
|
|
invoice.paid
|
|
invoice.payment_failed
|
|
customer.subscription.updated
|
|
customer.subscription.deleted
|
|
```
|
|
|
|
- **The Stripe catalogue.** Nothing can be sold until Stripe knows what it is
|
|
billing for:
|
|
|
|
```
|
|
docker compose exec app php artisan stripe:sync-catalogue --dry-run # look first
|
|
docker compose exec app php artisan stripe:sync-catalogue
|
|
```
|
|
|
|
Run it again after publishing any new plan version. It is idempotent, and it
|
|
has to be: a Stripe Price cannot be edited or deleted, so a duplicate is
|
|
permanent. `--dry-run` lists exactly what would be created.
|
|
|
|
- **A look at the catalogue itself.** `php artisan plans:check` reports
|
|
overlapping availability windows, plans on sale with no live version, and
|
|
missing prices. Worth running after any plan change — all of it is computed
|
|
when read, so a mistake announces itself at a customer's checkout otherwise.
|
|
- **Backups.** At minimum the MariaDB volume and `/etc/wireguard` in the
|
|
`wireguard` volume — losing the hub key means re-peering every host.
|
|
|
|
## Hiding the site while you build
|
|
|
|
Settings → Website visibility. Outsiders then get a placeholder (503,
|
|
`noindex`), while anyone on the management VPN and any signed-in operator keeps
|
|
seeing the real thing. The console stays reachable either way, so the switch can
|
|
always be flipped back.
|
|
|
|
## CI (Gitea Actions)
|
|
|
|
`.gitea/workflows/tests.yml` runs the Pest suite and an asset build on every
|
|
push, and tags a green `main` as `tested-…`. That tag is what the console will
|
|
offer as an available update — a red run produces no tag and therefore no
|
|
update.
|
|
|
|
Actions has to be switched on twice: globally in Gitea's `app.ini`
|
|
(`[actions] ENABLED = true`, then restart) and per repository under
|
|
Settings → Advanced. Verify with:
|
|
|
|
```bash
|
|
curl -s -H "Authorization: token <token>" \
|
|
https://git.bave.dev/api/v1/repos/boban/CluPilotCloud | grep -o '"has_actions":[a-z]*'
|
|
```
|
|
|
|
The runner is an opt-in compose service. Fetch a registration token from
|
|
Settings → Actions → Runners → "Create new runner" (Gitea 1.20 has no API for
|
|
it), put it in `.env` as `GITEA_RUNNER_TOKEN`, then:
|
|
|
|
```bash
|
|
docker compose --profile ci up -d runner
|
|
```
|
|
|
|
Keep `GITEA_RUNNER_VERSION` in step with the server: the registration protocol
|
|
moves with Gitea, and a runner newer than the server refuses to register.
|
|
The `uses:` steps are fetched from github.com, so the runner needs outbound
|
|
internet — the code itself never leaves your Gitea.
|