Appearance
A server: DigitalOcean, Hetzner, AWS EC2 and Lightsail
One VPS on Ubuntu 24.04 with Nginx, PHP-FPM and MySQL — a DigitalOcean Droplet, the platform the kitloom demo runs on, a Hetzner server, an AWS EC2 instance or Lightsail. The deploy kit sets it up and deploys the site in releases; the same steps by hand are below it. What the server needs and where else to host: Deployment.
With the deploy kit
bash
php artisan kitloom:deploy-kit server --domain=example.com| File | What it is |
|---|---|
deploy/server.sh | Steps 3 to 8 below as one script, run once as root: PHP-FPM with the extensions, Nginx with the site's server block, MySQL with the site's database, 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, the site's .env with a key and a database password made up, cron, the worker as a systemd service, a certificate. Safe to run again |
deploy/cloud-init.yaml | The same at the server's first boot: a Droplet's User data (step 2), Hetzner's Cloud config, an EC2 instance's User data, a Lightsail launch script. 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 under /var/www/example.com/releases, composer install, the themes built, optimize, migrate, then current switched to it, PHP-FPM reloaded, the worker restarted, the front page and the admin's login page asked for a 200. vendor/bin/dep rollback goes back; vendor/bin/dep kitloom:user makes the first administrator |
.github/workflows/deploy.yml | Every push to main deploys, with one secret, a private SSH key for CI (--ci=gitlab for .gitlab-ci.yml, --ci=none for neither). The token is on the server, not in CI |
deploy/README.md | The steps for this site, in order |
The first time: composer require --dev deployer/deployer; a server with 2 GB of memory (4 GB for a shop or Horizon) and the cloud-init file as its user data, or server.sh copied up and run as root; the DNS record and the certificate (server.sh takes it itself when the record is already there); the server's deploy key, printed at the end of the script, on the repository; vendor/bin/dep deploy, then vendor/bin/dep kitloom:user. After that, dep deploy from your machine or a push to main. A site moving from WordPress loads WordPress's database into the server's MySQL instead of making an administrator — see Migrate from WordPress — and DB_PREFIX in the server's .env (/var/www/example.com/shared/.env) is its $table_prefix.
On Hetzner and AWS the files are the same: only step 2 below is the provider's own screen. With a managed database (DigitalOcean's Managed MySQL, AWS RDS) run server.sh --without-mysql and put its address into the server's .env.
By hand, step by step
What server.sh does, as the steps it was written from — to read what it does, or to do it yourself. They 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 optimizebuild:themes also writes the themes' SSR bundles into bootstrap/ssr; the SSR server that renders the pages from them is a service of step 8. A 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 Vue or React theme's pages are rendered on the server by php artisan inertia:start-ssr, from the bundles build:themes wrote; the same kind of service, /etc/systemd/system/kitloom-ssr.service, keeps it running (User=deploy, WorkingDirectory the site's folder, Environment=NODE_ENV=production, ExecStart=/usr/bin/php artisan inertia:start-ssr, Restart=always), and a deploy restarts it after the build, as it restarts the worker. Without it the pages are drawn in the browser.
bash
systemctl enable --now kitloom-ssrA 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).