# Deploy Laravel to a VPS: Nginx, PHP-FPM and queues step by step

URL: https://cloudj.net/blog/laravel-vps-deploy
Published: 2026-11-11 · Updated: 2026-11-11

> Deploy Laravel to a VPS step by step: install PHP-FPM and Nginx, configure .env, run queues with Supervisor, schedule tasks and ship releases without downtime.

To deploy Laravel to a VPS you need four things working together: Nginx in front, PHP-FPM running your code, a process manager keeping queue workers alive, and a release routine that does not take the site down. Managed platforms hide these parts; on your own server you set them up once and then understand exactly what runs in production. This guide walks through each step on a fresh Ubuntu server, with commands you can copy.

If you have not chosen a location yet, the [Asia VPS guide](https://cloudj.net/blog/asia-vps-guide) compares the options. Cloudj has no servers in Vietnam; for apps serving Vietnamese users or a regional audience across Southeast Asia, the nearest locations are Singapore ([best VPS in Singapore](https://cloudj.net/blog/best-vps-singapore)), Hong Kong ([best VPS in Hong Kong](https://cloudj.net/blog/best-vps-hong-kong)) and Tokyo ([best VPS in Japan](https://cloudj.net/blog/best-vps-japan)).

## Before you deploy Laravel to a VPS

- **A server.** For a typical Laravel app with a database on the same machine, 2 vCPU and 4 GB RAM is a comfortable start. A smaller server is enough for a small app or staging. Deploy from the [Cloudj console](https://app.cloudj.net); the [cloud servers page](https://cloudj.net/cloud-servers) lists every location.
- **A domain** with an A record pointing to the server IP.
- **A Git repository** the server can pull from, with a deploy key.
- **A secured server.** Do the basics from the [new VPS security checklist](https://cloudj.net/blog/secure-new-vps-checklist) first: SSH keys, a non-root user, and a firewall open on 22, 80 and 443.

The commands below assume Ubuntu 24.04, PHP 8.3 and a user called `deploy`. Adjust versions to what your app requires.

## Step 1: install PHP-FPM, Nginx and the database

```bash
sudo apt update
sudo apt install -y nginx mariadb-server redis-server supervisor unzip git \
  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-intl php8.3-redis
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
```

Create a database and user:

```bash
sudo mysql -e "CREATE DATABASE app; CREATE USER 'app'@'localhost' IDENTIFIED BY 'change-me'; GRANT ALL ON app.* TO 'app'@'localhost';"
```

Prefer PostgreSQL? The [PostgreSQL on a VPS guide](https://cloudj.net/blog/postgresql-vps-setup) covers that setup instead.

## Step 2: lay out the directories for zero-downtime releases

Instead of pulling code into one folder and hoping nothing breaks mid-update, keep each release in its own folder and point a `current` symlink at the live one. Shared files (`.env` and `storage`) live outside the releases.

```text
/var/www/app/
  releases/20261111-1200/
  shared/.env
  shared/storage/
  current -> releases/20261111-1200
```

```bash
sudo mkdir -p /var/www/app/{releases,shared/storage}
sudo chown -R deploy:www-data /var/www/app
mkdir -p /var/www/app/shared/storage/{app/public,framework/{cache,sessions,views},logs}
```

Create `/var/www/app/shared/.env` with production values: `APP_ENV=production`, `APP_DEBUG=false`, your `APP_URL`, database credentials, `QUEUE_CONNECTION=redis` and `CACHE_STORE=redis`. Generate `APP_KEY` once and never change it, or encrypted data and sessions become unreadable.

## Step 3: configure Nginx

Point the web root at `current/public`, never at the project root.

```nginx
server {
    listen 80;
    server_name app.example.com;
    root /var/www/app/current/public;
    index index.php;
    charset utf-8;

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

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

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

`$realpath_root` matters here: it resolves the `current` symlink, so PHP-FPM sees the new release path after each switch. Enable the site, reload Nginx, then add HTTPS with Certbot as described in the [Nginx reverse proxy and SSL guide](https://cloudj.net/blog/nginx-reverse-proxy-ssl-vps).

## Step 4: the deploy script

Save this as `~/deploy.sh` and run it for every release.

```bash
#!/usr/bin/env bash
set -euo pipefail
APP=/var/www/app
REL=$APP/releases/$(date +%Y%m%d-%H%M%S)

git clone --depth 1 git@github.com:you/app.git "$REL"
cd "$REL"
ln -s $APP/shared/.env .env
rm -rf storage && ln -s $APP/shared/storage storage
composer install --no-dev --optimize-autoloader --no-interaction
# npm ci && npm run build   # if you build assets on the server
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link

ln -sfn "$REL" $APP/current
sudo systemctl reload php8.3-fpm
php artisan queue:restart

ls -1dt $APP/releases/* | tail -n +6 | xargs -r rm -rf
```

What each part does:

1. A fresh folder per release, so the live site is untouched until the switch.
2. Shared `.env` and `storage`, so uploads, logs and sessions survive releases.
3. Migrations run before the switch. Write migrations that the old code can live with for a moment (add columns first, remove them in a later release).
4. `ln -sfn` swaps the symlink in one step. Reloading PHP-FPM clears OPcache's view of old paths.
5. `queue:restart` tells workers to finish their current job and restart on the new code.
6. The last line keeps five releases. To roll back, point `current` at the previous folder and reload PHP-FPM.

Allow the `deploy` user to reload PHP-FPM without a password by adding a narrow sudoers rule for that single command.

## Step 5: keep queue workers alive with Supervisor

Queued jobs (emails, notifications, image processing) need a long-running worker. Supervisor restarts it if it crashes and on reboot.

```ini
[program:app-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
user=deploy
numprocs=2
autostart=true
autorestart=true
stopwaitsecs=3600
redirect_stderr=true
stdout_logfile=/var/www/app/shared/storage/logs/worker.log
```

Save it as `/etc/supervisor/conf.d/app-worker.conf`, then run `sudo supervisorctl reread && sudo supervisorctl update`. Start with one or two workers per vCPU and watch CPU and RAM in the console metrics.

## Step 6: the scheduler

Laravel's scheduler needs one cron entry for the `deploy` user:

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

## Troubleshooting the usual errors

- **500 error right after deploy:** check `storage/logs/laravel.log`; it is usually a missing `.env` value or a cached config. Run `php artisan config:clear` and cache again.
- **Permission denied on storage:** the PHP-FPM user (`www-data`) needs write access to `shared/storage`. Put `deploy` and `www-data` in the same group and set group write.
- **Old code still served:** Nginx is using `$document_root` instead of `$realpath_root`, or PHP-FPM was not reloaded.
- **Jobs run old code:** `queue:restart` was skipped.

The official [Laravel deployment documentation](https://laravel.com/docs/deployment) lists further production optimisations.

## FAQ

### What size VPS does a Laravel app need?

A small app with its database on the same server runs comfortably on 2 vCPU and 4 GB RAM. Add RAM as queues and traffic grow; you can resize CPU and RAM from the console.

### Do I need Docker to deploy Laravel?

No. Nginx, PHP-FPM and Supervisor directly on the server is simple and fast. Docker helps when you run several apps with different PHP versions.

### How do I roll back a bad release?

Point the `current` symlink at the previous release folder and reload PHP-FPM. Roll back database changes only if the migration was not backward compatible.

### Where should I host a Laravel app for users in Vietnam?

Cloudj has no servers in Vietnam, so a server abroad adds some network distance compared with an in-country host. Singapore suits a regional Southeast Asian audience; Hong Kong and Tokyo are worth testing too. Deploy small hourly servers in two or three locations, measure from your users' ISPs with `ping` and `mtr`, and keep the best one.

## Ship your first release

Deploy a server in the [Cloudj console](https://app.cloudj.net), run through the steps once, and every later release becomes a single script. Live prices for every location are on the [pricing page](https://cloudj.net/pricing).
