Websites & apps

Deploy a Laravel app on a Linux VPS with Nginx and PHP-FPM

Deploy Laravel to an Ubuntu VPS: PHP-FPM and extensions, Composer, Nginx server block, .env, permissions, queues with systemd, the scheduler and free SSL.

IntermediateLinux VPS3 min read

This guide shows how to deploy Laravel on a VPS the standard way: Nginx serving the public folder, PHP-FPM running the code, Composer for dependencies, and systemd keeping queue workers alive. Commands are for Ubuntu 22.04/24.04; AlmaLinux differs mainly in package names.

Before you start

  • A VPS with sudo access and a domain pointing to it.
  • A database (MySQL/MariaDB or PostgreSQL) and its credentials.
  • Your Laravel app in a Git repository.
  • For cPanel hosting, follow the cPanel Laravel guide instead.

Install PHP, Nginx and Composer

apt update && apt install nginx git unzip php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath php8.3-intl php8.3-gd

Check your Laravel version's PHP requirement first; install a newer PHP from the ondrej PPA if needed. Install Composer:

curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

Get the code

mkdir -p /var/www/app && chown deploy:www-data /var/www/app
sudo -u deploy git clone git@github.com:you/app.git /var/www/app
cd /var/www/app && sudo -u deploy composer install --no-dev --optimize-autoloader

Configure .env

sudo -u deploy cp .env.example .env && sudo -u deploy php artisan key:generate

Edit .env:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=strong-password

Danger: never run production with APP_DEBUG=true. Error pages would reveal your environment, including database passwords.

Run migrations and cache the config:

php artisan migrate --force && php artisan config:cache && php artisan route:cache && php artisan view:cache

Set permissions

Only storage and bootstrap/cache must be writable by PHP:

chown -R deploy:www-data storage bootstrap/cache && chmod -R ug+rwX storage bootstrap/cache

Create the public storage link if the app uses it: php artisan storage:link.

Configure Nginx

Create /etc/nginx/sites-available/app:

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/app/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;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }

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

The root must point at public, never the project root, or .env could be downloaded. Enable it:

ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx

Add SSL: apt install certbot python3-certbot-nginx && certbot --nginx -d example.com -d www.example.com.

Run queue workers with systemd

Create /etc/systemd/system/laravel-worker.service:

[Unit]
Description=Laravel queue worker
After=network.target

[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always

[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now laravel-worker

Enable the scheduler

Add one cron entry for the deploy user (crontab -u deploy -e):

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

Deploy updates

cd /var/www/app && php artisan down && git pull && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan optimize && php artisan up && systemctl restart laravel-worker && systemctl reload php8.3-fpm

Reloading PHP-FPM clears OPcache so new code is served; restarting workers loads the new code into queues.

Troubleshooting

  • 500 error with a blank page: read storage/logs/laravel.log. Most often it is permissions on storage or a missing APP_KEY.
  • The stream or file laravel.log could not be opened: PHP-FPM (www-data) cannot write storage. Fix ownership as above.
  • Config changes ignored: the config is cached. Run php artisan config:clear or cache again.
  • 404 on every route except /: the try_files line is missing.
  • Jobs not processed: check systemctl status laravel-worker and QUEUE_CONNECTION in .env.

Laravel deployment FAQ

Do I need Laravel Forge or Envoyer?

No. They automate these same steps; doing it by hand once helps you understand and debug them.

Can Laravel run on cPanel hosting?

Yes. Point the domain's document root to public and use the cPanel Laravel guide.

Should I use Redis for cache and queues?

For busy apps, yes: set CACHE_STORE=redis and QUEUE_CONNECTION=redis after installing Redis and the PHP extension.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.