Laravel runs beautifully on a plain VPS: Nginx, PHP-FPM, MySQL or MariaDB, Redis for cache and queues, and a couple of background processes. This guide deploys an existing Laravel project from Git to a fresh Ubuntu server.

1. Install the stack

sudo apt update
sudo apt install -y nginx mariadb-server redis-server git unzip \
  php-fpm php-cli php-mysql php-mbstring php-xml php-bcmath php-curl \
  php-zip php-intl php-gd php-redis
php -v

Check that the PHP version satisfies your Laravel version’s requirement (Laravel 11 and 12 need PHP 8.2+). Install Composer as described in installing Composer.

2. Database

sudo mariadb
CREATE DATABASE laravel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON laravel.* TO 'laravel'@'localhost';
EXIT;

3. Get the code

Use a deploy user that owns the code, with the web server in its group:

sudo adduser deploy
sudo usermod -aG www-data deploy
sudo mkdir -p /var/www/laravel && sudo chown deploy:www-data /var/www/laravel
sudo -u deploy git clone [email protected]:you/app.git /var/www/laravel
cd /var/www/laravel
sudo -u deploy composer install --no-dev --optimize-autoloader

4. 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=laravel
DB_USERNAME=laravel
DB_PASSWORD=a-long-random-password

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1

APP_DEBUG=false is critical: debug pages expose environment variables, including passwords.

sudo chmod 640 .env
sudo -u deploy php artisan migrate --force
sudo -u deploy php artisan storage:link

5. Permissions

Laravel writes to storage/ and bootstrap/cache/. Give the web server group write access there only:

sudo chown -R deploy:www-data /var/www/laravel
sudo find /var/www/laravel -type d -exec chmod 750 {} +
sudo find /var/www/laravel -type f -exec chmod 640 {} +
sudo chmod -R ug+rwX /var/www/laravel/storage /var/www/laravel/bootstrap/cache

6. Nginx

The document root is the public/ directory — never the project root.

server {
    listen 80;
    server_name example.com;
    root /var/www/laravel/public;

    index index.php;
    charset utf-8;
    client_max_body_size 20M;

    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;
    }
}

Only index.php is executed — other .php files in public/ aren’t. $realpath_root makes symlink-based releases work correctly.

sudo ln -s /etc/nginx/sites-available/laravel /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com

7. Optimise for production

sudo -u deploy php artisan optimize      # config, events, routes and views caches

Re-run after every deploy. Also enable OPcache in PHP (on by default in Ubuntu’s PHP-FPM) — see PHP OPcache.

8. Queue workers with Supervisor

sudo apt install -y supervisor

/etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/laravel/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
user=deploy
numprocs=2
autostart=true
autorestart=true
stopwaitsecs=3600
redirect_stderr=true
stdout_logfile=/var/www/laravel/storage/logs/worker.log
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl status

Details in Laravel queues with Supervisor.

9. The scheduler

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

See Laravel scheduler and cron.

10. Deploy updates

#!/bin/bash
set -euo pipefail
cd /var/www/laravel
php artisan down --retry=30
git pull --ff-only
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan optimize
php artisan queue:restart
php artisan up

queue:restart makes workers finish their current job and reload the new code. For zero-downtime deploys, tools like Deployer or Envoy build each release in a new directory and switch a symlink.

Troubleshooting

  • 500 error, blank page — check storage/logs/laravel.log; usually permissions on storage/ or a missing APP_KEY.
  • “The stream or file … could not be opened” — storage/logs not writable by www-data.
  • Changes in .env ignored — the config is cached; run php artisan optimize again (or config:clear).
  • 419 Page Expired — session/CSRF issue: check APP_URL, SESSION_DOMAIN and that HTTPS is detected (trusted proxies if behind a load balancer).

A Laravel app with MariaDB and Redis runs well on 2 GB of RAM. Our Ubuntu VPS plans give you root access to tune PHP-FPM, OPcache and workers yourself.

  • Laravel
  • PHP
  • Nginx