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>.
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:
| Step | What it does | Skip with |
|---|---|---|
| submodules | git 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 |
| composer | composer install with the site's PHP (--php) | --no-composer |
| pnpm | pnpm install, when there is a package.json | --no-pnpm |
| env | stores a .env template seeded from .env.example (an empty APP_KEY gets a fresh key) and creates the database | --no-env |
| up | site up at the end | only runs with --start |
--into <dir>picks the parked directory; it can be left out when you have only one.--namesets the folder and site name (default: from the URL),--branchthe branch to clone (default: the remote's default branch).- The URL can be
https://…,git@host:org/repo.gitor 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--startflags. Every step is safe to repeat. site setupalso works for a project you copied or cloned by hand.- Add
--progressto 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 server | php artisan serve on the host | your stack's Caddy container |
| PHP | a host binary (Homebrew, distro, phpenv…) | a PHP-FPM container of that version in one of your stacks |
| URL | http://localhost:<port>, one port per site | https://<site>.localhost with a trusted certificate |
| Needs | PHP on the host | a stack with a caddy and a php service, mkcert, devkit trust once |
| Xdebug | per site or lane | per PHP container, so for every site on it |
| Good for | most daily work: fast to start, nothing to set up | HTTPS-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
phpon yourPATH. - A lane always runs its site's version.
- The version must be installed. A missing one fails with
PHP_VERSION_MISSINGand the command that installs it (brew install php@8.2on macOS, your distribution's package on Linux). - A running site keeps its current binary until its next start;
site upthen 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 athttps://<lane>.<site>.<suffix>. The suffix islocalhostunless 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 statusand the app reportPORT_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
.envis kept once as~/.config/devkit/backups/<site>/env.orig, and the file before each write asenv.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_UNRESOLVEDnaming it, and the existing.envis 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
| Variable | Value |
|---|---|
{{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).
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:
| Action | Label | Runs |
|---|---|---|
clear-log | Clear log | empties storage/logs/laravel.log (no PHP needed) |
clear-cache | Clear cache | php artisan optimize:clear |
publish-core | Publish core assets | php 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-setadds your own action or overrides a default;phpin the command means the site's PHP.action-rmremoves an addition, or restores a default you overrode. The three defaults cannot be removed.- A non-zero exit is an
ACTION_FAILEDerror 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:
| Process | Command | Starts with the site |
|---|---|---|
queue | php artisan queue:work --tries=1 | yes |
schedule | php artisan schedule:work | no |
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
--portgives the process a port from the same allocator;{port}in the command is replaced by it andPORTis 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$VARare refused: wrap them insh -c '…'. - A command starting with
phpruns with the site's PHP (in its container under caddy); everything else runs on the host. - Status is
running,stoppedorexited(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 withPROC_FAILED. - The first
proc add,setorrmstores the whole list for that site; a newvite.config.*is then no longer picked up untilproc reset. - Each start appends a
--- devkit: startline 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 isNOT_FOUNDwith 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.lockorpnpm-lock.yamlis 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=falseto 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
--grouplimits a command to one group,--onlyto some submodules (path, name or folder, comma-separated).- A submodule with uncommitted changes is skipped with
MODULE_DIRTYand never touched. - Bulk commands report per repository:
done,unchanged,skippedorfailed. In a terminal they exit 1 when anything was skipped or failed. modules addrunsgit submodule addintoextensions/<name>(or--group,--name,--path), records the branch in.gitmodulesand 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 besidesvendor/andnode_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:
--trashmoves the folder to the Trash (~/.Trashon macOS,~/.local/share/Trashon 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-dbalso drops the databases and snapshots of the site and its lanes. Without it they stay on the server.- devkit refuses with
DIRTY_WORKTREEwhile 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).--forcedeletes 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.