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.
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/Dockerfile—FROM directus/directus:11.x, copiessnapshots/,db-init/,scripts/,entrypoint.sh,extensions/into the image. SetsENTRYPOINT ["/directus/entrypoint.sh"]. (Concrete entrypoint contents land in task 1.7; for now create a placeholder that justexecs the upstream entrypoint.)directus/compose.dev.yaml— two services:db:timescale/timescaledb-ha:pg16.6-ts2.17.2-all(the-allvariant bundles PostGIS binaries; the:pg16-latestfloating tag does NOT exist on Docker Hub — pin a concrete TS+PG version). Volume-mapped Postgres data dir, healthcheck.directus: built from localDockerfile, depends ondbhealthy, env vars for DB connection +KEY+SECRET+ admin bootstrap, port8055exposed.
directus/package.json— minimal, only for npm scripts (no runtime deps). Scripts:schema:snapshot—bash scripts/schema-snapshot.sh(script body lands in 1.6)schema:apply—bash scripts/schema-apply.shdb:init—bash scripts/apply-db-init.shdev—docker compose -f compose.dev.yaml up --builddev:down—docker compose -f compose.dev.yaml downdev:reset—docker 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/.gitignore—node_modules/,.env,.env.local,*.log,directus-data/(the local Postgres volume mount, if used).directus/.dockerignore—.git/,.planning/,node_modules/,.env*,*.mdexceptREADME.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 simplyexec /directus/cli.js start(or whatever the upstream image's start command is). Real wrapper lands in 1.7.directus/README.mdalready 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 supportCREATE 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) sodev:downpreserves data anddev:resetwipes it. - No secrets committed.
.envis gitignored..env.examplecarries placeholder values only. - No bind mounts of
snapshots/ordb-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 installsucceeds (no runtime deps; lockfile generated).docker compose -f compose.dev.yaml up --buildboots Directus successfully against a fresh TimescaleDB container.http://localhost:8055serves the Directus admin login.- First-time bootstrap with
ADMIN_EMAIL/ADMIN_PASSWORDfrom.envworks. pnpm dev:downstops the stack, preserves the volume.pnpm dev:resetwipes 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
postgisextension binaries (or document the alternative — e.g. switching topostgis/postgis:16-masterwith 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 than11.xfor 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-latestdoes not exist on Docker Hub (the agent's initial pin failed at pull time). The empirically-verified tag istimescale/timescaledb-ha:pg16.6-ts2.17.2-all. The-allsuffix 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: /pgdataplus a volume mount to/pgdataproduced "could not change permissions of directory" errors at initdb. Fixed by setting bothPGDATAand the volume target to/home/postgres/pgdata/data. - PostGIS extension. Binaries are bundled in the
-allimage but the extension is not auto-created on thedirectusdatabase. 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.4confirmed to exist on Docker Hub. Used as the image pin.
Deviations from task spec:
entrypoint.shdelegates tonode cli.js bootstrap && pm2-runtime start ecosystem.config.cjs(the upstream image's actual CMD) rather thanexec /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. Thebootstrapstep is idempotent (safe to run every boot) and handles admin user creation.compose.dev.yamlsetsPGDATA: /home/postgres/pgdata/dataand mounts the named volume to the same path. Required by thetimescaledb-ha:*-allimage; mounting elsewhere fails initdb.Dockerfileinstallsbashandpostgresql16-clientvia apk. The upstreamdirectus/directus:11.17.4is Alpine-based and shipsash(BusyBox) only — bash is required byscripts/apply-db-init.sh(task 1.2) which uses associative arrays,[[ ]],mapfile, andBASH_REMATCH.postgresql16-clientprovidespsql+pg_isreadyfor the same script. Both pre-installed in 1.1 to avoid cache-busting Dockerfile diffs in later phases. (Note:bashwas 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.)README.mdupdated: pinned11.x→11.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.