Deploying a Laravel application is more than copying PHP files to a server. A reliable production setup needs the correct PHP runtime, Nginx document root, environment variables, frontend assets, file permissions, database migrations, HTTPS, queues, and a repeatable release process.

This guide shows how to deploy a modern Laravel application with either Livewire or Inertia.js on Ubuntu Linux. The core server setup is the same for both stacks; the main differences are how frontend assets are built and whether an optional Inertia SSR process is required.

Deployment architecture

The production request flow will look like this:

  1. DNS points example.com to the Linux server.
  2. Nginx terminates HTTPS and serves files from Laravel's public directory.
  3. Dynamic requests are forwarded to PHP-FPM.
  4. Laravel connects to MySQL or PostgreSQL, cache, session storage, and queues.
  5. Vite-built CSS and JavaScript are served from public/build.
  6. Livewire handles component updates through Laravel routes, while Inertia renders Vue, React, or Svelte pages from the same Laravel application.

If Inertia server-side rendering is enabled, a separate Node.js SSR process runs in the background. It is optional and should not be added unless the project already uses SSR.

1. Check the application requirements

Before installing packages, inspect composer.json, package.json, and the lockfiles. The server must satisfy the versions required by the project rather than an arbitrary tutorial.

Laravel 13 requires PHP 8.3 or newer. A typical application also needs Ctype, cURL, DOM, Fileinfo, Mbstring, OpenSSL, PDO, Session, Tokenizer, and XML extensions. Add database-specific and application-specific extensions such as MySQL, PostgreSQL, Redis, GD, or Imagick when the project uses them.

cat composer.json
cat package.json
php -v
composer --version
node --version
npm --version

This guide uses Ubuntu 24.04, Nginx, PHP 8.3-FPM, and MySQL as the example. Replace the database and PHP packages when your application requires something different.

2. Prepare the Ubuntu server

sudo apt update
sudo apt upgrade -y

sudo apt install -y nginx git unzip curl composer mysql-server php8.3-fpm php8.3-cli php8.3-common php8.3-mysql php8.3-curl php8.3-mbstring php8.3-xml php8.3-bcmath php8.3-zip php8.3-gd

For PostgreSQL, install postgresql and php8.3-pgsql instead of the MySQL packages. If the project uses Redis for cache, sessions, or queues, install Redis and the matching PHP extension.

Install a current Node.js release for Vite builds. If you enable Inertia v3 SSR, use Node.js 22 or newer, as required by the current Inertia SSR documentation.

3. Create a deployment user and application directory

Avoid performing routine releases as root. Use a dedicated unprivileged user and let Nginx/PHP-FPM access only the directories they need.

sudo adduser deploy
sudo usermod -aG www-data deploy
sudo mkdir -p /var/www/myapp
sudo chown deploy:www-data /var/www/myapp

Clone the repository using a deploy key or another read-only repository credential:

sudo -u deploy git clone git@github.com:your-account/your-repository.git /var/www/myapp
cd /var/www/myapp

Never place Git credentials, database passwords, or application secrets inside the repository.

4. Install PHP and frontend dependencies

cd /var/www/myapp

composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

npm ci npm run build

composer install uses composer.lock, while npm ci uses the exact dependency tree in package-lock.json. Do not run composer update or npm update during production deployment because that changes dependency versions.

After the Vite build, confirm that the manifest exists:

test -f public/build/manifest.json && echo "Vite build is ready"

If your CI system builds assets, you can deploy the compiled output instead of installing Node.js on the web server. Choose one workflow and keep it consistent.

5. Configure the production environment

Create the production .env only on the server or inject its values through the deployment platform.

cp .env.example .env
nano .env

Use production-safe values:

APP_NAME="My Application"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com

LOG_CHANNEL=stack LOG_LEVEL=warning

DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=myapp DB_USERNAME=myapp DB_PASSWORD=use-a-strong-password

SESSION_SECURE_COOKIE=true QUEUE_CONNECTION=database

On the first deployment only, generate the application key:

php artisan key:generate --force
Do not regenerate APP_KEY on later deployments. Changing it invalidates encrypted cookies and may make existing encrypted application data unreadable.

6. Set safe file permissions

Laravel needs write access only to storage and bootstrap/cache. Do not solve permission errors with chmod -R 777.

sudo chown -R deploy:www-data /var/www/myapp

sudo find /var/www/myapp/storage /var/www/myapp/bootstrap/cache -type d -exec chmod 2775 {} ;

sudo find /var/www/myapp/storage /var/www/myapp/bootstrap/cache -type f -exec chmod 664 {} ;

The setgid bit on directories keeps new files in the www-data group, allowing both the deploy user and PHP-FPM to work with generated cache and log files.

7. Prepare the database and Laravel caches

php artisan migrate --force
php artisan storage:link
php artisan optimize

migrate --force explicitly allows migrations in production. Review migrations before deployment and back up important data. Laravel's optimize command caches production configuration, events, routes, and views where supported.

After caching configuration, changes to .env do not take effect until the configuration cache is rebuilt:

php artisan optimize:clear
php artisan optimize

8. Configure Nginx correctly

Create a site configuration:

sudo nano /etc/nginx/sites-available/myapp

Use Laravel's public directory as the document root. Never point Nginx at the project root because that can expose .env, source code, and other sensitive files.

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

root /var/www/myapp/public;
index index.php;
charset utf-8;

add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";

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

location = /favicon.ico {
    access_log off;
    log_not_found off;
}

location = /robots.txt {
    access_log off;
    log_not_found off;
}

error_page 404 /index.php;

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

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

}

Enable the site and verify the configuration before reloading Nginx:

sudo ln -s /etc/nginx/sites-available/myapp   /etc/nginx/sites-enabled/myapp

sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx sudo systemctl enable --now nginx php8.3-fpm

9. Livewire deployment notes

Livewire runs through Laravel and does not require a separate Node.js process in production. The regular Nginx try_files rule must forward Livewire update routes to public/index.php.

Use this checklist for a Livewire application:

  • Run the normal Vite production build when the project has CSS or JavaScript assets.
  • Keep APP_URL and HTTPS settings correct so generated asset and update URLs use the public domain.
  • Do not cache Livewire POST/update responses at Nginx or a CDN.
  • Make sure session storage is writable and shared if the application runs on multiple servers.
  • Rebuild Laravel caches after changing routes or configuration.

If Livewire components render but actions return 404, inspect the browser Network panel and verify that Nginx forwards the request to Laravel. If requests return 419, check the session driver, cookie domain, HTTPS cookie configuration, CSRF token, and system time.

10. Inertia.js deployment notes

A standard Inertia application does not require a persistent Node.js process. Laravel handles requests, while the compiled Vue, React, or Svelte application is served from public/build.

npm ci
npm run build
php artisan optimize

Laravel's Vite integration generates hashed assets, and Inertia can detect asset version changes so a user receives a full-page visit when a new frontend bundle is deployed.

If pages load but show a blank screen, check:

  • public/build/manifest.json exists.
  • The Vite build completed without errors.
  • Nginx can read public/build.
  • APP_URL uses the correct HTTPS domain.
  • The browser console does not show missing JavaScript chunks or mixed-content errors.

Optional Inertia SSR

Enable SSR only when the application is already configured for it. Inertia v3 SSR requires Node.js 22 or newer. Build the client and SSR bundles:

npm ci
npm run build:ssr

Then run the SSR server as a monitored background process. Confirm the exact Artisan command supported by the installed Inertia Laravel adapter:

php artisan list | grep inertia
php artisan inertia:start-ssr

Use Supervisor or systemd so the SSR process starts after reboot and restarts after a crash. If SSR is not configured in the project, skip this section; regular Inertia client-side rendering works without it.

11. Run queues with Supervisor

Queue workers are long-running processes and must be monitored. Install Supervisor:

sudo apt install -y supervisor
sudo nano /etc/supervisor/conf.d/myapp-worker.conf
[program:myapp-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/myapp/artisan queue:work --sleep=3 --tries=3 --timeout=90
directory=/var/www/myapp
user=deploy
numprocs=1
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/www/myapp/storage/logs/worker.log
stopwaitsecs=3600
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status

After each release, reload long-running Laravel services so they use the new code. Current Laravel versions provide:

php artisan reload

If your installed Laravel version does not provide that command, use the service-specific restart command such as php artisan queue:restart.

12. Configure the scheduler

Laravel's scheduler needs one cron entry:

sudo crontab -u deploy -e
* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1

Use absolute paths if the cron environment cannot find PHP. Check scheduled tasks with php artisan schedule:list.

13. Enable HTTPS

After DNS points to the server and HTTP works, install Certbot:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run

Allow only the required firewall services:

sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw enable

Once HTTPS is active, confirm APP_URL uses https://, keep APP_DEBUG=false, rebuild configuration cache, and test login, forms, uploads, Livewire actions, and Inertia navigation.

14. Use a repeatable deployment workflow

A simple update deployment can run the expensive dependency and asset steps before entering maintenance mode:

cd /var/www/myapp

git pull --ff-only

composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

npm ci npm run build

php artisan down --retry=60 php artisan migrate --force php artisan optimize php artisan reload php artisan up

For Inertia SSR, restart its monitored process after building the SSR bundle. For high-traffic applications, use timestamped release directories and an atomic symlink switch, but the workflow above is an appropriate starting point for a single-server deployment.

15. Verify the release

curl -I https://example.com
curl -I https://example.com/up

sudo systemctl status nginx php8.3-fpm sudo supervisorctl status

tail -f storage/logs/laravel.log sudo tail -f /var/log/nginx/error.log

Modern Laravel applications expose a health route at /up by default. Use it with an uptime monitor, load balancer, or deployment check.

Common deployment errors

  • 502 Bad Gateway: PHP-FPM is stopped or the Nginx socket path uses the wrong PHP version.
  • 403 Forbidden: the Nginx root or directory permissions are incorrect.
  • 500 with permission errors: PHP-FPM cannot write to storage or bootstrap/cache.
  • Vite manifest not found: run npm ci && npm run build and deploy public/build.
  • Livewire actions return 404: verify the Nginx try_files rule and Laravel route cache.
  • 419 Page Expired: check sessions, cookies, HTTPS, CSRF, and server time.
  • Inertia pages use old JavaScript: rebuild Vite assets, deploy the new manifest, and confirm asset versioning.
  • Queue jobs use old code: reload or restart workers after deployment.
  • Environment changes are ignored: clear and rebuild Laravel's configuration cache.

Production checklist

  1. Nginx serves only Laravel's public directory.
  2. The PHP version and extensions satisfy composer.json.
  3. APP_ENV=production and APP_DEBUG=false.
  4. The existing APP_KEY is preserved across deployments.
  5. Composer and npm install from lockfiles.
  6. Vite assets and the manifest are present.
  7. Only Laravel's writable directories are group-writable.
  8. Migrations, caches, queues, and scheduler are handled during releases.
  9. HTTPS renewal is tested.
  10. Livewire updates or Inertia navigation are tested after deployment.
  11. The public page and /up health route return HTTP 200.
  12. Database backups and restoration are tested.
Livewire and Inertia share the same Laravel production foundation. Deploy PHP, Nginx, storage, database, and Vite correctly first; then add only the stack-specific process your application actually uses.

References