Quickstart
Five minutes from git clone to a green validation run against a local Measured Cloud. Pick the path that matches what you're doing — for the longer walk-throughs, see Self-host and Run your first validation.
Two paths
| Path | When | What you need |
|---|---|---|
| A. Docker-only | You just want to try it. | Docker. That's it. |
| B. Local dev | You'll edit Python or UI code and want hot-reload. | Docker + Python 3.12 + uv + Node 22. |
A. Docker-only
The all-in-one compose brings up Postgres + Redis + API + worker + UI behind one command, runs the public Alembic migrations, and seeds a dev org for you.
git clone https://github.com/your-org/dq-cloud.git && cd dq-cloud
make compose-up
That's it. Open http://localhost:3000 and log in as admin@dqcloud.dev / devpassword. The seed script created org 00000000-0000-0000-0000-000000000001, workspace …002, a personal API key, and an agent token.
Tear down:
make compose-down # stop, keep data
docker volume rm dqcloud-local_dq_postgres_data # wipe the DB
For production deploys (managed Postgres + Redis, registry-pulled images), see Deploy with Docker. The same three images can also be pulled from GHCR (ghcr.io/<owner>/dq-cloud-{api,worker,ui}) — the CI workflow publishes them on every push to main and every v* tag.
B. Local dev — hot-reload on Python + UI
This is the path used by contributors. The Python services run on your host so a save reloads them; only Postgres + Redis come from Docker.
1. Boot the stack
git clone https://github.com/your-org/dq-cloud.git && cd dq-cloud
cp .env.example .env
# Generate the two secrets the API refuses to start without.
python3 - <<'PY' >> .env
import base64, secrets
print(f"SECRET_KEY={secrets.token_hex(32)}")
print(f"ENCRYPTION_KEY={base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip('=')}")
PY
make dev # docker compose up: Postgres on :5432, Redis on :6379
make migrate # public-schema alembic upgrade head
If you already have a Postgres on
:5432, either stop it or setDB_URLin.envto a free port. The Docker container shares the same default port and host-side Postgres wins.
2. Seed a dev org
uv run python scripts/seed_dev.py
This creates org 00000000-0000-0000-0000-000000000001, workspace …002, user admin@dqcloud.dev / devpassword, and a personal API key + agent token. Safe to re-run.
3. Start the API, worker, and UI
Three terminals (or background them):
# Terminal 1 — API on :8000
uv run uvicorn dq_cloud_api.main:app --reload --port 8000
# Terminal 2 — ARQ worker
uv run arq dq_cloud_worker.main.WorkerSettings
# Terminal 3 — Vite dev server on :3000 (proxies /api → :8000)
cd ui && npm install && npm run dev
Run a validation end-to-end
The fastest path is the UI: open http://localhost:3000, log in with admin@dqcloud.dev / devpassword, then click through Data Assets → Add source → tick tables → Add N tables → open the asset → Rules → Add rule → Validations → New validation → Run. The step-by-step version is Run your first validation.
If you'd rather drive it from a script, here's the same path against the seeded org via curl and jq (local-dev path B).
The demo points Measured Cloud at its own local Postgres. By default the network guard refuses loopback and private hosts as data sources (see DATASOURCE_NETWORK_ALLOWLIST in the configuration reference), so allow loopback first: add DATASOURCE_NETWORK_ALLOWLIST=127.0.0.1/32,::1/128 to .env and restart the API and the worker. Leave it unset anywhere your data sources aren't on the same machine.
ORG=00000000-0000-0000-0000-000000000001
WS=00000000-0000-0000-0000-000000000002
V1=localhost:8000/api/v1/organizations/$ORG/workspaces/$WS
V2=localhost:8000/api/v2/organizations/$ORG/workspaces/$WS
JWT=$(curl -s -X POST localhost:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@dqcloud.dev","password":"devpassword"}' | jq -r .access_token)
AUTH="Authorization: Bearer $JWT"
# 1. A data source (the local Postgres itself, for the demo).
DS=$(curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" $V2/datasources \
-d '{"name":"local-pg","type":"postgres","execution_mode":"direct",
"config_encrypted":{"connection_string":"postgresql://dqcloud:dqcloud@localhost:5432/dqcloud"}}' \
| jq -r .data.id)
# 2. A data asset: one table in that source.
ASSET=$(curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" $V1/data-assets \
-d "{\"data\":{\"datasource_id\":\"$DS\",\"name\":\"public.organisations\",
\"type\":\"table\",\"config\":{\"schema\":\"public\",\"table\":\"organisations\"}}}" \
| jq -r .data.id)
# 3. Rules on the asset.
curl -s -X PUT -H "$AUTH" -H "Content-Type: application/json" $V2/data-assets/$ASSET/rules \
-d '{"data":{"rules":[
{"rule_type":"expect_table_row_count_to_be_between","kwargs":{"min_value":1}},
{"rule_type":"expect_column_values_to_not_be_null","kwargs":{"column":"id"}}]}}' > /dev/null
# 4. A validation that holds the asset's current rules.
VAL=$(curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" $V2/validations \
-d '{"data":{"name":"quickstart"}}' | jq -r .data.id)
curl -s -X PUT -H "$AUTH" -H "Content-Type: application/json" $V2/validations/$VAL/assets/$ASSET/rules \
-d '{"data":{"mode":"all"}}' > /dev/null
# 5. Run it (202 Accepted).
curl -s -X POST -H "$AUTH" $V2/validations/$VAL/run
Within a minute the worker picks the job off Redis, runs the asset's rules in a sandboxed Great Expectations subprocess against your DSN, and records the results. Refresh the UI and you'll see one passing run under Validations → quickstart and on Results.
Verify
curl -s -H "$AUTH" $V2/validations/$VAL/execution-runs | jq '.data[0] | {status, health_score}'
You should see "status": "succeeded" and "health_score": 100.0. While the run is still going, status reads running.
Next
- Add real data quality checks — Add rules to a data asset covers the rule catalog, AI-drafted rules, and custom SQL.
- Put it on a schedule — Schedule a validation.
- Get notified on failures — Set up Slack or email alerts.
- Connect a real warehouse — Connect a Snowflake or BigQuery datasource.
- Ship to production — Deploy to production.