dbt (data build tool, https://docs.getdbt.com) is how modern data teams turn a swamp of raw tables into clean, documented, *tested* models — all in SQL, version-controlled, with lineage. The one thing dbt doesn't do is run itself on a schedule. dbt build has to be invoked by *something*, every hour or every night, reliably, with logs you can check when the numbers look wrong at 9am. That "something" is a perfect fit for a scheduled container. I run PandaStack; here's how to run a dbt project as a cronjob against managed Postgres.
Why a cronjob, not a server
A dbt run is a batch job: it starts, transforms data, and exits. Running it inside an always-on server wastes money and adds a failure mode (the server dies, your data goes stale silently). A cronjob — a container that runs on a schedule and stops — is exactly the right shape. PandaStack runs the container, captures the logs, and you get a run history you can audit.
Step 1: Your dbt project
Assume a normal dbt project with dbt_project.yml, a models/ directory, and a profiles.yml. The key is to make the connection environment-driven so no credentials live in the repo. A profiles.yml that reads from env:
my_project:
target: prod
outputs:
prod:
type: postgres
host: "{{ env_var('DBT_HOST') }}"
port: "{{ env_var('DBT_PORT') | int }}"
user: "{{ env_var('DBT_USER') }}"
password: "{{ env_var('DBT_PASSWORD') }}"
dbname: "{{ env_var('DBT_DBNAME') }}"
schema: analytics
threads: 4
sslmode: requireA tiny example model, models/daily_revenue.sql:
select
date_trunc('day', created_at) as day,
sum(amount_cents) / 100.0 as revenue
from {{ source('app', 'orders') }}
where status = 'paid'
group by 1And a test in models/schema.yml so bad data fails the run loudly:
models:
- name: daily_revenue
columns:
- name: day
tests: [not_null, unique]
- name: revenue
tests: [not_null]Step 2: Dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir dbt-core dbt-postgres
COPY . .
ENV DBT_PROFILES_DIR=/app
# `dbt build` runs models AND tests; if a test fails, the container exits non-zero.
CMD ["dbt", "build", "--fail-fast"]dbt build running your models and tests together, with --fail-fast, means a failed data test makes the container exit non-zero — which shows up as a failed cronjob run you can alert on. That's the whole point: silent bad data becomes a loud failed job.
Step 3: Managed Postgres
If your warehouse is PandaStack-managed Postgres:
- 1Databases → New Database → PostgreSQL.
- 2Copy the connection details (host, port, user, password, dbname).
If your warehouse is elsewhere (BigQuery, Snowflake, an external Postgres), swap the dbt adapter (dbt-bigquery, dbt-snowflake) and point the env vars at it — the cronjob mechanics are identical.
Step 4: Create the cronjob
- 1Push the dbt project to Git.
- 2https://dashboard.pandastack.io → Cronjobs → New Cronjob → connect the repo. PandaStack builds the Dockerfile.
- 3Set the schedule with a cron expression:
- 0 * * * * — hourly
- 0 6 * * * — 6am daily
- */15 * * * * — every 15 minutes
- 1Add environment variables:
DBT_HOST,DBT_PORT,DBT_USER,DBT_PASSWORD,DBT_DBNAME. These are encrypted and never in your repo. - 2Save. Each run appears in the run history with full logs.
CLI users can define the same via panda:
npm install -g @pandastack/cli
panda login
panda deployStep 5: Alert when a run fails
This is the step people skip and regret. In the cronjob's settings, add an alert on failure with a notification channel — Email, Slack, or Webhook. Now a failed dbt build (a broken model, a failed test, a warehouse hiccup) pings your team instead of quietly leaving yesterday's numbers on the dashboard. Wire it to Slack and the whole data team sees it at once.
Step 6: Selective runs (optional)
Full dbt build every 15 minutes is often overkill. Split schedules:
- A frequent job running only fast, incremental models:
dbt build --select tag:hourly - A nightly job running the heavy full-refresh models:
dbt build --select tag:nightly --full-refresh
Two cronjobs, two schedules, two Dockerfile CMDs — or one image with the selector passed as an env-driven argument.
Honest tradeoffs
- This is batch dbt, not streaming. If you need sub-minute freshness, dbt (and cron) is the wrong tool — look at streaming transforms. For the 95% of analytics that's hourly/daily, this is ideal.
- A full orchestrator (Airflow, Dagster, Prefect) gives you DAG dependencies across many jobs. If your dbt run is one step in a larger pipeline with upstream extract/load, an orchestrator earns its keep. For "run dbt on a schedule," a cronjob is simpler and cheaper.
- Free-tier databases have modest storage — fine for dev/staging analytics, size up for a real warehouse.
Wrap-up
Containerize your dbt project, drive the connection from env vars, run dbt build --fail-fast as a PandaStack cronjob against managed Postgres, and alert on failure. Clean, tested data on a schedule, with a run history you can trust.
Docs: https://docs.pandastack.io. Start free: https://dashboard.pandastack.io.