Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Deploying Laravel on a VPS means preparing a Linux server, installing a web server and the PHP runtime, uploading your application, configuring its environment, and securing and supervising the processes that keep it running. This guide uses Ubuntu with Nginx, PHP-FPM, Composer and a relational database. It covers Laravel 12 and 13, including production caching, HTTPS, queues and a safer release workflow.

Use PHP 8.2 or newer for Laravel 12. Laravel 13 requires PHP 8.3 or newer. Confirm the Laravel major version in your application’s composer.json before choosing the PHP package.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What you need before deploying

Have these items ready before changing the server:

  • An Ubuntu VPS with a public IPv4 address, a non-root sudo user and a DNS record pointing your domain to that address.
  • Your Laravel application’s Git repository and a production-ready composer.json.
  • Database credentials and a database server, either on the VPS or on a separate private service.
  • SSH access and enough disk space for the operating system, PHP packages, Composer dependencies, logs, uploads and database backups.
  • A plan for queues, scheduled tasks, storage and backups. A web request alone does not run queued jobs or Laravel’s scheduler.

For a small application, one VPS can run Nginx, PHP-FPM, a database, Redis and queue workers. Separating the database or cache later is easier if connection details are kept in environment variables from the beginning.

Match PHP to your Laravel release

Laravel release Minimum PHP version Required extension families
Laravel 12 PHP 8.2 or higher Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer and XML
Laravel 13 PHP 8.3 or higher Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer and XML

Applications commonly need additional drivers such as pdo_mysql or pdo_pgsql, plus zip, intl, gd, opcache or a Redis client. Check the application’s dependency constraints rather than installing an arbitrary PHP version.

Prepare Ubuntu and a deployment user

Log in as the sudo-enabled user, not as root for routine application work. Replace deploy and example.com with your own values.

ssh deploy@SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx unzip git curl ca-certificates supervisor

Keep SSH protected with keys, disable password authentication after confirming key login, and restrict the firewall to the services you actually use. A typical web server permits SSH, HTTP and HTTPS:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Do not expose a database port publicly when the application and database share the VPS. If the database is remote, allow access only from the VPS address and use TLS where the provider supports it.

Install PHP-FPM and Laravel extensions

Install the PHP version selected for your Laravel major release. Package names differ by Ubuntu release; the following example uses PHP 8.3. If your application requires PHP 8.2, substitute that version consistently in every package and service name.

sudo apt install -y php8.3-fpm php8.3-cli php8.3-common 
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip 
  php8.3-bcmath php8.3-intl php8.3-gd php8.3-mysql 
  php8.3-opcache

For PostgreSQL, use php8.3-pgsql instead of, or in addition to, the MySQL driver. If the application uses Redis, install a compatible Redis PHP extension or configure the client specified by the application’s dependencies.

Verify the command-line and FPM versions. They should use the same major and minor release so Composer checks and web requests do not disagree:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
php -v
php -m
systemctl status php8.3-fpm --no-pager

If the VPS repository does not provide the required PHP release, use a maintained distribution repository appropriate to your Ubuntu version and review its security and update policy. Do not mix packages from unrelated PHP versions.

Install Composer safely

Composer resolves and installs Laravel’s PHP dependencies. Install it globally from the official Composer installer, verify the installer signature using the current value published by Composer, and place the executable on the system path. The exact signature changes, so do not copy an old hash from an unrelated tutorial.

cd /tmp
curl -sS https://getcomposer.org/installer -o composer-setup.php
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
composer --version

Run Composer as the deployment user inside the application directory. Running it as root can leave root-owned files that PHP-FPM cannot update later.

Install and configure the database

Choose MySQL or PostgreSQL according to the application’s configured driver. Install the server only if you are hosting the database on this VPS.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo apt install -y mysql-server
sudo mysql_secure_installation

Create a database and a dedicated account with a strong password. The exact SQL should use your own database name and a password generated for this application:

sudo mysql
CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'REPLACE_WITH_A_LONG_RANDOM_PASSWORD';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

For PostgreSQL, create a database and role with equivalent least-privilege access. Record the host, port, database, username and password for the production environment file. Schedule tested backups; a database on the same VPS is not protected from disk failure or an account compromise.

Place the Laravel application on the VPS

Create a directory outside the web server’s default document root. The public directory, not the project root, will be exposed by Nginx.

sudo mkdir -p /var/www/example.com
sudo chown -R deploy:www-data /var/www/example.com
cd /var/www/example.com
git clone YOUR_REPOSITORY_URL .

Use a deploy key or another non-interactive Git credential. Never commit .env, private keys, database passwords or application secrets. If the repository contains only an example environment file, copy it and edit the production version:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cp .env.example .env
nano .env

Set at least these values:

APP_NAME="Your Application"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
APP_KEY=
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel_user
DB_PASSWORD=REPLACE_WITH_THE_DATABASE_PASSWORD

Set the mail, cache, session, queue, filesystem and Redis variables required by your application. APP_DEBUG must be false in production; true exposes implementation details and secrets in error pages. Generate the encryption key only after the production .env is in place:

php artisan key:generate --force

Changing an existing application’s key invalidates encrypted data and sessions, so preserve the original production key during later deployments.

Install dependencies, permissions and database schema

From the project directory, install only production dependencies and build Composer’s optimized autoloader:

composer install --no-dev --optimize-autoloader

Laravel must be able to write to storage and bootstrap/cache. Keep the code owned by the deploy user while granting the PHP-FPM group access to writable directories:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com/storage /var/www/example.com/bootstrap/cache -type d -exec chmod 775 {} ;
sudo find /var/www/example.com/storage /var/www/example.com/bootstrap/cache -type f -exec chmod 664 {} ;

If your distribution runs PHP-FPM under a different account, use that account’s group instead of www-data. Avoid making the entire project world-writable. Confirm that uploaded files and logs can be created before putting traffic on the site.

Run migrations in production mode so Laravel does not pause for an interactive confirmation:

php artisan migrate --force

Only run seeders when your deployment process explicitly intends to modify production data. Review migration compatibility before deploying code that removes columns, renames fields or changes a type used by the currently running release.

Configure Nginx to serve Laravel’s public directory

Create a server block whose root is /var/www/example.com/public. The try_files directive sends requests that are not static files to Laravel’s front controller, while the FastCGI block passes PHP scripts to PHP-FPM.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example.com/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.3-fpm.sock;
    }

    location ~ /. {
        deny all;
    }
}

The dotfile rule prevents accidental exposure of .env and other hidden files. Keep the PHP-FPM socket aligned with the installed version. Enable and test the site before reloading Nginx:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Visit the domain over HTTP and confirm that Laravel renders a response. A 404 from Laravel is different from an Nginx 404: the former indicates the request reached the framework, while the latter often indicates an incorrect root or server-name match.

Cache Laravel for production

Laravel’s production guidance recommends caching configuration, events, routes and views. The single command below builds those caches:

php artisan optimize

Run it after changing environment values or application code. To remove generated caches while diagnosing a configuration problem, use:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
php artisan optimize:clear

Clear and rebuild caches deliberately. If configuration was cached before an environment variable was corrected, Laravel will continue using the cached value until the cache is cleared and regenerated.

Enable HTTPS with Let’s Encrypt

After DNS resolves to the VPS and the HTTP server block works, install Certbot and its Nginx integration:

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

Certbot can update the Nginx configuration and schedule renewal. Test the renewal path rather than assuming it works:

sudo certbot renew --dry-run

Use the HTTPS URL in APP_URL, enable secure cookies through your framework configuration, and check that any reverse proxy or load balancer forwards the original HTTPS scheme. Mixed-content warnings usually mean an asset URL or application URL still uses HTTP.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Run queues with Supervisor

A web request does not keep a queue worker alive. Use Supervisor to restart workers after a crash and to start them when the VPS boots. Install it with apt, then create a program configuration:

sudo nano /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/example.com/artisan queue:work --sleep=3 --tries=3 --timeout=90
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=deploy
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/example.com/storage/logs/worker.log
stopwaitsecs=3600

Choose numprocs, timeout and retry values for the jobs and VPS capacity. Long-running jobs need a worker timeout longer than the job’s maximum runtime, while Supervisor’s stop wait must allow a worker to finish gracefully. Apply the configuration:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status

Laravel workers keep the old application code in memory. After every successful release, tell them to exit after the current job so Supervisor starts fresh processes:

php artisan queue:restart
sudo supervisorctl status

If the application uses Horizon, supervise the Horizon process instead and use the process-management instructions appropriate to that installation. Do not run both an unmanaged worker and a Supervisor-managed worker for the same queue unless you intentionally want both capacities.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Schedule Laravel’s recurring tasks

Define scheduled commands in the application’s scheduler and add one system cron entry. The cron invokes Laravel every minute; Laravel decides whether a task is due:

* * * * * cd /var/www/example.com && php artisan schedule:run >> /dev/null 2>&1

Use the deploy user’s crontab with crontab -e, or configure a system-wide entry with an explicit user. Confirm the server timezone and application timezone so reports, billing and maintenance jobs run at the intended time.

Choose an in-place or zero-downtime deployment workflow

Simple in-place deployment

For a small site that can tolerate a brief maintenance window, update the existing directory in this order:

  1. Put the application into maintenance mode if a migration or code change is not backward compatible: php artisan down.
  2. Fetch the approved commit and install dependencies: git pull --ff-only, then composer install --no-dev --optimize-autoloader.
  3. Run compatible migrations with php artisan migrate --force.
  4. Refresh caches with php artisan optimize.
  5. Restart workers with php artisan queue:restart.
  6. Reload PHP-FPM if OPcache or process state requires it, then leave maintenance mode with php artisan up.

Keep migrations backward-compatible when old workers may still process jobs during the update. Add new columns before deploying code that writes them, and remove obsolete columns only after no running release needs them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Timestamped releases and a current symlink

For a busier application, deploy each release into a timestamped directory such as /var/www/example.com/releases/20261014-120000. Keep shared .env and persistent storage outside the release, link them into the new directory, install dependencies, run checks and migrations, then atomically point a current symlink at the release.

  1. Create the release directory and check out the exact commit.
  2. Link the shared environment file and storage directory.
  3. Run Composer, permission checks and any asset build required by the project.
  4. Run migrations after verifying compatibility with both the current and new code.
  5. Switch the current symlink in one filesystem operation.
  6. Reload PHP-FPM if needed, run php artisan optimize from the new release, and issue php artisan queue:restart.
  7. Retain a small number of previous releases for rollback, and remove old releases only after confirming workers and requests use the new path.

This pattern reduces the period in which files are mixed between versions. It does not make an incompatible database migration safe; database changes still need an expand-and-contract plan.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Verify the deployment

Run a short acceptance checklist from outside the server:

  • The domain resolves to the VPS and HTTPS presents a valid certificate.
  • The home page, authentication, forms and representative API routes return expected responses.
  • Static assets load from the public directory and do not expose source files or .env.
  • A test upload can be written to the configured filesystem and retrieved through its intended URL.
  • A test queued job is processed, and Supervisor shows workers as running.
  • The scheduler’s command appears in cron and produces no permission or PHP-version errors.
  • Application, Nginx, PHP-FPM and worker logs are being written and rotated.
  • The database backup job completes and a restore procedure has been tested separately.

Useful local checks include:

sudo nginx -t
systemctl is-active nginx php8.3-fpm supervisor
sudo supervisorctl status
tail -n 100 /var/www/example.com/storage/logs/laravel.log

Common deployment failures and fixes

502 Bad Gateway

Nginx cannot reach PHP-FPM. Check that PHP-FPM is running and that fastcgi_pass names the existing socket:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
systemctl status php8.3-fpm
ls -l /run/php/
sudo journalctl -u php8.3-fpm -n 100 --no-pager

“Permission denied” in storage or bootstrap/cache

Correct ownership and group permissions for those two directories, then retry the failing request or Artisan command. Do not solve this by changing the whole project to mode 777.

Composer reports an incompatible PHP version or missing extension

Compare php -v and php -m with the application’s Composer constraints and Laravel’s required extension list. Install the missing extension for the same PHP version used by both CLI and FPM, then restart PHP-FPM.

The application ignores updated environment values

Cached configuration is still active. Run php artisan optimize:clear, verify the production .env, then run php artisan optimize. Never print the full environment file into a public log while debugging.

Queued jobs still execute old code

Workers are long-lived. Run php artisan queue:restart after the release and verify Supervisor starts replacement processes. A worker can finish its current job before exiting, so allow time for graceful turnover.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Routes return 404 through Nginx

Confirm that the root ends in /public, the try_files fallback points to /index.php?$query_string, the intended server block is enabled, and sudo nginx -t passes. Check the Laravel route list with php artisan route:list.

HTTPS renewal or redirects fail

Check DNS for both hostnames, ensure ports 80 and 443 are reachable, inspect the active Nginx server blocks, and run sudo certbot renew --dry-run. A proxy in front of the VPS must forward the original scheme correctly or Laravel may generate HTTP URLs.

Production security and maintenance

  • Keep APP_DEBUG=false and store secrets outside Git.
  • Patch Ubuntu, Nginx, PHP, Composer dependencies and the database on a regular schedule.
  • Use least-privilege database accounts and firewall rules; do not expose administrative database services to the public internet.
  • Limit SSH to keys, remove unused accounts and review authentication logs.
  • Rotate application secrets only with a plan for encrypted records, sessions and integrations that depend on them.
  • Configure log rotation so application and worker logs cannot fill the disk.
  • Back up the database, user uploads and the shared environment file securely; test restoration on a separate system.
  • Monitor disk space, memory, PHP-FPM saturation, queue latency, failed jobs, certificate expiry and backup results.

Deployment command checklist

A repeatable release normally follows this sequence after code review and a backup check:

  1. git pull --ff-only or check out the approved release commit.
  2. composer install --no-dev --optimize-autoloader.
  3. Confirm writable storage and bootstrap/cache directories.
  4. php artisan migrate --force after checking migration compatibility.
  5. php artisan optimize.
  6. php artisan queue:restart.
  7. Reload PHP-FPM or Nginx only when configuration, code loading or OPcache requires it.
  8. Check the site, logs, workers, scheduled tasks and a representative user flow.

Once this process is scripted, use the timestamped-release approach for applications that cannot tolerate files changing during a request. For a small application, the in-place process is easier to operate, provided maintenance windows and rollback steps are explicit.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.