Appearance
Deploy
The files that put the site on a server, written from the site as it is: a server script, cloud-init, a Deployer recipe, a CI workflow.
kitloom/deploy
The deploy kit. One command writes the files that put the site on a server, filled in from the site as it is — which admin and at which path, whether a theme is built with npm, whether the worker runs Horizon, whether the database is WordPress's — so nothing of the deploy is typed from a page of documentation and got wrong for this site:
bash
php artisan kitloom:deploy-kit # the targets
php artisan kitloom:deploy-kit server # one VPS on Ubuntu 24.04
php artisan kitloom:deploy-kit laravel-cloud # cloud.laravel.com
php artisan kitloom:deploy-kit forge # Laravel Forge or Ploi on any VPS
php artisan kitloom:deploy-kit docker # any Docker host: compose, Coolify, AWS ECS / App Runner
php artisan kitloom:deploy-kit digitalocean-app # DigitalOcean App PlatformThe files are plain text in the project, to read, edit and commit. Run the kit again after a change — a Vue theme added, Horizon installed, another domain — and it rewrites its own files and keeps the ones you edited. Nothing of the package runs while the site serves pages.
The server target
One server on Ubuntu 24.04 — a DigitalOcean Droplet, a Hetzner server, an AWS EC2 instance or Lightsail — with Nginx, PHP-FPM, MySQL on the same server (or a managed one elsewhere) and the site deployed in releases, so a deploy never serves half a site and can be rolled back.
| File | What it is |
|---|---|
deploy/server.sh | Sets the server up, run once as root: PHP-FPM with the extensions, Nginx with the site's server block, MySQL with the site's database and user, Composer with your kitloom token (and the Nova license on a Nova site), Node.js when a theme needs it, Redis when Horizon does; the deploy user that owns the site and runs PHP; the site's folder and its .env with a key and the database password made up; the scheduler in cron; the worker as a systemd service; a certificate from Let's Encrypt. Safe to run again: what is there is left, what is missing is added |
deploy/cloud-init.yaml | The same script run by cloud-init at the server's first boot — a Droplet's User data, Hetzner's Cloud config, an EC2 instance's User data. Fill in the credentials in a copy; don't commit the copy |
deploy.php | A Deployer recipe: each release in a folder of its own, composer install, the themes built, optimize, migrate, then current switched to it, PHP-FPM reloaded, the worker restarted, and the front page and the admin's login page asked for a 200. dep rollback switches back to the release before. dep kitloom:user makes the first administrator |
.github/workflows/deploy.yml | Every push to main runs the recipe, with one secret, a private SSH key. --ci=gitlab writes .gitlab-ci.yml instead, --ci=none neither |
deploy/README.md | The steps above for this site, in order |
The kitloom token is saved on the server by server.sh, in the deploy user's Composer configuration, and nowhere in the repository: CI needs only the SSH key.
The first time
composer require --dev deployer/deployer, and commit the kit's files.- A server with 2 GB of memory (4 GB for a shop or Horizon) and
deploy/cloud-init.yamlas its user data — orscp deploy/server.sh root@<ip>:andssh root@<ip> bash server.sh, which asks for the credentials. - The DNS record of the domain to the server, then the certificate:
certbot --nginx -d example.com -d www.example.com --redirectas root (server.shtakes it itself when the record is already there). - The server's deploy key, printed at the end of
server.sh(or/var/log/kitloom-server.logwith cloud-init), as a read-only deploy key of the repository. vendor/bin/dep deploy, thenvendor/bin/dep kitloom:user.
A site moving from WordPress loads WordPress's database into the server's MySQL instead of making an administrator; DB_PREFIX in the server's .env (/var/www/<domain>/shared/.env) is its $table_prefix. The mailer, the modules' keys and the rest of Configuration go into that .env too; a deploy keeps it.
The laravel-cloud target
Laravel Cloud keeps its settings in its dashboard, not in files, so the kit writes the checklist with this site's values and the variables to paste:
| File | What it is |
|---|---|
deploy/README.md | The checklist: the application and its PHP, the resources (Laravel MySQL, the public media bucket, a private bucket when Import & Export or Backup use one, Valkey with Horizon), the build commands with your token (and Nova's), the deploy command, the scheduler and the worker, the first deploy, backups |
deploy/laravel-cloud.env | The environment variables next to what Cloud fills in: MEDIA_DISK=media, MEDIA_UPLOADS_URL to fill, IMPORT_EXPORT_DISK=private when the module is there, KITLOOM_MODULES_CACHES=deploy, the queue on Valkey with Horizon, DB_PREFIX for a WordPress database |
Cloud resets the file system on every deploy and each replica has its own, so the uploads live in a bucket and the caches are built in the build; the README says why each line is there. The S3 adapter the bucket needs is asked for (composer require league/flysystem-aws-s3-v3) only while it is missing.
The forge target
Laravel Forge and Ploi set the server up and deploy on a push; what they cannot know is kitloom's:
| File | What it is |
|---|---|
deploy/forge.sh, deploy/ploi.sh | The deploy script to paste: composer install, the themes built when a theme needs it, migrate, optimize, PHP-FPM reloaded, the worker restarted the way it runs (queue:restart or horizon:terminate) |
deploy/forge.env | The lines to add to the site's environment under what Forge filled in: the theme, the prefix, the queue, the mailer to set |
deploy/README.md | The checklist of their screens: the server, the token as the server's user, the site, the environment, the deploy script, the PHP upload size, the scheduler, the worker as a daemon, SSL, the first administrator |
A Forge deploy updates the site in place, a few seconds of a half-updated site; for releases switched in one move the server target's Deployer recipe runs on a Forge server too.
The docker target
One image for any Docker host — a VPS with docker compose, Coolify, AWS ECS or App Runner: PHP-FPM and Nginx in one container, the packages installed with the kitloom token as a build argument of a stage that is not in the image, the themes built in; the same image runs the worker and the scheduler with another command.
| File | What it is |
|---|---|
Dockerfile | php:<version>-fpm with the extensions, Nginx and supervisord; a vendor stage with COMPOSER_AUTH (the token never reaches the image's history); a stage with Node for the themes when a theme needs it (PHP stays there: a TypeScript theme asks it for the page types); the image with the entrypoint, port 8080, a health check on /up |
docker/ | nginx.conf, php.ini (uploads, OPcache without file checks), supervisord.conf, entrypoint.sh (the caches from the container's environment, migrations and storage:link for the site, any command as www-data for the worker and the scheduler; a managed database's CA from DB_CA_CERT), auth.json.example |
.dockerignore | vendor, node_modules, the built themes, the environment and the credentials stay out of the build |
compose.yml | One host: the site on port 80, the worker, the scheduler, MySQL 8.4, Redis with Horizon; volumes for the uploads and the database; COMPOSER_AUTH from the shell |
deploy/docker.env | The environment of every container, and MySQL's; to fill in and keep out of the repository |
deploy/README.md | docker compose on one host, and what a platform that runs the image needs: the uploads on S3, HEALTH_EPHEMERAL_DISK, the worker and the scheduler as services, a private disk for Import & Export |
The digitalocean-app target
The docker image and .do/app.yaml, an App Platform spec for doctl apps create: the web service, the worker and the scheduler as components of the same image, built from the GitHub repository on every push; a managed MySQL bound into the environment (${db.HOSTNAME} and the rest, the CA certificate through the entrypoint); the uploads in a Space behind its CDN; Valkey with Horizon; the domain. The secrets — APP_KEY, COMPOSER_AUTH for the build, the Space's key and secret — are filled in the dashboard, so the spec can be committed. deploy/README.md has the steps and what the platform's reset disk asks of the site.
Options
| Option | |
|---|---|
--domain= | The site's domain; by default the host of APP_URL, or example.com while that is a local address |
--repository= | The Git repository the server clones; by default the origin remote |
--php= | The PHP the server gets; by default the lowest the project's composer.json allows (^8.4 — 8.4) |
--ci= | server only: github (default), gitlab or none |
--force | Rewrite files you edited, or files the kit did not write |
--path= | Write into another folder |
What it reads off the site
| Where it shows | |
|---|---|
| The admin — Nova or Filament, and its path | server.sh saves Laravel's Composer credential for Nova; deploy.php checks the admin's login page after a deploy |
| The themes — a Vue or React theme installed, or Blade only | Node.js on the server and npm run build:themes in the release, or neither |
| Horizon installed and on | Redis on the server, QUEUE_CONNECTION=redis, the worker runs php artisan horizon |
| Sentry installed | A SENTRY_LARAVEL_DSN line to fill in .env |
| The database's prefix | DB_PREFIX in .env, so a WordPress database is read as it is |
| Import & Export, Backup installed | A private bucket on Laravel Cloud, and its disk in the variables |
| The S3 adapter installed | Laravel Cloud's and App Platform's checklists ask for it only while it is missing |
| The repository on GitHub | .do/app.yaml names it (owner/name); another host leaves a placeholder and a step |
deploy/.kit.json remembers what the kit wrote, as a hash per file. Keep it with the files: it is how the next run tells its own files from yours.
For developers
A target is a Kitloom\Deploy\Kit\Target — its name, its options, the files it writes for a SiteFacts, the steps left — given to Targets in the provider. Stubs in stubs/<target>/ use [[value]], [[#flag]]…[[/flag]] and [[^flag]]…[[/flag]] (Kit\Stub); the flags come from SiteFacts::variables(). Tests render the targets with facts built by hand and run bash -n and php -l over what comes out.