DEV Community

SᕼᗩᕼIᗪ KᗩᖇIᗰI
SᕼᗩᕼIᗪ KᗩᖇIᗰI

Posted on Originally published at hanoo.io

How to Deploy Laravel 13 to Production on Your Own Ubuntu Server

Running php artisan serve on your laptop is easy. Running a Laravel 13 application in production — with HTTPS, zero-downtime deploys, migrations that don't take the site down, queue workers that survive reboots and a scheduler that actually fires — is where most teams reach for a PaaS and its bill.

You don't have to. This guide walks through a production-grade Laravel 13 deployment on a single Ubuntu 24.04 server you own: first the moving parts and how to wire them by hand, then the same result with Hanoo, where a git push becomes a built, verified and reversible release.

In short: Laravel 13 requires PHP 8.3 or newer (8.4 is a good default). A production setup needs PHP-FPM, a web server pointing at public/, a database, Composer, Node.js for Vite builds, an atomic release layout, a queue worker under a process supervisor, the scheduler running every minute, and HTTPS.

What Laravel 13 needs in production

Laravel 13 was released in March 2026 and raised the minimum PHP version to 8.3. A typical production stack looks like this:

Piece Recommended choice Why
PHP 8.4 (8.3–8.5 supported) Laravel 13's minimum is 8.3; 8.4 is the mature, fast default
Web server Caddy or Nginx with PHP-FPM Serves public/, hands PHP to FPM, terminates TLS
Database MariaDB / MySQL or PostgreSQL Bound to 127.0.0.1 on a single-server setup
Cache, sessions, queues Valkey (Redis-compatible) or the database file and sync drivers do not scale past one request
Assets Node.js 22 or 24 with npm, pnpm or Yarn vite build produces the files in public/build
Processes Supervisor Keeps queue:work running and restarts it on failure
Scheduler Cron, every minute Runs schedule:run, which runs your scheduled tasks

The zero-downtime release layout

The single most important production habit is to never build in the directory that is serving traffic. Instead, every deploy gets its own directory, and a symlink decides which one is live:

/srv/app/
├── releases/
│   ├── 20261015-142210/
│   └── 20261016-091503/    ← new release, built here
├── shared/
│   ├── .env                ← linked into every release
│   └── storage/            ← logs, sessions, uploads: survive deploys
└── current -> releases/20261016-091503
Enter fullscreen mode Exit fullscreen mode

A deploy clones the commit into a new release directory, links shared/.env and shared/storage into it, installs dependencies, builds assets, runs migrations, and only then repoints current. Switching a symlink is atomic, so visitors either get the old release or the new one — never a half-built mix. Rolling back is pointing current at the previous release.

Option 1: deploy Laravel 13 by hand

1. Install PHP 8.4, Composer and Node.js

Ubuntu 24.04 ships PHP 8.3, which meets Laravel 13's minimum. For PHP 8.4, use the Ondřej Surý PPA:

sudo add-apt-repository ppa:ondrej/php && sudo apt update
sudo apt install php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring php8.4-xml \
  php8.4-curl php8.4-zip php8.4-bcmath php8.4-intl php8.4-redis php8.4-opcache
Enter fullscreen mode Exit fullscreen mode

Install Composer 2 from getcomposer.org (verify the installer's checksum), and Node.js 22 or 24 from NodeSource or a version manager.

2. Create a deploy user and the database

Run the application as its own unprivileged user — never as root and never as www-data shared with other sites:

sudo adduser --system --group --home /srv/app laravel
Enter fullscreen mode Exit fullscreen mode

Create a dedicated database and user that can connect only from 127.0.0.1.

3. Point the web server at current/public

With Nginx, the essential bits are:

server {
    server_name app.example.com;
    root /srv/app/current/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.4-fpm-laravel.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note $realpath_root: it resolves the current symlink, so OPcache and FPM see the new release immediately after the switch instead of serving stale paths. Give the site its own PHP-FPM pool running as the laravel user, then add HTTPS with Certbot or switch to Caddy, which obtains certificates automatically.

4. Write a deploy script

The core of every Laravel deploy script:

set -euo pipefail
RELEASE=/srv/app/releases/$(date +%Y%m%d-%H%M%S)
git clone --depth 1 --branch main git@github.com:acme/shop.git "$RELEASE"
ln -s /srv/app/shared/.env "$RELEASE/.env"
rm -rf "$RELEASE/storage" && ln -s /srv/app/shared/storage "$RELEASE/storage"
cd "$RELEASE"
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
npm ci && npm run build
php8.4 artisan migrate --force
php8.4 artisan optimize
ln -sfn "$RELEASE" /srv/app/current
sudo systemctl reload php8.4-fpm
php8.4 artisan queue:restart
Enter fullscreen mode Exit fullscreen mode

Add release pruning, a health check after the switch, and a rollback path, and you have the beginnings of a deployment tool.

5. Queue workers with Supervisor

[program:laravel-queue]
command=php8.4 /srv/app/current/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=/srv/app/current
user=laravel
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
stopwaitsecs=3600
Enter fullscreen mode Exit fullscreen mode

--max-time makes each worker exit periodically so Supervisor starts a fresh one, which picks up new code and releases memory. queue:restart in the deploy script tells running workers to finish their current job and exit.

6. The scheduler

* * * * * cd /srv/app/current && php8.4 artisan schedule:run >> /dev/null 2>&1
Enter fullscreen mode Exit fullscreen mode

Install it in the laravel user's crontab, not root's.

Option 2: deploy Laravel 13 with Hanoo

Hanoo runs the release flow above for you, on your own server, with every step visible in the dashboard.

Step 1: connect your server

Run the one-time install command from Connect server on a clean Ubuntu 22.04, 24.04 or 26.04 LTS server. The agent connects out over mutually authenticated TLS — no inbound management port, no root password shared — and you approve the server in the dashboard. Laravel applications need at least 1 CPU core and 2 GB of RAM.

Step 2: create the application from your repository

Choose Create application, select Laravel and point it at your repository on GitHub, GitLab or Bitbucket. The server generates a read-only deploy key; you add its public half to the repository, and the private half never leaves your server. Pick PHP 8.4 (8.3–8.5 are available) and tick the database option if you want a private MariaDB database.

Hanoo creates an isolated Linux user and PHP-FPM pool for the application, and writes its .env from your .env.example — with APP_ENV=production, APP_DEBUG=false, APP_URL set to the application's address, the database credentials filled in when you asked for a database, and a freshly generated APP_KEY. That file lives in the application's shared directory and is linked into every release.

Step 3: what happens on every deploy

Each deploy:

  1. Fetches the selected commit into a new, immutable release.
  2. Detects your toolchain from the repository — composer.lock means Composer; pnpm-lock.yaml, package-lock.json or yarn.lock picks the JavaScript package manager — and installs checksum-verified versions of Composer and Node.js (20, 22 or 24).
  3. Runs composer install and your asset build as the application's own user.
  4. Links the shared .env and storage/ into the release and validates it.
  5. Runs your deploy pipeline — steps such as php artisan migrate --force, cache warm-up or queue restarts, grouped into build, before activation and after activation phases.
  6. Activates the release atomically, reloads PHP-FPM, and checks the site responds. A failed health check reverts to the previous release automatically.

Every step is timed and its output is shown, so a failed build tells you why without an SSH session. A failed build never touches the running site, and any retained release can be restored with one-click rollback.

Step 4: domain and HTTPS

Add app.example.com under the application's Domains tab. Hanoo checks that DNS points at your server, issues a Let's Encrypt certificate and switches the site over with zero downtime.

Step 5: queues and the scheduler

The default .env uses QUEUE_CONNECTION=sync, which runs jobs inside the web request. For real background processing:

  • Install Valkey from the server's Manage Services (it is Redis-compatible, so Laravel's redis driver works unchanged), or use the database queue driver. Then set QUEUE_CONNECTION and, if you use Valkey, CACHE_STORE and SESSION_DRIVER in .env over SSH as the application user.
  • Add a Supervisor program for the application running php8.4 artisan queue:work --sleep=3 --tries=3 --max-time=3600. Programs run as the application's user in its current release; you can start, stop and restart them and read their logs from the dashboard.
  • Add an entry under the application's Cron jobs tab running php8.4 artisan schedule:run every minute. You can run it on demand and see the latest result and output.

Production checklist for Laravel 13

  • APP_ENV=production and APP_DEBUG=false — debug mode leaks environment variables in error pages.
  • php artisan optimize after each deploy to cache config, routes, views and events.
  • OPcache enabled; with an atomic symlink layout, resolve paths with realpath so new releases are picked up.
  • Sessions, cache and queues on Valkey/Redis or the database — not file once you have more than one worker.
  • storage/ and .env outside the release directory, shared across releases.
  • Migrations that are backwards compatible with the previous release, so a rollback does not break.
  • Queue workers restarted on every deploy, with --max-time as a safety net.
  • Scheduler running every minute as the application user.
  • Off-server backups of the database and storage/app.

Frequently asked questions

What PHP version does Laravel 13 require?

Laravel 13 requires PHP 8.3 or newer and supports PHP 8.3, 8.4 and 8.5. PHP 8.4 is a sensible production default today.

How do I deploy Laravel without downtime?

Build each release in its own directory, share .env and storage/ between releases, run migrations before the switch, then atomically repoint a current symlink at the new release and reload PHP-FPM. Hanoo does exactly this on every deploy and reverts automatically if the new release fails its health check.

Do I need Laravel Forge or a PaaS to host Laravel?

No. Any Ubuntu server with PHP-FPM, a web server, a database and Supervisor can run Laravel in production. Tools like Hanoo automate the setup and the release flow while the server and code stay entirely yours.

How do I run Laravel queue workers in production?

Run php artisan queue:work under a process supervisor such as Supervisor, as the application's user, with --max-time so workers recycle, and call php artisan queue:restart after each deploy so they load the new code.

How do I run the Laravel scheduler on a server?

Add one cron entry that runs php artisan schedule:run every minute from the application's current directory. Laravel decides which scheduled tasks are due on each run.

Can I use Valkey instead of Redis with Laravel?

Yes. Valkey is an open-source fork of Redis that speaks the same protocol, so Laravel's redis cache, session and queue drivers work with it unchanged.


Originally published on the Hanoo blog.

Top comments (0)