Why three installs
Install Datris three times, once per environment, each in its ownDATRIS_DIR on its own host:
- Each install has its own MongoDB, MinIO, Vault, Postgres, users, agent policy and audit log. The installs share nothing, so the isolation needs no configuration.
- Compose publishes fixed ports (8080, 4200, 3000, 9000 and others), so running two stacks on one host is not a supported layout.
- The internal environment prefix (as in
oss-dataandoss/postgres) isossby default. Leave it at the default on every install so that secret paths and bucket names in a pipeline carry over from one install to another. - Give qa the same instance size as prod so that QA runs are representative. Dev can be smaller and can use the bundled Postgres and MongoDB as destinations.
Why not three catalogs on one install? Catalogs such as
dev_sales, qa_sales and prod_sales on one server do not work as environments:- Tap and pipeline names are unique across an install, so the same pipeline cannot exist in two catalogs. Promoting would mean renaming, and renaming breaks every reference to the item.
- Secrets belong to the install, not to a catalog. Dev taps could read production credentials.
- One install has one agent policy, one audit log, one user list and one upgrade. An experiment in dev can take production down.
MULTI_TENANT=true) isolates the stores per tenant, but it is meant for separate customers. Its tenants share one server process and one release, and the installer does not set it up.Make definitions portable
A catalog archive restores cleanly on the next environment only if the definitions in dev were written for it. Follow these rules in dev. Keep everything that differs per environment in a secret. Pipelines and taps refer to secrets by name only. Create the same names on qa and prod and give them each environment’s values in Configuration → Secrets. An archive never carries secret values.
The archive lists the secret names the catalog needs. The restore result and
datris catalog restore name any of them that do not exist on the target yet.
Avoid the four fields that do not carry over. These fields are specific to an environment and cannot come from a secret. If a pipeline sets one of them, the archive and the restore result carry a warning that the value may not be valid on another install:
destination.objectStore.endpointdestination.objectStore.destinationBucketOverridedestination.database.warehouse(Snowflake warehouse name or Databricks SQL warehouse id)dataQuality.validationSchemawhen it is an absolutes3://path
sales has the same name on all three installs and is promoted as a whole. Two teams that release on different schedules need two catalogs. Uncataloged cannot be exported, so create anything meant for production in a catalog. Keys scoped with pipeline:create:catalog=<name> enforce this for agents. See Data Catalog.
GitHub layout
Use one repository with one branch per environment:taps/), and to that environment’s branch: dev, qa or main. Give each install a fine-grained token scoped to that one repository with Contents: Read and write. Setup is described in Tap and pipeline scripts in GitHub.
Scripts move from one environment to the next through a pull request:
- A developer saves a tap in dev. Datris commits
taps/<tap>.pyondevand pins the tap to that commit. - Open a pull request from
devtoqa. Reviewers see the script diff in GitHub. Merge it. - Export the catalog from dev and restore it on qa (see Promote a catalog).
- Test, enable and run the taps on qa.
- Repeat with a pull request from
qatomainand a restore on prod.
Merge the pull request before you restore. Then the archive matches the branch and the restore leaves the repository unchanged. This comparison covers tap scripts only: AI data-quality and transformation scripts are regenerated on the target, as described under Limits today.
Branch protection on
main. Saving a tap or an AI script on a GitHub-backed install commits directly to its branch. You can set this up in one of two ways:
- Prod is GitHub-backed. The prod token must be allowed to push to
main, so branch protection onmaincannot block that token. Prod reads its scripts from the branch, so the restore comparison above applies to prod as well. - Prod uses built-in storage (the default, MinIO).
maincan be fully protected and only dev and qa use GitHub storage. Prod keeps its scripts in its own object store, and a restore on prod does not compare againstmain.
catalogs/ gives them a coarse history.
Configure each environment
Set these in each install’s.env (see Configuration Reference):
Configure qa the same way as prod. Permission and policy problems then show up in QA, before the prod restore.
DATRIS_ENV changes behaviour only when it is set to production: loads to external Postgres then require TLS.
The CodeGen model must be the same on all three installs. A restore regenerates AI data-quality and transformation scripts on the target. The script prod runs can only match the one QA tested if both were generated by the same provider and model. Set ai.codegen.* to the same values on every install.
Use these roles and keys (see API Keys, Agent Policy and Single sign-on (OIDC)):
Create keys from templates and rotate them on a schedule. Issue one key per agent per environment so that the audit log attributes every action to the right agent. On prod, start the agent policy from Use recommended and set every mutating action a prod agent can reach to Approve.
Promote a catalog
These steps promote thesales catalog from dev to qa. Repeat them from qa to prod.
-
Merge the pull request from
devtoqa(see GitHub layout). -
Export from the source environment:
-
Restore on the target environment. Use
--overwriteevery time; it also works the first time, when the catalog does not exist on the target yet. Use--no-state, because a tap’s incremental cursor records where dev got to, not where qa should start:A restore never merges. Overwrite replaces the catalog’s taps and pipelines that are in the archive, and never touches another catalog. Items in the target catalog that are not in the archive are kept. If the restore names missing secrets, create them on the target with that environment’s values before you test the taps. -
Restored taps start disabled, so a restore never starts a schedule on its own. Test each tap (
datris tap test <name>), then enable it (datris tap update <name> --enabled). When restored taps read each other’s tables, use Enable in order on the catalog card, or have an agent callenable_catalog_ordered. Either one tests, enables and runs the taps in dependency order. - Run the pipelines against the target’s destinations.
- The runner must be able to reach each environment’s MCP server. Use a self-hosted runner, or put the MCP port behind the TLS reverse proxy with
REQUIRE_API_KEY=true. datris catalog restoreexits 1 on a refusal or when an item could not be restored, so the job fails visibly.- The job cannot finish the promotion: the restored taps are disabled, and a person tests and enables them (step 4).
- Keep every archive, either as a build artifact (as above) or committed under
catalogs/, so that every prod state can be reproduced.
pipeline:create. If that action is on Approve in prod, the CI job’s restore waits as a pending approval that nobody polls. Per-resource overrides can only tighten the policy, so you cannot exempt the CI key. Choose one of these:
- A person restores on prod with their own key. CI promotes to qa only. The prod policy stays strict.
- The GitHub environment is the gate. Put the prod job behind a GitHub environment with required reviewers, and leave
pipeline:createon Auto in the prod policy. Agents on prod can then also create pipelines without approval, so keep the prod agent keys read-only.
Upgrade order
Run the same release on all three installs. Upgrade dev and let it run for a while, then upgrade qa and run one promotion end to end, then upgrade prod. Rundatris doctor --pre-upgrade on each install before you upgrade it (see Doctor and the upgrade procedure).
Never restore an archive from a newer Datris into an older one. An install refuses an archive whose format is newer than it supports. The archive records the source release but the restore does not check it, so keep the target on a release at least as new as the source.
Point agents and the CLI at an environment
Give each environment its own MCP client entry and its own key. Withmcp-remote (see Configuring Claude):
MCP_SERVER_URL and DATRIS_API_KEY alone (see CLI and MCP Server). Set both per shell, for example with direnv or one shell function per environment:
Limits today
- AI data-quality and transformation scripts regenerate on restore with the target’s CodeGen model, and the archived
codegen/*.pyfiles are reference copies only, so pin the same CodeGen model on all environments. - There is no dry run: restore is all-or-nothing, with no preview of what
--overwritewould change. - Restored taps start disabled and there is no catalog-level CLI command to test and enable them, so a CI promotion ends with a person (UI Enable in order) or an agent calling
enable_catalog_ordered. - The agent policy cannot exempt one key: a CI key under Approve queues
restore_catalog, so prod CI uses GitHub environment reviewers as its gate.
