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 onstorage/or a missingAPP_KEY. - “The stream or file … could not be opened” —
storage/logsnot writable bywww-data. - Changes in
.envignored — the config is cached; runphp artisan optimizeagain (orconfig:clear). - 419 Page Expired — session/CSRF issue: check
APP_URL,SESSION_DOMAINand 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.