devkit Tiếng Việt

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

  1. Install the tools: brew install php@8.3 composer pnpm mkcert git (use the PHP version the project needs).
  2. Install devkit (download), move it to Applications, run xattr -dr com.apple.quarantine /Applications/devkit.app once, and open it.
  3. 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
  4. Park the folder where your sites will live. App: Sites → Park directory…. CLI: mkdir -p ~/Project/sites && devkit park ~/Project/sites.
  5. 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
  6. Check it: devkit doctor core-development, or the Doctor column in Sites.
  7. 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.com asks for a passphrase or fails in a terminal, it fails in devkit too. Fix your key, then devkit site setup core-development runs the remaining steps.
  • composer fails on a missing extension: the step is named in the error. Install the extension for that PHP, then site setup again.
  • "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 composer or pnpm while the terminal can: quit and reopen the app so it reads your shell's PATH again.

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

  1. Leave your feature exactly as it is. Create a lane on a fix branch from main. App: … → New lane…, name checkout-fix, Create feat/checkout-fix from main, Fork with data, start on. CLI:
    devkit git fetch core-development
    devkit lane new core-development checkout-fix --base origin/main --start
  2. 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.
  3. Run the tests in the lane: devkit exec core-development--checkout-fix -- php artisan test --filter Checkout.
  4. 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.
  5. 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 migrate in the lane, or roll that migration back in the lane.
  • --base is any git ref; fetch first (as above) so origin/main is current. Use --branch for another branch name, such as hotfix/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 rm refuses 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

  1. 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
  2. Make a lane for each, so each has its own folder, branch, database and port:
    devkit lane new core-development agent-export --start
    devkit lane new core-development agent-search --start
    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."
  3. Start each agent in its lane's folder (.worktrees/agent-export, .worktrees/agent-search) and tell it to run tests with devkit's exec on its lane.
  4. Watch what they do: MCP → Audit log, or devkit mcp audit --limit 30.
  5. Review each branch, merge, then remove the lanes. When an agent calls lane_rm it first gets CONFIRM_REQUIRED with 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 php from their own shell may pick another PHP version and the wrong .env. Ask them to use exec.
  • 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

  1. Stop what writes to the database so the snapshot is consistent: devkit proc stop shop (queue, scheduler).
  2. Take a snapshot. App: … → Snapshot database, name before_orders_rewrite. CLI: devkit db snapshot shop before_orders_rewrite.
  3. Run the migration: devkit exec shop -- php artisan migrate.
  4. Check: devkit db query shop "SELECT status, COUNT(*) FROM orders GROUP BY status", and click through the site.
  5. 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.
  6. 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 migrations table, 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

  1. 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).
  2. 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 *)
  3. Fix what it reports (install the extension for 8.4, or update the package), and check again until it says ok.
  4. 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
  5. Run the tests: devkit exec shop -- php artisan test, and use the site.
  6. 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: pecl or Homebrew extensions installed for 8.2 are not there for 8.4. devkit php info 8.4 lists what is loaded.
  • A lane cannot try another PHP version on its own, since it always runs its site's. php check is 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

  1. Make sure Xdebug is installed for the site's PHP: devkit xdebug status shop. If not, it prints the install command (for example pecl install xdebug).
  2. Turn it on. App: … → Xdebug… → debug → Apply and restart. CLI: devkit xdebug on shop.
  3. 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).
  4. Set a breakpoint in the cart code, turn on the trigger in your browser (Xdebug Helper, or add ?XDEBUG_TRIGGER=1 to the URL) and load the page.
  5. Debug a CLI command the same way: XDEBUG_TRIGGER=1 devkit exec shop --xdebug -- php artisan cart:recalculate 42.
  6. 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 point xdebug.client_host at your machine (host.docker.internal; on Linux add extra_hosts: ["host.docker.internal:host-gateway"]).
  • A docker compose up -d of 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

  1. 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.
  2. 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.
  3. 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
  4. Register https://shop.localhost/auth/google/callback as the redirect URL in Google's console. Because APP_URL comes from {{site.url}}, Laravel already builds the right URL.
  5. 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: the caddy:2 image only listens for its admin API inside the container unless CADDY_ADMIN: 0.0.0.0:2019 is set and the port is published on 127.0.0.1.
  • Docker Desktop without privileged port mapping cannot publish 443: publish 8443:443, and URLs become https://shop.localhost:8443.
  • After restarting the Caddy container the site shows stopped: run site up again to put its route back.
  • If a provider refuses .localhost host names, use devkit settings set --domain-suffix test and devkit 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

  1. See where everything stands: devkit modules core-development --fetch, or the Modules tab after Fetch.
  2. 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.
  3. Create it in every module. App: Modules tab, type feat/cms-blocks in 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
  4. 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.
  5. 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.
  6. Later, to follow a teammate's work: Pull all / devkit modules pull core-development, then devkit 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 checkout never 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

  1. Stop the site so nothing writes during the import: devkit site down shop.
  2. Find the database: devkit stack list shows 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:
    cd ~/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'
    Use your database service's name instead of mysql; for MariaDB the client is mariadb and the variable MARIADB_ROOT_PASSWORD. A client on your machine works too: mysql -h 127.0.0.1 -P <port> -uroot -p shop.
  3. Start the site and run pending migrations: devkit site up shop && devkit exec shop -- php artisan migrate.
  4. Keep the clean copy: devkit db snapshot shop prod_clean (Database tab → Snapshot database…).
  5. Experiment freely. To start over: devkit db restore shop prod_clean --yes.
  6. 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 .env from the template, so production's APP_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_HOST and MAIL_PORT at 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_clean also restores its migrations table; run migrate again 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

  1. See what devkit detected: devkit proc list core-development (queue, schedule, vite, vite:admin, vite:core…).
  2. Start what you need today. App: Processes tab → start schedule and vite:admin. CLI:
    devkit proc start core-development schedule vite:admin
  3. 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
  4. Make the scheduler start with the site too: the Autostart switch, or devkit proc set core-development schedule --autostart=true.
  5. After changing a job class: devkit proc restart core-development queue (workers keep old code in memory).
  6. 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 --strictPort on the port devkit gave it; your vite.config should not hard-code another port.
  • Commands run without a shell: php artisan a && php artisan b is refused. Use sh -c 'php artisan a && php artisan b'.
  • After the first proc add, set or rm the list is stored, so a new vite.config.x.js is not picked up until devkit 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

  1. List what exists: devkit lane list, devkit db snapshots <site> for each site, devkit site list --all.
  2. Remove finished lanes. Check first what would be lost, then remove. App: Remove lane… shows it. CLI:
    devkit lane rm core-development--old-spike --dry-run
    devkit lane rm core-development--old-spike
    This drops the lane's database and its snapshots too.
  3. 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.
  4. 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
  5. 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 delete without --drop-db keeps the databases on the server, and dropping them is what frees the most Docker disk.
  • --trash must 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 up starts the stack services a site needs.
Port in use
status reports PORT_IN_USE: port 8100 of shop is held by another process, hint lsof -nP -iTCP:8100 -sTCP:LISTEN. Often it is a php artisan serve you 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, hint brew install php@8.1 (or your distribution's package). Install it, or move the site to an installed version with devkit site set shop --php 8.3.
Missing Xdebug
site.xdebug fails with XDEBUG_MISSING for 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.composer lists what composer check-platform-reqs reports 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 PATH at 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

  1. Install the basics: Docker Engine (and their user in the docker group), git, composer, and the app's needs: sudo apt install python3-gi gir1.2-gtk-4.0 librsvg2-common.
  2. PHP: Ubuntu ships one version (sudo apt install php8.3-cli plus extensions such as php8.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*.
  3. Install devkit:
    tar -xzf devkit_v1.0.0_linux_amd64_app.tar.gz
    ./devkit_v1.0.0_linux_amd64_app/install.sh
    devkit-gtk &
  4. Then everything is the same as on macOS: add the stack, park the sites folder, clone or start sites, lanes, MCP.
  5. 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-tray for a panel window.
  • DOCKER_UNAVAILABLE although Docker runs: the user cannot reach the socket. Add them to the docker group and log in again.
  • Lanes copy vendor/ and node_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.internal on native Docker unless the service has extra_hosts: ["host.docker.internal:host-gateway"].
  • Open in editor runs code; set another command in Settings → This app → Editor or export DEVKIT_EDITOR. Trust and hosts sync use $TERMINAL when 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

  1. 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 call lane_new with schema_only: true.
  2. Tell the agent: "Work only in .worktrees/agent-tests. Run tests with devkit's exec on core-development--agent-tests, shortcut phpunit. Read failures with logs. Never call tools on core-development itself."
  3. 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}}
  4. For one hard failure it may run a single test with xdebug: true; that does not turn Xdebug on for anything else.
  5. 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.xml or .env.testing. If that names a fixed database, two lanes running tests share it; prefer sqlite in 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.
  • exec output is capped at 64 KiB: ask for --stop-on-failure or 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.