Skip to content

Promote dbt module out of modules-experimental/ to production-suitable - #594

Open
RonaldHensbergen wants to merge 3 commits into
mainfrom
feat/dbt-production-suitable
Open

Promote dbt module out of modules-experimental/ to production-suitable#594
RonaldHensbergen wants to merge 3 commits into
mainfrom
feat/dbt-production-suitable

Conversation

@RonaldHensbergen

Copy link
Copy Markdown
Owner

Summary

The dbt module's productionSuitable: false flag reflected maturity gaps — no README, no demo profile wiring, no bespoke tests — rather than any inherent runtime limitation. That's a different situation from modules/secrets/vault, whose productionSuitable: false is permanent (it only ever runs Vault in ephemeral dev mode). dbt-core itself, running against a real Postgres with the module's existing hardened container settings (read-only root, dropped capabilities, non-root user), has no such limitation, so this PR closes the actual gaps and promotes it to modules/.

Module relocation

  • modules-experimental/transformation/dbt/modules/transformation/dbt/: drops productionSuitable: false and the (experimental) displayName/description suffix.
  • modules/transformation/dbt/README.md (new): Purpose/Known limitations/Upstream docs/Configuration notes, matching the structure of other production module READMEs (e.g. modules/warehouse/postgres/README.md).

Demo profile

  • profiles/local-dagster-postgres-superset-dbt/ (new): the reference local-dagster-postgres-superset stack plus dbt wired to postgres via the sql-database contract. Added as a new profile rather than editing the base profile's composition, following the same pattern local-dagster-postgres-superset-vault established for optional add-ons.

Tests

  • tests/test_dbt_hardening.py (new, 9 tests): image hardening (digest pin, non-root user, pip uninstalled in both build stages), compose service hardening (read_only, cap_drop, tmpfs paths, one-shot restart: "no", the docs sidecar's service_completed_successfully dependency), the required targetDatabase.contractRef schema field, and the entrypoint's required connection env vars — closing the "zero bespoke test files" gap the module had while experimental.

Docs

  • docs/architecture.md: Transformation layer row now lists dbt instead of (planned).
  • docs/roadmap.md: dbt module and new profile moved from the experimental/near-term list into Stable Components.
  • images/dbt/README.md: updated module path references, dropped the "It is experimental" note (the image's build/publish/scan/sign workflow wiring was already identical to the production Dagster/Superset images — nothing there needed to change).

Testing

  • make check — 623 tests, ruff, yamllint, markdownlint all pass.
  • cds validate / cds test (plan+render stages) / cds render pass for the new local-dagster-postgres-superset-dbt profile; manually confirmed the rendered dbt-run service correctly resolves DBT_HOST/DBT_PORT/DBT_DBNAME/DBT_USER/DBT_PASSWORD from the postgres.sql-database contract binding.
  • python -m unittest tests.test_module_isolation -v passes for the relocated module (no cross-module service-name hardcoding), confirming it holds up under the stricter scrutiny modules/ (vs. modules-experimental/) modules get.

The dbt module's productionSuitable: false flag reflected maturity gaps
(no README, no demo profile wiring, no bespoke tests) rather than any
inherent runtime limitation — unlike modules/secrets/vault, whose false
flag is permanent because it only runs Vault in ephemeral dev mode.
dbt-core itself, running against a real Postgres with the module's
existing hardened container settings (read-only root, dropped
capabilities, non-root user), has no such limitation.

- modules-experimental/transformation/dbt/ -> modules/transformation/dbt/:
  drops productionSuitable: false and the "(experimental)" displayName/
  description suffix.
- Adds modules/transformation/dbt/README.md (Purpose/Known limitations/
  Upstream docs/Configuration notes), matching the structure of other
  production module READMEs (e.g. modules/warehouse/postgres/README.md).
- Adds profiles/local-dagster-postgres-superset-dbt/: the reference
  local-dagster-postgres-superset stack plus dbt wired to postgres via
  the sql-database contract, following the same "extend via a new
  profile" pattern used for local-dagster-postgres-superset-vault
  (rather than editing the base profile's composition).
- Adds tests/test_dbt_hardening.py (9 tests) covering image hardening
  (digest pin, non-root user, pip uninstalled in both build stages),
  compose service hardening (read_only, cap_drop, tmpfs paths,
  one-shot restart policy, docs sidecar's service_completed_successfully
  dependency), the required targetDatabase.contractRef schema field, and
  the entrypoint's required connection env vars — closing the "zero
  bespoke test files" gap the module had as an experimental module.
- Updates docs/architecture.md (Transformation layer row: dbt instead of
  "(planned)") and docs/roadmap.md (dbt module + new profile moved from
  the module list into Stable Components).
- images/dbt/README.md: updated module path references and dropped the
  "It is experimental" note (the image build/publish/scan/sign workflow
  wiring was already identical to the production dagster/superset
  images).

Testing:
- make check (623 tests, ruff, yamllint, markdownlint) passes.
- cds validate / cds test (plan+render stages) / cds render pass for the
  new local-dagster-postgres-superset-dbt profile; manually confirmed
  the rendered dbt-run service correctly resolves DBT_HOST/DBT_PORT/
  DBT_DBNAME/DBT_USER/DBT_PASSWORD from the postgres.sql-database
  contract binding.
- tests.test_module_isolation passes for the relocated module (no
  cross-module service-name hardcoding).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@RonaldHensbergen RonaldHensbergen added area:configuration Configuration, wiring, and settings behavior enhancement New feature or request labels Sep 2, 2026
RonaldHensbergen and others added 2 commits September 2, 2026 22:29
Reported when running the local-dagster-postgres-superset-dbt profile:
  dbt-run-1  | cp: cannot stat '/app/images/dbt/profiles.yml': Permission denied

Root cause: images/dbt/Dockerfile's
  COPY --chmod=0444 images/dbt/profiles.yml /app/images/dbt/profiles.yml
was the first COPY to reference the /app/images/dbt directory, so
BuildKit created that directory tree itself to satisfy the destination
path - and applied the requested --chmod (0444, no execute bit) to the
newly created directories too, not just the file. That leaves the
non-root dbt user unable to traverse into /app/images/dbt at all,
so `cp` fails at stat() before it even gets to read permissions on the
file itself.

Reproduced in isolation with a minimal Dockerfile (a plain COPY --chmod
into a not-yet-existing nested directory) and confirmed the identical
"Permission denied" traversal failure; images/dagster/base/Dockerfile
avoids this by design - it has an earlier plain COPY (without --chmod)
that creates /app/images/dagster/ first, so its own
`COPY --chmod=0444 .../workspace.yaml` only touches the file.

Fix: copy profiles.yml to a flat path directly under /app
(/app/profiles.yml.template) instead of a new nested directory, so no
directory creation is implicated by --chmod. /app already exists (via
WORKDIR /app) with normal permissions, so only the file's mode is
affected. Updated entrypoint.sh's `cp` source path to match.

Testing:
- make check (623 tests, ruff, yamllint, markdownlint) passes.
- docker build -f images/dbt/Dockerfile . succeeds locally; manually
  verified as the non-root uid 999 user that /app/profiles.yml.template
  is now readable and copyable (previously reproduced the exact
  "Permission denied" failure with the old nested path in an isolated
  minimal Dockerfile).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dbt-target named volume is shared between dbt-run (writes dbt
artifacts) and the dbt-docs nginx sidecar (serves them read-only).
Docker populates a first-referenced named volume from whichever
image has content at the mount path. Mounting the shared volume at
nginx's stock docroot (/usr/share/nginx/html) risked seeding the
volume with nginx's own root-owned default index.html/50x.html,
permanently blocking dbt-run's non-root (uid 999) writes if the
nginx container was ever created/started before dbt-run's.

Move the mount target to /usr/share/nginx/dbt-docs, a path with no
default content in the nginx:alpine image, so the volume always
populates empty regardless of container creation order. Updated
nginx.conf's root directive to match, and added a regression test
asserting the shared mount never targets nginx's stock docroot.

Verified with a real docker build + docker create/start reproduction:
the new mount path populates empty (no leftover nginx defaults) even
when the nginx container is created first, and a uid 999 write to the
shared volume is correctly visible from the nginx container.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:configuration Configuration, wiring, and settings behavior enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant