Skip to content

Configuration ​

.dq/config.yml is the only file you maintain by hand. Everything else dqx writes is generated from it and gitignored.

yaml
version: 4
domainSuffix: example.test
client:
  key: my_app                     # the container prefix
  name: My App

sites:
  - domains:
      primary: my-app.test
      aliases: [www.my-app.test]
    php: '8.4'
    node: '22'
    publicDir: public
    database: my_app
    repositories:
      - repository: my-app
        branch: main

services: [db, redis, mailpit]
database:
  type: mysql
  version: '8.4'

install:
  gitHooks: true
  env: true
  composer: true
  npm: true
  ssl: true

Project level ​

keywhat it doeswhen absent
versionThe config format. 4 is currentrequired
client.keyPrefixes container names and the default schemarequired
client.nameHuman-readable, used in generated documentsrequired
domainSuffixThe suffix this project's domains sit underlocal.dolphiq.eu
layoutsingle or multi — where the code sitsmulti
runtimePin a backend for this projectthe machine's choice
servicesWhich services to runnone
databaseType and versionderived from the stack
installWhich install steps runeverything but flux
scaffoldThe recipe this project was created fromnone
repositoriesCode the project needs that belongs to no sitenone
hooks.postInstallCommands run after every install, project-widenone
composeCompose keys dqx does not generate, merged lastnone
requiresThe oldest dqx that reads this correctly — written by dqxderived
phpPHP settings the project needs, applied to every site's containernone
syncA backend's file sync: mode: auto|off, and directories to excludeauto
extrasKeys from an older config dqx does not own, carried through untouchednone

Per site ​

keywhat it doeswhen absent
domains.primaryThe domain this site servesrequired
domains.aliasesMore domains into the same webrootnone
domains.wildcardSubdomainsServe *.primary toooff
phpPHP version for this sitethe image's
nodeNode versionwhatever the image has
imageThe container image — docker onlyderived from php
publicDirThe docroot inside the checkoutpublic
directoryThe checkout's directory namethe primary domain
databaseThis site's schemaclient.key
servicesWhich of the project's services this site usesall of them
daemonsLong-running processes: a queue worker, a schedulernone
repositoriesWhat is checked out here, and from whererequired
hooks.postInstallCommands run after every install, for this sitenone
hooks.preCommitCommands run by the git hook dqx installsnone
securedWhether this site is served over httpstrue
storageDirectories a snapshot must carry beside the databasenone
packagesPackages this site is known to use, for checks that need to knownone
phpExtensionsExtensions the image must havenone
proxy · proxyCommandA dev server to put in front, and how to start itnone

Every key

This lists all of them. dqx config get prints what a project resolves to, including the defaults it never wrote down.

Repositories ​

yaml
repositories:
  - repository: my-app                              # resolved against the machine's base
    branch: main
  - repository: git@github.com:acme/other-app.git   # or a full URL, taken as written
    path: packages/other

A bare name resolves against repositoryBase from ~/.config/dqx/config.yml; a URL is used as it stands. That is what lets one project mix hosts — an application on one, a shared library on another.

Machine settings ​

~/.config/dqx/config.yml, read for every command:

yaml
repositoryBase: git@github.com:acme
docsPath: /opt/dqx/site/.vitepress/dist

dqx config machine reads and writes them.

The escape hatch ​

Anything dqx does not generate, per compose service, merged last:

yaml
compose:
  app:
    build:
      context: .
      dockerfile: docker/app/Dockerfile

Docker-only and deliberately uninterpreted. dqx adopt fills it in for a project whose stack has keys dqx would otherwise drop, so adopting an unusual project loses nothing.

Several databases, one user ​

A project often needs more than one schema: a site each, a _staging twin, a shared runner. All of it belongs here rather than in a hand-written init file.

yaml
database:
  type: mysql
  version: '8.4'
  user: om                          # one user, grants on all of them
  schemas: [app_api_staging]        # schemas no single site owns

sites:
  - domains: { primary: api.app.test }
    database: app_api               # app_api_test comes with it
  - domains: { primary: admin.app.test }
    database: app_admin_portal      # so does app_admin_portal_test

Each site's schema gets a _test twin automatically, and each gets a wildcard grant on <schema>_test_% so Laravel's parallel testing can create a database per worker without root.

It runs on every backend and on every install. One generator, four ways in: compose exec on docker, ddev mysql -uroot, podman exec on lerd, the host's mysql on herd. The SQL is idempotent — CREATE DATABASE IF NOT EXISTS, GRANT — so a schema added to the config later is created the next time somebody installs.

Why not docker/conf/mysql/init/*.sql?

That file runs once, on an empty data directory, and only on docker. A schema added to it a year later is never created on a machine that already has data — which is how one project spent an afternoon on Access denied for user. dqx still writes that file for docker, generated from these keys, but the provisioning step is what actually runs.

dqx adopt reads an existing one, wherever the project keeps it: the path comes from the stack's own /docker-entrypoint-initdb.d mount, so docker/conf/mysql/init and a directory of numbered files both work, read in the order the entrypoint would have run them. What it takes:

  • schemas nobody's .env mentions → database.schemas. A site's own database and the _test twin dqx creates from it are not repeated there.
  • the application user → database.user, from CREATE USER or, in a file that only grants, from the grant itself.
  • anything matched to a site by resemblance is reported as check this one.
  • a GRANT ALL ON *.* is reported rather than reproduced: dqx grants the declared schemas, so an account that had been quietly creating undeclared databases can no longer do so.

The password in that file is not read — the generated SQL uses the stack's own.

Server settings ​

yaml
database:
  settings:
    - innodb_buffer_pool_size = 1G
    - max_allowed_packet = 256M

Written into docker/conf/mysql/mysql_extra.cnf on top of the defaults every project here runs, and stated once — a project overriding sql_mode replaces that line rather than adding a second. dqx adopt reads an existing file into these keys.

Docker only, today

The other three backends run their own database server and do not read this yet. On DDEV, put a .cnf in .ddev/mysql/; herd and lerd use the server on your machine. The keys are backend-neutral on purpose (D39) — applying them everywhere is work that has not been done.

The password is not a key here ​

There is deliberately no password. On docker it comes from one place — the compose stub — and provisioning re-asserts it with ALTER USER on every install, because CREATE USER IF NOT EXISTS can never correct a password: that is how a project answered Access denied against a schema and a user that both existed. On the other backends the account dqx creates uses the user's own name.

A password in .dq/config.yml would be a credential in git, and a second place for a fact that already has one. Two sources of truth for one credential is precisely the failure this arrangement was built to end.

Every key, from the schema ​

Read out of the schema that validates your config, so it cannot fall behind it. The sections above say what the important ones are for; this says what exists.

keytypedefault
version4—
domainSuffixstring"local.dolphiq.eu"
runtimestringnull
layoutsingle · multi"multi"
composemap{}
requiresstringnull
clientobject—
client.keystring—
client.namestring—
databaseobjectnull
database.typestring"mysql"
database.versionstring"8.0"
database.userstring""
database.schemaslist of string[]
database.settingslist of string[]
repositorieslist of object[]
repositories[]object—
repositories[].repositorystring—
repositories[].branchstring"development"
repositories[].pathstring""
scaffoldstringnull
noteslist of string[]
hooksobject{}
hooks.preCommitlist of string[]
hooks.postInstalllist of string[]
siteslist of object—
sites[]object—
sites[].domainsobject—
sites[].domains.primarystring—
sites[].domains.wildcardSubdomainsoff · preferred · required"off"
sites[].domains.aliaseslist of string[]
sites[].proxynumber0
sites[].proxyCommandstring"npm run dev"
sites[].directorystring""
sites[].securedbooleanfalse
sites[].phpstringnull
sites[].nodestringnull
sites[].publicDirstring"public"
sites[].imagestringnull
sites[].databasestringnull
sites[].storagelist of string[]
sites[].phpExtensionslist of string[]
sites[].packageslist of string[]
sites[].daemonslist of object[]
sites[].daemons[]object—
sites[].serviceslist of stringnull
sites[].repositorieslist of object—
sites[].repositories[]object—
sites[].hooksobject{}
sites[].hooks.preCommitlist of string[]
sites[].hooks.postInstalllist of string[]
syncobject{"mode":"auto","exclude":[]}
sync.modeauto · off"auto"
sync.excludelist of string[]
phplist of string[]
serviceslist of object[]
services[]object—
services[].namestring—
services[].imagestring—
services[].environmentmap{}
installobject—
install.gitHooksbooleanfalse
install.envbooleanfalse
install.composerbooleanfalse
install.npmbooleanfalse
install.sslbooleanfalse
install.fluxbooleanfalse
extrasmap{}

Commands the project runs ​

Two kinds, and the difference is when they run rather than what they contain.

hooks.postInstall is the project's own, kept in the config and run by every install, on every machine. What a colleague needs done after composer and npm — php artisan key:generate, storage:link, migrate --seed, npm run build.

yaml
hooks:
  postInstall:
    - php artisan migrate --seed
    - npm run build

A site can have its own, in sites[].hooks.postInstall, for a multi-site project where one site needs something the others do not.

A scaffold's commands are different: they belong to the recipe, not to the project, and they run once — when dqx init creates it. See Scaffolds.

yaml
create:                          # before .env exists — the application does not yet
  - composer create-project laravel/laravel:^13 .
setup:                           # after .env, with the database reachable
  - php artisan key:generate
postInstall:                     # written into .dq/config.yml, so every install runs it
  - php artisan migrate
notes:                           # printed to the person, not run
  - 'Create a user: see https://docs.qore.works/packages/qore-cms/#creating-user'

postInstall in a recipe is not a fourth phase — it is how a recipe writes the project's hooks.postInstall, so what the author of the project ran once, every later install runs too.

What runs when ​

dqx install --dry-run prints this list for your project. The order is fixed:

clone · register · generate · upthe environment exists and is serving
schemasdatabases and grants
createscaffold only, before there is a .env
env.env written from the config
setupscaffold only, database reachable
certificateif install.ssl
env · composer · npm · git hooksper site, in that order
hooks.postInstallper site, then the project's own
notesprinted, last

There is deliberately no preInstall. Everything above runs inside the site's container, and before up there is no container — a hook there could only run on your machine, which is a different thing to trust and a decision nobody has needed to make yet. Nor is there a postSetup: the project-level hooks.postInstall already runs after every site is finished, which is what that name would mean.

Notes to the person installing ​

yaml
notes:
  - 'Ask a colleague for the API token — it is in ncr.pt under "My App staging".'
  - 'You need the VPN for the media server.'

Printed after a successful dqx install, under Still to do by hand, and after the summary rather than before it — a line above forty lines of installation output is a line nobody read.

This is for what no command can do: an account to create, a credential to fetch, a network to be on. Anything a command can do belongs in hooks.postInstall instead. A recipe's notes say this once, when the project is created; these say it to everybody who installs it afterwards.

dqx v1.1.0