Appearance
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: trueProject level
| key | what it does | when absent |
|---|---|---|
version | The config format. 4 is current | required |
client.key | Prefixes container names and the default schema | required |
client.name | Human-readable, used in generated documents | required |
domainSuffix | The suffix this project's domains sit under | local.dolphiq.eu |
layout | single or multi — where the code sits | multi |
runtime | Pin a backend for this project | the machine's choice |
services | Which services to run | none |
database | Type and version | derived from the stack |
install | Which install steps run | everything but flux |
scaffold | The recipe this project was created from | none |
repositories | Code the project needs that belongs to no site | none |
hooks.postInstall | Commands run after every install, project-wide | none |
compose | Compose keys dqx does not generate, merged last | none |
requires | The oldest dqx that reads this correctly — written by dqx | derived |
php | PHP settings the project needs, applied to every site's container | none |
sync | A backend's file sync: mode: auto|off, and directories to exclude | auto |
extras | Keys from an older config dqx does not own, carried through untouched | none |
Per site
| key | what it does | when absent |
|---|---|---|
domains.primary | The domain this site serves | required |
domains.aliases | More domains into the same webroot | none |
domains.wildcardSubdomains | Serve *.primary too | off |
php | PHP version for this site | the image's |
node | Node version | whatever the image has |
image | The container image — docker only | derived from php |
publicDir | The docroot inside the checkout | public |
directory | The checkout's directory name | the primary domain |
database | This site's schema | client.key |
services | Which of the project's services this site uses | all of them |
daemons | Long-running processes: a queue worker, a scheduler | none |
repositories | What is checked out here, and from where | required |
hooks.postInstall | Commands run after every install, for this site | none |
hooks.preCommit | Commands run by the git hook dqx installs | none |
secured | Whether this site is served over https | true |
storage | Directories a snapshot must carry beside the database | none |
packages | Packages this site is known to use, for checks that need to know | none |
phpExtensions | Extensions the image must have | none |
proxy · proxyCommand | A dev server to put in front, and how to start it | none |
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/otherA 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/distdqx 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/DockerfileDocker-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_testEach 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
.envmentions →database.schemas. A site's own database and the_testtwin dqx creates from it are not repeated there. - the application user →
database.user, fromCREATE USERor, 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 = 256MWritten 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.
| key | type | default |
|---|---|---|
version | 4 | — |
domainSuffix | string | "local.dolphiq.eu" |
runtime | string | null |
layout | single · multi | "multi" |
compose | map | {} |
requires | string | null |
client | object | — |
client.key | string | — |
client.name | string | — |
database | object | null |
database.type | string | "mysql" |
database.version | string | "8.0" |
database.user | string | "" |
database.schemas | list of string | [] |
database.settings | list of string | [] |
repositories | list of object | [] |
repositories[] | object | — |
repositories[].repository | string | — |
repositories[].branch | string | "development" |
repositories[].path | string | "" |
scaffold | string | null |
notes | list of string | [] |
hooks | object | {} |
hooks.preCommit | list of string | [] |
hooks.postInstall | list of string | [] |
sites | list of object | — |
sites[] | object | — |
sites[].domains | object | — |
sites[].domains.primary | string | — |
sites[].domains.wildcardSubdomains | off · preferred · required | "off" |
sites[].domains.aliases | list of string | [] |
sites[].proxy | number | 0 |
sites[].proxyCommand | string | "npm run dev" |
sites[].directory | string | "" |
sites[].secured | boolean | false |
sites[].php | string | null |
sites[].node | string | null |
sites[].publicDir | string | "public" |
sites[].image | string | null |
sites[].database | string | null |
sites[].storage | list of string | [] |
sites[].phpExtensions | list of string | [] |
sites[].packages | list of string | [] |
sites[].daemons | list of object | [] |
sites[].daemons[] | object | — |
sites[].services | list of string | null |
sites[].repositories | list of object | — |
sites[].repositories[] | object | — |
sites[].hooks | object | {} |
sites[].hooks.preCommit | list of string | [] |
sites[].hooks.postInstall | list of string | [] |
sync | object | {"mode":"auto","exclude":[]} |
sync.mode | auto · off | "auto" |
sync.exclude | list of string | [] |
php | list of string | [] |
services | list of object | [] |
services[] | object | — |
services[].name | string | — |
services[].image | string | — |
services[].environment | map | {} |
install | object | — |
install.gitHooks | boolean | false |
install.env | boolean | false |
install.composer | boolean | false |
install.npm | boolean | false |
install.ssl | boolean | false |
install.flux | boolean | false |
extras | map | {} |
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 buildA 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 · up | the environment exists and is serving |
| schemas | databases and grants |
create | scaffold only, before there is a .env |
| env | .env written from the config |
setup | scaffold only, database reachable |
| certificate | if install.ssl |
| env · composer · npm · git hooks | per site, in that order |
hooks.postInstall | per site, then the project's own |
notes | printed, 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.