Add Directus service configuration and environment variables to deployment stack
This commit is contained in:
@@ -18,16 +18,121 @@ Currently:
|
||||
|
||||
- **redis** — telemetry queue + future Phase 2 connection registry. Internal-only, persisted via named volume.
|
||||
- **tcp-ingestion** — Teltonika telemetry TCP server. Image built by [`trm/tcp-ingestion`](https://git.dev.microservices.al/trm/tcp-ingestion)'s Gitea workflow.
|
||||
- **postgres** — PostgreSQL 16 + TimescaleDB + PostGIS via `timescale/timescaledb-ha`. Schema authority is split: the `positions` hypertable is owned by [`trm/processor`](https://git.dev.microservices.al/trm/processor)'s migration runner; everything else is owned by `trm/directus` via its snapshot YAML. Internal-only, persisted via named volume.
|
||||
- **processor** — consumes telemetry from Redis, writes to Postgres. Image built by [`trm/processor`](https://git.dev.microservices.al/trm/processor)'s Gitea workflow.
|
||||
- **directus** — business-plane API + admin UI + schema authority. Image built by [`trm/directus`](https://git.dev.microservices.al/trm/directus)'s Gitea workflow. Boot pipeline runs db-init pre-schema → bootstrap → schema-apply → db-init post-schema → start; first boot on a fresh DB takes ~60–90 s.
|
||||
|
||||
Planned (will be added as they land):
|
||||
|
||||
- **processor** — consumes telemetry from Redis, writes to PostgreSQL/Timescale.
|
||||
- **postgres** — with TimescaleDB extension.
|
||||
- **directus** — business-plane API and admin UI.
|
||||
- **react-spa** — front-end SPA (static bundle, served via reverse proxy).
|
||||
|
||||
See `../docs/wiki/` for the full architecture.
|
||||
|
||||
## First-deploy checklist
|
||||
|
||||
Run through this once per environment before clicking deploy. It covers the security-critical secrets (which must NOT use the compose.yaml placeholder defaults) and the Portainer setup.
|
||||
|
||||
### 1. Generate per-environment secrets
|
||||
|
||||
These values are unique per environment — never reuse them across stage/prod, and never reuse them after a compromise. Run on any machine with `openssl` and `uuidgen` (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
echo "POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=')"
|
||||
echo "DIRECTUS_KEY=$(uuidgen)"
|
||||
echo "DIRECTUS_SECRET=$(openssl rand -hex 64)"
|
||||
echo "DIRECTUS_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')"
|
||||
```
|
||||
|
||||
Keep the output somewhere safe (1Password, Vaultwarden, etc.) — you'll paste it into Portainer next, and you'll need `DIRECTUS_ADMIN_PASSWORD` again to log in for the first time.
|
||||
|
||||
> The compose defaults for these (`trm-pilot-change-me`, `REPLACE-ME-WITH-A-UUID`, `REPLACE-ME-WITH-A-LONG-RANDOM-STRING`, `CHANGE-ON-FIRST-LOGIN`) are deliberately broken-looking. Anything still using them after deploy is a misconfiguration.
|
||||
|
||||
### 2. Set Portainer stack environment variables
|
||||
|
||||
Stack → **Environment variables** in Portainer's Add stack form. Required for first deploy:
|
||||
|
||||
| Variable | Value |
|
||||
|---|---|
|
||||
| `POSTGRES_PASSWORD` | from step 1 |
|
||||
| `DIRECTUS_KEY` | from step 1 |
|
||||
| `DIRECTUS_SECRET` | from step 1 |
|
||||
| `DIRECTUS_ADMIN_EMAIL` | the email you'll log in with |
|
||||
| `DIRECTUS_ADMIN_PASSWORD` | from step 1 |
|
||||
| `DIRECTUS_PUBLIC_URL` | external URL of the Directus admin UI (e.g. `https://directus.stage.example.com`). Used in password-reset emails and OAuth redirects. |
|
||||
|
||||
Recommended for any non-throwaway environment:
|
||||
|
||||
| Variable | Value |
|
||||
|---|---|
|
||||
| `TCP_INGESTION_TAG` | a specific commit SHA (not `main`) for reproducibility |
|
||||
| `PROCESSOR_TAG` | same |
|
||||
| `DIRECTUS_TAG` | same |
|
||||
| `LOG_LEVEL` | `info` or `warn` for prod; `debug` only for active troubleshooting |
|
||||
| `LOG_STYLE` | `json` for log aggregators; default is already `json` |
|
||||
|
||||
Optional:
|
||||
|
||||
| Variable | When to set |
|
||||
|---|---|
|
||||
| `TCP_INGESTION_PORT` | Change if `5027` is already in use on the host. GPS devices need a real host port — this one is published. |
|
||||
| `DIRECTUS_CORS_ENABLED` / `DIRECTUS_CORS_ORIGIN` | Set to `true` and the SPA's origin URL once the React SPA is deployed. |
|
||||
|
||||
> **Directus is internal-only by design.** It listens on `8055` inside the `trm_default` Compose network and is **not** published to the host. Wire a reverse proxy (Traefik / Caddy / nginx) on the host or attached to the network and forward your public domain to `http://directus:8055`. The proxy handles TLS, optional WAF / rate-limit, and any auth-header rewriting. Set `DIRECTUS_PUBLIC_URL` to the proxy-served URL (e.g. `https://directus.stage.example.com`) so password-reset emails and OAuth redirects work. The dev compose in `trm/directus` does host-publish `8055` for local iteration; this stage/prod stack deliberately does not.
|
||||
|
||||
### 3. Authenticate the host to the Gitea registry
|
||||
|
||||
First deploy only (Portainer's **Registries** UI is preferred over manual login):
|
||||
|
||||
```bash
|
||||
docker login git.dev.microservices.al
|
||||
```
|
||||
|
||||
### 4. Deploy the stack
|
||||
|
||||
Stack → **Add stack** → **Repository** → fill in repo URL, branch, compose path → **Deploy the stack**.
|
||||
|
||||
### 5. Watch the first boot
|
||||
|
||||
Directus's first boot runs ~30–45 s of internal migrations on top of the project's own boot pipeline. Total time-to-healthy on a fresh DB is ~60–90 s. Tail the logs in Portainer → Stack → directus → Logs:
|
||||
|
||||
Expected progression:
|
||||
|
||||
```
|
||||
[entrypoint] step 1/5: db-init (pre-schema)
|
||||
[db-init] db-init complete: 3 applied, 0 skipped
|
||||
[entrypoint] step 2/5: directus bootstrap
|
||||
INFO: Initializing bootstrap...
|
||||
INFO: Installing Directus system tables...
|
||||
INFO: Running migrations...
|
||||
... ~80 internal migrations ...
|
||||
INFO: Setting up first admin role...
|
||||
INFO: Adding first admin user...
|
||||
INFO: Done
|
||||
[entrypoint] step 3/5: directus schema apply
|
||||
INFO: Snapshot applied successfully
|
||||
[entrypoint] step 4/5: db-init (post-schema)
|
||||
[db-init] db-init complete: 2 applied, 0 skipped
|
||||
[entrypoint] step 5/5: directus start (pm2-runtime)
|
||||
PM2 log: App [directus:0] online
|
||||
INFO: Server started at http://0.0.0.0:8055
|
||||
```
|
||||
|
||||
The healthcheck flips to `healthy` once the server is serving (~5–10 s after the "Server started" log line).
|
||||
|
||||
### 6. First login
|
||||
|
||||
Browse to `${DIRECTUS_PUBLIC_URL}` (or `http://<host>:8055` if you didn't put it behind a proxy). Log in with `DIRECTUS_ADMIN_EMAIL` + `DIRECTUS_ADMIN_PASSWORD`.
|
||||
|
||||
**Change the admin password immediately** via the user's own profile in the admin UI. The `DIRECTUS_ADMIN_PASSWORD` env var is only the first-boot seed — changing it post-deploy has no effect on the running user. Same goes for `POSTGRES_PASSWORD`: it's baked into the persistent volume on first boot and must be rotated via `ALTER USER` inside psql, not by changing the env var.
|
||||
|
||||
### 7. Verify the schema landed
|
||||
|
||||
Admin UI → **Settings → Data Model**. You should see 12 user collections: `organizations`, `users` (built-in `directus_users` with custom fields), `organization_users`, `vehicles`, `organization_vehicles`, `devices`, `organization_devices`, `events`, `classes`, `entries`, `entry_crew`, `entry_devices`. If a collection is missing, the schema-apply step failed and the boot logs will say so.
|
||||
|
||||
> **Schema-as-code reminder:** do NOT add or remove collections via the admin UI on this stage instance. Schema changes flow through `trm/directus` (admin-UI edit → `pnpm run schema:snapshot` → commit → CI dry-run → image rebuild → redeploy). Edits made directly here will be DROPPED on the next image rebuild — schema-apply enforces the committed snapshot. See `docs/wiki/entities/directus.md` "destructive-apply hazard."
|
||||
|
||||
---
|
||||
|
||||
## Deploy via Portainer (Repository Stack)
|
||||
|
||||
1. **Stack → Add stack → Repository** in Portainer.
|
||||
@@ -72,9 +177,10 @@ To pin a specific build for production, set the relevant `*_TAG` variable in `.e
|
||||
## Network model
|
||||
|
||||
- One internal Compose network (`trm_default`).
|
||||
- Redis is **not** bound to a host port — only reachable from other services in the stack via service-name DNS (`redis://redis:6379`).
|
||||
- tcp-ingestion's TCP port (5027 by default) is bound to the host so devices can reach it.
|
||||
- Other Redis instances on the same host can keep using port 6379 freely; this stack does not collide with them.
|
||||
- **Redis**, **postgres**, **directus**, and **processor** are not bound to host ports — only reachable from other services in the stack via service-name DNS (`redis://redis:6379`, `postgres:5432`, `http://directus:8055`).
|
||||
- **tcp-ingestion**'s TCP port (`5027` by default) is bound to the host so GPS devices can reach it. This is the only host-published port in the stack.
|
||||
- **Directus admin UI / API access** goes through a reverse proxy (Traefik / Caddy / nginx) on the host or attached to the `trm_default` network. The proxy handles TLS, public-DNS routing, and any WAF / rate-limit / auth-header policy. Wire your proxy to forward your domain to `http://directus:8055`. The proxy itself is not part of this stack — add it as a sibling stack in Portainer or run it on the host.
|
||||
- Other Redis / Postgres instances on the same host can keep using their default ports freely; this stack does not collide with them. The default postgres-on-host you may already have running on `5432` is untouched — this stack's postgres is internal-only.
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
||||
Reference in New Issue
Block a user