Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 32: Deploy ứng dụng Laravel 13 lên VPS

Ở bài trước TaskFlow đã có đủ tính năng, nhưng mới chỉ chạy trên máy của mình. Bài cuối của series, mình sẽ hướng dẫn mọi người deploy ứng dụng Laravel 13 lên VPS.

1. Chuẩn bị server.

VPS là máy chủ ảo riêng, các bạn có toàn quyền cài đặt. Nginx nhận request rồi chuyển phần PHP cho PHP-FPM (trình quản lý các process PHP) xử lý. Mình dùng VPS chạy Ubuntu 26.04 LTS, tên miền taskflow.example.com đã trỏ về IP của server. Kho phần mềm của Ubuntu 26.04 có sẵn mọi thứ TaskFlow cần: Nginx, PHP 8.5-FPM, MySQL 8.4, Supervisor, Composer và Node.js 22 (để build CSS/JS bằng Vite):

sudo apt update
sudo apt install -y nginx mysql-server supervisor git unzip composer nodejs npm \
    php8.5-fpm php8.5-cli php8.5-mysql php8.5-mbstring php8.5-xml php8.5-curl php8.5-zip php8.5-intl
php -v
# PHP 8.5.4 (cli) (built: Oct 19 2026 14:35:18) (NTS)

Các extension còn lại mà Laravel yêu cầu đã có sẵn trong gói PHP. Laravel 13 hỗ trợ PHP 8.3–8.5, riêng composer.lock của TaskFlow dùng Symfony 8 nên cần PHP ≥ 8.4, mình đã chạy lại cả bộ test trên PHP 8.5. Nếu muốn đúng PHP 8.4 thì dùng Ubuntu 24.04 kèm PPA ondrej/php (PPA này chưa hỗ trợ 26.04).

Tiếp theo tạo database và user MySQL riêng cho ứng dụng (sudo mysql):

CREATE DATABASE taskflow CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'taskflow'@'localhost' IDENTIFIED BY 'mat-khau-that-manh';
GRANT ALL PRIVILEGES ON taskflow.* TO 'taskflow'@'localhost';

2. Đưa code lên server.

Code được đặt ở /var/www/taskflow, thuộc user deploy (user mình dùng để SSH, nằm trong group www-data của Nginx và PHP-FPM). Lấy code từ git rồi cài package:

sudo mkdir -p /var/www/taskflow
sudo chown deploy:www-data /var/www/taskflow
git clone https://github.com/ten-cua-ban/taskflow.git /var/www/taskflow
cd /var/www/taskflow
composer install --no-dev --optimize-autoloader
npm ci
npm run build
cp .env.example .env

--no-dev bỏ các package chỉ dùng khi phát triển (Pest, Faker...), --optimize-autoloader tạo classmap cho autoload nhanh hơn (Bài 15). public/build không có trong git nên phải build trên server.

Sửa các giá trị trong .env (Bài 2) cho production:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://taskflow.example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=taskflow
DB_USERNAME=taskflow
DB_PASSWORD=mat-khau-that-manh

# gửi mail thật thay cho log (Bài 27)
MAIL_MAILER=smtp
MAIL_HOST=smtp.nha-cung-cap.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS="[email protected]"

# chỉ cần khi dùng nút "Tóm tắt bằng AI" (Bài 31)
OPENAI_API_KEY=...

Chú ý: APP_DEBUG=false là bắt buộc, bật debug trên production sẽ lộ cấu hình, mật khẩu cho người dùng (Bài 11). File .env chỉ nằm trên server, đã có trong .gitignore, không bao giờ commit.

Tiếp theo phân quyền, tạo key, tạo bảng và cache cấu hình:

chmod 640 .env
sudo chgrp www-data .env
php artisan key:generate
sudo chown -R deploy:www-data storage bootstrap/cache
sudo chmod -R ug+rwX,o-rwx storage bootstrap/cache
sudo -u www-data php artisan migrate --force
sudo -u www-data php artisan optimize
#   INFO  Caching framework bootstrap, configuration, and metadata.
#   config ........................................................ 11.66ms DONE
#   events ......................................................... 0.98ms DONE
#   routes ........................................................ 23.98ms DONE
#   views ......................................................... 45.46ms DONE
  • 640: chỉ deploy và group www-data (PHP-FPM) đọc được .env, mặc định cp tạo file 644, user nào cũng đọc được.
  • ug+rwX,o-rwx: deploy và group www-data được đọc, ghi storage, bootstrap/cache (X chỉ thêm quyền vào thư mục), user khác không vào được vì config cache chứa mật khẩu như .env.
  • sudo -u www-data: artisan chạy cùng user với PHP-FPM, worker và cron. Chạy bằng deploy thì không ghi được vào storage/logs/laravel.log (do www-data tạo, quyền 644), lệnh lỗi sẽ báo "could not be opened in append mode" thay cho lỗi thật, lỗi cũng không vào được log.
  • migrate --force: ở production, migrate hỏi lại trước khi chạy, --force bỏ qua câu hỏi để chạy được trong script. Không chạy --seed vì seeder tạo user demo mật khẩu password.
  • optimize: cache config, event, route và view. Sau khi cache config, .env không được đọc nữa, sửa .env xong phải chạy lại.
  • Không cần storage:link (Bài 20) vì file đính kèm nằm ở disk local (private).

3. Cấu hình Nginx và HTTPS.

Mình lưu bản mẫu server block trong project, dựa theo tài liệu Laravel. Điểm quan trọng nhất là root trỏ vào thư mục public, mọi request đi qua public/index.php, nếu trỏ vào thư mục gốc thì ai cũng tải được .env.

- deploy/nginx.conf

# bản mẫu: sudo cp vào /etc/nginx/sites-available/taskflow (không symlink)
server {
    listen 80;
    listen [::]:80;
    server_name taskflow.example.com;
    root /var/www/taskflow/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    # file đính kèm tối đa 1 MB (Bài 20), mặc định của Nginx là 1m nên nới ra 2m
    client_max_body_size 2m;

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

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

    # chặn mọi file .php khác và file ẩn (.env, .git...)
    location ~ \.php$ {
        return 404;
    }

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

Mình copy bản mẫu vào /etc/nginx chứ không tạo symlink: Nginx chạy bằng root không nên đọc file mà user deploy sửa được, và certbot sửa cấu hình thì file trong git không bị đổi (bị đổi thì lần git pull sau sẽ lỗi). Sửa bản mẫu thì copy lại. Bật site, kiểm tra cú pháp rồi reload Nginx:

sudo cp deploy/nginx.conf /etc/nginx/sites-available/taskflow
sudo ln -s /etc/nginx/sites-available/taskflow /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

Lúc này http://taskflow.example.com/up (health route có sẵn) trả về 200, dùng để gắn vào công cụ theo dõi uptime. Bật HTTPS miễn phí với Let's Encrypt:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d taskflow.example.com

Certbot lấy chứng chỉ, sửa server block sang HTTPS và cài sẵn certbot.timer để tự gia hạn.

4. Queue worker và scheduler.

Worker (Bài 28) là process chạy mãi, cần một chương trình giữ cho nó luôn chạy và tự bật lại khi bị dừng. Mình dùng Supervisor, nhớ --queue=reminders,default vì job nhắc việc nằm ở queue reminders:

- deploy/taskflow-worker.conf

; bản mẫu: sudo cp vào /etc/supervisor/conf.d/ (không symlink)
[program:taskflow-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/taskflow/artisan queue:work --queue=reminders,default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/taskflow/storage/logs/worker.log
stopwaitsecs=3600

numprocs=2 chạy 2 worker song song, user=www-data giống PHP-FPM, stopwaitsecs cho job đang chạy thời gian làm xong trước khi bị dừng.

Supervisor cũng chạy bằng root và làm theo user=, command= trong file này nên mình cũng copy:

sudo cp deploy/taskflow-worker.conf /etc/supervisor/conf.d/
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status
# taskflow-worker:taskflow-worker_00   RUNNING   pid 330, uptime 0:00:07
# taskflow-worker:taskflow-worker_01   RUNNING   pid 331, uptime 0:00:07

Scheduler (Bài 29) cần một dòng cron chạy schedule:run mỗi phút để lệnh tasks:remind chạy lúc 8 giờ sáng. Mở crontab của www-data bằng sudo crontab -u www-data -e rồi thêm:

* * * * * cd /var/www/taskflow && php artisan schedule:run >> /dev/null 2>&1

5. Cập nhật phiên bản mới.

Mỗi lần có code mới, các bạn SSH vào server và chạy script này:

- deploy/deploy.sh

#!/usr/bin/env bash
# Cập nhật TaskFlow trên server: bash deploy/deploy.sh
set -e
cd /var/www/taskflow

# artisan chạy bằng www-data, cùng user với PHP-FPM, worker, cron nên ghi được log
sudo -u www-data php artisan down
git pull origin master
composer install --no-dev --optimize-autoloader --no-interaction
npm ci
npm run build
sudo -u www-data php artisan migrate --force
sudo -u www-data php artisan optimize
sudo -u www-data php artisan reload
sudo -u www-data php artisan up

down bật chế độ bảo trì (trả về 503) trong lúc cập nhật, up tắt đi. reload chạy queue:restart và schedule:interrupt: worker làm xong job hiện tại rồi thoát, Supervisor bật lại với code mới. Thiếu bước này worker vẫn chạy code cũ.

Cách này vẫn có vài giây site báo bảo trì. Muốn deploy không gián đoạn (zero-downtime) thì dùng Deployer, Envoyer: mỗi bản được clone vào một thư mục release mới rồi đổi symlink sang. Không muốn tự quản lý server thì có Laravel Forge hoặc Laravel Cloud.

6. Lời kết.

Như vậy chúng ta đã đưa TaskFlow lên VPS: Nginx, PHP-FPM, MySQL, .env production, quyền file, worker với Supervisor, cron cho scheduler và HTTPS. Các bạn xem thêm tại tài liệu chính thức.

Đây cũng là bài cuối của series Laravel 13: từ một project trống, TaskFlow giờ có task, file đính kèm, đăng nhập, phân quyền, API, mail, queue, lịch nhắc việc, cache, test và AI. Tiếp theo các bạn có thể tìm hiểu Livewire hoặc Inertia cho giao diện, Horizon, Reverb, Octane nhé!

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha