Laravel deployment guide: from scratch to production
1. Initial server setup
Start by getting the basic server packages in place.
Connect and update
SSH into your server and update your packages:
ssh user@your_server_ip
sudo apt update && sudo apt upgrade -y
See full guide on security here.
Configure the firewall
Lock down your ports so you don’t leave database or Redis ports open to the internet. We’ll also rate-limit SSH to slow down brute-force attempts.
sudo apt install ufw -y
sudo ufw limit 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status
limit on port 22 drops any IP that hits SSH more than six times in 30 seconds, enough to frustrate most brute-force scripts. Logging stays on too. The firewall log is the only place you’d spot someone probing your ports.
Install Nginx and Certbot
Grab Nginx and Certbot (for SSL later):
sudo apt install nginx certbot python3-certbot-nginx -y
2. Application setup
Clone the repository
Create a directory for your site and pull your code.
sudo mkdir -p /var/www/example.com
# Take ownership so you don't need sudo to run git
sudo chown -R $USER:$USER /var/www/example.com
cd /var/www/example.com
git clone <repository-url> .
Configure Nginx
Create a new Nginx config file:
sudo nano /etc/nginx/sites-available/example.com
Paste the following configuration (adapted from the official Laravel documentation). Be sure to update server_name and root to match your domain and path, and ensure the fastcgi_pass matches your PHP version (we will use PHP 8.3 as an example in the next step):
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
index index.php;
charset utf-8;
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:/var/run/php/php8.3-fpm.sock; # Update 8.3 to your PHP version
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:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
Install PHP and Composer
Install PHP 8.x (we’ll use 8.3 as an example) along with the extensions Laravel needs, plus Composer:
sudo apt install php8.3-fpm php8.3-curl php8.3-common php8.3-cli php8.3-mbstring php8.3-xml php8.3-zip php-bcmath php-curl composer -y
(If you use a different PHP version, replace 8.3 with your version).
3. Database setup
You don’t always need a heavy database server. SQLite is actually great for small to medium apps now. But if you need MariaDB, here’s how to set it up.
Option A: SQLite
Install SQLite:
sudo apt install php-sqlite3 sqlite3 -y
In your Laravel .env file, set the connection to sqlite and delete the other DB variables:
DB_CONNECTION=sqlite
Create the empty database file:
touch database/database.sqlite
Option B: MariaDB (MySQL)
Install MariaDB and the PHP MySQL extension (matching your PHP version):
sudo apt install mariadb-server php8.3-mysql -y
sudo mysql_secure_installation
Log into MariaDB:
sudo mysql
Create the database and user:
CREATE DATABASE laravel;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON laravel.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Update your .env file:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel_user
DB_PASSWORD=your_password
4. Laravel configuration
Set directory permissions
Nginx runs as www-data. It needs permission to write to your storage and cache directories:
sudo chown -R www-data:www-data /var/www/example.com/storage
sudo chown -R www-data:www-data /var/www/example.com/bootstrap/cache
(If you chose SQLite, give Nginx access to the database file too):
sudo chown -R :www-data /var/www/example.com/database
sudo chmod -R 775 /var/www/example.com/database
Install dependencies and migrate
Install your PHP packages, generate the app key, and run migrations:
composer install --no-dev --optimize-autoloader
cp .env.example .env
# Edit .env now to add your database credentials and set APP_ENV=production and APP_DEBUG=false
nano .env
php artisan key:generate
php artisan migrate --force
Your .env holds the database password and app key, so don’t leave it world-readable. Lock it down:
sudo chown $USER:www-data /var/www/example.com/.env
chmod 640 /var/www/example.com/.env
640 lets your deploy user edit it and www-data read it. Nobody else on the box gets in.
5. Queue setup (Supervisor)
If you dispatch jobs or send emails in the background, you can’t just run queue:work and close your terminal. Supervisor keeps that process alive.
Install Supervisor:
sudo apt install supervisor -y
Create a config file for your worker:
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
Add this configuration:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/example.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/example.com/storage/logs/worker.log
stopwaitsecs=3600
The worker runs as www-data, so its log goes in storage/logs, which that user already owns from the permissions step above.
Start the worker:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
6. Final configuration
Create a deployment script
You don’t want to run all these commands manually every time you push code. Create a quick bash script:
nano /var/www/example.com/update.sh
#!/bin/bash
cd /var/www/example.com
php artisan down
git pull
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
php artisan migrate --force
php artisan optimize:clear
php artisan optimize
php artisan queue:restart
php artisan up
Make it executable:
chmod +x /var/www/example.com/update.sh
Secure with SSL and reload Nginx
Get your SSL certificate:
sudo certbot --nginx -d example.com -d www.example.com
Test auto-renewal:
sudo certbot renew --dry-run
Reload Nginx:
sudo nginx -t && sudo systemctl reload nginx
Your app is live.