# Deploy Laravel lên VPS: Nginx, PHP-FPM, queue từng bước

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

> Hướng dẫn deploy Laravel lên VPS từng bước: cài PHP-FPM, Nginx, cấu hình .env, chạy queue bằng Supervisor, lập lịch và phát hành không gián đoạn.

Để deploy Laravel lên VPS, bạn cần bốn thứ phối hợp với nhau: Nginx đứng trước, PHP-FPM chạy code, một trình quản lý tiến trình giữ cho queue worker luôn sống, và một quy trình phát hành không làm sập trang. Các nền tảng managed giấu những phần này đi; trên máy chủ của mình, bạn dựng một lần và hiểu rõ chính xác thứ gì đang chạy trên production. Bài này đi qua từng bước trên một máy chủ Ubuntu mới, kèm lệnh có thể chép dùng ngay.

Nếu chưa chọn vị trí, [hướng dẫn VPS châu Á](https://cloudj.net/vi/blog/asia-vps-guide) so sánh các lựa chọn. Cloudj không có máy chủ tại Việt Nam; với ứng dụng phục vụ người dùng Việt Nam hoặc người dùng khắp Đông Nam Á, các vị trí gần nhất là Singapore (bài [chọn VPS Singapore](https://cloudj.net/vi/blog/best-vps-singapore)), Hong Kong (bài [chọn VPS Hong Kong](https://cloudj.net/vi/blog/best-vps-hong-kong)) và Tokyo (bài [chọn VPS Nhật Bản](https://cloudj.net/vi/blog/best-vps-japan)).

## Chuẩn bị trước khi deploy Laravel lên VPS

- **Máy chủ.** Với một ứng dụng Laravel thông thường, database chạy chung máy, 2 vCPU và 4 GB RAM là mức khởi đầu thoải mái. Máy nhỏ hơn đủ cho ứng dụng nhỏ hoặc môi trường staging. Tạo máy chủ trong [console Cloudj](https://app.cloudj.net); [trang cloud server](https://cloudj.net/vi/cloud-servers) liệt kê mọi vị trí.
- **Domain** có bản ghi A trỏ về IP máy chủ.
- **Repository Git** mà máy chủ pull được, dùng deploy key.
- **Máy chủ đã gia cố.** Làm các bước cơ bản trong [checklist bảo mật VPS mới](https://cloudj.net/vi/blog/secure-new-vps-checklist) trước: SSH key, user không phải root, firewall chỉ mở 22, 80 và 443.

Các lệnh bên dưới giả định Ubuntu 24.04, PHP 8.3 và user tên `deploy`. Hãy chỉnh phiên bản theo yêu cầu của ứng dụng.

## Bước 1: cài PHP-FPM, Nginx và 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
```

Tạo database và user:

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

Dùng PostgreSQL? Bài [cài PostgreSQL trên VPS](https://cloudj.net/vi/blog/postgresql-vps-setup) hướng dẫn phần đó.

## Bước 2: tổ chức thư mục để phát hành không gián đoạn

Thay vì pull code vào một thư mục và cầu mong không có gì hỏng giữa chừng, hãy để mỗi bản phát hành trong một thư mục riêng và trỏ symlink `current` vào bản đang chạy. Các file dùng chung (`.env` và `storage`) nằm ngoài các bản phát hành.

```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}
```

Tạo `/var/www/app/shared/.env` với giá trị production: `APP_ENV=production`, `APP_DEBUG=false`, `APP_URL` của bạn, thông tin database, `QUEUE_CONNECTION=redis` và `CACHE_STORE=redis`. Tạo `APP_KEY` một lần và đừng bao giờ đổi, nếu không dữ liệu mã hóa và session sẽ không đọc được nữa.

## Bước 3: cấu hình Nginx

Trỏ web root vào `current/public`, tuyệt đối không trỏ vào thư mục gốc của dự án.

```nginx
server {
    listen 80;
    server_name app.example.vn;
    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` rất quan trọng ở đây: nó phân giải symlink `current`, nên PHP-FPM thấy đường dẫn bản mới sau mỗi lần chuyển. Bật site, reload Nginx, rồi thêm HTTPS bằng Certbot như trong bài [Nginx reverse proxy kèm SSL](https://cloudj.net/vi/blog/nginx-reverse-proxy-ssl-vps).

## Bước 4: script deploy

Lưu đoạn sau thành `~/deploy.sh` và chạy cho mỗi lần phát hành.

```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   # nếu build asset trên máy chủ
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
```

Từng phần làm gì:

1. Mỗi lần phát hành một thư mục mới, nên trang đang chạy không bị đụng tới cho tới lúc chuyển.
2. `.env` và `storage` dùng chung, nên file upload, log và session không mất qua các bản phát hành.
3. Migration chạy trước khi chuyển. Hãy viết migration sao cho code cũ vẫn sống được trong chốc lát (thêm cột trước, xóa cột ở bản phát hành sau).
4. `ln -sfn` đổi symlink trong một bước. Reload PHP-FPM để OPcache bỏ đường dẫn cũ.
5. `queue:restart` báo worker làm xong job hiện tại rồi khởi động lại với code mới.
6. Dòng cuối giữ lại năm bản phát hành. Muốn rollback, trỏ `current` về thư mục trước đó và reload PHP-FPM.

Cho phép user `deploy` reload PHP-FPM không cần mật khẩu bằng một quy tắc sudoers hẹp, chỉ cho đúng lệnh đó.

## Bước 5: giữ queue worker luôn chạy với Supervisor

Các job trong queue (gửi email, thông báo, xử lý ảnh) cần một worker chạy lâu dài. Supervisor tự khởi động lại worker khi nó lỗi và khi máy chủ 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
```

Lưu thành `/etc/supervisor/conf.d/app-worker.conf`, rồi chạy `sudo supervisorctl reread && sudo supervisorctl update`. Bắt đầu với một đến hai worker cho mỗi vCPU và theo dõi CPU, RAM trong biểu đồ của console.

## Bước 6: scheduler

Scheduler của Laravel chỉ cần một dòng cron cho user `deploy`:

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

## Xử lý các lỗi thường gặp

- **Lỗi 500 ngay sau khi deploy:** xem `storage/logs/laravel.log`; thường là thiếu biến trong `.env` hoặc config đã cache cũ. Chạy `php artisan config:clear` rồi cache lại.
- **Permission denied trên storage:** user của PHP-FPM (`www-data`) cần quyền ghi vào `shared/storage`. Cho `deploy` và `www-data` chung group và bật quyền ghi cho group.
- **Vẫn chạy code cũ:** Nginx đang dùng `$document_root` thay vì `$realpath_root`, hoặc chưa reload PHP-FPM.
- **Job chạy code cũ:** quên chạy `queue:restart`.

[Tài liệu deployment chính thức của Laravel](https://laravel.com/docs/deployment) liệt kê thêm các tối ưu cho production.

Về chi phí, bạn có thể trả theo giờ cho máy staging (nhớ xóa khi không dùng, vì máy tắt vẫn bị tính phí) và trả trước 1, 3, 6 hoặc 12 tháng cho production. Số dư tài khoản tính bằng USD; khách hàng Việt Nam nạp được bằng chuyển khoản ngân hàng bằng VND.

## Câu hỏi thường gặp

### Ứng dụng Laravel cần VPS cấu hình bao nhiêu?

Ứng dụng nhỏ, database chạy chung máy, chạy thoải mái trên 2 vCPU và 4 GB RAM. Tăng RAM khi queue và lưu lượng tăng; bạn có thể nâng CPU và RAM trong console.

### Có cần Docker để deploy Laravel không?

Không. Nginx, PHP-FPM và Supervisor chạy trực tiếp trên máy chủ vừa đơn giản vừa nhanh. Docker hữu ích khi bạn chạy nhiều ứng dụng cần các phiên bản PHP khác nhau.

### Rollback một bản phát hành lỗi thế nào?

Trỏ symlink `current` về thư mục phát hành trước đó và reload PHP-FPM. Chỉ rollback database khi migration không tương thích ngược.

### Nên đặt ứng dụng Laravel ở đâu cho người dùng Việt Nam?

Cloudj không có máy chủ tại Việt Nam, nên máy chủ ở nước ngoài sẽ xa người dùng hơn so với một nhà cung cấp trong nước. Singapore hợp với người dùng khắp Đông Nam Á; Hong Kong và Tokyo cũng đáng thử. Tạo vài máy chủ nhỏ theo giờ ở hai hoặc ba vị trí, đo từ nhà mạng của người dùng bằng `ping` và `mtr`, rồi giữ nơi tốt nhất.

## Phát hành bản đầu tiên

Tạo máy chủ trong [console Cloudj](https://app.cloudj.net), làm qua các bước này một lần, và mọi lần phát hành sau chỉ còn là chạy một script. Giá cập nhật cho mọi vị trí có trên [trang bảng giá](https://cloudj.net/vi/pricing).
