How to Deploy a Laravel App to a VPS (Production-Ready)

Last updated July 14, 2026
# the short answer

To deploy a Laravel app to a VPS, provision an Ubuntu 22.04 or 24.04 server, install a LEMP stack (Nginx, PHP 8.3, MySQL or PostgreSQL) plus Composer and Node, then clone your repo with a Git deploy key. Set your .env, run composer install --no-dev, php artisan migrate --force, and cache config and routes. Point Nginx at the public directory, secure it with a Let's Encrypt certificate, and run queue workers under Supervisor with the scheduler on cron. For zero downtime, deploy into a timestamped release folder and atomically swap a symlink.

Skip the manual setup. DeployManage provisions your server and ships zero-downtime deploys on any cloud.

$ get started free

Deploying a Laravel app to a VPS means getting your code onto a Linux server, wiring up PHP, a web server, and a database, and then running the framework's build and cache steps so the app serves fast and stays up. This guide walks the full path from a bare Ubuntu box to an atomic, zero-downtime release you can roll back in seconds.

What you need before you start

Have three things ready: a VPS, a domain, and SSH access. Any provider works (Hetzner, DigitalOcean, Vultr, Linode, or AWS Lightsail). For a small to mid-size Laravel app, 2 vCPUs and 4 GB of RAM is a comfortable starting point. Ubuntu 22.04 LTS or 24.04 LTS are the safe choices because most PHP packages target them directly.

Point an A record for your domain at the server's public IP before you request a TLS certificate, since Let's Encrypt validates over HTTP and needs DNS to resolve first.

How do I deploy a Laravel project to a server?

You deploy a Laravel project to a server in six stages: provision and harden the OS, install the runtime stack, pull the code, configure the environment, run the build and migration commands, then front it with Nginx and SSL. Do them in that order and nothing surprises you later.

1. Provision and harden Ubuntu

SSH in as root, create a non-root deploy user, and lock down access before anything else.

adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

# Basic firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

Disable password logins in /etc/ssh/sshd_config (set PasswordAuthentication no) and reload SSH. From here on, work as the deploy user.

2. Install the LEMP stack and PHP 8.3

Laravel 11 requires PHP 8.2 or newer, so PHP 8.3 is a solid target. On Ubuntu the cleanest route is Ondrej Sury's PPA, which keeps modern PHP builds current.

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mysql-server git unzip

sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql \
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip \
  php8.3-bcmath php8.3-gd php8.3-intl

Prefer PostgreSQL? Install postgresql and php8.3-pgsql instead of the MySQL packages. Then secure the database and create an app user:

sudo mysql_secure_installation

sudo mysql -e "CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -e "CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'strong-password-here';"
sudo mysql -e "GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost'; FLUSH PRIVILEGES;"

3. Install Composer and Node

Composer handles PHP dependencies. Node builds your front-end assets (Vite is the Laravel default).

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

4. Clone the repo with a Git deploy key

Generate a key on the server and add the public half as a read-only deploy key in your Git host. This keeps deploys automated without exposing a personal account.

ssh-keygen -t ed25519 -C "deploy@yourdomain" -f ~/.ssh/deploy_key
cat ~/.ssh/deploy_key.pub   # paste into GitHub/GitLab deploy keys

cd /var/www
sudo mkdir yourapp && sudo chown deploy:deploy yourapp
git clone [email protected]:you/yourapp.git yourapp

5. Configure the environment and build

Copy .env.example to .env, set your database credentials, APP_URL, mail, and any queue or cache drivers. Set APP_ENV=production and APP_DEBUG=false so stack traces never leak to users. Then install dependencies for production and build assets.

composer install --no-dev --optimize-autoloader
npm ci && npm run build

php artisan key:generate
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

The --force flag lets migrations run non-interactively in production. Caching config, routes, and views compiles them into single files, which noticeably cuts request overhead. Remember: after any config change you must rerun config:cache, because a cached config ignores .env at runtime.

6. Set storage permissions

Nginx and PHP-FPM run as the www-data user, so the framework's writable directories need the right ownership. Getting this wrong is the most common cause of a blank 500 on a fresh deploy.

sudo chown -R deploy:www-data /var/www/yourapp/storage /var/www/yourapp/bootstrap/cache
sudo chmod -R 775 /var/www/yourapp/storage /var/www/yourapp/bootstrap/cache
php artisan storage:link

How do I point Nginx at a Laravel app?

Point Nginx at the app's public directory, never the project root, and route all non-file requests through index.php. Create /etc/nginx/sites-available/yourapp:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourapp/current/public;

    index index.php;
    charset utf-8;

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

    location ~ \.php$ {
        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; }
}

Enable the site and reload:

sudo ln -s /etc/nginx/sites-available/yourapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

How do I add a free SSL certificate?

Use Certbot with Let's Encrypt to issue and auto-renew a free certificate. It edits your Nginx config to serve HTTPS and sets up a renewal timer automatically.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certificates last 90 days and the installed systemd timer renews them well before expiry. Confirm the timer is active with systemctl status certbot.timer.

Queue workers and the scheduler

Two background pieces make a Laravel app production-ready: queue workers for jobs (email, notifications, exports) and the scheduler for recurring tasks. Run workers under Supervisor so they restart on failure and after deploys.

Create /etc/supervisor/conf.d/yourapp-worker.conf:

[program:yourapp-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourapp/current/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=deploy
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/yourapp/shared/worker.log
stopwaitsecs=3600
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start yourapp-worker:*

For the scheduler, add one cron entry that fires Laravel's scheduler every minute. Laravel decides internally what actually runs.

* * * * * cd /var/www/yourapp/current && php artisan schedule:run >> /dev/null 2>&1

How do I deploy Laravel with zero downtime?

You deploy Laravel with zero downtime by building each release in its own timestamped folder and switching a current symlink to it only after the build succeeds. Because the symlink swap is atomic, in-flight requests never see a half-updated app, and rolling back is just repointing the symlink at the previous release.

The layout looks like this:

/var/www/yourapp/
  releases/
    20260714093000/
    20260714101500/   <- newly built
  shared/
    .env
    storage/
  current -> releases/20260714101500

A deploy script clones or checks out the new commit into a fresh release directory, symlinks the shared .env and storage into it, installs dependencies, runs migrations, warms the caches, then swaps the symlink and reloads PHP-FPM:

REL=/var/www/yourapp/releases/$(date +%Y%m%d%H%M%S)
git clone --depth 1 [email protected]:you/yourapp.git $REL
ln -sfn /var/www/yourapp/shared/.env $REL/.env
ln -sfn /var/www/yourapp/shared/storage $REL/storage

cd $REL
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan config:cache && php artisan route:cache

ln -sfn $REL /var/www/yourapp/current
sudo systemctl reload php8.3-fpm
php /var/www/yourapp/current/artisan queue:restart

Call queue:restart after each deploy so workers pick up new code instead of running stale classes. Keep the last three to five releases so a rollback is instant. One caveat: a migration that drops a column mid-deploy can still break the old release for a moment, so treat schema changes as backward compatible (expand first, contract later).

Do I need a control panel to deploy Laravel?

No, you do not need a control panel: everything above works with plain SSH and shell scripts. The question is whether you want to own the maintenance. Manual setup gives you full control and costs nothing beyond the server, but you become responsible for security patches, PHP upgrades, certificate edge cases, and writing your own release and rollback tooling. A managed panel automates provisioning and turns deploys into a button press or a Git push.

TaskManual VPS setupManaged control panel
Initial provisioning1 to 3 hours by handMinutes, automated
Security patchingYour responsibilityAutomated updates
Zero-downtime deploysCustom scripts you maintainBuilt in
RollbackManual symlink swapOne click
SSL renewalCertbot timer you verifyManaged and monitored
Ongoing costServer onlyServer plus subscription

If you manage more than one app or a client fleet, a server management panel pays for itself quickly in saved hours and fewer 2 a.m. incidents. DeployManage provisions servers on any provider and runs zero-downtime deploys for you, so the release, symlink swap, and worker restart all happen without hand-rolled scripts. As you spread across more boxes and providers, it is worth tracking your cloud spend so a fleet of idle servers does not quietly inflate the bill.

Common gotchas

  • 500 after deploy, no error shown: almost always storage permissions or a stale cached config. Rerun config:cache and recheck ownership.
  • Vite assets 404: you forgot npm run build, or APP_URL is wrong.
  • Queued jobs run old code: you skipped queue:restart.
  • Certbot fails: DNS has not propagated, or port 80 is blocked by the firewall.

Get these six stages right once, script them, and every future deploy becomes boring in the best way.

Frequently asked questions

How do I deploy a Laravel project to a server?

Provision an Ubuntu VPS, install Nginx, PHP 8.3, and a database, then clone your repo with a Git deploy key. Configure the .env, run composer install --no-dev, php artisan migrate --force, and cache config and routes. Point Nginx at the public folder and add SSL with Certbot.

Do I need a control panel to deploy Laravel?

No. You can deploy Laravel with plain SSH and shell scripts. A control panel is optional but it automates provisioning, SSL, security patching, and zero-downtime deploys, which saves significant time once you manage more than one app or run production for clients.

How do I deploy Laravel with zero downtime?

Build each release in a timestamped folder, then atomically swap a current symlink to it after the build and migrations succeed. Since the swap is instant, requests never hit a half-updated app, and rolling back means repointing the symlink at the previous release.

Which PHP version does Laravel need on a VPS?

Laravel 11 requires PHP 8.2 or higher, so PHP 8.3 is a good production target. Install it via the ondrej/php PPA on Ubuntu along with common extensions like mbstring, xml, curl, mysql or pgsql, bcmath, and intl for full framework support.

How do I run Laravel queue workers on a VPS?

Run php artisan queue:work under Supervisor so workers start on boot and restart after crashes or deploys. Configure numprocs for concurrency, set autorestart to true, and call php artisan queue:restart after every deploy so workers load your newest code.

How do I get free SSL for a Laravel app on a VPS?

Install Certbot with the Nginx plugin and run certbot --nginx -d yourdomain.com. It issues a free Let's Encrypt certificate, edits your Nginx config for HTTPS, and installs a systemd timer that auto-renews the 90-day certificate before it expires.

# related

Provision and deploy from one dashboard.

Connect a provider and ship your first zero-downtime deploy in minutes.

$ get started free