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

Source: https://hanoo.io/blog/deploy-laravel-13-production-ubuntu · Published 2026-10-16 · Updated 2026-10-16 · 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.

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

```text
/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:

```bash
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:

```bash
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:

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

```cron
* * * * * 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.
