Appearance
Deployment
Introduction
A kitloom site is a Laravel application, and it is deployed like one: the code from your Git repository, the packages from Composer, the database and the uploads kept on the server or beside it. Nothing about it needs a special host. This page covers what the server has to provide, where to host it, and a step-by-step setup on a DigitalOcean Droplet, the platform the kitloom demo runs on.
What the server needs
| PHP 8.4 or newer, as PHP-FPM | with intl, gd (or imagick), pdo_mysql, mbstring, xml, curl, zip, bcmath, opcache; redis for a cache or queue in Redis, pcntl for Horizon |
| A database | MySQL 8 or MariaDB — a new database of the site's own, or an existing WordPress one. For WordPress, DB_PREFIX is the $table_prefix from its wp-config.php (wp_ unless it was changed). SQLite also works for a small site on one server |
| A web server | Nginx or Caddy, with public/ as the document root |
| Composer 2 | with your kitloom token saved, to install the packages from https://packages.kitloom.dev |
| Node.js 20+ | only to build Vue and React themes (npm run build:themes); a Blade theme needs none. The builds can also be made in CI and copied up |
| Cron | php artisan schedule:run every minute: backups, Site Health, link checks, booking reminders, unpaid order cancellation and other modules' tasks are on Laravel's schedule |
| A queue worker | php artisan queue:work kept running: imports, search indexing, "Back up now", webhooks and mail wait for it (QUEUE_CONNECTION=database by default) |
| Memory | 2 GB is enough to run a site and build its themes; 4 GB for a shop, several themes or Horizon |
The packages come with your purchase's token, the same one you use on your own machine (see After your purchase). Save it once on the server and on any CI that installs the site; never commit it.
Where to host
| Option | Who it suits | Notes |
|---|---|---|
| DigitalOcean Droplet | one site, one server, full control | What the kitloom demo runs on. Below, step by step. Managed MySQL, Spaces (S3) for uploads and backups, and uptime checks are a few clicks away in the same account |
| Laravel Forge on any VPS | teams that would rather not run a server by hand | Forge installs PHP, Nginx, MySQL, the queue worker and the scheduler on a server at DigitalOcean, Hetzner, Vultr, Akamai or AWS, and deploys from Git with zero downtime. Add the kitloom token to the server's Composer (composer config --global http-basic.packages.kitloom.dev …) and npm ci && npm run build:themes to the deploy script |
| Ploi on any VPS | the same, from Europe | Like Forge: provisioning, deploys, workers, cron and certificates on your own server |
| Hetzner Cloud | the lowest price for the memory | A plain VPS in Germany, Finland, the US or Singapore: the DigitalOcean steps below apply as they are, from step 2 |
| Laravel Cloud | sites that should scale without a server to look after | Laravel's own platform: the app, MySQL, buckets, workers and the scheduler from one dashboard, a deploy on every push. The file system is reset on every deploy, so the uploads go to a bucket. Below, step by step |
| DigitalOcean App Platform | containers without a server | The same as Laravel Cloud for the uploads, which go to Spaces; the queue worker and the scheduler run as separate components. Set HEALTH_EPHEMERAL_DISK=true, so Site Health checks what Laravel Cloud gets checked by itself |
| Shared hosting | not recommended | Only with SSH, PHP 8.4, Composer and cron. Without a long-running worker, run the queue from cron (queue:work --stop-when-empty every minute) |
Several servers behind a load balancer work too: give them a shared cache and session store (Redis) and the uploads on a bucket, and set HEALTH_EPHEMERAL_DISK=true for Site Health to check that nothing is left on one server's disk. See Benchmarks for what a live site switches on.
DigitalOcean step by step
The steps set up one site on one Droplet: Ubuntu 24.04, Nginx, PHP-FPM 8.4, MySQL on the same server, a certificate from Let's Encrypt. Replace example.com with your domain.
1. An account
Sign up at digitalocean.com and add a payment method. Everything below is in one account: the Droplet, its DNS, backups and monitoring, and Managed MySQL or Spaces if you take them.
2. The Droplet
Create → Droplets:
- Region — the one closest to your visitors.
- Image — Ubuntu 24.04 (LTS) x64.
- Size — Basic, Regular, 2 GB of memory or more (4 GB for a shop). The size can be raised later.
- Authentication — an SSH key, not a password.
- Monitoring on (free: CPU, memory and disk alerts), and Backups if the site's data matters to you: weekly or daily copies of the whole server.
Point the domain at the Droplet's address: an A record for example.com and for www, at your registrar or in Networking → Domains.
DigitalOcean blocks outgoing SMTP on new accounts (ports 25, 465 and 587). Send the site's mail through a provider's API — Postmark, Mailgun, Amazon SES, Resend — with Laravel's mailer for it, or through a port the provider offers outside those three (often 2525).
3. The software
Sign in as root and install PHP 8.4 (Ubuntu 24.04 ships 8.3, so it comes from the ondrej/php repository), Nginx, MySQL, Composer and Node.js:
bash
apt update && apt upgrade -y
add-apt-repository -y ppa:ondrej/php
apt install -y nginx mysql-server git unzip certbot python3-certbot-nginx \
php8.4-fpm php8.4-cli php8.4-mysql php8.4-intl php8.4-gd php8.4-mbstring \
php8.4-xml php8.4-curl php8.4-zip php8.4-bcmath php8.4-opcache php8.4-redis
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejsUploads up to 64 MB, the media library's limit, need PHP to take them:
bash
printf 'upload_max_filesize=64M\npost_max_size=72M\nmemory_limit=256M\nexpose_php=Off\n' \
> /etc/php/8.4/fpm/conf.d/90-kitloom.iniA user of its own owns the site and runs PHP, so the files PHP writes belong to the user that deploys:
bash
adduser --disabled-password --gecos '' deploy
mkdir -p /home/deploy/.ssh && cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
mkdir -p /var/www/example.com && chown deploy:deploy /var/www/example.com
sed -i 's/^user = www-data/user = deploy/; s/^group = www-data/group = deploy/' /etc/php/8.4/fpm/pool.d/www.conf
systemctl restart php8.4-fpm4. The database
bash
mysql -e "CREATE DATABASE kitloom CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'kitloom'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL ON kitloom.* TO 'kitloom'@'localhost';"Then the data. A site that moves from WordPress brings WordPress's database — follow Migrate from WordPress for the order of the steps. A new site can start empty: the migrations in step 6 create its tables, and php artisan kitloom:user its first administrator. Or it brings the database from your machine or staging, with its pages, settings and users:
bash
# on your machine (MySQL); a SQLite site: export it with the Import & Export module instead
mysqldump --single-transaction kitloom | gzip > kitloom.sql.gz
scp kitloom.sql.gz [email protected]:
# on the server
gunzip < kitloom.sql.gz | mysql -u kitloom -p kitloomA Managed MySQL cluster (Create → Databases) can take the place of the local server: backups, updates and a standby node are DigitalOcean's job then. Put its host, port (25060), user and password into .env, add the Droplet to its trusted sources, and set MYSQL_ATTR_SSL_CA to the CA certificate it offers for download.
5. The code
As deploy, save the kitloom token and clone your site's repository — the project you created with composer create-project and committed:
bash
su - deploy
composer config --global http-basic.packages.kitloom.dev <your-name> <your-token>
git clone [email protected]:you/example-site.git /var/www/example.com
cd /var/www/example.com
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generateThe server needs read access to the repository: a deploy key on GitHub or GitLab (ssh-keygen -t ed25519 as deploy, the public half in the repository's settings).
6. The environment
Edit .env:
ini
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
APP_THEME=Journal # the theme, or empty for a headless site
LOG_LEVEL=warning
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kitloom
DB_USERNAME=kitloom
DB_PASSWORD=a-long-random-password
# DB_PREFIX=wp_ # a WordPress database: its $table_prefix
SESSION_DRIVER=database
CACHE_STORE=database
QUEUE_CONNECTION=database
MAIL_MAILER=postmark # or ses, mailgun, resend — see the warning above
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"The modules' variables are on Configuration. Then the migrations, the link to the uploads, the themes and Laravel's caches:
bash
php artisan migrate --force
php artisan storage:link
npm ci && npm run build:themes
php artisan optimizeA site that started empty gets its first administrator now. The command asks for the email and, twice, the password; the login name is made from the email unless --login gives one:
bash
php artisan kitloom:user --login=admin [email protected]php artisan db:seed is not for a server: the skeleton's seeder writes demo content and makes its administrator with a model factory, which needs Faker, and composer install --no-dev leaves Faker out. kitloom:user takes --role=editor (or any role of the site) for other accounts, and --name for the name shown on the site. In a script, give --password too, or run it with --no-interaction: the password is then made up and shown once. To fill an empty site with the active theme's demo content, run its <name>:demo command (see the theme catalog).
7. Nginx and HTTPS
As root, /etc/nginx/sites-available/example.com:
nginx
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php;
charset utf-8;
client_max_body_size 72M;
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; try_files $uri /index.php?$query_string; }
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}robots.txt goes to Laravel when there is no file, so the SEO module can serve it. Switch the site on and take a certificate; Certbot rewrites the server block for HTTPS and renews the certificate on its own:
bash
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
certbot --nginx -d example.com -d www.example.com --redirect8. The scheduler and the queue
The scheduler, in deploy's crontab (crontab -e as deploy):
* * * * * cd /var/www/example.com && php artisan schedule:run >> /dev/null 2>&1The queue worker, as a systemd service that starts with the server and comes back when it stops — /etc/systemd/system/kitloom-queue.service:
ini
[Unit]
Description=kitloom queue worker
After=network.target mysql.service
[Service]
User=deploy
WorkingDirectory=/var/www/example.com
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetbash
systemctl daemon-reload
systemctl enable --now kitloom-queueA site with the Horizon module and its queue in Redis runs php artisan horizon in that service instead. Site Health in the admin (Health module) shows whether the scheduler and the worker come by.
Open https://example.com and /admin (Filament) or /nova (Nova).
Laravel Cloud step by step
Laravel Cloud is Laravel's own platform: no server to set up or update, a deploy on every push, zero-downtime releases, the database, buckets, workers and the scheduler added from its dashboard. Two things about it shape a kitloom site there: the file system is reset on every deploy, and each replica of the app has its own. So the uploads live in a bucket, and nothing the site writes to its own disk is meant to last.
1. The application
Sign up at cloud.laravel.com, connect GitHub, GitLab or Bitbucket, and create an application from your site's repository — the project you created with composer create-project and committed. Pick the region closest to your visitors. In the environment's General settings, choose PHP 8.4 or newer; the extensions kitloom needs (intl, gd, imagick, pdo_mysql, redis, pcntl) are all there.
On your machine, add the S3 adapter the bucket needs, and commit it:
bash
composer require league/flysystem-aws-s3-v32. The resources
On the environment's canvas:
- Add database → Laravel MySQL. Cloud fills
DB_HOST,DB_DATABASEand the rest itself. Not Serverless Postgres: kitloom runs on WordPress's MySQL tables. SQLite does not work on Cloud, its file would be gone with the next deploy. A site moving from WordPress loads WordPress's database into it through the database's public endpoint, with any MySQL client (see Migrate from WordPress) and setsDB_PREFIXto its$table_prefix(wp_unless it was changed). - Add bucket → Laravel Object Storage, with public visibility and the disk name
media: the media library's uploads. Public files are what the site shows; a public bucket is read by everyone who has an address. - A second, private bucket, disk name
private, for what must not be public: a WXR file waiting for its import (the queue worker reads it, and it may run on another instance) and backup archives. - Cache: not needed at first, the database keeps the cache and the sessions. Add Laravel Valkey when the site grows or runs several replicas.
- Email: Cloud's email resource (Resend), or any mailer of your own.
3. The environment
In General settings → Environment variables, next to what Cloud fills in itself:
ini
APP_THEME=Journal # the theme, or empty for a headless site
MEDIA_DISK=media
MEDIA_UPLOADS_URL=https://… # the public bucket's address, from its settings page
IMPORT_EXPORT_DISK=private
KITLOOM_MODULES_CACHES=deploy # see below
CACHE_STORE=database
SESSION_DRIVER=database
QUEUE_CONNECTION=database
# DB_PREFIX=wp_ # a WordPress database: its $table_prefix
# SENTRY_LARAVEL_DSN=… # with the Sentry module, optionalWhy these differ from a server:
KITLOOM_MODULES_CACHES=deploy. Switching a module on the Modules screen works as anywhere, but the caches aren't rebuilt in place on Cloud. They come fromphp artisan optimizein the build, which may not reach your database; then it builds them with every module on, and the site uses them only while every module is on. While one is switched off, the site runs without its config and route caches — a little slower, never wrong — and a redeploy builds them the same way again, so there is no need to redeploy after a switch. See Enabling & Disabling.SENTRY_LARAVEL_DSN, optional. The Sentry module keeps a copy of its settings on each instance's disk, so that errors reach Sentry even when the database is down. After a deploy each instance writes its copy again on its first request; the DSN as a variable also covers a database that is down right after a deploy.- The page cache (Settings → Cache) works as it is, but
SITE_CACHE_STATICdoes not: its files would sit on one replica and vanish with the next deploy.
4. Build and deploy commands
Build commands (environment settings → Deployments), which also save your kitloom token for this build:
bash
composer config http-basic.packages.kitloom.dev <your-name> <your-token>
composer install --no-dev --optimize-autoloader
npm ci --audit false
npm run build:themes
php artisan optimizeThe token stays in Cloud's settings, not in your repository. A Nova site adds the same line for nova.laravel.com with your Nova license. A Blade-only site can leave out the two npm lines. php artisan optimize belongs here, in the build, not in the deploy commands: the caches are then part of the release.
Deploy commands, run just before a release goes live:
bash
php artisan migrate --forceNo storage:link (the uploads are in the bucket), no queue:restart (Cloud restarts the workers itself), no optimize:clear.
5. The queue and the scheduler
Click the App cluster:
- Scheduler on: Cloud runs
schedule:runevery minute, and wakes a sleeping environment to do it. - Background processes → New background process → Queue worker, one process for a start. A site with the Horizon module and its queue in Valkey runs
php artisan horizonas a custom process instead. When imports, backups or search indexing get heavy, move the workers to a worker cluster of their own, so they don't take the CPU from visitors.
6. The first deploy
Deploy. Then, with the dashboard's Commands, make the first administrator — or skip it when the database came from WordPress or from your machine with its users:
bash
php artisan kitloom:user --login=admin [email protected] --no-interactionWith --no-interaction the password is made up and shown once in the command's output. Add your domain under Domains; until then the site answers on its free laravel.cloud address. Site Health in the admin then shows whether the scheduler and the worker come by, and checks the settings above. Laravel Cloud is recognised by itself, so Files kept between deploys lists whatever the site still keeps on the instance's disk — uploads, IMPORT_EXPORT_DISK, SITE_CACHE_STATIC, the cache or sessions on file, SQLite. Uploads address warns when the bucket gives the uploads no public address, and Backup destinations when backups are in use and their only destination is "This server". Each is a warning to act on, not an error.
Backups on Cloud: Laravel MySQL keeps its own for the days its backup retention is set to (two or more for a live site), and the Backup module archives the database to the private bucket (its S3 destination, with the key, secret, endpoint and bucket from the bucket's settings page) or any other storage (its "This server" destination would be gone with the next deploy). The uploads in the bucket are not in the archive: the bucket keeps them.
Updates
On Laravel Cloud, a push to the environment's branch is the deploy. On a server, a deploy of a new version, as deploy in the site's folder:
bash
php artisan down --refresh=15
git pull
composer install --no-dev --optimize-autoloader
npm ci && npm run build:themes
php artisan migrate --force
php artisan optimize
php artisan queue:restart
php artisan upWith OPcache's file checks off (opcache.validate_timestamps=0), PHP keeps the old code until PHP-FPM is reloaded: systemctl reload php8.4-fpm as root after the deploy. With preload, run php artisan site:preload and then systemctl restart php8.4-fpm: a reload does not replace preloaded classes. With PHP's defaults neither is needed.
New versions of kitloom come in through composer update on your machine: you test them, commit composer.lock, and the server installs exactly that. Installing a module is the same kind of change — see Installing Modules. php artisan down closes the site for the minute the deploy takes; for no downtime at all, deploy each version into a folder of its own and switch a link to it, which is what Forge, Ploi and Deployer do.
For speed on a live site — the page cache, static pages, preload — see Benchmarks.
Uploads on Spaces or S3
Uploads stay in storage/app/public on the server by default. To keep them in DigitalOcean Spaces (or any S3-compatible bucket), behind its CDN:
bash
composer require league/flysystem-aws-s3-v3ini
MEDIA_DISK=s3
AWS_ACCESS_KEY_ID=…
AWS_SECRET_ACCESS_KEY=…
AWS_DEFAULT_REGION=fra1
AWS_BUCKET=example-media
AWS_ENDPOINT=https://fra1.digitaloceanspaces.com
AWS_URL=https://example-media.fra1.cdn.digitaloceanspaces.comMove the files that are already there into the bucket before switching. A site on several servers, Laravel Cloud and App Platform need this; a site on one Droplet can leave the uploads where they are.
Backups
- The server: Droplet backups (step 2) keep whole copies of the server for a few weeks.
- The site: the Backup module puts the database and the uploads into an archive on a schedule and copies it off the server — to Spaces, S3, SFTP or FTP. Keep at least one copy outside the Droplet.
- Managed MySQL backs itself up every day.
Watching the site
- Uptime: DigitalOcean's Monitoring → Uptime checks an address from several regions and emails when it is down, slow, or its certificate is about to expire.
- Errors: the Sentry module sends the site's exceptions to Sentry; without it they are in
storage/logs. - The site itself: Site Health in the admin checks the database, the cache, the disk and writable folders, the PHP version, the environment, the scheduler and the queue, among others, and emails the site's administration address when one fails.