Skip to content

Docker Compose ​

The default, and the one dqx generates most for.

What dqx writes ​

The whole stack: the app container, nginx, the database, and whatever services the project declares. It is generated from .dq/config.yml, so it is disposable — delete it, run dqx sync, get the same thing back.

Generated files live under .dq/ in a single-repository project, or beside the wrapper in a multi-repository one, and they are gitignored. The declaration is the only thing committed.

Ports ​

Nothing binds a fixed host port except the shared router, which owns 80 and 443. Everything else — the database, redis, mail — is published on an ephemeral port that changes on every up. That is deliberate: two projects running together never collide.

dqx info and dqx db --url print the ports that are true right now.

What it costs ​

  • File access is a bind mount, which on macOS is the slow path. A large composer install or a Vite build feels it.

    Try OrbStack before switching backend. Same engine, same commands, nothing in the project changes. Measured on one machine, both engines, the same 48,108-file project tree, warm:

    Docker DesktopOrbStack
    walking the tree1,780–1,856 ms401–430 ms4.2×
    reading 3,000 PHP files450–465 ms88–89 ms5.1×
    du over vendor1,422–2,355 ms341–349 ms4.2× and better

    This is not the old comparison against the FUSE mount Docker Desktop used to have: that machine runs Docker Desktop with VirtioFS and the Virtualization framework, the same modern base OrbStack builds on. What OrbStack adds is its own caching and tuning on top of it.

    The ceiling is the boundary itself: the same tree inside a VM, with no mount to cross, walks in tens of milliseconds. Both engines pay for going between macOS and Linux; one pays less. If that remaining gap is still your bottleneck, DDEV is the answer — at the cost of much slower starts.

    What the same machine does with the heaviest project in one fleet — four sites, code on a bind mount, PHP and MySQL and Elasticsearch and redis:

    Docker DesktopOrbStack
    dqx up31.7–31.9 s31.5–31.7 sthe same
    dqx down2.3–2.4 s3.1–3.3 sDesktop, by a second
    a warm page26–34 ms25–37 msthe same
    the first page after a restart426–458 ms228–301 ms1.8×
    composer dump-autoload -o4.8–6.6 s2.6–4.7 s1.4–1.8×

    So the syscall gap is real but it is not the whole story. up is dominated by healthcheck intervals, which belong to the compose file rather than the engine, and a warm page is served out of opcache without touching the mount at all. Where the gap shows is everything cold: the first request after a restart or a cache clear, and composer.

    There is nothing to tell dqx. Which application provides docker is read from the docker context, so dqx up starts OrbStack with orb start and Docker Desktop with docker desktop start, and dqx kill --all asks the right one for its memory back. A setting in dqx would be a second place that can be wrong while the machine says otherwise.

    dqx doctor and dqx info --json both say which one you are on.

Choosing between OrbStack and Docker Desktop ​

The switch is docker's own, not dqx's:

bash
docker context use orbstack        # to OrbStack
docker context use desktop-linux   # back to Docker Desktop
docker context show                # which one is current

What comes with you, and what does not

The two are separate virtual machines, and that splits three ways rather than one:

  • Containers and images do not move. They are rebuilt or re-pulled on the other side. Measured at the moment of one switch: 19 containers on Docker Desktop, 0 on OrbStack.
  • Named volumes do not move. Dump them with dqx snapshot before you switch or they are gone. In this fleet that is 1 project in 32.
  • Bind-mounted data directories do come with you. They live in the project, on the Mac, and neither VM owns them. The other 31 keep their databases across the switch, untouched.

For almost every project here, then, the switch is:

bash
docker context use orbstack
dqx up      # re-pulls the images, reuses the data already on disk

A floating tag is not the same image on both engines

Each engine keeps its own image cache, so redis:alpine is whatever that engine happened to pull last. This bites in practice: OrbStack's redis 8.10.1 wrote dump.rdb in RDB format 15, and Docker Desktop's older redis:alpine would not start on it —

# Can't handle RDB format version 15
# Fatal error loading the DB, check server logs. Exiting.

The data was fine. The reader was older. docker pull redis:alpine on whichever engine lags behind settles it, and pinning the tag stops it happening again — this fleet carries 44 floating references (29 × mailpit:latest, 13 × redis:alpine, 2 × redis-stack-server:latest).

  • Docker Desktop holds memory it does not hand back until it restarts. dqx memory shows what each project is using and what the VM is holding beyond that.

What it does better than the rest ​

  • Several sites in one project are ordinary here, each with its own domain, docroot and PHP version if it needs one.
  • Complete isolation. Two projects on different PHP versions with different extensions is a configuration, not a conflict.
  • It is the same everywhere. The one backend that behaves identically on a colleague's Linux machine and on CI.

dqx v1.1.0