Skip to content

Laravel

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

Deploy a Laravel 13 app to an Ubuntu 24.04 server: PHP 8.4, zero-downtime releases, Composer and Vite builds, migrations, queue workers, the scheduler and HTTPS.

8 min read By the Hanoo team

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.

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:

PieceRecommended choiceWhy
PHP8.4 (8.3–8.5 supported)Laravel 13's minimum is 8.3; 8.4 is the mature, fast default
Web serverCaddy or Nginx with PHP-FPMServes public/, hands PHP to FPM, terminates TLS
DatabaseMariaDB / MySQL or PostgreSQLBound to 127.0.0.1 on a single-server setup
Cache, sessions, queuesValkey (Redis-compatible) or the databasefile and sync drivers do not scale past one request
AssetsNode.js 22 or 24 with npm, pnpm or Yarnvite build produces the files in public/build
ProcessesSupervisorKeeps queue:work running and restarts it on failure
SchedulerCron, every minuteRuns 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:

Output
/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

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:

Terminal
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

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:

Terminal
sudo adduser --system --group --home /srv/app laravel

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:

Nginx
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;
    }
}

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:

Terminal
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

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

INI
[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

--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

crontab
* * * * * cd /srv/app/current && php8.4 artisan schedule:run >> /dev/null 2>&1

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.