
How to Deploy Laravel with Livewire or Inertia.js on Ubuntu Linux
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:
- DNS points
example.comto the Linux server. - Nginx terminates HTTPS and serves files from Laravel's
publicdirectory. - Dynamic requests are forwarded to PHP-FPM.
- Laravel connects to MySQL or PostgreSQL, cache, session storage, and queues.
- Vite-built CSS and JavaScript are served from
public/build. - 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_URLand 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.jsonexists.- The Vite build completed without errors.
- Nginx can read
public/build. APP_URLuses 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
storageorbootstrap/cache. - Vite manifest not found: run
npm ci && npm run buildand deploypublic/build. - Livewire actions return 404: verify the Nginx
try_filesrule 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
- Nginx serves only Laravel's
publicdirectory. - The PHP version and extensions satisfy
composer.json. APP_ENV=productionandAPP_DEBUG=false.- The existing
APP_KEYis preserved across deployments. - Composer and npm install from lockfiles.
- Vite assets and the manifest are present.
- Only Laravel's writable directories are group-writable.
- Migrations, caches, queues, and scheduler are handled during releases.
- HTTPS renewal is tested.
- Livewire updates or Inertia navigation are tested after deployment.
- The public page and
/uphealth route return HTTP 200. - 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.