Scenarios
Real situations, start to finish. Each one says what you have, what to do in the app and in a terminal, what you should see, and what tends to go wrong. The examples use a WebPress project called core-development, a shop called shop, and sites parked in ~/Project/sites; use your own names.
1. A new developer sets up core-development from scratch
Situation
It is your first day. You have a Mac with Docker Desktop and Homebrew, access to the team's git repositories, and the infra repository with the team's compose file. You have never run the project.
Steps
- Install the tools:
brew install php@8.3 composer pnpm mkcert git(use the PHP version the project needs). - Install devkit (download), move it to Applications, run
xattr -dr com.apple.quarantine /Applications/devkit.apponce, and open it. - Clone the infra repository and add it as a stack. App: Stacks → Add stack…. CLI:
git clone git@github.com:acme/local-infra.git ~/Project/infra/local-infra devkit stack add ~/Project/infra/local-infra devkit stack up local-infra - Park the folder where your sites will live. App: Sites → Park directory…. CLI:
mkdir -p ~/Project/sites && devkit park ~/Project/sites. - Clone the project with everything set up. App: Clone from git… next to the parked directory, URL, PHP 8.3 for composer, tick Start the site when ready. CLI:
devkit site clone git@github.com:acme/core-development.git --php 8.3 --start --progress - Check it:
devkit doctor core-development, or the Doctor column in Sites. - Run the project's own first-time commands:
devkit exec core-development -- php artisan migrate --seed.
What you see
The clone sheet ticks off submodules, composer, pnpm and the .env template one by one, then core-development is up at http://localhost:8100. The site's row shows main, PHP 8.3, the database core_development and a green OK from doctor. The queue worker runs under Processes.
Pitfalls
- Private submodules: the clone uses your SSH agent. If
ssh -T git@github.comasks for a passphrase or fails in a terminal, it fails in devkit too. Fix your key, thendevkit site setup core-developmentruns the remaining steps. - composer fails on a missing extension: the step is named in the error. Install the extension for that PHP, then
site setupagain. - "PHP 8.3 not found": Homebrew installed it but devkit had not looked yet; any command rescans, or press Rescan in PHP.
- The app cannot find
composerorpnpmwhile the terminal can: quit and reopen the app so it reads your shell'sPATHagain.
2. An urgent bug while a feature is half done
Situation
You are on feat/loyalty-points in core-development with 14 uncommitted files and a migration that only exists on your branch. Production has a checkout bug that needs a fix on main today.
Steps
- Leave your feature exactly as it is. Create a lane on a fix branch from
main. App:…→ New lane…, namecheckout-fix, Createfeat/checkout-fixfrommain, Fork with data, start on. CLI:devkit git fetch core-development devkit lane new core-development checkout-fix --base origin/main --start - Open the lane in your editor (⌘E in its
…menu, or open~/Project/sites/core-development/.worktrees/checkout-fix), reproduce the bug at the lane's URL, fix it. - Run the tests in the lane:
devkit exec core-development--checkout-fix -- php artisan test --filter Checkout. - Commit in the lane folder, then push: the lane's branch bar Push, or
devkit git push core-development--checkout-fix. Open the pull request as usual. - When it is merged: Remove lane… with Also delete the local branch, or
devkit lane rm core-development--checkout-fix --delete-branch.
What you see
Two rows: core-development on feat/loyalty-points with its orange dot, and under it checkout-fix on feat/checkout-fix with its own port (for example http://localhost:8107) and database core_development_l1. Both can run at once.
Pitfalls
- The lane's database is a copy of your site's, so it contains your feature's migration. If the fix must be tested without it, choose Schema only and run
migratein the lane, or roll that migration back in the lane. --baseis any git ref; fetch first (as above) soorigin/mainis current. Use--branchfor another branch name, such ashotfix/checkout.- Without a
{{lane.suffix}}prefix in the template, the lane's queue worker can take jobs from the site's queue. Add the prefix (see the Redis warning). lane rmrefuses while the fix is not pushed. That is the point; push first.
3. Two AI agents in parallel, each on its own lane
Situation
You want Claude Code to build a CSV export while Codex CLI refactors the search, both in core-development, without them stepping on each other or on your own work.
Steps
- Connect both agents once (see setup). App: MCP → pick the agent → Copy or Write to project…. CLI:
cd ~/Project/sites/core-development devkit mcp config claude-code --write devkit mcp config codex --write - Make a lane for each, so each has its own folder, branch, database and port:
Or let each agent do it: "Create a lane named agent-export of core-development with lane_new (start: true) and work only in its folder."devkit lane new core-development agent-export --start devkit lane new core-development agent-search --start - Start each agent in its lane's folder (
.worktrees/agent-export,.worktrees/agent-search) and tell it to run tests with devkit'sexecon its lane. - Watch what they do: MCP → Audit log, or
devkit mcp audit --limit 30. - Review each branch, merge, then remove the lanes. When an agent calls
lane_rmit first getsCONFIRM_REQUIREDwith what would go; it should show you that and ask.
What you see
The menu bar shows 2 AI agents connected. Sites lists both lanes under core-development with their branches feat/agent-export and feat/agent-search. The audit log shows each call with the agent's name, tool and arguments.
Pitfalls
- An agent started in the site's own folder edits your working tree. Start it in the lane folder.
- Agents running
phpfrom their own shell may pick another PHP version and the wrong.env. Ask them to useexec. - Running both test suites at once on a small laptop is slow; each lane is a full copy of the app.
- Set the Redis prefix in the template before you start, or the two lanes and the site share cache and queues.
4. Trying a risky migration safely
Situation
A migration rewrites the orders table of shop. You want to run it against real data, check the result, and get back to the exact previous state if it goes wrong.
Steps
- Stop what writes to the database so the snapshot is consistent:
devkit proc stop shop(queue, scheduler). - Take a snapshot. App:
…→ Snapshot database, namebefore_orders_rewrite. CLI:devkit db snapshot shop before_orders_rewrite. - Run the migration:
devkit exec shop -- php artisan migrate. - Check:
devkit db query shop "SELECT status, COUNT(*) FROM orders GROUP BY status", and click through the site. - If it is wrong: App: Database tab → Restore… on
before_orders_rewrite, keep Snapshot the current database first on. CLI:devkit db restore shop before_orders_rewrite --yes. - Fix the migration and repeat from step 3. When you are done, drop the snapshots you no longer need (Drop…, or
devkit db snapshot rm shop before_orders_rewrite --yes).
What you see
The Restore sheet lists What happens: stop the site's server and PHP processes, copy the snapshot into a scratch database and swap it in, start them again. Afterwards the Database tab has two snapshots: yours and pre_restore_20261026_153012, the broken state, in case you need to compare.
Pitfalls
- Restore also rolls back the
migrationstable, so Laravel will run the migration again next time. That is what you want here. - Views, triggers and stored procedures are not part of a snapshot. If the migration changes them, recreate them after a restore.
- Snapshots live on the same database server and take the same disk space as the database. Drop them when done.
- Even safer: do it in a lane (
devkit lane new shop orders-rewrite) and never touch the site's data at all.
5. Testing an upgrade to a new PHP version
Situation
shop runs on PHP 8.2. You want to know whether it works on 8.4, and go back in one click if it does not.
Steps
- Install 8.4 if it is missing:
brew install php@8.4(Linux: your distribution's package; the PHP section shows the right command on a not installed card). - Ask composer before switching. App: PHP → select 8.4 → Check another site →
shop, marked (if switched). CLI:devkit php check 8.4 shop shop with PHP 8.4: fails ext-imagick n/a missing (spatie/image requires *) - Fix what it reports (install the extension for 8.4, or update the package), and check again until it says ok.
- Switch. App: the site detail's PHP card → 8.4, then Restart now in the banner. CLI:
devkit site set shop --php 8.4 devkit site up shop - Run the tests:
devkit exec shop -- php artisan test, and use the site. - Not ready? Press Undo in the banner, or
devkit site set shop --php 8.2 && devkit site up shop.
What you see
The banner reads Switched to PHP 8.4, notes that serve and the PHP processes keep 8.2 until the site restarts, that lanes follow the site, and any doctor finding for the new version. After Restart now the site's line says PHP 8.4.
Pitfalls
- Switching does not restart a running site by itself; until it does, the old binary keeps serving. Watch for "it still works" that is really still 8.2.
- Lanes always run their site's version, so they switch too.
- Extensions are per PHP version:
peclor Homebrew extensions installed for 8.2 are not there for 8.4.devkit php info 8.4lists what is loaded. - A lane cannot try another PHP version on its own, since it always runs its site's.
php checkis the way to ask without switching anything.
6. Step-debugging a request with Xdebug
Situation
A discount is applied twice on shop's cart page and you want to step through the request in PhpStorm or VS Code.
Steps
- Make sure Xdebug is installed for the site's PHP:
devkit xdebug status shop. If not, it prints the install command (for examplepecl install xdebug). - Turn it on. App:
…→ Xdebug… → debug → Apply and restart. CLI:devkit xdebug on shop. - In the IDE, listen on port 9003: PhpStorm's Start Listening for PHP Debug Connections, or VS Code's Listen for Xdebug configuration (see Connecting your IDE).
- Set a breakpoint in the cart code, turn on the trigger in your browser (Xdebug Helper, or add
?XDEBUG_TRIGGER=1to the URL) and load the page. - Debug a CLI command the same way:
XDEBUG_TRIGGER=1 devkit exec shop --xdebug -- php artisan cart:recalculate 42. - When you are done, turn it off (off in the sheet, or
devkit xdebug off shop): Xdebug slows every request.
What you see
The Xdebug sheet for debug shows Start a session: On trigger, Client host: 127.0.0.1, Port: 9003 and Path mapping: not needed. The IDE stops on the breakpoint.
Pitfalls
- Nothing happens: the trigger is missing. devkit's setting debugs only requests that carry it.
- Under the caddy engine Xdebug is the PHP container's, so turning it on affects every site on that container; the sheet lists them. The compose file must pass
XDEBUG_MODE: ${XDEBUG_MODE:-off}, and the image must pointxdebug.client_hostat your machine (host.docker.internal; on Linux addextra_hosts: ["host.docker.internal:host-gateway"]). - A
docker compose up -dof the stack outside devkit recreates the container with Xdebug off. - The queue worker runs jobs in another process. To debug a job, turn the trigger on for that process with
XDEBUG_TRIGGER=1 devkit exec shop --xdebug -- php artisan queue:work --once.
7. When you need HTTPS
Situation
shop logs in with Google, whose OAuth client only accepts https:// redirect URLs; its session cookie is Secure; and its PWA registers a service worker. None of this works on http://localhost:8100.
Steps
- Install mkcert and trust its CA once. App: Settings → Certificates → Trust…, which opens a terminal that may ask for your password; then Check again. CLI:
brew install mkcert && devkit trust. - Make sure one of your stacks has a Caddy and a PHP-FPM service of the site's version that mount your sites at the same path (a ready compose file is in HTTPS with the caddy engine), and add it:
devkit stack add ~/Project/infra/web-stack. - Switch the engine. App:
…→ Change engine… → caddy; the sheet lists What changes and the Requirements. CLI:devkit site set shop --engine caddy --php 8.3 devkit site up shop # https://shop.localhost - Register
https://shop.localhost/auth/google/callbackas the redirect URL in Google's console. BecauseAPP_URLcomes from{{site.url}}, Laravel already builds the right URL. - Run
devkit doctor shop: it checks mkcert, the Caddy admin API, the mounts and the certificate.
What you see
The URL column shows https://shop.localhost with a valid certificate (no browser warning). Processes shows caddy route → php-fpm for serve. Lanes of the site get https://<lane>.shop.localhost.
Pitfalls
SITE_NOT_MOUNTED: the Caddy or PHP container does not see the site at the same absolute path. Fix the volume, for example${HOME}/Project:${HOME}/Project.CADDY_UNREACHABLE: thecaddy:2image only listens for its admin API inside the container unlessCADDY_ADMIN: 0.0.0.0:2019is set and the port is published on127.0.0.1.- Docker Desktop without privileged port mapping cannot publish 443: publish
8443:443, and URLs becomehttps://shop.localhost:8443. - After restarting the Caddy container the site shows stopped: run
site upagain to put its route back. - If a provider refuses
.localhosthost names, usedevkit settings set --domain-suffix testanddevkit hosts sync(it asks for your password).
8. Working across many modules and submodules
Situation
A CMS feature touches core-development itself and four of its modules/. Every repository needs a feat/cms-blocks branch, and everything must be pushed together.
Steps
- See where everything stands:
devkit modules core-development --fetch, or the Modules tab after Fetch. - Create the branch in the site: branch bar → New branch… →
feat/cms-blocks→ Create and check out. CLI:devkit git checkout core-development feat/cms-blocks --create. - Create it in every module. App: Modules tab, type
feat/cms-blocksin Branch, tick Create if missing, press Checkout in all modules. CLI (all, or only some):devkit modules checkout core-development feat/cms-blocks --create devkit modules checkout core-development feat/cms-blocks --create --only common,framework,media,page-builder - Work and commit in each module folder as usual (the row's Terminal button opens one there). Then commit the new submodule pointers in the site.
- Push everything: branch bar Push with Include submodules on, or
devkit git push core-development: modules that are ahead first, then the site, setting upstreams on the first push. - Later, to follow a teammate's work: Pull all /
devkit modules pull core-development, thendevkit git pull core-development.
What you see
The checkout reports one line per module: done, unchanged (already there), or skipped with MODULE_DIRTY. The Modules rows show feat/cms-blocks with a blue dot (not the branch .gitmodules tracks) and Push ↑1 until pushed.
Pitfalls
- A module with uncommitted changes is skipped, never touched. Commit or stash it, then run the checkout again; it only acts where needed.
- If one module push fails, the site is not pushed, so the site never points at a commit nobody else has. Fix that module and push again.
- Checking out another site branch moves submodules to the commits that branch records. Commit module work before switching the site's branch.
modules checkoutnever fetches: fetch first when the branch exists only on the remote.
9. Bringing in a copy of production data, then keeping a clean snapshot
Situation
You received a sanitised dump shop-prod.sql.gz and want shop to run on it, with a clean copy to go back to whenever your experiments make a mess.
Steps
- Stop the site so nothing writes during the import:
devkit site down shop. - Find the database:
devkit stack listshows the MySQL service and its published port; the root password is in the stack's compose file. devkit has no import command; use the stack's own client:
Use your database service's name instead ofcd ~/Project/infra/local-infra gunzip -c ~/Downloads/shop-prod.sql.gz | docker compose exec -T mysql sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" shop'mysql; for MariaDB the client ismariadband the variableMARIADB_ROOT_PASSWORD. A client on your machine works too:mysql -h 127.0.0.1 -P <port> -uroot -p shop. - Start the site and run pending migrations:
devkit site up shop && devkit exec shop -- php artisan migrate. - Keep the clean copy:
devkit db snapshot shop prod_clean(Database tab → Snapshot database…). - Experiment freely. To start over:
devkit db restore shop prod_clean --yes. - Give existing lanes the new data: Reset database from shop… on each lane, or
devkit db reset shop--feature-x --yes.
What you see
devkit db snapshots shop lists prod_clean with its size. New lanes made with lane new fork the imported data automatically.
Pitfalls
- The site keeps its own
.envfrom the template, so production'sAPP_URL, mail and payment keys never reach it through the dump. Settings stored in the database (URLs, API keys in a settings table) do: check and clean them. - Real customer e-mail addresses are in the data. A seeded template points
MAIL_HOSTandMAIL_PORTat the stack's Mailpit or MailHog when the stack has one, so mail lands in its Inbox; check your template does before running queued jobs. - Each snapshot and lane fork is a full copy on the same server: a 5 GB database plus two lanes and three snapshots is 30 GB of Docker disk.
- Restoring
prod_cleanalso restores itsmigrationstable; runmigrateagain afterwards if your branch has newer migrations.
10. Running queue workers, the scheduler and Vite together
Situation
core-development has a queue, scheduled tasks, Laravel Reverb for websockets, and several Vite configs (vite.config.js, vite.config.core.js, vite.config.admin.js). You want them all running while you work on the admin, and only the queue on other days.
Steps
- See what devkit detected:
devkit proc list core-development(queue, schedule, vite, vite:admin, vite:core…). - Start what you need today. App: Processes tab → start
scheduleandvite:admin. CLI:devkit proc start core-development schedule vite:admin - Add Reverb with a port of its own, starting with the site:
devkit proc add core-development reverb --command "php artisan reverb:start --port={port}" --port --autostart - Make the scheduler start with the site too: the Autostart switch, or
devkit proc set core-development schedule --autostart=true. - After changing a job class:
devkit proc restart core-development queue(workers keep old code in memory). - Something stopped?
devkit proc logs core-development vite:admin --lines 50, or the log pane under the table.
What you see
Each Vite process has its own port in the Port column (for example 8101, 8102), allocated once and kept, so a lane can run the same Vite config on another port at the same time. A crashed process shows exited with its last log lines.
Pitfalls
- Vite runs with
--strictPorton the port devkit gave it; yourvite.configshould not hard-code another port. - Commands run without a shell:
php artisan a && php artisan bis refused. Usesh -c 'php artisan a && php artisan b'. - After the first
proc add,setorrmthe list is stored, so a newvite.config.x.jsis not picked up untildevkit proc reset core-development(which also forgets your changes). - devkit does not restart crashed processes. A queue worker that dies on a fatal error stays exited until you start it.
11. Cleaning up disk space
Situation
Docker says its disk is almost full. Over months you made lanes, snapshots and test sites you no longer need.
Steps
- List what exists:
devkit lane list,devkit db snapshots <site>for each site,devkit site list --all. - Remove finished lanes. Check first what would be lost, then remove. App: Remove lane… shows it. CLI:
This drops the lane's database and its snapshots too.devkit lane rm core-development--old-spike --dry-run devkit lane rm core-development--old-spike - Drop old snapshots: Database tab → Drop…, or
devkit db snapshot rm shop pre_restore_20260901_101500 --yes. Safety snapshots (pre_restore_…) pile up after many restores. - Delete sites you are done with, with their databases, folder to the Trash. App: Delete site…, type the name, tick Drop database. CLI:
devkit site delete old-landing --trash --drop-db --yes - Empty the Trash when you are sure.
What you see
lane rm prints removed … and dropped core_development_l3, …. site delete prints where the folder went and which databases were dropped.
Pitfalls
DIRTY_WORKTREE: there is work that exists only on your machine (uncommitted, unpushed, a stash). Push it, or decide to lose it and pass--force(app: Remove anyway and discard this work).site deletewithout--drop-dbkeeps the databases on the server, and dropping them is what frees the most Docker disk.--trashmust stay on the same disk; a site on an external drive cannot go to your home Trash, and devkit says so before doing anything.- Not sure yet? Hide the site instead: nothing is deleted, but it stops and leaves the lists.
12. Troubleshooting with doctor
Situation
Monday morning, Start all leaves some sites red. Instead of guessing, ask doctor.
Steps
devkit doctor # the machine and every visible site
devkit doctor shop # one site in detail
devkit status # what runs right now, and issues like PORT_IN_USE
In the app: the Doctor badge on each row, Run doctor in the menu, or Settings → Doctor; Copy report copies it for a teammate or for support.
Common findings
- Docker is down
fail docker … cannot connect to the Docker daemon, hint install or start Docker (Docker Desktop, OrbStack or Colima). Start Docker, then Start all again;site upstarts the stack services a site needs.- Port in use
statusreportsPORT_IN_USE: port 8100 of shop is held by another process, hintlsof -nP -iTCP:8100 -sTCP:LISTEN. Often it is aphp artisan serveyou started by hand, or a site of another tool. Stop it; the site keeps its port and serves there on its next start.- A missing PHP version
fail site.php shop … PHP 8.1 not found on this machine, hintbrew install php@8.1(or your distribution's package). Install it, or move the site to an installed version withdevkit site set shop --php 8.3.- Missing Xdebug
site.xdebugfails withXDEBUG_MISSINGfor a site with Xdebug on, hint…/pecl install xdebug. Install it for that PHP, or turn Xdebug off for the site.- Template does not render
fail site.template … unresolved template variables: s3.endpoint: the template uses a service the stack lacks. Remove the line or add the service.- Platform requirements
site.composerlists whatcomposer check-platform-reqsreports for the site's PHP (a missing extension, a too-new version).
Pitfalls
- The app finds less than your terminal: it reads your login shell's
PATHat launch. Fix your shell profile, then quit and reopen the app. - Hidden sites are not checked unless you name them.
- doctor never changes anything; its hints are commands for you to run.
13. A team member on Linux
Situation
A new colleague uses Ubuntu 24.04 and wants the same setup as the Mac users.
Steps
- Install the basics: Docker Engine (and their user in the
dockergroup), git, composer, and the app's needs:sudo apt install python3-gi gir1.2-gtk-4.0 librsvg2-common. - PHP: Ubuntu ships one version (
sudo apt install php8.3-cliplus extensions such asphp8.3-mysql php8.3-redis php8.3-intl php8.3-gd); other versions come from the ondrej/php repository. devkit finds them in/usr/bin/php*. - Install devkit:
tar -xzf devkit_v1.0.0_linux_amd64_app.tar.gz ./devkit_v1.0.0_linux_amd64_app/install.sh devkit-gtk & - Then everything is the same as on macOS: add the stack, park the sites folder, clone or start sites, lanes, MCP.
- For HTTPS:
sudo apt install mkcert libnss3-tools, then Settings → Certificates → Trust…, which opens a terminal for the password.
What you see
A devkit icon in the tray (Ubuntu's GNOME has the AppIndicator extension), with the same menu as the Mac menu bar, and the same manager window, light or dark as the desktop prefers.
Pitfalls
- No tray icon on plain GNOME: install the AppIndicator extension, or run
devkit-gtk --no-trayfor a panel window. DOCKER_UNAVAILABLEalthough Docker runs: the user cannot reach the socket. Add them to thedockergroup and log in again.- Lanes copy
vendor/andnode_modules/fully on ext4 (no copy-on-write). Expect lanes to take longer and more disk than on a Mac. - Xdebug in a PHP container cannot reach
host.docker.internalon native Docker unless the service hasextra_hosts: ["host.docker.internal:host-gateway"]. - Open in editor runs
code; set another command in Settings → This app → Editor or exportDEVKIT_EDITOR. Trust and hosts sync use$TERMINALwhen it is set.
14. Letting an agent run tests without touching your main site
Situation
You want your agent to fix a failing test suite. Its tests run migrations and truncate tables, and you do not want that anywhere near the database you are using.
Steps
- Make a lane with an empty schema for it: App: New lane… → Schema only. CLI:
devkit lane new core-development agent-tests --schema-only --start. Or ask the agent to calllane_newwithschema_only: true. - Tell the agent: "Work only in
.worktrees/agent-tests. Run tests with devkit'sexeconcore-development--agent-tests, shortcutphpunit. Read failures withlogs. Never call tools oncore-developmentitself." - The agent's calls look like this:
{"tool":"exec","arguments":{"site":"core-development--agent-tests","shortcut":"phpunit","command":["--filter","InvoiceTest"]}} {"tool":"exec","arguments":{"site":"core-development--agent-tests","command":["php","artisan","test"],"timeout_seconds":1200}} - For one hard failure it may run a single test with
xdebug: true; that does not turn Xdebug on for anything else. - Check its work in the audit log and on the lane's branch, then remove the lane.
What you see
exec results come back as JSON with exitCode and the last 64 KiB of output; a failing test run is ok: true with a non-zero exitCode, which is what lets the agent read the failure. Your site's database and processes are untouched; the audit log shows only …--agent-tests as target.
Pitfalls
- Laravel tests often use their own database from
phpunit.xmlor.env.testing. If that names a fixed database, two lanes running tests share it; prefersqlitein memory, or a name with the lane in it. - Long suites: raise
timeout_seconds(default 600). A timed-out command is stopped, in the PHP container too. execoutput is capped at 64 KiB: ask for--stop-on-failureor a filter, not the whole suite's output.- An agent that also has its own shell can bypass devkit. Use your agent's permission settings to keep it inside the lane folder.