Skip to content

Running a copy beside the original ​

Reviewing a colleague's pull request means leaving the branch you were on. Trying an upgrade against real data means the working version is gone until you are done. dqx clone makes a second copy of a project that runs at the same time as the first, on its own URLs.

bash
cd ~/projects/northwind
dqx down
dqx clone pr-1234
dqx clone pr-1234
text
→ copying /Users/you/projects/northwind
  to /Users/you/projects/northwind-pr-1234
  .dq/config.yml: client nwd-pr-1234, domainSuffix pr-1234.local.dolphiq.eu
  www/api.northwind.local.dolphiq.eu/.env: APP_URL, SESSION_DOMAIN
→ regenerating the stack for the new names

the copy answers on:
  https://api.northwind.pr-1234.local.dolphiq.eu
  https://admin.northwind.pr-1234.local.dolphiq.eu

Both are reachable in two tabs, with their own containers, database and mail catcher.

What gets a new name ​

Only what would collide.

originalcopy
Client keynwdnwd-pr-1234
Containersnwd_web, nwd_dbnwd-pr-1234_web, nwd-pr-1234_db
Domainsapi.northwind.local.dolphiq.euapi.northwind.pr-1234.local.dolphiq.eu
Directory~/projects/northwind~/projects/northwind-pr-1234
Host portsderived from nwdderived from nwd-pr-1234, so they differ on their own

The segment goes in the suffix, not in front of the host. A copy at pr-1234.api.northwind.local.dolphiq.eu would sit underneath the original, and a project with wildcard subdomains matches everything underneath it, so the copy would be served by the original. That is the one failure mode that looks like the copy working.

Ports need no handling: every one is derived from the client key, which the copy does not share, so the database, redis, mailpit and Vite all differ without anyone arranging it.

The URLs resolve with no setup

*.local.dolphiq.eu resolves to 127.0.0.1 however deep the name goes, and /etc/resolver/dqx covers .dqx the same way. No /etc/hosts line, no sudo.

An environment for an agent

dqx clone is for a copy you work in, and it keeps the database it copied. For a copy an agent works in, dqx agent-env is the same engine with different answers: its database is its own, it is always temporary, and it does not ask you to stop the original.

Which projects can be copied ​

Anything that serves a site. A project with a v4 config is copied from it; one with an older config, or none at all, has a v4 config derived for the copy, landing in the copy and never in the original.

That makes dqx clone the safe way to see what adoption would do: copy it, look at the copy, throw it away if the answer is no.

A copy of a project that predates the router is not a faithful copy. It is routed where the original is not, so its container names differ and it publishes nothing on 80 and 443. For reviewing a branch that is the point; for reproducing a bug in the old stack's own ports, it may not show it.

The one refusal left is a project that declares no site, because a copy of it would have no URL to be told apart by.

Stop the original first ​

dqx clone copies the database directory as it is on disk, and doing that under a running MySQL can catch a half-written page:

dqx clone pr-1234
text
Northwind is running (7 container(s)), and copying its database
directory now can produce a corrupt copy. Stop it first:

  dqx down

--force is there for somebody who has read that and decided. dqx does not stop the original by itself: that is a running environment somebody may be using.

From the dashboard the dialog offers it instead. In a terminal "run dqx down first" is one command away; on a card it is a dead end with the Stop button right there. So the dialog says the project is running and will stay stopped, and its button reads Stop It and Copy.

What the copy costs ​

Nothing, at first. On APFS the copy is made with clonefile, so every file is shared until one side writes to it. Measured on a real 3.0 GB project: 51 seconds and zero bytes. The file count costs the time, not the size, and node_modules and vendor are more than half the files. They are copied rather than reinstalled, so the copy runs against exactly the dependencies the original resolved.

The two drift apart as either side writes, so a copy that has lived through a few composer update runs is no longer free.

The database is a copy, including its URLs ​

The copy gets the original's data. A Laravel database holds absolute URLs - mail templates, media paths, a settings table - and those still name the original afterwards. dqx says so and does not touch them: a blind search-and-replace across client data is irreversible, and one wrong column leaves corrupted data somebody later draws conclusions from.

Sharing the original's git database ​

bash
dqx clone pr-1234 --branch feature/X --worktree

Attaches the repositories that know the branch as git worktrees of the original's rather than copies, so both share one object database and git worktree list at home shows the copy. A copy needs --branch for this, because it checks out a branch that already exists. It writes into the original's .git, so it is a flag and not the default: the AI tooling page explains the trade.

Reviewing a pull request ​

bash
dqx clone pr-1234 --temp --branch feature/OA-27-group-document-types

Each checkout that has the branch is switched to it before dqx writes anything of its own; the ones that do not stay where they are, which is the normal shape of a multi-repo project.

Almost every checkout here has uncommitted changes, because dqx writes .mcp.json and an install block into them. git refuses to switch across those, so dqx stashes them in the copy first:

text
  www/my-app.dqx: on feature/financial
    stashed what was uncommitted first - `git stash pop` in the copy to get it back

A stash is a save, not a delete, and it happens in the copy.

Options ​

dqx clonecopies with a generated segment: x2, then x3
dqx clone <segment>names it yourself - a ticket key beats x2 in three weeks
--branch <name>switch every checkout that has it to this branch
--worktree [checkouts]attach them as worktrees of the original's, rather than copying
--tempa throwaway: --prune may remove it
--to <path>somewhere other than beside the original
--forcecopy although the original is running
--no-upwrite the copy and stop, rather than starting it
--listthe copies on this machine
--removestop this copy and move it to the trash
--pruneremove every copy made with --temp
--yesskip the confirmation on --remove and --prune

A segment has to be a valid domain label - lowercase letters, digits and hyphens - and cannot be one the project's own domains already use.

The dashboard's destination is confined; the terminal's is not

From a card, a copy may only be written beside a project dqx already knows about. A path typed in your own terminal has no such limit. The difference is where the request came from, not who you are: anything holding the dashboard token can ask it to write files.

From the dashboard ​

··· → copy this project, on the cards dqx can copy. The dialog asks which of the two you want - a copy to work in, or an environment for an agent - then the identifier (pr-1234, x2) and where it goes, plus an optional branch, the worktree box and the temporary box. The destination follows what you type until you edit it yourself. Under the identifier it shows the URL the copy will answer on, so the field is not an abstraction.

The output streams into the card the way a start does, because this takes about a minute.

The copy then gets a card of its own, with a copy of <project> badge and a temp badge when it is one. The original's card carries a line under environments that leads to it. A card rather than a row inside the original, because a copy has its own containers, database and checkouts, and none of that fits in a row.

Where a copy records that it is one

In the copy's own .dq/config.yml, and nowhere else:

yaml
clone:
  of: /Users/you/projects/northwind
  segment: pr-1234
  temporary: false
  createdAt: 2026-09-01T20:14:03.000Z

The original records nothing. A list kept there would go stale the moment a copy is moved or deleted, and the original would carry a claim nothing can check.

Removing one ​

bash
cd ~/projects/northwind-pr-1234
dqx clone --remove

dqx down first, which is also what removes the route file, then the directory goes to the trash, not to rm -rf. A copy is a working tree and can hold a review note or a branch nobody pushed.

It refuses anywhere without a clone: block, so it can never be pointed at an original:

text
this project is not a copy - its .dq/config.yml has no `clone:` block, so dqx will not
remove it. Run this inside the copy you want gone.

dqx clone --prune does the same for every copy made with --temp, after one confirmation, and never touches a permanent one however old. Age is not evidence: a pull request under review for three weeks is exactly the copy somebody would be furious to lose. dqx prune is the wider sweep, and it asks a permanent copy for proof that it is finished before taking it.

From the dashboard it is remove this copy in the copy's own ··· menu, which asks first and says what happens. The item appears only on a copy.

The trash is not the delete. Everything the copy held - its database directory, its .env files, an unpushed branch - is on disk until the trash is emptied. That is the point of using it, and worth knowing when the copy held something that should not linger.

dqx v1.78.0