Labels every command with who runs it and where
Four contexts got tangled together across the doc as it grew: pi-main as root, pi-main as gitea-workflow, the separate proxy Pi, and Gitea's own web UI. Added a legend up front and tagged every single command block so it's no longer something the reader has to infer.
This commit is contained in:
@@ -1409,3 +1409,13 @@ by Infrastructure Design. U5 carries REF-U3-01's Umami origin gate plus FR-21 an
|
|||||||
|
|
||||||
**Context**: OPERATIONS — Deployment Setup, real host config revealed the reference project's nginx Sentry-tunnel pattern was carried over unnecessarily and conflicts with this app's own built-in tunnel
|
**Context**: OPERATIONS — Deployment Setup, real host config revealed the reference project's nginx Sentry-tunnel pattern was carried over unnecessarily and conflicts with this app's own built-in tunnel
|
||||||
---
|
---
|
||||||
|
## Deployment Setup — Explicit Who/Where Labels on Every Command
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-29T01:00:00Z
|
||||||
|
|
||||||
|
**User Input**: "in de deployment instructions i snou niet echt duidelijk wanneer je als welke gebruiker de commando's moet draaien"
|
||||||
|
|
||||||
|
**AI Response**: "Added a legend near the top of deployment-instructions.md defining four contexts (pi-main/root, pi-main/gitea-workflow, proxy Pi/root, Gitea web UI) and labelled every single command block throughout the document with one of them — account creation, lingering, directory setup, shared/env, systemd units, the entire nginx/certbot section (now clearly proxy-Pi-only except the pi-main firewall step), backup credentials, the backup script itself and its manual verification, and the Gitea Actions variables (explicitly called out as not a host command at all)."
|
||||||
|
|
||||||
|
**Context**: OPERATIONS — Deployment Setup, clarity pass across the whole instructions document
|
||||||
|
---
|
||||||
|
|||||||
+55
-12
@@ -10,6 +10,15 @@ software itself. SlpModularCms can host multiple customers; `slpsoftware` is thi
|
|||||||
and the owner's own site. A future second customer on the same Pi would get its own instance name
|
and the owner's own site. A future second customer on the same Pi would get its own instance name
|
||||||
throughout, following the same pattern.
|
throughout, following the same pattern.
|
||||||
|
|
||||||
|
**Who runs what, where** — three hosts and two accounts appear below; every command block is
|
||||||
|
labelled with one of these:
|
||||||
|
- 🖥️ **pi-main / root** — as yourself with `sudo`, on the Pi this application runs on
|
||||||
|
- 👤 **pi-main / `gitea-workflow`** — as the deploy account, in a **real login shell** (SSH in
|
||||||
|
directly as `gitea-workflow`, not `su -`/`sudo -i -u` from your own session — § 1.3 explains why
|
||||||
|
that distinction matters)
|
||||||
|
- 🌐 **proxy Pi / root** — as yourself with `sudo`, on the *separate* Pi that terminates TLS (§ 1.7)
|
||||||
|
- ☁️ **Gitea web UI** — not a host command at all; done in this repository's Actions settings
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. One-Time Host Setup
|
## 1. One-Time Host Setup
|
||||||
@@ -37,6 +46,8 @@ named after this project, since it may end up deploying other projects on this P
|
|||||||
`SlpSoftware` project's own pipeline may already have exactly this kind of account; check its
|
`SlpSoftware` project's own pipeline may already have exactly this kind of account; check its
|
||||||
`PI_MAIN_USERNAME` secret first and reuse it directly if it already has SSH shell access, rather than
|
`PI_MAIN_USERNAME` secret first and reuse it directly if it already has SSH shell access, rather than
|
||||||
creating a second one). Example name: `gitea-workflow`.
|
creating a second one). Example name: `gitea-workflow`.
|
||||||
|
|
||||||
|
🖥️ **pi-main / root:**
|
||||||
```bash
|
```bash
|
||||||
sudo useradd -m -s /bin/bash gitea-workflow
|
sudo useradd -m -s /bin/bash gitea-workflow
|
||||||
sudo passwd gitea-workflow
|
sudo passwd gitea-workflow
|
||||||
@@ -56,6 +67,8 @@ The rest of this document uses `gitea-workflow` as the example account name —
|
|||||||
if you pick something else.
|
if you pick something else.
|
||||||
|
|
||||||
### 1.3 Enable Lingering (INFRA-U6-01 — do this first, easy to forget)
|
### 1.3 Enable Lingering (INFRA-U6-01 — do this first, easy to forget)
|
||||||
|
|
||||||
|
🖥️ **pi-main / root:**
|
||||||
```bash
|
```bash
|
||||||
sudo loginctl enable-linger gitea-workflow
|
sudo loginctl enable-linger gitea-workflow
|
||||||
```
|
```
|
||||||
@@ -69,6 +82,8 @@ running independent of any login session.
|
|||||||
setting the rest of this up manually (before the deploy workflow ever runs): enabling lingering
|
setting the rest of this up manually (before the deploy workflow ever runs): enabling lingering
|
||||||
does not retroactively start the user manager — that happens on the next real login, reboot, or
|
does not retroactively start the user manager — that happens on the next real login, reboot, or
|
||||||
manually:
|
manually:
|
||||||
|
|
||||||
|
🖥️ **pi-main / root:**
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl start user@$(id -u gitea-workflow).service
|
sudo systemctl start user@$(id -u gitea-workflow).service
|
||||||
```
|
```
|
||||||
@@ -84,6 +99,8 @@ Two fixes, in order of preference:
|
|||||||
- **SSH in directly as `gitea-workflow`** instead of logging in as yourself and switching user —
|
- **SSH in directly as `gitea-workflow`** instead of logging in as yourself and switching user —
|
||||||
a real SSH login does go through `sshd`'s PAM stack and sets `XDG_RUNTIME_DIR` correctly
|
a real SSH login does go through `sshd`'s PAM stack and sets `XDG_RUNTIME_DIR` correctly
|
||||||
- Or, after `su -`/`sudo -i -u`, just set it by hand once per shell:
|
- Or, after `su -`/`sudo -i -u`, just set it by hand once per shell:
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`:**
|
||||||
```bash
|
```bash
|
||||||
export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||||||
```
|
```
|
||||||
@@ -102,6 +119,7 @@ Deliberately placed under `gitea-workflow`'s **own home directory**, not under
|
|||||||
for the CMS's own release/current/shared structure to live anywhere near the other websites at all,
|
for the CMS's own release/current/shared structure to live anywhere near the other websites at all,
|
||||||
which directly avoids interfering with them (as you asked in Q2):
|
which directly avoids interfering with them (as you asked in Q2):
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`:**
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/apps/slpsoftware/<env>/releases
|
mkdir -p ~/apps/slpsoftware/<env>/releases
|
||||||
```
|
```
|
||||||
@@ -113,6 +131,7 @@ uploads *this* customer's website via FileZilla, e.g. `/mnt/storage1/www/html/sl
|
|||||||
(adjust the exact folder name to whatever convention the other sites under `html/` already use, if
|
(adjust the exact folder name to whatever convention the other sites under `html/` already use, if
|
||||||
one exists). Rather than a plain directory, make it a symlink across accounts:
|
one exists). Rather than a plain directory, make it a symlink across accounts:
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`:**
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/apps/slpsoftware/<env>
|
mkdir -p ~/apps/slpsoftware/<env>
|
||||||
ln -s /mnt/storage1/www/html/slpsoftware/<env> ~/apps/slpsoftware/<env>/shared/wwwroot-web
|
ln -s /mnt/storage1/www/html/slpsoftware/<env> ~/apps/slpsoftware/<env>/shared/wwwroot-web
|
||||||
@@ -122,12 +141,17 @@ ln -s /mnt/storage1/www/html/slpsoftware/<env> ~/apps/slpsoftware/<env>/shared/w
|
|||||||
needs no changes for this — it only ever resolves the symlink chain, it doesn't care how many hops
|
needs no changes for this — it only ever resolves the symlink chain, it doesn't care how many hops
|
||||||
that chain has. What **does** need attention: `gitea-workflow` needs read + traverse permission on
|
that chain has. What **does** need attention: `gitea-workflow` needs read + traverse permission on
|
||||||
`/mnt/storage1/www/html/slpsoftware/<env>/` and its parent directories, which `webadmin` owns.
|
`/mnt/storage1/www/html/slpsoftware/<env>/` and its parent directories, which `webadmin` owns.
|
||||||
Simplest fix: put both accounts in a shared group (e.g. `webshared`), `chgrp -R webshared` that
|
Simplest fix: put both accounts in a shared group (e.g. `webshared`), then, 🖥️ **pi-main / root**:
|
||||||
folder, and make sure webadmin's FTP server creates new uploads group-readable (`g+rx`, not just
|
```bash
|
||||||
|
sudo chgrp -R webshared /mnt/storage1/www/html/slpsoftware/<env>
|
||||||
|
```
|
||||||
|
and make sure webadmin's FTP server creates new uploads group-readable (`g+rx`, not just
|
||||||
owner-readable) — a one-time permission setup, not something either pipeline touches per deploy.
|
owner-readable) — a one-time permission setup, not something either pipeline touches per deploy.
|
||||||
|
|
||||||
### 1.5 Runtime Configuration File
|
### 1.5 Runtime Configuration File
|
||||||
One file per environment, **outside** the release directory so it survives every switch:
|
One file per environment, **outside** the release directory so it survives every switch:
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`:**
|
||||||
```bash
|
```bash
|
||||||
touch ~/apps/slpsoftware/<env>/shared/env
|
touch ~/apps/slpsoftware/<env>/shared/env
|
||||||
chmod 600 ~/apps/slpsoftware/<env>/shared/env
|
chmod 600 ~/apps/slpsoftware/<env>/shared/env
|
||||||
@@ -187,7 +211,8 @@ gate only catches drift between the frontend build and that Gitea variable; it c
|
|||||||
so keeping the two in sync is a manual discipline, not something enforced automatically.
|
so keeping the two in sync is a manual discipline, not something enforced automatically.
|
||||||
|
|
||||||
### 1.6 systemd User Units
|
### 1.6 systemd User Units
|
||||||
Create `~/.config/systemd/user/slpsoftware-test.service`:
|
|
||||||
|
👤 **pi-main / `gitea-workflow`** — create `~/.config/systemd/user/slpsoftware-test.service`:
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=SlpModularCms API (test)
|
Description=SlpModularCms API (test)
|
||||||
@@ -209,6 +234,8 @@ And `~/.config/systemd/user/slpsoftware-production.service` — identical, with
|
|||||||
`production` throughout (including the port inside `shared/env`).
|
`production` throughout (including the port inside `shared/env`).
|
||||||
|
|
||||||
Enable both (does not start them yet — nothing is deployed there until the first CI run):
|
Enable both (does not start them yet — nothing is deployed there until the first CI run):
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`** (real login shell — § 1.3):
|
||||||
```bash
|
```bash
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable slpsoftware-test.service
|
systemctl --user enable slpsoftware-test.service
|
||||||
@@ -251,6 +278,7 @@ or container on the LAN — pi-main and its two ports, in this deployment's case
|
|||||||
server block for the domain to attach to and to answer the HTTP-01 validation challenge; asking
|
server block for the domain to attach to and to answer the HTTP-01 validation challenge; asking
|
||||||
for a certificate before this exists will fail.
|
for a certificate before this exists will fail.
|
||||||
|
|
||||||
|
🌐 **proxy Pi / root:**
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@@ -275,12 +303,15 @@ is answering on port 80, which is the proxy Pi.
|
|||||||
**Step 2 — request and install the certificate.** The nginx plugin edits the file from step 1 in
|
**Step 2 — request and install the certificate.** The nginx plugin edits the file from step 1 in
|
||||||
place: it adds the `listen 443 ssl` block, the certificate/key paths, and (by default) an
|
place: it adds the `listen 443 ssl` block, the certificate/key paths, and (by default) an
|
||||||
HTTP→HTTPS redirect for the port 80 block:
|
HTTP→HTTPS redirect for the port 80 block:
|
||||||
|
|
||||||
|
🌐 **proxy Pi / root:**
|
||||||
```bash
|
```bash
|
||||||
sudo certbot --nginx -d <new-domain>
|
sudo certbot --nginx -d <new-domain>
|
||||||
```
|
```
|
||||||
Certbot's own systemd timer handles renewal automatically — nothing further to set up for that.
|
Certbot's own systemd timer handles renewal automatically — nothing further to set up for that.
|
||||||
|
|
||||||
**Step 3 — verify.** After certbot finishes:
|
**Step 3 — verify.** Can be run from anywhere with network access to the domain (your own machine
|
||||||
|
is fine — this isn't tied to either Pi). After certbot finishes:
|
||||||
```bash
|
```bash
|
||||||
curl -I https://<new-domain>/health # or whichever path this new site/API actually serves
|
curl -I https://<new-domain>/health # or whichever path this new site/API actually serves
|
||||||
```
|
```
|
||||||
@@ -292,8 +323,9 @@ site — nginx and certbot don't know or care what's actually listening on the b
|
|||||||
|
|
||||||
**Step 4 — restrict the backend port on pi-main (do this once the domain works end to end).**
|
**Step 4 — restrict the backend port on pi-main (do this once the domain works end to end).**
|
||||||
Since Kestrel now listens on `0.0.0.0:<backend-port>` (§ 1.5), anything on the LAN can reach it
|
Since Kestrel now listens on `0.0.0.0:<backend-port>` (§ 1.5), anything on the LAN can reach it
|
||||||
directly, bypassing the proxy Pi's TLS entirely, unless pi-main's firewall says otherwise. On
|
directly, bypassing the proxy Pi's TLS entirely, unless pi-main's firewall says otherwise.
|
||||||
**pi-main**:
|
|
||||||
|
🖥️ **pi-main / root:**
|
||||||
```bash
|
```bash
|
||||||
sudo ufw allow from <proxy-pi-lan-ip> to any port <backend-port> proto tcp
|
sudo ufw allow from <proxy-pi-lan-ip> to any port <backend-port> proto tcp
|
||||||
sudo ufw deny <backend-port>/tcp
|
sudo ufw deny <backend-port>/tcp
|
||||||
@@ -331,6 +363,8 @@ server {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 1.8 Database Backup Credentials (§ 4 depends on this)
|
### 1.8 Database Backup Credentials (§ 4 depends on this)
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`:**
|
||||||
```bash
|
```bash
|
||||||
touch ~/.config/slpsoftware-db-backup.env
|
touch ~/.config/slpsoftware-db-backup.env
|
||||||
chmod 600 ~/.config/slpsoftware-db-backup.env
|
chmod 600 ~/.config/slpsoftware-db-backup.env
|
||||||
@@ -347,7 +381,9 @@ credential, ideally scoped to just read access (`SELECT`, `LOCK TABLES` — ever
|
|||||||
needs) rather than the application's own data-access login.
|
needs) rather than the application's own data-access login.
|
||||||
|
|
||||||
### 1.9 Gitea Actions Variables and Secrets
|
### 1.9 Gitea Actions Variables and Secrets
|
||||||
Set once, in this repository's Gitea Actions settings. This is exactly where the real path lives —
|
|
||||||
|
☁️ **Gitea web UI** — not a host command, no SSH involved. Set once, in this repository's Actions
|
||||||
|
settings. This is exactly where the real path lives —
|
||||||
`deploy-scp.yaml` never hardcodes it, it only reads `${{ vars.DEPLOY_PATH_TEST }}` /
|
`deploy-scp.yaml` never hardcodes it, it only reads `${{ vars.DEPLOY_PATH_TEST }}` /
|
||||||
`${{ vars.DEPLOY_PATH_PRODUCTION }}`, so changing the path later is a variable edit, not a workflow
|
`${{ vars.DEPLOY_PATH_PRODUCTION }}`, so changing the path later is a variable edit, not a workflow
|
||||||
change:
|
change:
|
||||||
@@ -389,13 +425,15 @@ Already built (U5/U6) — this is the read-only walkthrough for whoever operates
|
|||||||
- `wwwroot/web/` will be empty until a website workspace deploys into it — `/` serves the built-in
|
- `wwwroot/web/` will be empty until a website workspace deploys into it — `/` serves the built-in
|
||||||
placeholder until then (this is expected, not a failure)
|
placeholder until then (this is expected, not a failure)
|
||||||
- The very first run has no "previous release" to keep — pruning naturally has nothing to prune
|
- The very first run has no "previous release" to keep — pruning naturally has nothing to prune
|
||||||
- Verify manually after the first run: `curl https://test.slpsoftware.nl/health` and
|
- Verify manually after the first run (from anywhere — not tied to either Pi):
|
||||||
`curl https://slpsoftware.nl/health` (after the first production run) both return `200`
|
`curl https://test.slpsoftware.nl/health` and `curl https://slpsoftware.nl/health` (after the
|
||||||
|
first production run) both return `200`
|
||||||
|
|
||||||
## 4. Database Backup Script
|
## 4. Database Backup Script
|
||||||
|
|
||||||
Create `~/scripts/backup-slpmodularcms-db.sh` on the Pi (this script is host-side by design — never
|
👤 **pi-main / `gitea-workflow`** — create `~/scripts/backup-slpmodularcms-db.sh` on the Pi (this
|
||||||
part of this repository, so no DB credential ever reaches Gitea):
|
script is host-side by design — never part of this repository, so no DB credential ever reaches
|
||||||
|
Gitea):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -433,11 +471,13 @@ ls -1t "$BACKUP_DIR"/*.sql.gz 2>/dev/null | tail -n +8 | xargs -r rm -f
|
|||||||
`mariadb-dump` is MariaDB's own name for the tool (present since MariaDB 10.4-ish); if the host only
|
`mariadb-dump` is MariaDB's own name for the tool (present since MariaDB 10.4-ish); if the host only
|
||||||
has the older `mysqldump` name, substitute it — same tool, same flags.
|
has the older `mysqldump` name, substitute it — same tool, same flags.
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`:**
|
||||||
```bash
|
```bash
|
||||||
chmod +x ~/scripts/backup-slpmodularcms-db.sh
|
chmod +x ~/scripts/backup-slpmodularcms-db.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Verify once, manually**, before relying on it in a real deploy:
|
**Verify once, manually**, before relying on it in a real deploy — 👤 **pi-main / `gitea-workflow`**
|
||||||
|
(this matches exactly how `deploy-scp.yaml` itself invokes the script over SSH):
|
||||||
```bash
|
```bash
|
||||||
~/scripts/backup-slpmodularcms-db.sh production
|
~/scripts/backup-slpmodularcms-db.sh production
|
||||||
```
|
```
|
||||||
@@ -445,6 +485,9 @@ Confirm a `.sql.gz` file appears under `~/backups/slpsoftware/production/` and t
|
|||||||
silently fail (the script uses `set -euo pipefail`, so a real error does propagate as a non-zero
|
silently fail (the script uses `set -euo pipefail`, so a real error does propagate as a non-zero
|
||||||
exit — which fails the calling `deploy-scp.yaml` step, correctly blocking the deploy). Worth a
|
exit — which fails the calling `deploy-scp.yaml` step, correctly blocking the deploy). Worth a
|
||||||
one-time restore rehearsal too — an untested backup is not a verified one:
|
one-time restore rehearsal too — an untested backup is not a verified one:
|
||||||
|
|
||||||
|
👤 **pi-main / `gitea-workflow`** (or wherever you have a MariaDB client and network access to
|
||||||
|
`127.0.0.1`'s database — this one isn't identity-sensitive, just needs DB access):
|
||||||
```bash
|
```bash
|
||||||
gunzip -c ~/backups/slpsoftware/production/<file>.sql.gz | mariadb -h 127.0.0.1 -u root -p <a-scratch-database>
|
gunzip -c ~/backups/slpsoftware/production/<file>.sql.gz | mariadb -h 127.0.0.1 -u root -p <a-scratch-database>
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user