Part of the DigitalOcean deploy series. Tested on Laravel 13, PHP 8.4, Ubuntu 24.04, June 2026.

This used to be the long step with Nginx and a manual certificate dance. Caddy handles HTTPS for you, so it is shorter now. We install PHP, the database, Redis and the web server, and we tune PHP and MySQL so they behave on a small box. Work as deploy.

PHP 8.4 FPM

Ubuntu 24.04 ships PHP 8.3, but you want 8.4 for the latest performance and to match Laravel 13. Add Ondřej Surý's well known PPA.

sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y php8.4-fpm php8.4-cli php8.4-mysql php8.4-redis \
  php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-bcmath \
  php8.4-gd php8.4-intl php8.4-opcache

Confirm the extensions loaded for both the web (FPM) and command line (CLI).

php -m

One extension is easy to miss: intl. Without it, Laravel Cashier and any locale aware NumberFormatter fall back to a polyfill that only knows English, which quietly breaks non-English locales on Stripe redirects. Make sure it is in the list.

Production PHP settings

The defaults are made for shared hosting. Put your production overrides in a drop-in file, and mirror it to the CLI so artisan, the scheduler and Horizon behave the same.

sudo nano /etc/php/8.4/fpm/conf.d/99-laravel.ini
max_execution_time = 120
memory_limit = 256M
upload_max_filesize = 50M
post_max_size = 60M
display_errors = Off
log_errors = On
date.timezone = UTC
expose_php = Off

opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60
opcache.save_comments = 1

realpath_cache_size = 4096K
realpath_cache_ttl = 600

Copy the same file to the CLI config.

sudo cp /etc/php/8.4/fpm/conf.d/99-laravel.ini /etc/php/8.4/cli/conf.d/99-laravel.ini

Tune the FPM pool

A small box should cap how many PHP workers can run, and recycle them now and then to keep memory steady.

sudo nano /etc/php/8.4/fpm/pool.d/www.conf
pm.max_children = 8
pm.max_requests = 500
request_terminate_timeout = 150

Set request_terminate_timeout a little above your max_execution_time. Reload FPM after the changes.

sudo systemctl reload php8.4-fpm

Composer and Node

Composer, installed globally.

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Node.js 22, needed to build your Vite assets on the server.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

MySQL 8

sudo apt install mysql-server -y

Skip mysql_secure_installation, whose prompts are fiddly, and clean up with SQL instead.

sudo mysql
DELETE FROM mysql.user WHERE User='';
DROP DATABASE IF EXISTS test;
FLUSH PRIVILEGES;

Leave the MySQL root account on auth_socket. That means root has no password and is only reachable locally through sudo mysql, which is safer than a password you have to store.

Tune MySQL for a small droplet

sudo nano /etc/mysql/mysql.conf.d/99-tuning.cnf
[mysqld]
innodb_buffer_pool_size = 512M
performance_schema = OFF
max_connections = 50
sudo systemctl restart mysql
sudo systemctl is-active mysql

Create the app database and user

Use utf8mb4, which handles all Unicode including emojis and right to left scripts. Generate a strong password and grant only this database.

openssl rand -base64 32   # copy this into your password manager
sudo mysql
CREATE DATABASE techalyst CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'techalyst'@'localhost' IDENTIFIED BY 'paste-the-generated-password';
GRANT ALL PRIVILEGES ON techalyst.* TO 'techalyst'@'localhost';
FLUSH PRIVILEGES;
EXIT;

These details go into the app .env in step four.

Redis

sudo apt install redis-server -y
sudo systemctl enable --now redis-server
redis-cli ping      # PONG

Redis binds to localhost only by default, and UFW blocks outside traffic, so it needs no password here.

Caddy

Install from the official Cloudsmith repository.

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy -y

Let PHP and Caddy talk

Set the PHP-FPM pool to run as deploy, and let the caddy group reach its socket. This avoids permission hacks later, because PHP can write to your project and Caddy can reach PHP.

In /etc/php/8.4/fpm/pool.d/www.conf, set:

user = deploy
group = deploy
listen.owner = deploy
listen.group = caddy
sudo systemctl restart php8.4-fpm

The stack is ready: PHP 8.4 tuned for production, MySQL tuned for a small box, Redis on localhost, and a web server that does HTTPS on its own. The last step is to put your Laravel app on it and wire up the queue, scheduler and deploys.