Deploying a Laravel Site on a Fresh Linux Server

A practical walkthrough covering user setup, PHP-FPM, Nginx, MariaDB, Supervisor, and a deploy script — everything needed to get a Laravel application running in production.

These are my notes for setting up a PHP Laravel site on a fresh Linux server (Ubuntu, but the instructions work for Debian as well).

I want you to get from a standing start to a running application and understand what all of these bits do. If you go with one of the magical all-in-one systems out there later (forge), that's fine, but you should know what is going on under the hood. The goal here is to walk your through all of the steps involved in going from a blank server to a fully deployed Laravel application. I am assuming that your code is hosted on Github and that you are signed in to your new server as `root`. That's all you need to get started, a server you have root access to and a Laravel application in a Github repository.

In practical terms, we want to create a dedicated system user that will only own and manage the Laravel application files. This user will not have any sudo privileges except for the ability to reload PHP-FPM. This way, if the server is ever compromised, the attacker will only have access to that one user and won't be able to do things like read other users' files or modify the system configuration. We'll also set up a deploy key for GitHub so the server can pull code without needing a password, and we'll use Supervisor to keep the Laravel queue worker running in the background. Finally, we'll write a simple deploy script that handles pulling the latest code, installing dependencies, running migrations, and reloading services with zero downtime.

I have tried to be as practical as possible in my directions, as always, I am writing this with 20 years of Linux experience under me, if I am making assumptions in these instructions that you don't understand, please reach out to me and I will clarify or expand on the instructions. I am using mariadb as the database, but you can easily swap that out for MySQL if you prefer. The instructions for setting up the database and configuring Laravel will be the same regardless of which one you choose.

Caveat: This is just the site, I am assuming you already have a domain registered and can point it at your server's IP address. In this example, I am setting up the 'site.com' domain, but you should replace that with your own domain or subdomain in the instructions below. This is not a real site.

1. Create a Dedicated System User

Running your site under a dedicated system user (rather than www-data or root) limits the blast radius if something goes wrong.

# Create the 'boingo' system user with a home directory and bash shell
sudo adduser --system --group --shell /bin/bash --create-home boingo

# Create the site directory and set ownership
sudo mkdir -p /home/boingo/site
printf "set mouse-=a\nsyntax on\n" > /home/boingo/.vimrc
sudo chown -R boingo:boingo /home/boingo
sudo chmod -R 755 /home/boingo
# I use 'vim' for my editor in terminal, I recommend becoming familiar with this handy tool. The modification to the .vimrc file above makes vim understand how copy/pasting works with your mousy, handy.

2. Set Up a Deploy Key for GitHub

If your repository is private, the server needs its own SSH key so it can clone and pull without using a personal password.

# Generate an SSH key for the boingo user
sudo su boingo
mkdir -p /home/boingo/.ssh
cd /home/boingo/.ssh
ssh-keygen -t ed25519 -C "boingo-server-key"

# Print the public key — you'll need to copy this
less /home/boingo/.ssh/id_ed25519.pub

Go to your GitHub repository → SettingsDeploy KeysAdd deploy key. Paste the public key and give it a name like Production Server. This grants read-only access to that single repository only.

3. Grant Limited sudo for PHP-FPM Reloads

During deployment you'll want to reload PHP-FPM for zero-downtime. Rather than giving boingo full sudo, grant exactly one permission via the sudoers.

Create file in the /etc/sudoers.d/ directory


sudo vim /etc/sudoers.d/php-fpm-reload
 

and two lines into it:


boingo ALL=NOPASSWD: /usr/bin/systemctl reload php8.4-fpm
boingo ALL=NOPASSWD: /usr/bin/supervisorctl reload
      

Your deploy script can now safely call:

sudo systemctl reload php8.4-fpm and/or sudo supervisorctl reload

4. Clone the Repository

mkdir -p /home/boingo/
cd /home/boingo/
git clone git@github.com:yourorg/site.git

The assumption through the rest of this tutorial is that you now have a working user 'boingo' and you now have a copy of your code checked out and sitting in the /home/boingo/site directory. Everywhere you see 'boingo' or this site directory, replace it with YOUR user and directory information.

5. Install PHP and Dependencies

# if you want mysql, you can swap out the mariadb packages for mysql-server and mysql-client. The php extensions I am installing here are ones that *I* usually need for Laravel apps, you may need to add or remove some depending on your app's requirements. You can always install more later with apt if you find you need them.
sudo apt install \
  php8.4-common php-common php8.4-opcache php8.4-readline nginx vim \
  php8.4-cli php8.4-fpm php8.4-redis php8.4-ssh2 php8.4 \
  php8.4-intl php8.4-mbstring php8.4-xml php8.4-zip \
  ssl-cert mysql-common lsof \
  mariadb-client-core mariadb-client \
  mariadb-server-core mariadb-server default-mysql-server-core \
  supervisor redis wget unzip locate php-mysql nodejs npm

sudo updatedb
# locate and 'updatedb' make finding files on your system easier, you can
locate php8.4-fpm
or whatever you need to find. updatedb must be run as root but 'locate' will work for ordinary users, it's basically the index of where all the files are on your server. Later on, if you have problems with commands not running, check the path like this 'locate supervisorctl' or 'locate npm'

6. Set Up the Database

We are going to get the database set up for your new application. Log in as the 'root' user this first time, set a root password, create a user for your application and a database for it to use. The database user should have permissions only for that one database, and the password should be strong and unique. This way, if your application is compromised, the attacker won't have access to other databases or system files.

# As the root user, you can get into mysql with a simple
        
mysql -u root

CREATE DATABASE boingo CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'your-root-password';
CREATE USER 'boingo'@'localhost' IDENTIFIED BY 'your-app-password';
GRANT ALL ON boingo.* TO 'boingo'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Then update your application's .env file with the database credentials.

7. Configure PHP-FPM

Because everything should run under the boingo user, you need a dedicated PHP-FPM pool. If this is the only Laravel app on the server, edit the default pool file. If there are multiple apps, copy it to a new file.

# For a single-app server:
sudo vim /etc/php/8.4/fpm/pool.d/www.conf

# Or copy it for a multi-app setup:
sudo cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/boingo.conf
sudo vim /etc/php/8.4/fpm/pool.d/boingo.conf

Make these changes inside the file:

[boingo]                        ; pool name at the top
user = boingo
group = boingo
listen = /run/php8.4-fpm-boingo.sock
listen.owner = boingo
listen.group = www-data
sudo /etc/init.d/php8.4-fpm restart

8. Configure Nginx

sudo vim /etc/nginx/sites-available/site.com
server {
    listen 80;
    listen [::]:80;
    server_name site.com; # if your site will serve www.site.com, add that here too
    server_tokens off;

    root /home/boingo/site/public;

    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";

    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php8.4-fpm-boingo.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
# Enable the site and reload Nginx
sudo ln -s /etc/nginx/sites-available/site.com \
           /etc/nginx/sites-enabled/site.com

# Verify DNS resolves correctly before this step
sudo /etc/init.d/nginx reload

9. Obtain an SSL Certificate

sudo certbot --nginx -d site.com

Certbot will automatically update your Nginx config to handle HTTPS and set up auto-renewal. If you want to walk through this step by step, you can use the command sudo certbot and it will prompt you for the necessary information and guide you through the process interactively.

10. Set Up the Queue Worker with Supervisor

Supervisor keeps the Laravel queue worker running and automatically restarts it if it crashes or the server reboots. If you are not using Laravel's queue system, you can skip this step, but it's a good idea to have it set up for background jobs and async processing.

supervisorctl start  # Confirm Supervisor is running
systemctl status supervisor

        # Create a worker config (one per Laravel app)
sudo vim /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
directory=/home/boingo/site/
command=php8.4 /home/boingo/site/artisan queue:work database --sleep=10 --daemon --quiet --timeout=0 --delay=0 --memory=512 --tries=3 --queue=default
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
user=boingo
numprocs=1
redirect_stderr=true
stdout_logfile=/home/boingo/worker.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=3
stopasgroup=true
killasgroup=true
# Careful! that 'directory' and 'command' line actually need to point to the correct directory where your code is living.
# Load the new config and start the worker
supervisorctl reread
supervisorctl update
supervisorctl reload

# Make Supervisor start on boot
systemctl enable supervisor

11. Install Composer

sudo php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
HASH="$(wget -q -O - https://composer.github.io/installer.sig)"
sudo php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') {
    echo 'Installer verified';
} else {
    echo 'Installer corrupt';
    unlink('composer-setup.php');
} echo PHP_EOL;"
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
sudo rm composer-setup.php

12. Build and Deploy the Application

This is the part of the tutorial where everything gets tied together and we find out if it worked.

        cd /home/boingo/site

git pull # to get the freshest copy of your code

# Install PHP dependencies
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

# Run database migrations
php artisan migrate --force

# Cache everything for production
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan event:cache
php artisan view:cache

# Build frontend assets
npm ci
npm run build --silent

# Reload PHP-FPM
sudo /usr/sbin/service php8.4-fpm reload

13. Future deployments

For future updates to the site, here are the list of commands to run to deploy your code with minimal downtime. You can put these in a simple shell script and run it whenever you need to deploy new code.


sudo su boingo
cd /home/boingo/site
git pull
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
npm ci
npm run build --silent
php artisan migrate --force
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan event:cache
php artisan view:cache
sudo /usr/sbin/service php8.4-fpm reload
sudo /usr/bin/supervisorctl reload