Two parallel tasks landing together. The boot pipeline is now wired end-to-end: db-init → schema apply → directus bootstrap → pm2-runtime. Live-verified by booting a fresh compose stack to a serving Directus admin UI on :8055. Task 1.6 — snapshot tooling: - scripts/schema-snapshot.sh — host-side, dev-time. Verifies docker is on PATH and the directus compose service is running, runs `node /directus/cli.js schema snapshot --yes` inside the container, copies the YAML out to ./snapshots/schema.yaml. Used after admin-UI schema changes to capture the new state for git commit. - scripts/schema-apply.sh — image-side, boot-time. Reads /directus/snapshots/schema.yaml, runs a dry-run preview, then applies. Gracefully skips when the snapshot is absent or whitespace- only (Phase 1 first-boot path before tasks 1.4/1.5 produce collections). SNAPSHOT_PATH env var override for CI flexibility. - snapshots/README.md — lifecycle doc; warns against hand-editing. Task 1.7 — real entrypoint flow: - entrypoint.sh rewritten from Phase 1.1's placeholder to the 4-step boot per ROADMAP design rule #3: 1/4 db-init → /directus/scripts/apply-db-init.sh 2/4 schema apply → /directus/scripts/schema-apply.sh 3/4 directus bootstrap → node /directus/cli.js bootstrap 4/4 directus start → exec pm2-runtime start ecosystem.config.cjs set -euo pipefail halts boot on any step's non-zero exit. Each step emits a [entrypoint] log marker so an operator reading container logs sees which step failed. Bug found and fixed during live verification: - Both 1.6 scripts initially called bare `directus schema ...` as if the CLI were on PATH. Upstream directus/directus:11.17.4 does NOT expose `directus` on PATH — invocation is via `node /directus/cli.js`, same pattern as the entrypoint's bootstrap step. Both scripts corrected. Also added -T to docker compose exec in schema-snapshot.sh so the script works in non-TTY contexts (CI). Phase 5 follow-up (non-blocking) flagged in 07's Done section: Directus warns "Collection 'positions' doesn't have a primary key column and will be ignored". The positions table uses UNIQUE INDEX (device_id, ts) matching processor's pattern, not a PK constraint. Means positions is not auto-registered as a Directus collection — fine for Phase 1, but the operator faulty-flag workflow will need a custom endpoint or manual collection registration in Phase 5. ROADMAP marks 1.6 + 1.7 done. Phase 1 progress: 5/9 tasks complete (1.1, 1.2, 1.3, 1.6, 1.7); 1.4, 1.5, 1.8, 1.9 remain.
directus
The TRM business plane. Directus 11 instance owning the relational schema (organizations, users, events, entries, course definition, penalty system, timing tables), exposing it through auto-generated REST/GraphQL APIs and the admin UI, and enforcing role-based permissions.
For the architectural specification see ../docs/wiki/entities/directus.md. For the work plan and task status see .planning/ROADMAP.md.
This service is part of the TRM (Time Racing Management) platform.
Schema management — at a glance
Schema is defined and migrated through Directus, with two artifact directories:
snapshots/schema.yaml— Directus collections, fields, relations. Generated locally viadirectus schema snapshot, applied at container startup viadirectus schema apply.db-init/*.sql— schema Directus does not manage: the postgres-timescaledbpositionshypertable, thefaultycolumn, PostGIS-specific DDL, etc. Sequential numbered files (001_,002_, …) applied byscripts/apply-db-init.shwith amigrations_appliedguard table to skip already-run files.
Apply order at boot: db-init first, then directus schema apply, then directus start. Any failure halts boot.
Quick start (local)
Prerequisites: Docker, the directus/directus:11.17.4 image (pulled automatically by compose), a running Postgres 16 + TimescaleDB + PostGIS instance (provided by compose.dev.yaml).
git clone <repo-url>
cd directus
cp .env.example .env
# Edit .env — at minimum set DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE, KEY, SECRET
docker compose -f compose.dev.yaml up --build
Admin UI lands at http://localhost:8055. Default admin credentials are read from ADMIN_EMAIL / ADMIN_PASSWORD in .env.
After making schema changes in the admin UI, snapshot before commit:
pnpm run schema:snapshot
git add snapshots/schema.yaml && git commit
Test the image locally
compose.dev.yaml builds the image from source and runs it next to a TimescaleDB+PostGIS container. Useful for verifying Dockerfile changes, db-init migrations, or snapshot apply behavior before pushing.
docker compose -f compose.dev.yaml down -v # wipe volumes for a fresh run
docker compose -f compose.dev.yaml up --build
The entrypoint runs db-init, then directus schema apply, then directus start. Watch the logs to confirm each step exits 0.
Production / stage deployment
This service is not deployed standalone. It runs as part of the platform stack defined in the deploy/ repo, which Portainer pulls and runs on the stage and production hosts.
The image itself is published to git.dev.microservices.al/trm/directus:main on every push to main (see CI behavior below). The deploy/ repo's compose.yaml references that image.
To pin a specific commit in production, set DIRECTUS_TAG=<sha> in the deploy stack's environment variables.
Note: The
deploy/compose.yamlwill need adirectusservice entry referencing this image, plus a TimescaleDB+PostGIS service if not already present, before this service can run in stage/production. See.planning/phase-1-slice-1-schema/07-image-and-dockerfile.md.
Environment variables
See .env.example for the full list. Required for boot:
| Variable | Description |
|---|---|
DB_CLIENT |
pg (always) |
DB_HOST / DB_PORT / DB_DATABASE / DB_USER / DB_PASSWORD |
Postgres connection |
KEY |
Directus instance key (random UUID) |
SECRET |
Directus JWT signing secret (random) |
ADMIN_EMAIL / ADMIN_PASSWORD |
Bootstrap admin (only used on first init) |
PUBLIC_URL |
External-facing URL of the instance |
All other Directus envs (cache, logging, CORS, etc.) follow upstream defaults unless overridden.
CI behavior
Gitea Actions workflow lands at .gitea/workflows/build.yml in Phase 1 task 1.8 — not yet present.
When the workflow exists:
- Push to
main(only whensnapshots/,db-init/,extensions/,Dockerfile, or the workflow file itself changes): builds the image, spins up a throwaway Postgres + TimescaleDB + PostGIS viaservices:, runsapply-db-init.shanddirectus schema apply --yesagainst it as a dry-run, then publishes the image tagged:mainif the dry-run exits 0. Auto-deploys to stage if a Portainer webhook is configured viasecrets.PORTAINER_WEBHOOK_URL. - Manual trigger (
workflow_dispatch): same flow, run on demand.
The dry-run is non-negotiable — it catches snapshot drift, broken db-init scripts, and incompatible schema changes before they touch any real DB.