docs(license): runbook fixes from PR 1 self-host deploy
Two real-world footguns surfaced during the first live deploy: 1. docker-compose's uid/gid/mode long-form on file-based secrets is silently ignored — that's a swarm-mode-only feature. The container app user (UID 10001 from the Dockerfile) cannot read a mode-400 file whose host UID it doesn't match. Fix is to chown the secret files to 10001 directly; host-side access control stays gated by the parent dir's mode 750. 2. nginx 1.24 (Ubuntu 24.04 default) rejects the standalone "http2 on;" directive (that arrived in 1.25). Use the legacy "listen 443 ssl http2;" combined form. Noted prominently so the next deploy doesn't trip on it. Also realigned §3's compose example to what actually got deployed for PR 1 — only pg_password + admin_token secrets, postmark / gumroad / license_privkey commented out as PR 2 / production-key follow-ups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,16 @@ signing key and Postmark token live there as mode-400 files — never
|
|||||||
in environment-variable-via-systemd-EnvironmentFile, never in the
|
in environment-variable-via-systemd-EnvironmentFile, never in the
|
||||||
docker-compose file, never anywhere `root` doesn't need to look.
|
docker-compose file, never anywhere `root` doesn't need to look.
|
||||||
|
|
||||||
|
> **Gotcha — secret file ownership UID.** Docker compose's
|
||||||
|
> `uid:`/`gid:`/`mode:` long-form on `secrets:` is silently ignored
|
||||||
|
> for **file-based** secrets (it's a swarm-mode-only feature). The
|
||||||
|
> file inside the container appears with whatever ownership it has
|
||||||
|
> on the host, and the API runs as UID 10001 (the `app` user from
|
||||||
|
> the Dockerfile). So chown the actual files to **10001** (a numeric
|
||||||
|
> UID that doesn't exist on the host — that's fine, chown accepts
|
||||||
|
> it) and rely on the parent dir's mode 750 + ownership for host-side
|
||||||
|
> access control. See §3 below for the corrected `chown` step.
|
||||||
|
|
||||||
### Firewall recommendation (separate decision)
|
### Firewall recommendation (separate decision)
|
||||||
|
|
||||||
The box currently runs without UFW. Enabling it now would affect all
|
The box currently runs without UFW. Enabling it now would affect all
|
||||||
@@ -181,24 +191,26 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: datatools-license-api:latest # built locally; see §4
|
build:
|
||||||
|
context: ./app
|
||||||
|
dockerfile: server/Dockerfile
|
||||||
|
image: datatools-license-api:latest
|
||||||
container_name: datatools-api
|
container_name: datatools-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://datatools_api@postgres:5432/datatools_licenses
|
DATABASE_URL: postgresql+psycopg://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
|
PG_PASSWORD_FILE: /run/secrets/pg_password
|
||||||
|
DATATOOLS_ADMIN_TOKEN_FILE: /run/secrets/admin_token
|
||||||
|
# PR 2 adds: POSTMARK_TOKEN_FILE, GUMROAD_WEBHOOK_SECRET_FILE.
|
||||||
|
# Production keypair (replaces in-tree dev key): set
|
||||||
|
# DATATOOLS_LICENSE_PRIVKEY_FILE: /run/secrets/license_privkey
|
||||||
|
# and DATATOOLS_LICENSE_PUBKEY: <hex> before shipping v1.0.
|
||||||
secrets:
|
secrets:
|
||||||
- pg_password
|
- pg_password
|
||||||
- postmark_token
|
- admin_token
|
||||||
- license_privkey
|
|
||||||
- gumroad_secret
|
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8090:8000" # localhost-only; nginx is the only path in
|
- "127.0.0.1:8090:8000" # localhost-only; nginx is the only path in
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -208,10 +220,10 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
pg_password: { file: ./secrets/pg_password }
|
pg_password: { file: ./secrets/pg_password }
|
||||||
postmark_token: { file: ./secrets/postmark_token }
|
admin_token: { file: ./secrets/admin_token }
|
||||||
license_privkey: { file: ./secrets/license_privkey }
|
# PR 2 adds: postmark_token, gumroad_secret. Production keypair
|
||||||
gumroad_secret: { file: ./secrets/gumroad_secret }
|
# rotation adds: license_privkey.
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
datatools_pg_data:
|
datatools_pg_data:
|
||||||
@@ -219,7 +231,8 @@ volumes:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Populate the secrets (each file should contain the value with no
|
Populate the secrets (each file should contain the value with no
|
||||||
trailing newline):
|
trailing newline). For PR 1, only `pg_password` and `admin_token`
|
||||||
|
are required; the rest land in PR 2 / production key rotation.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /srv/datatools-license
|
cd /srv/datatools-license
|
||||||
@@ -227,18 +240,20 @@ cd /srv/datatools-license
|
|||||||
# Random 32-char hex DB password
|
# Random 32-char hex DB password
|
||||||
openssl rand -hex 32 > secrets/pg_password
|
openssl rand -hex 32 > secrets/pg_password
|
||||||
|
|
||||||
# Postmark server API token from §1b
|
# Random admin Bearer token (CLI auth). Save this — you'll need it
|
||||||
echo -n "<paste-token>" > secrets/postmark_token
|
# on your laptop to talk to /internal/* via the SSH tunnel.
|
||||||
|
openssl rand -hex 32 > secrets/admin_token
|
||||||
|
|
||||||
# Ed25519 private signing key (from password manager OR generate-keypair.py)
|
# --- PR 2 / production-key follow-ups (skip for PR 1 bring-up) ---
|
||||||
echo -n "<paste-hex-privkey>" > secrets/license_privkey
|
# echo -n "<postmark-server-token>" > secrets/postmark_token
|
||||||
|
# echo -n "<ed25519-private-hex>" > secrets/license_privkey
|
||||||
|
# openssl rand -hex 32 > secrets/gumroad_secret
|
||||||
|
|
||||||
# Gumroad webhook secret from §1d (generate when needed; placeholder OK for PR 1)
|
# Lock everything down. The numeric 10001 matches the in-container
|
||||||
openssl rand -hex 32 > secrets/gumroad_secret
|
# `app` user (Dockerfile-defined), letting the API read the file
|
||||||
|
# while keeping host-side access gated by the parent dir's mode 750.
|
||||||
# Lock everything down.
|
|
||||||
chmod 400 secrets/*
|
chmod 400 secrets/*
|
||||||
chown -R datatools-api:datatools-api secrets/
|
chown 10001:10001 secrets/*
|
||||||
```
|
```
|
||||||
|
|
||||||
The corresponding **public** key for `DATATOOLS_LICENSE_PUBKEY` goes
|
The corresponding **public** key for `DATATOOLS_LICENSE_PUBKEY` goes
|
||||||
@@ -281,6 +296,21 @@ curl -s http://127.0.0.1:8090/health
|
|||||||
|
|
||||||
## 5. nginx config
|
## 5. nginx config
|
||||||
|
|
||||||
|
> **Gotcha — nginx version syntax.** Ubuntu 24.04 ships nginx 1.24,
|
||||||
|
> which uses the legacy `listen 443 ssl http2;` form. The standalone
|
||||||
|
> `http2 on;` directive arrived in nginx 1.25 and will error on 1.24
|
||||||
|
> with `unknown directive "http2"`. The config below uses the 1.24
|
||||||
|
> form.
|
||||||
|
>
|
||||||
|
> **Bring-up sequence.** This config references a TLS cert at
|
||||||
|
> `/etc/letsencrypt/live/datatools.unalogix.com/`, which doesn't
|
||||||
|
> exist on a fresh install — nginx would refuse to start. The
|
||||||
|
> working sequence is: (a) install a temporary HTTP-only config
|
||||||
|
> that serves `.well-known/acme-challenge/` and returns 503 for
|
||||||
|
> everything else, (b) `nginx -s reload`, (c) run `certbot
|
||||||
|
> certonly --webroot`, (d) replace with the HTTPS config below,
|
||||||
|
> (e) `nginx -s reload` again. See §6.
|
||||||
|
|
||||||
`/etc/nginx/sites-available/unalogix` — **new file**, do not merge
|
`/etc/nginx/sites-available/unalogix` — **new file**, do not merge
|
||||||
into `invixiom`:
|
into `invixiom`:
|
||||||
|
|
||||||
@@ -293,7 +323,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2; # nginx 1.24 syntax (Ubuntu 24.04)
|
||||||
server_name datatools.unalogix.com;
|
server_name datatools.unalogix.com;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/datatools.unalogix.com/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/datatools.unalogix.com/fullchain.pem;
|
||||||
@@ -309,7 +339,7 @@ server {
|
|||||||
|
|
||||||
# License operations subdomain.
|
# License operations subdomain.
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2; # nginx 1.24 syntax (Ubuntu 24.04)
|
||||||
server_name licenses.datatools.unalogix.com;
|
server_name licenses.datatools.unalogix.com;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/datatools.unalogix.com/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/datatools.unalogix.com/fullchain.pem;
|
||||||
|
|||||||
Reference in New Issue
Block a user