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.

Sơ đồ: nginx phục vụ bản release Laravel hiện tại trên VPS, bên cạnh là queue worker và scheduler

Ý 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 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ợ 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.

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 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:

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:

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):

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:

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:

[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:

/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 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:

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:

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.

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 đó:

#!/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 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.

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

Tài liệu queue 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:

[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
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 chạy mỗi phút. Thêm bằng sudo crontab -u myapp -e:

* * * * * 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:

  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.

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 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 ($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 là công cụ dòng lệnh miễn phí, giấy phép MIT, 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 (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.