How to Host Multiple Laravel Apps on One Server

We earn commissions when you shop through the links below.

If you’re building multiple client projects or side products, knowing how to host multiple Laravel apps on one server is one of the most practical skills you can develop. Instead of spinning up a new VPS for every project, you can consolidate everything onto a single machine, cut hosting costs significantly, and still keep each app properly isolated. In this guide I’ll walk through the exact setup I use: Nginx virtual hosts, separate database users, isolated environment files, and a clean directory structure.

Why Run Multiple Apps on One Server?

The main driver is cost. A single DigitalOcean droplet running 2 GB of RAM can comfortably serve four or five low-to-medium traffic Laravel apps. At $18/month that’s a fraction of what you’d pay running separate droplets. Beyond cost, centralised logging, one crontab, one set of SSL certificates to renew — it’s just simpler to manage once you have the pattern down.

Prerequisites

  • A Ubuntu 24.04 VPS with root or sudo access
  • Nginx installed
  • PHP 8.3 (or your target version) with php-fpm
  • MySQL 8 or MariaDB
  • Composer
  • Certbot for SSL

If you’re starting fresh and want a reliable host, Hostinger VPS plans come with Ubuntu and a clean slate — good value for running multiple projects on one box.

Directory Structure

I keep all Laravel apps under /var/www with one folder per domain. This makes permissions and backups straightforward.

/var/www/
  app-one.com/
    public/
    .env
    ...
  app-two.com/
    public/
    .env
    ...
  app-three.com/
    public/
    .env
    ...

Create the directories and set ownership to your deploy user (I use deploy):

sudo mkdir -p /var/www/app-one.com
sudo mkdir -p /var/www/app-two.com
sudo chown -R deploy:www-data /var/www/app-one.com
sudo chown -R deploy:www-data /var/www/app-two.com
sudo chmod -R 755 /var/www/app-one.com

Nginx Virtual Host Configuration

Each Laravel app gets its own Nginx server block. The key is pointing root at the app’s public/ directory. Here’s a complete server block for one app:

server {
    listen 80;
    server_name app-one.com www.app-one.com;
    root /var/www/app-one.com/public;
    index index.php index.html;

    access_log /var/log/nginx/app-one.access.log;
    error_log  /var/log/nginx/app-one.error.log;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    client_max_body_size 64M;
}

Save this to /etc/nginx/sites-available/app-one.com then repeat for each app, changing server_name, root, and log paths. Enable each site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/app-one.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/app-two.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Separate Databases and Users

Never share a database between apps, and never let all apps connect as root. Create a dedicated database and user for each application:

-- Run inside MySQL as root
CREATE DATABASE app_one CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app_one_user'@'localhost' IDENTIFIED BY 'strong_password_here';
GRANT ALL PRIVILEGES ON app_one.* TO 'app_one_user'@'localhost';

CREATE DATABASE app_two CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app_two_user'@'localhost' IDENTIFIED BY 'another_strong_password';
GRANT ALL PRIVILEGES ON app_two.* TO 'app_two_user'@'localhost';

FLUSH PRIVILEGES;

Each app’s .env file then references its own credentials. This means a compromised app can’t touch another app’s data.

Environment File Isolation

Laravel’s .env file handles all per-app configuration. The critical fields to get right when hosting multiple apps on one server are APP_KEY, APP_URL, session and cache prefixes, and queue connection names if you’re using the database driver.

# /var/www/app-one.com/.env
APP_NAME="App One"
APP_ENV=production
APP_KEY=base64:YOUR_UNIQUE_KEY_HERE
APP_URL=https://app-one.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=app_one
DB_USERNAME=app_one_user
DB_PASSWORD=strong_password_here

SESSION_DRIVER=file
SESSION_COOKIE=app_one_session

CACHE_STORE=file
CACHE_PREFIX=app_one_

The SESSION_COOKIE and CACHE_PREFIX values are easy to overlook but important. If two apps both default to laravel_session as the cookie name, users could get unexpected session conflicts in the browser when visiting both apps from the same client. Set unique values for every app.

SSL with Certbot

Getting HTTPS on each domain is straightforward with Certbot. Run it once per domain:

sudo certbot --nginx -d app-one.com -d www.app-one.com
sudo certbot --nginx -d app-two.com -d www.app-two.com

Certbot will modify your Nginx server blocks to add the SSL configuration and set up automatic renewal. Verify renewal works:

sudo certbot renew --dry-run

Shared Queue Workers and Scheduled Tasks

Each Laravel app that needs a queue worker needs its own Supervisor program. Create separate .conf files under /etc/supervisor/conf.d/:

[program:app-one-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app-one.com/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=deploy
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/supervisor/app-one-worker.log

[program:app-two-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app-two.com/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=deploy
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/supervisor/app-two-worker.log

For scheduled tasks, a single crontab entry calling each app’s scheduler is all you need:

* * * * * deploy php /var/www/app-one.com/artisan schedule:run >> /dev/null 2>&1
* * * * * deploy php /var/www/app-two.com/artisan schedule:run >> /dev/null 2>&1

PHP-FPM Pool Isolation (Optional but Recommended)

For stronger process isolation, create a separate PHP-FPM pool per app. Copy the default pool config and adjust:

# /etc/php/8.3/fpm/pool.d/app-one.conf
[app-one]
user = deploy
group = www-data
listen = /run/php/php8.3-fpm-app-one.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Then update your Nginx server block to use the app-specific socket: fastcgi_pass unix:/run/php/php8.3-fpm-app-one.sock;. This means each app runs as its own process pool — a crash or memory spike in one app’s workers is less likely to affect the others.

Deployment Workflow

I use a simple bash script per app for zero-downtime-ish deployments. Nothing fancy — just a git pull and cache refresh:

#!/bin/bash
set -e
cd /var/www/app-one.com
git pull origin main
composer install --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
sudo supervisorctl restart app-one-worker:*
echo "Deployed app-one successfully"

Monitoring Resource Usage

Running multiple apps on one server means you need to watch memory and CPU. I keep htop and mytop installed, and check php-fpm status pages periodically. If a server starts struggling under load, it’s usually either a memory leak in one app’s queue worker or unoptimised queries hammering MySQL. Check /var/log/nginx/*.error.log per app first — isolated logs per domain make debugging much faster.

Common Pitfalls

  • Shared storage paths: Make sure storage/ and bootstrap/cache/ are writable and unique per app. Never symlink storage across apps.
  • APP_KEY collisions: Generate a fresh key for each app with php artisan key:generate. Never copy a key from another app.
  • Session cookie names: Set SESSION_COOKIE uniquely per app in .env.
  • File upload limits: Set client_max_body_size in Nginx and upload_max_filesize / post_max_size in PHP per pool if apps have different requirements.

Wrapping Up

Knowing how to host multiple Laravel apps on one server properly comes down to three things: clean Nginx virtual hosts, isolated databases and environment files, and separate supervisor workers per app. Once you have a template for this setup, adding a new app is a 15-minute job. You get the cost efficiency of shared hosting with the control and performance of a VPS — which is the best of both worlds for indie developers and small agencies running a portfolio of projects.