Files
directus/.planning/phase-1-slice-1-schema/01-project-scaffold.md
T
julian dec2d190ce Task 1.2 — db-init runner script
scripts/apply-db-init.sh implements the boot-time runner that walks
db-init/*.sql in numeric-prefix order, applies each via psql, and
records successful applications in a migrations_applied guard table
so re-runs are no-ops.

All 7 acceptance criteria pass live against the dev compose stack:
empty dir, missing env var, apply, idempotent re-run, checksum
mismatch, filename collision, broken SQL.

Two retroactive Dockerfile corrections folded in (exposed by the
first live-test attempt of 1.2's script):

1. apk add bash. The directus/directus:11.17.4 base is Alpine and
   ships ash via BusyBox, not bash. The script uses bash-specific
   features (associative arrays, [[ ]], mapfile, BASH_REMATCH) and
   fails at line 69 in sh.

2. .gitattributes added at repo root forcing LF on *.sh, *.sql,
   *.yaml, *.yml. Without it, Windows checkouts with core.autocrlf=true
   (the Git-for-Windows default) silently inject CRLF, causing
   "bad interpreter: /usr/bin/env bash^M" inside the Linux container.
   This failure mode only manifests in the container.

Both corrections are documented in 01-project-scaffold.md's Done
section; 02-db-init-runner.md's Done section captures the live-test
results, the corrected docker compose run --entrypoint commands, and
the gotcha about compose env defaults masking missing-env-var tests.

ROADMAP marks 1.2 done; 1.3 next.
2026-05-01 22:35:17 +02:00

7.9 KiB

Task 1.1 — Project scaffold

Phase: 1 — Slice 1 schema + deploy pipeline Status: 🟩 Done Depends on: None Wiki refs: docs/wiki/entities/directus.md, docs/wiki/synthesis/directus-schema-draft.md

Goal

Initialize the directus/ service folder with the directory layout from the Phase 1 README, the config files needed for local Docker compose dev, and a minimal compose.dev.yaml that boots Directus + TimescaleDB so the next tasks have something to iterate against. No Directus collections are created in this task — that starts in 1.4.

Deliverables

  • directus/DockerfileFROM directus/directus:11.x, copies snapshots/, db-init/, scripts/, entrypoint.sh, extensions/ into the image. Sets ENTRYPOINT ["/directus/entrypoint.sh"]. (Concrete entrypoint contents land in task 1.7; for now create a placeholder that just execs the upstream entrypoint.)
  • directus/compose.dev.yaml — two services:
    • db: timescale/timescaledb-ha:pg16.6-ts2.17.2-all (the -all variant bundles PostGIS binaries; the :pg16-latest floating tag does NOT exist on Docker Hub — pin a concrete TS+PG version). Volume-mapped Postgres data dir, healthcheck.
    • directus: built from local Dockerfile, depends on db healthy, env vars for DB connection + KEY + SECRET + admin bootstrap, port 8055 exposed.
  • directus/package.json — minimal, only for npm scripts (no runtime deps). Scripts:
    • schema:snapshotbash scripts/schema-snapshot.sh (script body lands in 1.6)
    • schema:applybash scripts/schema-apply.sh
    • db:initbash scripts/apply-db-init.sh
    • devdocker compose -f compose.dev.yaml up --build
    • dev:downdocker compose -f compose.dev.yaml down
    • dev:resetdocker compose -f compose.dev.yaml down -v && docker compose -f compose.dev.yaml up --build
  • directus/.env.example — full list of env vars with descriptions and defaults. Required: DB_HOST, DB_PORT, DB_DATABASE, DB_USER, DB_PASSWORD, KEY, SECRET, ADMIN_EMAIL, ADMIN_PASSWORD, PUBLIC_URL. Plus optional: LOG_LEVEL, LOG_STYLE, CACHE_ENABLED, CORS_ENABLED, CORS_ORIGIN, WEBSOCKETS_ENABLED.
  • directus/.gitignorenode_modules/, .env, .env.local, *.log, directus-data/ (the local Postgres volume mount, if used).
  • directus/.dockerignore.git/, .planning/, node_modules/, .env*, *.md except README.md, compose.dev.yaml (compose isn't part of the image), directus-data/.
  • Empty placeholder directories with .gitkeep:
    • snapshots/ (1.6 fills it)
    • db-init/ (1.3 fills it)
    • scripts/ (1.2, 1.6 fill it)
    • extensions/ (Phase 5)
  • directus/entrypoint.sh — placeholder that simply exec /directus/cli.js start (or whatever the upstream image's start command is). Real wrapper lands in 1.7.
  • directus/README.md already exists from this scaffold pass — verify it's accurate.

Specification

  • Postgres image choice. Pin to a TimescaleDB image that includes PostgreSQL 16. PostGIS will be installed via db-init/ in Phase 2; the base image must support CREATE EXTENSION postgis (most TimescaleDB-HA images do). Document the pinned tag in compose.dev.yaml.
  • Volume policy in compose.dev.yaml. Use a named volume (directus-pg-data) so dev:down preserves data and dev:reset wipes it.
  • No secrets committed. .env is gitignored. .env.example carries placeholder values only.
  • No bind mounts of snapshots/ or db-init/ in compose.dev.yaml. The image bakes them in. (Implementer can override with a bind mount during local iteration but the committed file does not.)
  • Entrypoint is a placeholder in this task. Real flow (db-init → schema apply → start) lands in 1.7. Keep the placeholder simple to unblock 1.4 testing.

Acceptance criteria

  • pnpm install succeeds (no runtime deps; lockfile generated).
  • docker compose -f compose.dev.yaml up --build boots Directus successfully against a fresh TimescaleDB container.
  • http://localhost:8055 serves the Directus admin login.
  • First-time bootstrap with ADMIN_EMAIL / ADMIN_PASSWORD from .env works.
  • pnpm dev:down stops the stack, preserves the volume.
  • pnpm dev:reset wipes the volume and reboots clean.
  • No collection definitions exist yet — the Directus instance is empty by design.

Risks / open questions

  • TimescaleDB-HA image PostGIS support. Verify the chosen tag includes postgis extension binaries (or document the alternative — e.g. switching to postgis/postgis:16-master with manual TimescaleDB install). Capture the answer in this task's Done section.
  • Directus 11.x patch version. Pin a specific tag (e.g. 11.5.1) rather than 11.x for reproducible builds. Update the pin via PR when bumping.

Done

Pending commit by user. All deliverables created in the same working tree pass.

Open question resolutions (corrected against live pnpm dev boot 2026-05-01):

  • TimescaleDB-HA image tag. :pg16-latest does not exist on Docker Hub (the agent's initial pin failed at pull time). The empirically-verified tag is timescale/timescaledb-ha:pg16.6-ts2.17.2-all. The -all suffix bundles PostGIS binaries.
  • PGDATA path. Not /pgdata (the agent's first guess); the actual data directory baked into this image is /home/postgres/pgdata/data. PGDATA: /pgdata plus a volume mount to /pgdata produced "could not change permissions of directory" errors at initdb. Fixed by setting both PGDATA and the volume target to /home/postgres/pgdata/data.
  • PostGIS extension. Binaries are bundled in the -all image but the extension is not auto-created on the directus database. Directus boot logs warn: PostGIS isn't installed. Geometry type support will be limited. Resolution: CREATE EXTENSION IF NOT EXISTS postgis; lands in db-init when geometry types are needed (Phase 2). Phase 1 has no geometry columns so the warning is benign.
  • Directus version pin: directus/directus:11.17.4 confirmed to exist on Docker Hub. Used as the image pin.

Deviations from task spec:

  1. entrypoint.sh delegates to node cli.js bootstrap && pm2-runtime start ecosystem.config.cjs (the upstream image's actual CMD) rather than exec /directus/cli.js start. The upstream image uses pm2-runtime to manage the process; bypassing it would skip crash recovery and signal handling that pm2 provides. The bootstrap step is idempotent (safe to run every boot) and handles admin user creation.
  2. compose.dev.yaml sets PGDATA: /home/postgres/pgdata/data and mounts the named volume to the same path. Required by the timescaledb-ha:*-all image; mounting elsewhere fails initdb.
  3. Dockerfile installs bash and postgresql16-client via apk. The upstream directus/directus:11.17.4 is Alpine-based and ships ash (BusyBox) only — bash is required by scripts/apply-db-init.sh (task 1.2) which uses associative arrays, [[ ]], mapfile, and BASH_REMATCH. postgresql16-client provides psql + pg_isready for the same script. Both pre-installed in 1.1 to avoid cache-busting Dockerfile diffs in later phases. (Note: bash was added retroactively after 1.2's first live test exposed the dependency — 1.1's original commit shipped without it. Folded into 1.2's commit.)
  4. README.md updated: pinned 11.x11.17.4; CI section notes workflow file is pending (task 1.8).

Live boot acceptance (2026-05-01):

pnpm dev against fresh volumes succeeded: db became healthy, Directus ran 60+ system migrations, PM2 cluster started, server bound at http://0.0.0.0:8055, GraphQL Subscriptions and WebSocket Server started. Admin bootstrap (creating first admin role + admin user) completed. One benign warning ("PostGIS isn't installed" — see Open question resolutions above). One harmless migration warning about a non-existent constraint on directus_comments_collection_foreign (a Directus internal migration's expected idempotent guard, not caused by this task). All Phase 1 task 1.1 acceptance criteria met.