devkit Tiếng Việt

Sites

A site is one Laravel folder: it has an artisan file at its root. devkit gives it a database on your stack, an .env that points at that stack, a web server, its background processes and a set of one-click actions. This page covers all of it.

Adding sites: park and link

Park a folder that holds your projects. Every subfolder with an artisan file is a site, named after its folder, and a project you add there later shows up on its own.

devkit park ~/Project/sites
devkit site list
SITE              ENGINE   PHP  DB                BRANCH  URL                    STATE
core-development  artisan  8.3  core_development  main    http://localhost:8100  running
shop              artisan  8.4  shop              main*                          stopped

Link one folder that lives somewhere else. --name gives it another name than its folder's.

devkit link ~/work/legacy-crm --name crm

In the app, Sites → Park directory… and Link folder… open a folder picker. Parked sites are listed per directory, linked sites in their own group.

A site's name (its slug) is its folder name, and its database is that name with dots and dashes turned into underscores: core-development gets core_development, webpress.vn gets webpress_vn. Change it with devkit site set <site> --db <name>.

Unpark and unlink. devkit 1.0 has no command to forget a parked directory, a linked folder or a stack. To take a site out of the way, hide it: it stops, leaves the lists and keeps everything. A parked site whose folder is deleted outside devkit disappears from the list by itself; a linked one stays listed until devkit site delete <site> --yes, which then only forgets it.

The site list

devkit site list shows every site with its engine, PHP version, database, git branch, URL and live state. Lanes are listed under their site. In the branch column * means uncommitted changes and ↑2 / ↓3 commits ahead of / behind the upstream (as of the last fetch). --all includes hidden sites. The app shows the same table with a doctor badge per row.

Clone from git

devkit site clone makes a new site from a repository in one go: it clones into a parked directory, then runs the setup steps.

devkit site clone git@github.com:acme/shop.git --php 8.3 --start --progress
… git clone into ~/Project/sites/shop
… setup step submodules
… setup step composer
… setup step pnpm
… setup step env
… setup step up
shop at ~/Project/sites/shop
  clone      done    git@github.com:acme/shop.git (…)
  submodules done    …
  composer   done    …
  pnpm       done    …
  env        done    …
  up         done    up at http://localhost:8102
shop is up at http://localhost:8102

The steps, in order, each of which you can skip:

StepWhat it doesSkip with
submodulesgit submodule update --init --recursive, then each submodule goes on its .gitmodules branch instead of a detached commit when that branch holds the recorded commit (otherwise it stays detached, with a warning)--no-submodules
composercomposer install with the site's PHP (--php)--no-composer
pnpmpnpm install, when there is a package.json--no-pnpm
envstores a .env template seeded from .env.example (an empty APP_KEY gets a fresh key) and creates the database--no-env
upsite up at the endonly runs with --start
  • --into <dir> picks the parked directory; it can be left out when you have only one. --name sets the folder and site name (default: from the URL), --branch the branch to clone (default: the remote's default branch).
  • The URL can be https://…, git@host:org/repo.git or a local repository. devkit uses your own git credentials (SSH agent or credential helper) and never asks for or stores a password, so a private repository needs your access set up already.
  • If the clone itself fails, nothing is left behind. If a later step fails, the clone stays and devkit names the step. Fix the cause, then run the remaining steps again with devkit site setup <site>, which takes the same --no-… and --start flags. Every step is safe to repeat.
  • site setup also works for a project you copied or cloned by hand.
  • Add --progress to see each step as it happens (the app always shows them).

In the app: Clone from git… (next to each parked directory in Sites) asks for the Repository URL, the Folder name, the Branch and, when you park more than one directory, where to clone. Under After cloning every step is on except Start the site when ready, and composer's PHP version has a picker. The sheet then shows each step as it runs.

Engines: artisan or caddy

An engine is how a site is served. Each site has one; sites that set none follow the default engine in Settings (artisan unless you change it).

artisan (default)caddy
Web serverphp artisan serve on the hostyour stack's Caddy container
PHPa host binary (Homebrew, distro, phpenv…)a PHP-FPM container of that version in one of your stacks
URLhttp://localhost:<port>, one port per sitehttps://<site>.localhost with a trusted certificate
NeedsPHP on the hosta stack with a caddy and a php service, mkcert, devkit trust once
Xdebugper site or laneper PHP container, so for every site on it
Good formost daily work: fast to start, nothing to set upHTTPS-only features, production-like routing, PHP extensions you do not want on the host

Choose artisan unless something needs HTTPS or a real host name: OAuth and payment callbacks that insist on https://, Secure or SameSite=None cookies, service workers, or subdomain routing. Then use caddy. Its setup is in Stacks, PHP and HTTPS.

devkit site set shop --engine caddy --php 8.3
devkit site up shop          # https://shop.localhost

Changing the engine of a running site stops it and its lanes with the old engine first; site up starts them with the new one. In the app: the site's … menu → Change engine…, which says what will change before you switch.

PHP version per site

devkit site set shop --php 8.2     # this site runs PHP 8.2
devkit site set shop --php ""      # back to the default PHP
devkit php check 8.4 shop          # would it run on 8.4? (composer check-platform-reqs)
  • A site without its own version uses the default PHP (Settings), and an empty default means the php on your PATH.
  • A lane always runs its site's version.
  • The version must be installed. A missing one fails with PHP_VERSION_MISSING and the command that installs it (brew install php@8.2 on macOS, your distribution's package on Linux).
  • A running site keeps its current binary until its next start; site up then restarts the server and the processes whose command changed.
  • Under the caddy engine the same choice picks the PHP container of that version.

In the app the PHP column of the Sites table and the PHP card of the site detail are pickers. After a switch from the site detail a banner says Switched to PHP 8.2, with Undo and, while the site still runs the old binary, Restart now.

Ports and URLs

  • artisan sites get a port from the range in Settings, 8100–8999 by default. One allocator hands out every port of every site, lane and Vite process, and checks that the port is free.
  • A site keeps its port across restarts, even when you later change the range.
  • caddy sites are served at https://<site>.<suffix> and lanes at https://<lane>.<site>.<suffix>. The suffix is localhost unless you change it. When Caddy's HTTPS port is published on another port than 443 (for example 8443), the URL includes it.
  • When a stopped site's port is held by another program, devkit status and the app report PORT_IN_USE.

.env templates

Every site has an .env template. On every site up devkit renders it to the site's .env, so the site always points at the stack's real ports and the right database. A lane renders its site's template with its own database, port and URL.

  • Seeding. Until you store one, the template is your current .env (or .env.example) with these values replaced by variables: APP_URL, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, MAIL_HOST, MAIL_PORT (each only when the stack has the service). Every other line stays as it is.
  • Backups. Your very first .env is kept once as ~/.config/devkit/backups/<site>/env.orig, and the file before each write as env.prev. Nothing is written when the content did not change. Backups never go into the site folder.
  • Errors. An unknown variable, or one whose service the stack lacks, fails with TEMPLATE_VAR_UNRESOLVED naming it, and the existing .env is left alone.
devkit template get shop > shop.env.tpl     # the stored template, or the seeded one
$EDITOR shop.env.tpl
devkit template render shop --file shop.env.tpl   # the .env it would write now; writes nothing
devkit template set shop --file shop.env.tpl

In the app: Templates → Edit… (or Edit .env template… in the site's … menu) shows the template, the .env it renders to right now (passwords hidden until you show them) and every variable with its value. Save stores it; Save and restart site also starts the site and its running lanes again with it.

Variables

VariableValue
{{site.url}}artisan: http://localhost:<port>; caddy: https://<host>
{{site.host}}, {{site.port}}the host name and port (caddy: Caddy's HTTPS port)
{{site.slug}}, {{site.path}}the site's (or lane's) name and folder
{{lane.name}}, {{lane.index}}, {{lane.suffix}}a lane's name, its number and _l<index>; all three are empty for the site itself
{{db.name}}the site's database, or the lane's fork
{{mysql.host}}, {{mysql.port}}, {{mysql.user}}, {{mysql.password}}the stack's MySQL or MariaDB, as root (lanes need to create databases)
{{redis.host}}, {{redis.port}}, {{redis.password}}the stack's Redis
{{mail.host}}, {{mail.port}}, {{mail.ui}}the stack's Mailpit or MailHog: SMTP, and the inbox URL for your browser
{{s3.endpoint}}, {{s3.key}}, {{s3.secret}}, {{s3.bucket}}the stack's MinIO, LocalStack or Garage. The keys are MinIO's MINIO_ROOT_USER / MINIO_ROOT_PASSWORD, LocalStack's test / test, and empty for Garage (make its keys with its own CLI). The bucket is the site's database name
{{env.NAME}}the variable NAME from devkit's own environment

With the artisan engine, services are addressed as published on your machine (127.0.0.1 and the host port). With the caddy engine PHP runs in a container, so they are addressed by service name and container port (database:3306, redis:6379).

Lanes share Redis. Give cache, session and queue keys a per-lane prefix, or a lane will read the site's cache and steal its queue jobs:
CACHE_PREFIX=webpress{{lane.suffix}}_
REDIS_PREFIX=webpress{{lane.suffix}}_
lane new warns when the template uses no {{lane.*}} variable.

Quick actions

One-click maintenance for a site or lane, run in its folder with its PHP:

ActionLabelRuns
clear-logClear logempties storage/logs/laravel.log (no PHP needed)
clear-cacheClear cachephp artisan optimize:clear
publish-corePublish core assetsphp artisan vendor:publish --provider="Webpress\Core\Providers\WebpressCoreProvider" --force
devkit site actions shop
devkit site action shop clear-cache
devkit site action-set shop migrate-fresh --command "php artisan migrate:fresh --seed" --label "Fresh database"
devkit site action-rm shop migrate-fresh
  • action-set adds your own action or overrides a default; php in the command means the site's PHP. action-rm removes an addition, or restores a default you overrode. The three defaults cannot be removed.
  • A non-zero exit is an ACTION_FAILED error with the last lines of output.
  • In the app the actions are buttons in the site detail's header and entries of the site's … menu, each with its command as a hint.

Processes

Besides the web server a site has long-running processes that start and stop with it. Until you change anything devkit detects them from the folder:

ProcessCommandStarts with the site
queuephp artisan queue:work --tries=1yes
schedulephp artisan schedule:workno
vite, vite:<name>one per vite.config.js / vite.config.<name>.js (also .mjs, .ts) at the root: pnpm vite --config <file> --port {port} --strictPort (yarn or npx --no-install after the lockfile)no
devkit proc list shop
PROCESS   STATUS   PORT  AUTOSTART  COMMAND
queue     running        true       php artisan queue:work --tries=1
schedule  stopped        false      php artisan schedule:work
vite      running  8104  false      pnpm vite --config vite.config.js --port {port} --strictPort

devkit proc start shop schedule vite      # without names: the autostart ones
devkit proc restart shop queue            # e.g. after changing a job class
devkit proc stop shop                     # without names: all running ones
devkit proc logs shop queue --lines 50

Your own processes

devkit proc add shop horizon --command "php artisan horizon" --autostart
devkit proc add shop reverb --command "php artisan reverb:start --port={port}" --port
devkit proc set shop schedule --autostart=true
devkit proc set shop vite --port=false
devkit proc rm shop reverb          # a stopped process
devkit proc reset shop              # forget your changes, detect the defaults again
  • --port gives the process a port from the same allocator; {port} in the command is replaced by it and PORT is set. Ports stay reserved while stopped, so two lanes can run Vite at once.
  • Commands run without a shell. Quotes work as in a shell, but pipes, &&, redirections and $VAR are refused: wrap them in sh -c '…'.
  • A command starting with php runs with the site's PHP (in its container under caddy); everything else runs on the host.
  • Status is running, stopped or exited (it crashed: the last lines of its log are shown). devkit does not restart a crashed process on its own. A process that exits within a second fails with PROC_FAILED.
  • The first proc add, set or rm stores the whole list for that site; a new vite.config.* is then no longer picked up until proc reset.
  • Each start appends a --- devkit: start line to the log at ~/.config/devkit/logs/<site>.<process>.log, then the process's output.

In the app, the site detail's Processes tab lists serve and every process with start, stop and restart, the Autostart switch, the port it got and its log below.

exec and logs

devkit exec shop -- php artisan tinker
devkit exec shop -- composer require spatie/laravel-backup
devkit exec shop --timeout 30m -- php artisan import:catalog
devkit exec shop --xdebug -- php vendor/bin/phpunit --filter CheckoutTest

exec runs a command in the site's folder with the site's PHP first on PATH, so composer and scripts use the right version too. Under caddy, php commands run in the PHP container at the same path. Output streams to your terminal and the exit code is the command's own (124 when --timeout, 10 minutes by default, stops it). --xdebug runs this one command with Xdebug without changing the site's setting.

devkit logs shop                          # storage/logs/laravel.log
devkit logs shop --source serve           # the web server (caddy: the Caddy container's log)
devkit logs shop --source proc:queue
devkit logs shop --source container:mysql --lines 50

In the app: View logs in the site's … menu, with a tab per source (laravel.log, serve, each process, each container), Filter lines, Follow and Copy visible lines. The Processes tab also shows the selected process's log below the table.

Git: branch, fetch, pull, push

Every site and lane shows its own repository's branch. The site detail's branch bar has a branch picker (with New branch…), Fetch, Pull, Push and Include submodules (on by default).

devkit git status shop
devkit git checkout shop feat/cart             # local, or a new one tracking origin/feat/cart
devkit git checkout shop feat/new-idea --create  # new branch at HEAD
devkit git fetch shop
devkit git pull shop                           # --ff-only
devkit git push shop                           # submodules first, then the site
  • Checkout refuses with DIRTY_WORKTREE, listing every reason, when the site (or a submodule) has uncommitted changes, untracked files included. Commit or stash first. devkit never fetches implicitly: an unknown branch is NOT_FOUND with the hint to fetch.
  • With submodules, checkout moves each one to the commit the branch records and puts it on its branch. A running site's server and PHP processes restart. A changed composer.lock or pnpm-lock.yaml is pointed out with the install command to run.
  • Pull refuses a detached HEAD, a dirty repository and a branch without upstream. Push sets the upstream on a first push. If a submodule push fails, the site is not pushed.
  • Add --with-submodules=false to leave the submodules out.

Modules and Extensions (submodules)

Submodules are grouped by their top-level folder: Modules (modules/*) and Extensions (extensions/*) by default, one tab each in the site detail. Each row shows the submodule's branch (or detached), the branch .gitmodules tracks, its changes, ahead/behind, and Pull, Push and Terminal. Each tab has Checkout in all, Pull all and Push all, and Extensions has Add extension….

devkit modules core-development --fetch
GROUP    PATH               BRANCH          TRACKS          HEAD     SYNC    DIRTY
modules  modules/common     release/v12.45  release/v12.45  3f2a9c1  ↑0 ↓2   -
modules  modules/framework  feat/cms        release/v12.45  8b41d0e  ↑1 ↓0   3
…

devkit modules checkout core-development feat/cms             # in every submodule
devkit modules checkout core-development feat/cms --only common,framework
devkit modules checkout core-development feat/cms --create --group extensions
devkit modules pull core-development
devkit modules push core-development --group modules
devkit modules add core-development git@github.com:acme/payments.git --branch main
  • --group limits a command to one group, --only to some submodules (path, name or folder, comma-separated).
  • A submodule with uncommitted changes is skipped with MODULE_DIRTY and never touched.
  • Bulk commands report per repository: done, unchanged, skipped or failed. In a terminal they exit 1 when anything was skipped or failed.
  • modules add runs git submodule add into extensions/<name> (or --group, --name, --path), records the branch in .gitmodules and stages the change. Commit it yourself.
  • Other folders: devkit site set core-development --submodule-groups modules,extensions,packages=libs (a bare path is its own name); --submodule-groups "" restores the two defaults.

Site settings

devkit site set shop --php 8.3 --engine artisan --db shop_dev --stack local-stack
devkit site set shop --copy-files storage/app/public,.env.testing
  • --stack: which stack's database and Redis the site uses (default: the first one you added).
  • --db: the database name; two sites cannot share one.
  • --copy-files: gitignored files and folders a new lane copies besides vendor/ and node_modules/ ("" clears the list).

The site detail's Settings tab shows the same choices.

Hide and unhide

devkit site hide legacy-crm
devkit site unhide legacy-crm

Hiding stops the site and its lanes (removing a caddy site's route) and leaves it out of site list and doctor. Its files, database and template stay. A hidden site cannot be started until you unhide it. In the app: Hide from devkit in the site's … menu; hidden sites are listed under Settings → Hidden sites with Show again.

Delete

devkit site delete shop                     # without --yes: says what would go, changes nothing
devkit site delete shop --trash --yes       # folder to the Trash
devkit site delete shop --trash --drop-db --yes

Deleting removes the site's lanes (as lane rm does), stops the site, removes its Caddy route and certificate, its record, .env backups and logs, and its folder:

  • --trash moves the folder to the Trash (~/.Trash on macOS, ~/.local/share/Trash on Linux) instead of deleting it. The Trash must be on the same disk; devkit checks before changing anything. On macOS there is no Put Back: drag the folder back yourself.
  • --drop-db also drops the databases and snapshots of the site and its lanes. Without it they stay on the server.
  • devkit refuses with DIRTY_WORKTREE while the site, a submodule or a lane has uncommitted changes (untracked files included), commits on no remote (on a branch or a detached HEAD) or stashes, and for a folder that is not a git repository (its files exist nowhere else). --force deletes anyway.
  • It never deletes your home folder or a folder that holds a parked directory, a stack or another site.

In the app, Delete site… first asks devkit what would be lost and shows it, makes you type the site's name, offers Move the folder to the Trash instead of deleting it permanently (on by default) and the database, and asks devkit again just before acting. If anything changed in between, nothing is deleted.