Take dbt from your laptop to production — environments and profiles, scheduling with an orchestrator, and a CI check that builds and tests only the models a pull request changed.
dbt separates environments through targets in your profile: developers build into their own schema, production builds into the shared one, using the same code. Credentials come from environment variables, never the repo. This is what lets every analyst safely develop against real data without touching production tables.
# profiles.yml — same project, different targets (schemas/credentials)
analytics:
target: dev
outputs:
dev:
type: postgres
schema: dbt_alice # each dev gets their own schema
user: "{{ env_var('DBT_USER') }}"
password: "{{ env_var('DBT_PASSWORD') }}" # from env, never in git
prod:
type: postgres
schema: analytics # shared production schema
user: "{{ env_var('DBT_PROD_USER') }}"
password: "{{ env_var('DBT_PROD_PASSWORD') }}"
# build to prod: dbt build --target proddbt builds the models but does not schedule itself — a job runs dbt on a cadence and in coordination with ingestion. That job is dbt build (models + tests + snapshots) invoked by an orchestrator like Airflow or Dagster, or dbt Cloud. The Airflow course shows how to wire this as a task in a larger pipeline that ingests first, then transforms.
# a scheduled production job = one command
dbt build --target prod # models + tests + snapshots + seeds, in DAG order
# run it on a schedule via an orchestrator (see the Airflow course):
# ingest raw data -> dbt build -> refresh BI extracts
# if 'dbt build' fails a test, the downstream steps don't run.
# dbt artifacts (run_results.json, manifest.json) capture what ran +
# timings for observability and for state-based CI (next).The key to fast dbt CI is state comparison: dbt can diff your branch against the production manifest and build only the modified models plus everything downstream. A pull request thus tests exactly the slice it affects against real data, in minutes, before merge — the analytics equivalent of running the affected unit tests.
# .github/workflows/dbt-ci.yml (excerpt)
jobs:
dbt-ci:
steps:
- uses: actions/checkout@v4
- run: pip install dbt-postgres
- name: Build + test only changed models (and downstream)
run: |
# compare against the prod manifest to find what changed
dbt build --select state:modified+ --defer --state ./prod-manifest
# a failed model or data test fails the check -> PR can't merge.
# fast because it builds the changed slice, not the whole project.Putting it together, a mature dbt deployment looks like software: code in git, PRs with a CI build-and-test on the changed slice, scheduled production runs via an orchestrator, source freshness monitoring, and generated docs the whole company can browse. At that point your warehouse transformations are as trustworthy and maintainable as application code.
Production dbt checklist:
[ ] project in git; changes go through pull requests
[ ] CI runs 'dbt build --select state:modified+' on every PR
[ ] prod runs on a schedule via an orchestrator (ingest -> dbt build)
[ ] tests on every model's keys + business rules (build fails on red)
[ ] source freshness monitored ('dbt source freshness')
[ ] snapshots scheduled for the dimensions that need history
[ ] 'dbt docs' published so anyone can see lineage + definitions
At this point warehouse SQL is as trustworthy + maintainable as app code.