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.

Ý 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 installvà 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 -edừng script trước khicurrentbị đổi. php artisan optimizecache config, event, route và view, đúng như tài liệu deployment khuyên. Khi config đã được cache,.envkhông còn được đọc lúc chạy, nên chỉ gọienv()bên trong các fileconfig/.migrate --forcechạ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 -Tthay symlink bằng một lệnhrename()duy nhất, nên không request nào gặp lúccurrentbiến mất. Chỉ dùngln -sfnthì 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:
- 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ạykey:generate,migrate --forcevàstorage:link. - Git deploy cho repository của bạn. ZoPanel nhận diện Laravel qua file
artisanhoặclaravel/framework, chọn phiên bản PHP theorequire.phptrongcomposer.json, và đề xuấtcomposer install --no-dev --optimize-autoloadercùngconfig:cache,route:cache,view:cache. Nếupackage.jsoncó scriptbuild, nó thêmnpm 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 --forcevà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ạyqueue:work --stop-when-empty --max-time=55mỗ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.
Đọc tiếp

Deploy tự động từ GitHub lên VPS: webhook, Actions, không downtime
Ba cách deploy tự động từ GitHub lên VPS: GitHub Actions qua SSH, webhook, hoặc Git deploy của panel, kèm script release, health check và rollback.

Cài Nginx, PHP, MariaDB trên Ubuntu 24.04 (LEMP) từng bước
Hướng dẫn cài nginx php mariadb trên Ubuntu 24.04 và Debian 12: tường lửa, PHP-FPM, server block, database, SSL Let's Encrypt và các lỗi bảo mật hay gặp.

Deploy ASP.NET Core lên VPS Linux
Cách deploy .NET lên Linux: cài .NET 10 trên Ubuntu 24.04, publish, chạy Kestrel bằng systemd sau nginx với SSL, làm tay hoặc bằng ZoPanel.