docs(license): self-hosted server runbook + multi-tenancy plan
Adds SETUP-LICENSE-SERVER.md — end-to-end install runbook for the license server on the existing invixiom box (Ubuntu 24.04). Covers DNS, system packages, Postgres + API in Docker, dedicated system user, secrets layout under /srv/datatools-license/secrets (mode 400), nginx config in a separate sites-available/unalogix file, Let's Encrypt cert issuance, smoke tests, backups, monitoring, key rotation, and rollback. Multi-tenancy is explicit at every layer: separate DNS zone (unalogix.com vs invixiom.com), separate nginx file, separate TLS cert, dedicated backend ports (8090 for the API, 5433 for Postgres, both localhost-only), separate docker compose project and volume. No invixiom service is touched. LICENSE-SERVER.md updated: hosting choice moved from "Fly.io / Render" (rejected) to self-hosted (decided). Points at the new runbook for ops specifics. ADMIN.md pointer table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -266,3 +266,4 @@ independent secure locations.
|
||||
| `~/.datatools/license.json` | Where activated licenses are stored on each machine |
|
||||
| `~/.datatools-creator/issued.jsonl` | Creator-side issuance log (one JSON line per mint) |
|
||||
| `docs/LICENSE-SERVER.md` | Design for the future online issuance + record-keeping system |
|
||||
| `docs/SETUP-LICENSE-SERVER.md` | Self-hosted server install runbook (DNS, Docker, nginx, TLS, backups) |
|
||||
|
||||
@@ -223,9 +223,11 @@ across any of them — that's the whole point.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Hosting choice.** A managed Postgres + small Python app is well
|
||||
under $20/mo at the expected volume. Fly.io and Render are the
|
||||
obvious candidates; AWS is overkill at this scale.
|
||||
- **Hosting choice.** *Decided: self-hosted* on the existing
|
||||
`46.225.166.142` box alongside the `*.invixiom.com` services.
|
||||
Runbook in `SETUP-LICENSE-SERVER.md`. Operator owns uptime,
|
||||
backups, TLS renewal, and key custody — see that doc's
|
||||
"Operational concerns" section.
|
||||
- **Per-seat or per-device limits?** v1 says no. Revisit if/when
|
||||
abuse is observable.
|
||||
- **Email delivery.** Postmark or SES — both fine. Pick whichever the
|
||||
|
||||
553
docs/SETUP-LICENSE-SERVER.md
Normal file
553
docs/SETUP-LICENSE-SERVER.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# SETUP — Self-hosted license server runbook
|
||||
|
||||
End-to-end build instructions for `licenses.datatools.unalogix.com` on
|
||||
the existing invixiom box (Ubuntu 24.04, public IP `46.225.166.142`).
|
||||
|
||||
Audience: creator/operator. Read top to bottom on first install; use as
|
||||
a reference thereafter.
|
||||
|
||||
Companions:
|
||||
- `LICENSE-SERVER.md` — the architecture / design rationale
|
||||
- `ADMIN.md` — day-2 ops (minting comps, looking at the issuance log)
|
||||
|
||||
---
|
||||
|
||||
## 0. Multi-tenancy: where this lands among existing services
|
||||
|
||||
This box already hosts the `*.invixiom.com` family (kasm, files, lifeos,
|
||||
code, gitea) via one shared nginx + one shared Let's Encrypt cert.
|
||||
DataTools is intentionally separated from that stack at every layer:
|
||||
|
||||
| Layer | Existing | New |
|
||||
|---|---|---|
|
||||
| **DNS zone** | `invixiom.com` | `unalogix.com` (different TLD) |
|
||||
| **nginx file** | `/etc/nginx/sites-available/invixiom` | `/etc/nginx/sites-available/unalogix` |
|
||||
| **nginx symlink** | `sites-enabled/invixiom` | `sites-enabled/unalogix` |
|
||||
| **TLS cert** | `letsencrypt/live/kasm.invixiom.com[-0001]` | `letsencrypt/live/datatools.unalogix.com` |
|
||||
| **Backend port** | 8000, 8002, 8003, 8080, 8081, 8443 | **8090** (mint API), **5433** (Postgres, localhost-only) |
|
||||
| **Docker compose project** | per-service (kasm, lifeos, gitea) | `datatools-license` |
|
||||
| **Docker volume** | per service | `datatools_pg_data` |
|
||||
| **Filesystem root** | various | `/srv/datatools-license/` |
|
||||
| **System user** | various | `datatools-api` (UID auto-assigned, no shell) |
|
||||
|
||||
Nothing in the invixiom stack is read, modified, or referenced by the
|
||||
datatools stack. Restart, upgrade, or remove either without affecting
|
||||
the other.
|
||||
|
||||
---
|
||||
|
||||
## 1. Pre-flight checklist (off-box, before any commands run)
|
||||
|
||||
These have to be done by the operator outside this box. The build
|
||||
won't proceed without them.
|
||||
|
||||
### 1a. DNS records
|
||||
|
||||
In your `unalogix.com` registrar / DNS panel, add:
|
||||
|
||||
```
|
||||
A datatools.unalogix.com 46.225.166.142
|
||||
A licenses.datatools.unalogix.com 46.225.166.142
|
||||
```
|
||||
|
||||
Verify before continuing:
|
||||
|
||||
```bash
|
||||
dig +short datatools.unalogix.com
|
||||
dig +short licenses.datatools.unalogix.com
|
||||
# Both should print: 46.225.166.142
|
||||
```
|
||||
|
||||
DNS propagation can take 1–60 minutes. Let's Encrypt won't issue
|
||||
certs until DNS resolves correctly.
|
||||
|
||||
### 1b. Postmark account (transactional email)
|
||||
|
||||
1. Sign up at https://postmarkapp.com (free 100 emails/mo, $15/mo for
|
||||
the volume range we'll be in).
|
||||
2. Verify the `unalogix.com` domain (DNS TXT/CNAME records — Postmark
|
||||
will tell you exactly what to add).
|
||||
3. Create a Server, copy the **Server API Token**. Stash it; we'll put
|
||||
it in the app's `.env`.
|
||||
4. Configure the sender address: `licenses@datatools.unalogix.com`.
|
||||
|
||||
If you prefer SES, Mailgun, Resend, etc. — fine, just swap the
|
||||
adapter (see §6). Postmark is the recommended default.
|
||||
|
||||
### 1c. Cloudflare in front (recommended)
|
||||
|
||||
Move `unalogix.com` DNS hosting to Cloudflare and enable proxy ("orange
|
||||
cloud") on both subdomains. Gets you free DDoS protection, WAF, and rate
|
||||
limiting. **Origin TLS still goes through Let's Encrypt on this box**;
|
||||
Cloudflare adds a second TLS hop in front. Cert renewal still works
|
||||
because we use HTTP-01 challenge on the origin, which Cloudflare
|
||||
proxies transparently.
|
||||
|
||||
If you skip this, the public webhook endpoint is directly hammerable.
|
||||
Not catastrophic at low scale, but the free protection is worth taking.
|
||||
|
||||
### 1d. Gumroad webhook secret
|
||||
|
||||
In Gumroad's seller dashboard → Settings → Advanced → "Ping URL":
|
||||
|
||||
```
|
||||
URL: https://licenses.datatools.unalogix.com/webhooks/gumroad
|
||||
Secret: <generate a random 32-char hex; save it for the .env>
|
||||
```
|
||||
|
||||
Don't enter this until §10 ("PR 2 cutover") — the endpoint won't exist
|
||||
yet during the Mint API build.
|
||||
|
||||
---
|
||||
|
||||
## 2. One-time host setup
|
||||
|
||||
Run as `root` (or via `sudo`).
|
||||
|
||||
```bash
|
||||
# Update apt cache and pull in the bits the rest of the doc needs.
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
docker-compose-plugin \
|
||||
certbot \
|
||||
python3-certbot-nginx \
|
||||
postgresql-client-16 # for psql to reach the containerized DB
|
||||
|
||||
# Sanity check: docker + compose v2 are already installed via Docker CE.
|
||||
docker --version
|
||||
docker compose version
|
||||
|
||||
# Create the system user the app process will run as (no shell, no home).
|
||||
adduser --system --group --no-create-home --shell /usr/sbin/nologin datatools-api
|
||||
|
||||
# Filesystem layout under /srv (separate from /opt to make the
|
||||
# multi-tenant boundary obvious on disk).
|
||||
install -d -o datatools-api -g datatools-api -m 750 /srv/datatools-license
|
||||
install -d -o datatools-api -g datatools-api -m 750 /srv/datatools-license/app
|
||||
install -d -o datatools-api -g datatools-api -m 750 /srv/datatools-license/secrets
|
||||
install -d -o datatools-api -g datatools-api -m 750 /srv/datatools-license/backups
|
||||
```
|
||||
|
||||
The `secrets/` dir is mode 750 owned by `datatools-api`. The private
|
||||
signing key and Postmark token live there as mode-400 files — never
|
||||
in environment-variable-via-systemd-EnvironmentFile, never in the
|
||||
docker-compose file, never anywhere `root` doesn't need to look.
|
||||
|
||||
### Firewall recommendation (separate decision)
|
||||
|
||||
The box currently runs without UFW. Enabling it now would affect all
|
||||
existing services. Two options:
|
||||
|
||||
- **(A) Don't enable UFW.** Leave the cloud provider's network firewall
|
||||
as the perimeter. This is the current state.
|
||||
- **(B) Enable UFW with `allow 22, 80, 443` only.** Forces every Docker
|
||||
service to bind to `127.0.0.1` (some currently bind `0.0.0.0`). Will
|
||||
break any direct-port access until those binds are updated.
|
||||
|
||||
Default for this runbook: **(A)**. Revisit independently of the
|
||||
DataTools rollout. The DataTools containers always bind to `127.0.0.1`
|
||||
regardless.
|
||||
|
||||
---
|
||||
|
||||
## 3. Database (Postgres in Docker)
|
||||
|
||||
Postgres lives inside the datatools compose project — separate from
|
||||
every other service on the box, separate volume, separate port,
|
||||
localhost-only binding.
|
||||
|
||||
`/srv/datatools-license/compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: datatools-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: datatools_licenses
|
||||
POSTGRES_USER: datatools_api
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
|
||||
secrets:
|
||||
- pg_password
|
||||
volumes:
|
||||
- datatools_pg_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:5433:5432" # localhost-only, non-default port
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U datatools_api -d datatools_licenses"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
api:
|
||||
image: datatools-license-api:latest # built locally; see §4
|
||||
container_name: datatools-api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://datatools_api@postgres:5432/datatools_licenses
|
||||
DATATOOLS_LICENSE_PUBKEY: ${DATATOOLS_LICENSE_PUBKEY}
|
||||
POSTMARK_TOKEN_FILE: /run/secrets/postmark_token
|
||||
DATATOOLS_LICENSE_PRIVKEY_FILE: /run/secrets/license_privkey
|
||||
GUMROAD_WEBHOOK_SECRET_FILE: /run/secrets/gumroad_secret
|
||||
PG_PASSWORD_FILE: /run/secrets/pg_password
|
||||
secrets:
|
||||
- pg_password
|
||||
- postmark_token
|
||||
- license_privkey
|
||||
- gumroad_secret
|
||||
ports:
|
||||
- "127.0.0.1:8090:8000" # localhost-only; nginx is the only path in
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
secrets:
|
||||
pg_password: { file: ./secrets/pg_password }
|
||||
postmark_token: { file: ./secrets/postmark_token }
|
||||
license_privkey: { file: ./secrets/license_privkey }
|
||||
gumroad_secret: { file: ./secrets/gumroad_secret }
|
||||
|
||||
volumes:
|
||||
datatools_pg_data:
|
||||
name: datatools_pg_data
|
||||
```
|
||||
|
||||
Populate the secrets (each file should contain the value with no
|
||||
trailing newline):
|
||||
|
||||
```bash
|
||||
cd /srv/datatools-license
|
||||
|
||||
# Random 32-char hex DB password
|
||||
openssl rand -hex 32 > secrets/pg_password
|
||||
|
||||
# Postmark server API token from §1b
|
||||
echo -n "<paste-token>" > secrets/postmark_token
|
||||
|
||||
# Ed25519 private signing key (from password manager OR generate-keypair.py)
|
||||
echo -n "<paste-hex-privkey>" > secrets/license_privkey
|
||||
|
||||
# Gumroad webhook secret from §1d (generate when needed; placeholder OK for PR 1)
|
||||
openssl rand -hex 32 > secrets/gumroad_secret
|
||||
|
||||
# Lock everything down.
|
||||
chmod 400 secrets/*
|
||||
chown -R datatools-api:datatools-api secrets/
|
||||
```
|
||||
|
||||
The corresponding **public** key for `DATATOOLS_LICENSE_PUBKEY` goes
|
||||
in `/srv/datatools-license/.env` (it's not secret — it's already in
|
||||
every shipped binary):
|
||||
|
||||
```bash
|
||||
echo "DATATOOLS_LICENSE_PUBKEY=<hex-pubkey>" > /srv/datatools-license/.env
|
||||
chmod 640 /srv/datatools-license/.env
|
||||
chown datatools-api:datatools-api /srv/datatools-license/.env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. App image build
|
||||
|
||||
The Mint API source lives in this repo under `server/` (new directory
|
||||
introduced by PR 1). Build the Docker image:
|
||||
|
||||
```bash
|
||||
cd /srv/datatools-license/app
|
||||
git clone https://git.invixiom.com/giteadmin/datatools-dev.git .
|
||||
docker build -t datatools-license-api:latest -f server/Dockerfile server/
|
||||
```
|
||||
|
||||
Schema bootstrap (one-time, after first `docker compose up`):
|
||||
|
||||
```bash
|
||||
docker compose exec api alembic upgrade head
|
||||
```
|
||||
|
||||
Smoke test:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8090/health
|
||||
# expects: {"status":"ok","db":"ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. nginx config
|
||||
|
||||
`/etc/nginx/sites-available/unalogix` — **new file**, do not merge
|
||||
into `invixiom`:
|
||||
|
||||
```nginx
|
||||
# Marketing / product site (datatools.unalogix.com) — static for now.
|
||||
server {
|
||||
listen 80;
|
||||
server_name datatools.unalogix.com licenses.datatools.unalogix.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name datatools.unalogix.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/datatools.unalogix.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/datatools.unalogix.com/privkey.pem;
|
||||
|
||||
root /srv/datatools-license/site; # static landing page; create later
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
|
||||
# License operations subdomain.
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name licenses.datatools.unalogix.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/datatools.unalogix.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/datatools.unalogix.com/privkey.pem;
|
||||
|
||||
# Block /internal/* from the public side as defense-in-depth.
|
||||
# (The app also enforces this server-side; this is layered.)
|
||||
location /internal/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8090;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
|
||||
# Gumroad webhook payloads are tiny but tighten anyway.
|
||||
client_max_body_size 1m;
|
||||
|
||||
# Basic rate limiting: 30 req/min/IP on /webhooks/* and /portal/*.
|
||||
# Tune in nginx.conf with a `limit_req_zone` directive.
|
||||
# limit_req zone=licenses burst=10 nodelay;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable + reload:
|
||||
|
||||
```bash
|
||||
ln -s /etc/nginx/sites-available/unalogix /etc/nginx/sites-enabled/unalogix
|
||||
nginx -t # validate
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. TLS cert
|
||||
|
||||
Use the standalone http-01 challenge (nginx-plugin works too; this is
|
||||
slightly more explicit):
|
||||
|
||||
```bash
|
||||
certbot certonly \
|
||||
--webroot -w /var/www/html \
|
||||
-d datatools.unalogix.com \
|
||||
-d licenses.datatools.unalogix.com \
|
||||
--agree-tos \
|
||||
--email michael.dombaugh@gmail.com \
|
||||
--non-interactive
|
||||
```
|
||||
|
||||
Cert lands at `/etc/letsencrypt/live/datatools.unalogix.com/`.
|
||||
Auto-renewal is already configured by the certbot package (systemd
|
||||
timer `certbot.timer`). Confirm:
|
||||
|
||||
```bash
|
||||
systemctl list-timers certbot.timer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Bring it up
|
||||
|
||||
```bash
|
||||
cd /srv/datatools-license
|
||||
docker compose up -d
|
||||
docker compose ps # both services should be 'running (healthy)'
|
||||
docker compose logs -f api
|
||||
```
|
||||
|
||||
Public smoke test:
|
||||
|
||||
```bash
|
||||
curl -s https://licenses.datatools.unalogix.com/health
|
||||
# expects: {"status":"ok","db":"ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Verification — end-to-end internal mint
|
||||
|
||||
From your laptop (NOT the server), open an SSH tunnel for the internal
|
||||
endpoint:
|
||||
|
||||
```bash
|
||||
ssh -L 8090:127.0.0.1:8090 michael@46.225.166.142 -N
|
||||
# Leave running; in another terminal:
|
||||
|
||||
curl -X POST http://127.0.0.1:8090/internal/mint \
|
||||
-H "Authorization: Bearer $DATATOOLS_ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name":"Test Buyer",
|
||||
"email":"test@example.com",
|
||||
"tier":"core",
|
||||
"years":1,
|
||||
"source":"manual",
|
||||
"notes":"smoke test"
|
||||
}'
|
||||
```
|
||||
|
||||
Expected: 200 + a `DTLIC2:...` blob + a row inserted in the `licenses`
|
||||
table. Confirm with:
|
||||
|
||||
```bash
|
||||
docker compose exec postgres \
|
||||
psql -U datatools_api -d datatools_licenses \
|
||||
-c "SELECT license_key, email, tier, source FROM licenses;"
|
||||
```
|
||||
|
||||
Then **revoke the test row** before going further:
|
||||
|
||||
```bash
|
||||
docker compose exec postgres \
|
||||
psql -U datatools_api -d datatools_licenses \
|
||||
-c "DELETE FROM licenses WHERE email = 'test@example.com';"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Operational concerns
|
||||
|
||||
### Backups (Postgres → off-site)
|
||||
|
||||
`/etc/cron.daily/datatools-license-backup`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
OUT=/srv/datatools-license/backups/db-${TS}.sql.gz
|
||||
docker compose -f /srv/datatools-license/compose.yml exec -T postgres \
|
||||
pg_dump -U datatools_api datatools_licenses | gzip > "$OUT"
|
||||
chmod 600 "$OUT"
|
||||
# Off-site copy — pick one:
|
||||
# rclone copy "$OUT" remote:datatools-license-backups/
|
||||
# aws s3 cp "$OUT" s3://datatools-backups/db/ --sse AES256
|
||||
find /srv/datatools-license/backups -name 'db-*.sql.gz' -mtime +30 -delete
|
||||
```
|
||||
|
||||
Pick an off-site target. Without one, a disk failure loses every
|
||||
customer record. Test the restore at least once on a staging copy.
|
||||
|
||||
### Monitoring
|
||||
|
||||
External uptime probe (free):
|
||||
1. UptimeRobot account → add monitor for `https://licenses.datatools.unalogix.com/health`.
|
||||
2. 5-minute interval, alert to email/SMS.
|
||||
|
||||
Container health is already handled by `restart: unless-stopped` +
|
||||
healthcheck. To see recent failures:
|
||||
|
||||
```bash
|
||||
docker compose ps # last health-check status
|
||||
docker compose logs api --tail 200
|
||||
journalctl -u docker --since '1 hour ago' | grep datatools
|
||||
```
|
||||
|
||||
### Log rotation
|
||||
|
||||
Docker handles container logs; cap their size in
|
||||
`/etc/docker/daemon.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then `systemctl restart docker` (this restarts all containers — schedule
|
||||
during a quiet window).
|
||||
|
||||
### Key rotation (future)
|
||||
|
||||
If the private signing key is ever compromised:
|
||||
|
||||
1. Generate a new keypair (`scripts/generate_keypair.py`).
|
||||
2. Build and ship a desktop release with the new pubkey embedded.
|
||||
3. Update `/srv/datatools-license/secrets/license_privkey` and
|
||||
`/srv/datatools-license/.env`'s pubkey.
|
||||
4. `docker compose restart api`.
|
||||
5. Re-issue every active license (script that queries the DB, calls
|
||||
`/internal/mint`, emails buyers). Old blobs will fail verification
|
||||
in the new desktop build.
|
||||
|
||||
Plan a 90-day overlap window where the desktop verifies against
|
||||
*both* keys before retiring the old pubkey. (Verification logic
|
||||
change to the desktop app — not in scope for PR 1.)
|
||||
|
||||
---
|
||||
|
||||
## 10. PR cutover sequence
|
||||
|
||||
This runbook covers the box-level scaffolding. Application code lands
|
||||
in three independently shippable PRs:
|
||||
|
||||
| PR | Adds | Ship gate | Webhook live? |
|
||||
|---|---|---|---|
|
||||
| **1** | Source-agnostic Mint API + Postgres + `datatools-admin mint` CLI | Operator can mint a comp license through the server | No |
|
||||
| **2** | Gumroad adapter + webhook receiver + email send | Real Gumroad sale auto-mints + emails buyer | **Yes** (enable in Gumroad dashboard at this PR's deploy) |
|
||||
| **3** | Renewal / re-delivery portal | Buyer self-services renewals and lost-blob re-delivery | (unchanged) |
|
||||
|
||||
§1d (Gumroad webhook URL) is **filled in during PR 2's deploy**, not
|
||||
before. Until then the endpoint returns 404.
|
||||
|
||||
---
|
||||
|
||||
## 11. Rollback
|
||||
|
||||
Each component is independently reversible.
|
||||
|
||||
```bash
|
||||
# Stop and remove containers (DB volume persists)
|
||||
docker compose -f /srv/datatools-license/compose.yml down
|
||||
|
||||
# Full teardown including DB (DESTRUCTIVE — backup first)
|
||||
docker compose -f /srv/datatools-license/compose.yml down -v
|
||||
|
||||
# Remove nginx site
|
||||
rm /etc/nginx/sites-enabled/unalogix
|
||||
nginx -t && systemctl reload nginx
|
||||
|
||||
# Revoke + delete TLS cert
|
||||
certbot delete --cert-name datatools.unalogix.com
|
||||
|
||||
# Remove filesystem
|
||||
rm -rf /srv/datatools-license # NOTE: includes secrets dir; backup first
|
||||
|
||||
# Remove system user
|
||||
deluser datatools-api
|
||||
delgroup datatools-api
|
||||
```
|
||||
|
||||
DNS records can stay or be removed — they're not on this host.
|
||||
Reference in New Issue
Block a user