[ Back to log ]

Laravel deployment guide: from scratch to production

Laravel

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.