# Deploy Laravel lên VPS: queue, scheduler và cập nhật không gián đoạn

> Cách deploy Laravel lên VPS Ubuntu 24.04: PHP-FPM, nginx, .env, queue worker, scheduler và release không downtime, làm tay hoặc với ZoPanel.

Source: https://zopanel.net/vi/blog/deploy-laravel-to-vps  
Updated: 2026-10-21

## Ý chính

- Laravel 13 cần PHP 8.3 trở lên, nên gói php8.3 có sẵn của Ubuntu 24.04 là đủ.
- Trỏ nginx vào thư mục public của dự án và dùng $realpath_root để việc đổi release bằng symlink được nhận đúng.
- Queue worker giữ code cũ trong bộ nhớ: chạy php artisan queue:restart mỗi lần deploy và để systemd tự khởi động lại.
- Không downtime nghĩa là build mỗi release trong thư mục riêng, dùng chung storage và .env, rồi đổi symlink current trong một bước nguyên tử.

Để deploy Laravel lên VPS, bạn cần PHP-FPM với đủ extension Laravel yêu cầu, nginx trỏ vào thư mục `public` của dự án, một database, file `.env` cho production, và ba thứ hay bị quên: queue worker được systemd giữ cho luôn chạy, một dòng cron cho scheduler, và quy trình release không làm sập website. Cách an toàn là build mỗi release trong một thư mục riêng, dùng chung `storage` và `.env` giữa các release, rồi đổi symlink `current` trong một bước nguyên tử. Làm tay lần đầu mất khoảng một tiếng; với panel hiểu Laravel thì chỉ vài phút.

Bài này đi qua cả hai cách trên Ubuntu 24.04: từng lệnh khi làm tay, sau đó là cách làm với ZoPanel. Nếu bạn chưa từng dựng nginx và PHP-FPM, hãy đọc bài [cài LEMP stack trên Ubuntu](/vi/blog/lemp-stack-ubuntu) trước, bài này xây tiếp trên đó.

## Laravel cần phiên bản PHP nào?

Theo [chính sách hỗ trợ](https://laravel.com/docs/releases#support-policy) của Laravel, tại thời điểm viết (tháng 10/2026):

| Laravel | PHP được hỗ trợ | Phát hành | Sửa lỗi đến | Vá bảo mật đến |
| --- | --- | --- | --- | --- |
| 13 | 8.3 - 8.5 | 17/03/2026 | Quý 3/2027 | 17/03/2028 |
| 12 | 8.2 - 8.5 | 24/02/2025 | 13/08/2026 | 24/02/2027 |
| 11 | 8.2 - 8.4 | 12/03/2024 | 03/09/2025 | 12/03/2026 |

Laravel 11 đã hết hỗ trợ, Laravel 12 giờ chỉ còn nhận bản vá bảo mật, nên dự án mới hãy bắt đầu với Laravel 13. Ubuntu 24.04 có sẵn PHP 8.3 trong kho chính thức, đáp ứng mức tối thiểu của Laravel 13. Nếu cần 8.4 hoặc chạy nhiều phiên bản song song, hãy thêm PPA của Ondřej Surý như trong bài [chạy nhiều phiên bản PHP](/vi/blog/multiple-php-versions).

## Cách deploy Laravel lên VPS thủ công

Các ví dụ dùng ứng dụng tên `myapp`, tên miền `example.com` và thư mục `/var/www/myapp`.

### Bước 1: Cài PHP, Composer và database

[Tài liệu deployment](https://laravel.com/docs/deployment#server-requirements) của Laravel liệt kê các extension bắt buộc (ctype, cURL, DOM, fileinfo, filter, hash, mbstring, OpenSSL, PCRE, PDO, session, tokenizer, XML). Trên Ubuntu, phần lớn đã có trong `php8.3-common`; các gói dưới đây bổ sung phần còn lại cùng MySQL, Redis và những thứ hay dùng:

```bash
sudo apt update
sudo apt install -y nginx mariadb-server git unzip \
  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
```

Cài Composer bằng bộ cài chính thức, kiểm tra chữ ký được công bố trước khi chạy:

```bash
EXPECTED="$(curl -fsSL https://composer.github.io/installer.sig)"
curl -fsSL https://getcomposer.org/installer -o composer-setup.php
echo "$EXPECTED  composer-setup.php" | sha384sum -c -
sudo php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
```

Tạo database và user (sau khi chạy `sudo mariadb-secure-installation`):

```sql
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';
```

### Bước 2: User riêng, PHP-FPM pool riêng và cấu trúc thư mục

Chạy ứng dụng bằng một user riêng thay vì `www-data`, để app ghi được vào `storage` và `bootstrap/cache` mà không phải nới quyền cho mọi website trên server:

```bash
sudo adduser --disabled-password --gecos "" myapp
sudo usermod -aG myapp www-data          # nginx may read public files
sudo mkdir -p /var/www/myapp/{releases,shared}
sudo chown -R myapp:myapp /var/www/myapp
sudo chmod 750 /var/www/myapp
```

Tạo file `/etc/php/8.3/fpm/pool.d/myapp.conf`:

```ini
[myapp]
user = myapp
group = myapp
listen = /run/php/php8.3-fpm-myapp.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
```

Rồi chạy `sudo systemctl reload php8.3-fpm`. Cấu trúc thư mục sẽ như sau:

```text
/var/www/myapp/
  releases/20261021093000/   one folder per deploy
  shared/.env                production settings, kept forever
  shared/storage/            uploads, logs, sessions, kept forever
  current -> releases/20261021093000
```

Đặt cấu hình production vào `shared/.env` (`APP_ENV=production`, `APP_DEBUG=false`, `APP_URL=https://example.com`, các giá trị `DB_*`) và tạo `APP_KEY` một lần bằng `php artisan key:generate --show`. Giữ nguyên key này: đổi key sẽ đăng xuất tất cả người dùng và khiến dữ liệu đã mã hóa không đọc được nữa. Ở lần deploy đầu, chép thư mục `storage` của repository sang `shared/storage`.

### Bước 3: Server block cho nginx

Bắt đầu từ [cấu hình nginx mẫu chính thức](https://laravel.com/docs/deployment#nginx) của Laravel, đổi root sang `current/public` và dùng socket của pool riêng. Lưu thành `/etc/nginx/sites-available/myapp`:

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

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

    error_page 404 /index.php;

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

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

`$realpath_root` là chi tiết quan trọng: nó phân giải symlink `current` thành đường dẫn thật, nên PHP và OPcache luôn thấy đúng thư mục release và không bao giờ trộn file của hai release. Bật site rồi cài SSL miễn phí với Let's Encrypt:

```bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
```

Chi tiết về gia hạn và chứng chỉ wildcard có trong bài [SSL miễn phí với Let's Encrypt](/vi/blog/free-ssl-lets-encrypt).

### Bước 4: Script deploy không downtime

Lưu script sau thành `/var/www/myapp/deploy.sh`, chủ sở hữu là `myapp`, và chạy bằng user đó:

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

git clone --depth 1 --branch main git@github.com:you/myapp.git "$REL"
cd "$REL"
rm -rf storage && ln -s "$APP/shared/storage" storage
ln -s "$APP/shared/.env" .env

composer install --no-dev --optimize-autoloader --no-interaction
# npm ci && npm run build      # if you build Vite assets on the server
php artisan storage:link
php artisan optimize           # config, events, routes and views
php artisan migrate --force

ln -sfn "$REL" "$APP/current.new"
mv -Tf "$APP/current.new" "$APP/current"   # atomic swap

sudo /usr/bin/systemctl reload php8.3-fpm
php artisan queue:restart

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

Lý do của từng phần:

- **Mỗi release một thư mục mới.** `composer install` và bước build cache chạy trong khi release cũ vẫn phục vụ khách. Nếu có lệnh nào lỗi, `set -e` dừng script trước khi `current` bị đổi.
- **`php artisan optimize`** cache config, event, route và view, đúng như [tài liệu deployment](https://laravel.com/docs/deployment#optimization) khuyên. Khi config đã được cache, `.env` không còn được đọc lúc chạy, nên chỉ gọi `env()` bên trong các file `config/`.
- **`migrate --force`** chạy trước khi đổi release, nên trong chốc lát code cũ chạy trên schema mới. Hãy viết migration mà cả hai release đều chịu được: thêm cột trước, xóa cột ở một release sau.
- **`mv -T`** thay symlink bằng một lệnh `rename()` duy nhất, nên không request nào gặp lúc `current` biến mất. Chỉ dùng `ln -sfn` thì không đảm bảo tính nguyên tử.
- **Reload PHP-FPM** khởi động lại các worker một cách nhẹ nhàng, xóa OPcache và realpath cache của PHP. Cho phép riêng lệnh này trong sudoers: `myapp ALL=(root) NOPASSWD: /usr/bin/systemctl reload php8.3-fpm`.

Rollback chỉ là phép đổi symlink tương tự, trỏ về thư mục trước đó, rồi reload và `queue:restart`. Database thì không được rollback. Muốn script tự chạy mỗi lần `git push`, bằng GitHub Actions hoặc webhook có xác thực chữ ký, xem bài [tự động deploy từ GitHub lên VPS](/vi/blog/git-auto-deploy).

### Bước 5: Queue worker chạy bằng systemd

[Tài liệu queue](https://laravel.com/docs/queues#queue-workers-and-deployment) của Laravel nói rất rõ: worker là tiến trình chạy lâu, giữ ứng dụng đã khởi động trong bộ nhớ, nên nó không thấy code mới cho đến khi được khởi động lại. `queue:restart` yêu cầu worker thoát sau khi xong job hiện tại; sau đó phải có một trình quản lý tiến trình bật lại chúng. Tài liệu dùng Supervisor, nhưng systemd đã có sẵn trên server. Tạo file `/etc/systemd/system/myapp-queue.service`:

```ini
[Unit]
Description=myapp queue worker
After=network.target mariadb.service

[Service]
User=myapp
Group=myapp
WorkingDirectory=/var/www/myapp/current
ExecStart=/usr/bin/php /var/www/myapp/current/artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=3
TimeoutStopSec=3600

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-queue
```

`Restart=always` là bắt buộc, vì worker dừng do `queue:restart` thoát bình thường (exit code 0), và `on-failure` sẽ để nó nằm im luôn. Mỗi lần khởi động lại, đường dẫn `current` được phân giải lại nên worker chạy code của release mới. `queue:restart` lưu tín hiệu trong cache, vì vậy ứng dụng phải có cache driver hoạt động. `TimeoutStopSec` đóng vai trò như `stopwaitsecs` của Supervisor: đặt lớn hơn thời gian của job dài nhất.

### Bước 6: Scheduler

Scheduler chỉ cần [một dòng cron](https://laravel.com/docs/scheduling#running-the-scheduler) chạy mỗi phút. Thêm bằng `sudo crontab -u myapp -e`:

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

Lệnh `php artisan schedule:work` cũng có, nhưng Laravel mô tả nó cho việc chạy scheduler trên máy local; trên server, cron là cách được tài liệu hướng dẫn. Nếu bạn có task chạy dưới một phút, thêm `php artisan schedule:interrupt` vào cuối script deploy để `schedule:run` đang chạy không tiếp tục dùng code cũ.

## Checklist

| Hạng mục | Cách làm tay | Quên thì hỏng gì |
| --- | --- | --- |
| Root của nginx | `current/public`, `$realpath_root` | Lộ mã nguồn và `.env`, hoặc còn file cũ sau khi đổi release |
| `.env` và `storage` | Nằm trong `shared/`, link vào mỗi release | Mất file upload, session và log sau mỗi lần deploy |
| `APP_KEY` | Tạo một lần, không bao giờ đổi | Mọi người bị đăng xuất, dữ liệu mã hóa không đọc được |
| Cache | `php artisan optimize` trong mỗi release | Request chậm hơn; sửa `.env` tưởng như không có tác dụng |
| Migration | `migrate --force` trước khi đổi release | Code cần cột chưa tồn tại |
| Queue | Unit systemd và `queue:restart` | Job chạy code cũ hoặc ngừng hẳn |
| Scheduler | Một dòng cron, mỗi phút | Email định kỳ, dọn dẹp, báo cáo không bao giờ chạy |
| SSL | `certbot --nginx` | Trình duyệt cảnh báo, link `APP_URL` lẫn http/https |

## Cách deploy Laravel với ZoPanel

ZoPanel chạy Laravel như một website PHP: nginx phục vụ thư mục `public`, còn PHP chạy trong PHP-FPM pool riêng của hosting account, dưới Linux user riêng và giới hạn tài nguyên riêng của account đó. Có hai cách, đều được mô tả trong [tài liệu Laravel](/vi/docs/laravel):

1. **Trình cài một cú nhấp** cho dự án mới, trống: chạy `composer create-project laravel/laravel`, tạo database MariaDB, ghi `.env`, rồi chạy `key:generate`, `migrate --force` và `storage:link`.
2. **Git deploy** cho repository của bạn. ZoPanel nhận diện Laravel qua file `artisan` hoặc `laravel/framework`, chọn phiên bản PHP theo `require.php` trong `composer.json`, và đề xuất `composer install --no-dev --optimize-autoloader` cùng `config:cache`, `route:cache`, `view:cache`. Nếu `package.json` có script `build`, nó thêm `npm ci && npm run build` để build Vite.

Git deploy đi theo đúng mô hình của script ở trên: mỗi bản build nằm trong `releases/<timestamp>`, `storage` và `.env` được giữ lại và link vào, `current` đổi trong một bước kèm reload PHP-FPM nhẹ nhàng. Nếu bước install hoặc build lỗi, release trước vẫn chạy bình thường. ZoPanel tạo `APP_KEY` một lần nếu còn trống, đặt `APP_ENV=production` và `APP_DEBUG=false`, và dùng Redis của account cho session và cache khi có. Mục Releases giữ 5 bản build gần nhất, rollback bằng một cú nhấp, và mỗi lần push lên branch có thể tự deploy qua webhook có ký chữ ký từ GitHub, GitLab hoặc Gitea. Chi tiết xem [Git deploy](/vi/docs/git-deploy).

Nói thẳng những chỗ ZoPanel còn yếu:

- **Migration không tự chạy.** Bạn tự thêm `php artisan migrate --force` vào build command.
- **Không có queue worker chạy liên tục cho website PHP.** Git deploy mặc định `QUEUE_CONNECTION=sync`. Với job chạy nền, tài liệu ZoPanel dùng một cron job chạy `queue:work --stop-when-empty --max-time=55` mỗi phút. Cách này hợp với queue nhẹ và vừa; nếu bạn cần Horizon hoặc nhiều worker luôn chạy, cách systemd làm tay ở trên, hoặc Forge, sẽ phù hợp hơn.
- **Scheduler là cron job bạn tự thêm** trong **Cron Jobs**, trỏ vào `current`.
- **Rollback chỉ đổi code, không đổi database.** Hãy viết migration mà release trước vẫn chịu được.

## Forge, Envoyer, Deployer hay một panel?

[Laravel Forge](https://laravel.com/forge/pricing) là lựa chọn chính chủ và làm việc này rất tốt: tạo server (Laravel VPS của họ hoặc nhà cung cấp cloud của bạn), cài và quản lý nginx, PHP, MySQL, Redis và phần còn lại của stack, kèm deploy không downtime trên một server. Tại thời điểm viết (tháng 10/2026), các gói có giá $12, $19 và $39 mỗi tháng. [Envoyer](https://envoyer.io/) ($10 đến $50 mỗi tháng) bổ sung deploy không downtime một dự án lên nhiều server; FAQ của Forge cho biết gói Forge mới không cần Envoyer nữa, trừ khi bạn deploy lên nhiều server. [Deployer](https://deployer.org/) là công cụ dòng lệnh miễn phí, [giấy phép MIT](https://github.com/deployphp/deployer), hỗ trợ Laravel, làm đúng kiểu releases và symlink như trên từ máy bạn hoặc từ CI.

ZoPanel phù hợp với trường hợp khác: panel cài trên chính server của bạn, cấp phép theo server (miễn phí, theo năm hoặc mua một lần) thay vì thuê bao hằng tháng, và cùng một panel còn host được website WordPress, email, DNS và tài khoản khách hàng, mỗi account tách biệt với user và giới hạn tài nguyên riêng. Nếu bạn chỉ chạy ứng dụng Laravel và muốn có sẵn quản lý worker, Forge là công cụ Laravel đầy đủ hơn.

## Kết luận

Deploy Laravel lên VPS gói gọn trong vài điểm: root nginx đúng, `storage` và `.env` dùng chung, config được cache, migration chạy trước khi đổi release, symlink `current` đổi nguyên tử, và hai tiến trình nằm ngoài vòng đời request là queue worker và scheduler. Nếu bạn muốn có sẵn release, rollback và SSL mà không phải tự viết script, hãy thử [gói Free của ZoPanel](/vi/pricing) (3 hosting account, 10 website, 10 database, không giới hạn thời gian) trên một VPS thử nghiệm và deploy repository của chính bạn.

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

### Năm 2026 nên dùng PHP phiên bản nào cho Laravel?

Theo chính sách hỗ trợ chính thức, Laravel 13 hỗ trợ PHP 8.3 đến 8.5, còn Laravel 12 hỗ trợ 8.2 đến 8.5. Trên Ubuntu 24.04, PHP 8.3 có sẵn là lựa chọn đơn giản nhất cho cả hai. Chỉ lên bản mới hơn khi các gói Composer của bạn đã hỗ trợ.

### Có bắt buộc dùng Supervisor để chạy queue Laravel không?

Không. Tài liệu Laravel dùng Supervisor, nhưng systemd làm được việc tương tự và đã có sẵn: chạy `queue:work` như một service với `Restart=always`. Điều không thể bỏ là phải có một trình quản lý tiến trình nào đó, vì `queue:restart` và timeout đều khiến worker thoát.

### Vì sao sửa .env trên production mà không có tác dụng?

Vì config đang được cache. Sau `php artisan config:cache` hoặc `optimize`, Laravel không đọc `.env` nữa và `env()` trả về `null` ở ngoài các file config. Chạy lại `php artisan config:cache`, hoặc deploy lại, sau khi sửa `.env`.

### Ứng dụng Laravel cần bao nhiêu RAM trên VPS?

Còn tùy lượng truy cập và số worker, nên hãy đo thực tế. Một app nhỏ với MariaDB có thể bắt đầu ở 1 GB, nhưng 2 GB dễ thở hơn khi bạn thêm Redis, queue worker và build Vite ngay trên server. Mỗi tiến trình con PHP-FPM và mỗi queue worker giữ một bản ứng dụng đã khởi động, nên RAM tăng theo `pm.max_children` và số worker.

### Có thể deploy Laravel lên shared hosting thay vì VPS không?

Đôi khi được, nếu nhà cung cấp cho trỏ document root vào `public` và cho chạy Composer, cron. Nhưng queue worker và release không downtime thường không làm được ở đó. VPS, hoặc một panel cài trên VPS, cho bạn những thứ này.
