Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
DevOpsLaravelMySQLPHPTutorial

How to deploy Laravel 13 on Ubuntu 24.04 with nginx, PHP-FPM, MySQL and Redis

Laravel 13 Ubuntu deployment with nginx, PHP-FPM, MySQL and Redis: private data services, persistent storage, queue workers and recovery checks.

Cloudsoft Zone

9 min read

Laravel 13 on Ubuntu: Production deployment. Sourced Laravel 13, Ubuntu 24.04, nginx artwork with article-specific deployment checkpoints.
Image: Cloudsoft Zone layout; Laravel / Public domain; Canonical Ltd / Public domain; Igor Sysoev / Public domain
In this article

A Laravel application can return a healthy page while its queue worker is stopped, its scheduler has never run and uploaded files will disappear during the next deployment. Getting PHP to execute is only one part of putting an application on a VPS.

This guide walks through Laravel 13 Ubuntu deployment with nginx, PHP-FPM, MySQL and Redis. The target is a single Ubuntu 24.04 server with a non-root deployment account, private database services, persistent application data and supervised background work. It is a conventional PHP-FPM deployment, not Octane or a high-availability cluster.

The configuration is researched against official documentation on 10 October 2026. Run the walkthrough on an isolated Ubuntu staging server before applying it to retained data. Package patches, the application lockfile and FPM pool names must be recorded from that actual run; a successful macOS PHP command is not evidence that the Ubuntu deployment works.

Laravel deployment topology: public nginx routes to PHP-FPM; queue workers and the application use private MySQL and Redis.
Keep database services private and observe web, queue and scheduler behaviour separately.

Step 1: Establish the server and version boundary

Use a fresh Ubuntu 24.04 VM, a domain you control, console recovery access and an administrator account with sudo. Keep administration separate from the application identity. The commands below assume a deployment user named deploy and Ubuntu's default PHP-FPM worker group, www-data. Create these identities deliberately; do not run Composer or your queue worker as root.

Laravel 13 requires PHP 8.3 or later. Ubuntu's Noble package index includes PHP 8.3 FPM, so this walkthrough uses the distribution's 8.3 package family without adding an unofficial package repository. Check supported versions and your dependencies before choosing a newer PHP line. Mixing CLI 8.5 with FPM 8.3 can leave migrations working while web requests fail.

Shell
sudo apt update
apt-cache policy php8.3-fpm mysql-server redis-server nginx
sudo apt install nginx php8.3-fpm php8.3-cli php8.3-mysql \
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip \
  php8.3-redis php8.3-opcache mysql-server redis-server \
  supervisor git unzip acl
php8.3 -v
php8.3 -m
sudo systemctl status nginx php8.3-fpm mysql redis-server --no-pager

Install Composer using its official verified installer or your approved package source, and record composer --version. The operating system owns security patches for its package build; do not compare only the upstream patch number without considering distribution backports.

Build JavaScript assets in CI with a pinned supported Node release and the project's committed package-manager lockfile. Transfer the resulting artifact rather than turning the web server into an uncontrolled build machine. If building on staging instead, use the same lockfile and runtime as CI.

Step 2: Create a release directory without giving PHP ownership of the code

A useful layout separates a replaceable code release from retained state:

Text
/srv/laravel-app/
  releases/20261010-01/    application code and vendor dependencies
  shared/.env            private configuration
  shared/storage/        logs, sessions and uploaded files
  current -> releases/20261010-01

As administrator, create the directories and give the deployment account control of the code area:

Shell
sudo install -d -o deploy -g www-data -m 2750 /srv/laravel-app
sudo install -d -o deploy -g www-data -m 2750 /srv/laravel-app/releases
sudo install -d -o deploy -g www-data -m 2750 /srv/laravel-app/shared

Deploy a reviewed commit into a new release directory. Your artifact or checkout must supply the complete application, including its lockfiles. Avoid embedding a private Git token in a URL or copying your laptop's .env to the server. Verify the release path before changing ownership or permissions.

PHP needs to write storage and bootstrap/cache, not the entire application. Prepare shared/storage from the application's expected directory structure and link it into the release. Use a group or ACL policy that lets both the deployment account and PHP create files there; do not use chmod -R 777. For a first release, after placing code and establishing the storage link:

Shell
sudo setfacl -R -m u:deploy:rwx,u:www-data:rwx /srv/laravel-app/shared/storage
sudo setfacl -R -d -m u:deploy:rwx,u:www-data:rwx /srv/laravel-app/shared/storage
sudo setfacl -R -m u:deploy:rwx,u:www-data:rwx /srv/laravel-app/releases/20261010-01/bootstrap/cache
sudo setfacl -R -d -m u:deploy:rwx,u:www-data:rwx /srv/laravel-app/releases/20261010-01/bootstrap/cache

Apply these only to the named writable directories. Readable source files and traversable parents are still necessary for FPM; verify the full path with namei -l. A later deployment must recreate the cache-directory permission policy for its new release.

Release directory layout separates replaceable code from shared configuration and storage, with a current symlink.
Storage survives a release switch. A code rollback cannot undo every database change.

Step 3: Provision the database with separate runtime and migration responsibilities

Keep MySQL and Redis off the public network. Verify their bind addresses and host firewall rules instead of assuming package defaults. Use a MySQL account restricted to this application's schema and connection origin. A runtime account can commonly be limited to SELECT, INSERT, UPDATE and DELETE, while a separate migration account temporarily needs schema-change privileges. The precise privilege set depends on the application's real migrations and features.

Create the schema and accounts through an administrator session or your secret-management workflow. Do not paste generated passwords into a tutorial command, shell history or CI log. Avoid granting privileges on every database. Run migrations with the intended deployment identity, then restore the runtime credentials before starting long-lived processes.

Redis serves the queue and optionally cache or sessions. Those uses have different durability requirements: losing a cache entry is different from losing a queued task. For a shared instance, choose memory limits and eviction policy with queue data in mind. Do not expose an unauthenticated Redis endpoint just because the application is on the same VPS.

Step 4: Supply production configuration and install the locked dependencies

Create the private shared environment file with restrictive permissions, then link it into the release. Use the chosen database and Redis credentials privately:

INI
APP_ENV=production
APP_DEBUG=false
APP_URL=https://app.example.com
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel_runtime
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=redis
REDIS_HOST=127.0.0.1

These are tutorial-server values, not Cloudsoft Zone's local infrastructure addresses. Supply APP_KEY, database password and Redis authentication privately. Generate an application key once for a new environment. Rotating the key on every deployment can invalidate encrypted data and cookies; a deployment should reuse the established key.

Run dependency commands as the deployment user in the release directory:

Shell
cd /srv/laravel-app/releases/20261010-01
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
composer check-platform-reqs --no-dev
php8.3 artisan about
php8.3 artisan migrate:status

Composer installation scripts are application code: review them rather than blindly disabling all hooks or running them with root privileges. Install from the lockfile; composer update belongs in a reviewed dependency change, not a live release.

For a new empty staging database, apply the reviewed migrations with the migration credentials using php8.3 artisan migrate --force. On a retained database, first prove backup restoration and understand each migration's compatibility and locking behaviour. The flag suppresses a prompt; it does not make destructive SQL safe.

Step 5: Route nginx only to the public entry point

Save a site configuration under /etc/nginx/sites-available/laravel-app. Replace the hostname and confirm the real socket in your FPM pool configuration. This initial HTTP block is for controlled staging or certificate bootstrapping; finish TLS before accepting production credentials.

Nginx
server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;
    root /srv/laravel-app/current/public;
    index index.php;
    charset utf-8;

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

    location ~ ^/index\.php(/|$) {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ \.php$ { return 404; }
    location ~ /\.(?!well-known).* { deny all; }
}

Only the front controller may execute as PHP. Pointing the document root at the repository instead of public risks exposing configuration. $realpath_root resolves the current release path before handing the script to FPM; test it with your actual symlink layout.

After creating the current link and enabling this site:

Shell
sudo nginx -t
sudo systemctl reload nginx
curl -I -H 'Host: app.example.com' http://127.0.0.1/up
curl -I -H 'Host: app.example.com' http://127.0.0.1/.env

A normal health route should succeed, while .env must never return its contents. Ensure the default site does not unintentionally serve the wrong application. Configure a trusted TLS certificate and test renewal using the certificate tool's official instructions; see our nginx HTTPS guide.

Step 6: Start queues and the scheduler as explicit services

A worker must outlive your SSH session. An example Supervisor program is:

INI
[program:laravel-app-worker]
directory=/srv/laravel-app/current
command=/usr/bin/php8.3 artisan queue:work redis --queue=default --sleep=3 --tries=3 --timeout=60
user=deploy
numprocs=1
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=120
redirect_stderr=true
stdout_logfile=/srv/laravel-app/shared/storage/logs/worker.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5

The numbers describe one small-job example, not universal tuning. Configure the queue connection's retry interval above the worker timeout and leave enough shutdown time for a valid running job. Check application-specific timeouts and idempotency before increasing attempts or process counts.

Shell
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status laravel-app-worker

Add the scheduler entry to the deployment user's crontab, not an unrelated system account:

cron
* * * * * cd /srv/laravel-app/current && /usr/bin/php8.3 artisan schedule:run >> /srv/laravel-app/shared/storage/logs/scheduler.log 2>&1

Use rotation and retention for this log. Confirm schedule:list, a due task and its observable effect; the presence of a cron line is not proof of execution. Tasks that run on multiple servers require deliberate overlap and single-server locking decisions.

Step 7: Cache, verify and retain a recovery path

Run the application's reviewed optimisation step in the release, then reload or restart long-lived processes through the documented release procedure. A cached configuration means arbitrary calls to env() in business code may no longer read the file as expected. Configuration values should be accessed through Laravel's configuration layer.

Deployment acceptance requires browser and security checks, database and background work, and a rehearsed recovery path.
Use observable outcomes to accept the deployment instead of relying on running-process labels.

Check Evidence to collect
Browser HTTPS route, authentication, assets and one representative user workflow
Database A synthetic read/write through the application with the runtime identity
Queue A dispatched job reaches its recorded completion once
Scheduler A due task produces its expected effect
Uploads A private upload remains available after switching code releases
Exposure Database/Redis not public; hidden files inaccessible; debug off
Recovery A backup restores into an isolated environment and a previous compatible release works

Keep the previous release artifact and the shared files. Rolling application code back does not automatically roll back database writes or destructive migrations. If compatibility was broken, use the rehearsed recovery procedure rather than guessing with migrate:rollback.

For repeat deployments, use the Laravel production checklist and the separate zero-downtime guide when it is available. If you need a scoped deployment review or maintenance plan, discuss your application with Cloudsoft Zone.

Featured image: Cloudsoft Zone explanatory layout with sourced product artwork: laravel — Laravel (Public domain); ubuntu — Canonical Ltd (Public domain); nginx — Igor Sysoev (Public domain). Product marks resized and arranged for identification; no endorsement implied. Layout: CC BY-SA 4.0.

Frequently asked questions

  • Can I use Ubuntu's PHP 8.3 packages for Laravel 13?

    Laravel 13's documented minimum is PHP 8.3. Confirm your lockfile, required extensions and the distribution package build, then check both CLI and FPM.

  • Why not give PHP ownership of the whole application?

    The request process generally needs to write storage and bootstrap/cache. Keeping source deployment-owned reduces the consequences of a compromised request process.

  • Does switching the previous release undo migrations?

    No. Code rollback only works while the database and shared state remain compatible. Destructive changes need a separate reviewed recovery plan.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Laravel 13 deployment — Laravel
  2. Laravel 13 queues — Laravel
  3. Laravel 13 task scheduling — Laravel
  4. Ubuntu PHP installation — Canonical
  5. Ubuntu Noble PHP 8.3 FPM package — Canonical
  6. Composer CLI — Composer
  7. Supervisor configuration — Supervisor

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.