Back to Blog
Tutorial9 min read2026-07-17

Running dbt Transformations on a Schedule with PandaStack Cronjobs (2026)

dbt turns raw warehouse tables into clean, tested models — but something has to run `dbt build` on a schedule. Here's how to containerize a dbt project and run it as a PandaStack cronjob against Postgres.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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: require

A 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 1

And 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:

  1. 1Databases → New Database → PostgreSQL.
  2. 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

  1. 1Push the dbt project to Git.
  2. 2https://dashboard.pandastack.io → Cronjobs → New Cronjob → connect the repo. PandaStack builds the Dockerfile.
  3. 3Set the schedule with a cron expression:

- 0 * * * * — hourly

- 0 6 * * * — 6am daily

- */15 * * * * — every 15 minutes

  1. 1Add environment variables: DBT_HOST, DBT_PORT, DBT_USER, DBT_PASSWORD, DBT_DBNAME. These are encrypted and never in your repo.
  2. 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 deploy

Step 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.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also