Skip to content

Instantly share code, notes, and snippets.

@anhtuank7c
Last active June 10, 2026 11:06
Show Gist options
  • Select an option

  • Save anhtuank7c/c2b5a523e2bf8bdcacad8b7c0e4856ac to your computer and use it in GitHub Desktop.

Select an option

Save anhtuank7c/c2b5a523e2bf8bdcacad8b7c0e4856ac to your computer and use it in GitHub Desktop.
lara - PHP/Laravel site manager

lara — Công cụ quản lý website PHP/Laravel (một file duy nhất)

lara giúp tạo, triển khai và quản lý nhiều website PHP/Laravel trên cùng một máy chủ Ubuntu. Mỗi website chạy dưới một user hệ thống riêng với pool PHP-FPM, socket và cấu hình nginx riêng — cô lập hoàn toàn để site này không đọc được dữ liệu của site kia.

  • ✅ Hỗ trợ PHP 7.4 hoặc 8.4, Laravel 8–13 (tự kiểm tra tương thích)
  • 🗄️ Tạo sẵn database + user MySQL/MariaDB riêng cho mỗi site (tùy chọn)
  • 🔒 Quản lý chứng chỉ SSL qua certbot (phát hành / gia hạn / thu hồi)
  • 🇻🇳 Menu tương tác tiếng Việt (phím mũi tên hoặc gõ số)
  • 🧪 Chạy thử an toàn với --dry-run, tự cập nhật bằng lara update
  • 🛡️ Sửa quyền nhanh sau khi upload code qua SFTP (lara fixperms)
  • 💻 Hệ điều hành: Ubuntu 18.04 – 26.04 LTS

🚀 Cài đặt nhanh (một lệnh)

Chạy trên server mới (cần quyền sudo/root):

sudo curl -fsSL "https://gist.githubusercontent.com/anhtuank7c/c2b5a523e2bf8bdcacad8b7c0e4856ac/raw/lara.sh" -o /usr/local/bin/lara && sudo chmod 755 /usr/local/bin/lara && lara version

Nếu báo thiếu curl: sudo apt update && sudo apt install -y curl rồi chạy lại.

Xong! Giờ gõ sudo lara ở bất cứ đâu để mở menu.


✅ Kiểm tra môi trường trước khi dùng

sudo lara check

lara cần: nginx, php<ver>-fpm + php<ver>-cli, composer. Tùy chọn: mariadb-server (nếu dùng --db), redis-server, certbot + python3-certbot-nginx (nếu dùng SSL). Lệnh check sẽ chỉ rõ thứ còn thiếu kèm lệnh cài tương ứng.

Gợi ý cài nhanh các gói (Ubuntu, dùng PPA ondrej cho PHP):

sudo apt update
sudo apt install -y nginx composer mariadb-server certbot python3-certbot-nginx
sudo add-apt-repository -y ppa:ondrej/php && sudo apt update
# ví dụ cho PHP 8.4:
sudo apt install -y php8.4-fpm php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl \
  php8.4-bcmath php8.4-mysql php8.4-zip php8.4-gd php8.4-intl

📖 Cách dùng

Menu tương tác (khuyến nghị)

sudo lara

Dùng phím ↑/↓ rồi Enter, hoặc gõ số rồi Enter. Nhập 0 để quay lại / thoát.

Hoặc dùng lệnh trực tiếp

Lệnh Tác dụng
sudo lara create <domain> --php <7.4|8.4> --laravel <8-13> [--repo <git-url>] [--db] Tạo website mới
sudo lara deploy <domain> Cập nhật mã nguồn (git pull + composer + migrate + cache)
sudo lara fixperms <domain> Sửa lại quyền (sau khi upload code qua SFTP bằng root)
sudo lara remove <domain> Xoá website
sudo lara cert issue <domain> Phát hành chứng chỉ SSL
sudo lara cert renew [<domain>] Gia hạn chứng chỉ
lara list Liệt kê các website do lara quản lý
sudo lara check Kiểm tra môi trường máy chủ
sudo lara update Tự cập nhật lara lên bản mới nhất
lara changelog Xem lịch sử thay đổi

💡 Thêm --dry-run vào bất kỳ lệnh nào để chạy thử (chỉ in ra các bước, không thay đổi gì).


🌱 Ví dụ: tạo một site Laravel 12 kèm database

sudo lara create shop.example.com --php 8.4 --laravel 12 --db
  • Tạo /var/www/shop.example.com, user hệ thống riêng, pool PHP-FPM + socket, vhost nginx (HTTP)
  • Tạo sẵn database + user MySQL và ghi thông tin vào .env

Trỏ bản ghi DNS A của tên miền về server, rồi bật HTTPS:

sudo lara cert issue shop.example.com

Triển khai từ một git repository có sẵn:

sudo lara create app.example.com --php 8.4 --laravel 12 --repo git@github.com:user/app.git --db

🔄 Cập nhật & gỡ cài

sudo lara update      # lên bản mới nhất (tải từ gist này)
sudo lara uninstall   # gỡ lara — các website vẫn được giữ nguyên

⚠️ Luôn chạy lara bằng sudo/root cho các thao tác quản trị. Mỗi website chạy dưới user hệ thống riêng (lv_<domain>) để đảm bảo cô lập giữa các site.

#!/bin/bash
#===============================================================================
# lara - Công cụ quản lý website PHP/Laravel trong một file duy nhất
#
# Phiên bản : 1.13.0 (xem 'lara changelog' để biết lịch sử thay đổi)
# Hệ điều hành : Ubuntu 18.04 - 26.04 LTS (tương thích Debian)
# Cấu trúc : mã nguồn /var/www/[domain]
# socket /var/run/php/[php-version]_[domain].sock
# nginx /etc/nginx/sites-available/[domain].conf (chỉ HTTP)
# fpm pool /etc/php/[php-version]/fpm/pool.d/[domain].conf
#
# Mỗi website chạy dưới một user hệ thống riêng (lv_<domain>) để các website
# trên cùng máy chủ không thể đọc dữ liệu của nhau. User nginx (www-data)
# chỉ kết nối tới socket php-fpm của website và đọc tài nguyên tĩnh trong
# public/ thông qua group www-data trên thư mục mã nguồn.
#
# Cách dùng:
# sudo lara # mở menu tương tác (phím ↑/↓ hoặc gõ số)
#
# sudo ./lara.sh install # cài vào /usr/local/bin/lara + banner chào mừng SSH
# sudo lara create <domain> --php <7.4|8.4> --laravel <8-13> [--repo <git-url>] [--db]
# # có --repo: clone mã nguồn từ git
# # không có --repo: chỉ tạo thư mục trống
# # có --db: tạo database + user MySQL/MariaDB riêng
# sudo lara deploy <domain>
# sudo lara remove <domain>
# sudo lara fixperms <domain> # sửa lại quyền (sau khi upload code/SFTP)
# sudo lara cert <list|status|issue|renew|revoke> [domain] [--no-www] [--force]
# lara list
# lara check [--php <7.4|8.4>]
# lara version # in phiên bản hiện tại
# lara changelog # xem lịch sử thay đổi giữa các bản
# sudo lara update # tự tải bản mới nhất từ gist công khai
# sudo lara uninstall # gỡ /usr/local/bin/lara (website giữ nguyên)
#
# Thêm --dry-run vào bất kỳ lệnh nào để CHẠY THỬ: các bước kiểm tra vẫn chạy
# thật, còn mọi thay đổi hệ thống chỉ được in ra chứ không thực thi.
# Ví dụ: lara create example.com --php 8.4 --laravel 12 --db --dry-run
#===============================================================================
set -euo pipefail
LARA_VERSION="1.13.0"
INSTALL_PATH="/usr/local/bin/lara"
MOTD_PATH="/etc/update-motd.d/99-lara" # banner chào mừng khi đăng nhập SSH
DRY_RUN="" # đặt bằng cờ --dry-run: chỉ hiển thị thay đổi, không thực thi
# Nguồn tự cập nhật ('lara update'): URL raw "MỚI NHẤT" của gist CÔNG KHAI chỉ
# chứa lara.sh (KHÔNG kèm SHA commit để luôn lấy bản mới nhất). Sau khi tạo gist,
# sửa dòng này thành URL của bạn rồi cập nhật lại gist; hoặc ghi đè khi chạy bằng
# biến môi trường: sudo LARA_UPDATE_URL="https://.../raw/lara.sh" lara update
LARA_UPDATE_URL_DEFAULT="https://gist.githubusercontent.com/anhtuank7c/c2b5a523e2bf8bdcacad8b7c0e4856ac/raw/lara.sh"
WWW_ROOT="/var/www"
SOCK_DIR="/var/run/php"
NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"
SUPPORTED_PHP=("7.4" "8.4")
USER_PREFIX="lv_"
# Tương thích Laravel <-> PHP (theo yêu cầu chính thức)
# Laravel 8 : PHP 7.3 - 8.1 -> 7.4
# Laravel 9 : PHP 8.0 - 8.2 -> không có 7.4 lẫn 8.4
# Laravel 10 : PHP 8.1 - 8.3 -> không có 7.4 lẫn 8.4
# Laravel 11 : PHP 8.2 - 8.4 -> 8.4
# Laravel 12 : PHP 8.2 - 8.4 -> 8.4
# Laravel 13 : PHP >= 8.3 -> 8.4
compat_php_for_laravel() {
case "$1" in
8) echo "7.4" ;;
11|12|13) echo "8.4" ;;
9|10) echo "" ;;
*) echo "" ;;
esac
}
REQUIRED_EXTENSIONS=(mbstring xml curl bcmath ctype fileinfo pdo_mysql tokenizer zip gd intl)
# Tên gói apt cho từng extension. Lưu ý: ctype/fileinfo/tokenizer được biên dịch
# sẵn trong nhân PHP (gói php-common), KHÔNG có gói riêng; pdo_mysql nằm trong
# gói php-mysql. Trả về đúng tên gói để hướng dẫn cài không bị sai.
ext_package() {
local v="$1" ext="$2"
case "$ext" in
ctype|fileinfo|tokenizer) echo "php$v-common" ;;
pdo_mysql) echo "php$v-mysql" ;;
*) echo "php$v-$ext" ;;
esac
}
#-------------------------------------------------------------------------------
# Hàm xuất thông báo
#-------------------------------------------------------------------------------
C_RED='\033[0;31m'; C_GRN='\033[0;32m'; C_YLW='\033[0;33m'; C_BLU='\033[0;34m'; C_RST='\033[0m'
# Dùng %b cho phần màu (cần diễn giải \033) và %s cho nội dung (KHÔNG diễn giải
# escape) để tránh chèn mã ANSI/giả mạo dòng log qua dữ liệu đầu vào.
info() { printf '%b %s\n' "${C_BLU}[INFO]${C_RST}" "$*"; }
ok() { printf '%b %s\n' "${C_GRN}[ OK ]${C_RST}" "$*"; }
warn() { printf '%b %s\n' "${C_YLW}[CẢNH BÁO]${C_RST}" "$*"; }
die() { printf '%b %s\n' "${C_RED}[LỖI]${C_RST}" "$*" >&2; exit 1; }
usage() {
sed -n '/^# Cách dùng:/,/^#====/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
#-------------------------------------------------------------------------------
# Kiểm tra dữ liệu đầu vào
#-------------------------------------------------------------------------------
require_root() {
[[ $EUID -eq 0 ]] || die "Lệnh này cần quyền root. Hãy chạy lại với sudo."
}
validate_domain() {
local d="$1"
[[ "$d" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]] \
|| die "Tên miền không hợp lệ: '$d' (ví dụ hợp lệ: example.com)"
[[ ${#d} -le 253 ]] || die "Tên miền quá dài: $d"
}
validate_php_version() {
local v="$1" s
for s in "${SUPPORTED_PHP[@]}"; do [[ "$v" == "$s" ]] && return 0; done
die "Phiên bản PHP '$v' không được hỗ trợ. Hỗ trợ: ${SUPPORTED_PHP[*]}"
}
validate_laravel_version() {
local v="$1"
[[ "$v" =~ ^(8|9|10|11|12|13)$ ]] || die "Phiên bản Laravel '$v' không được hỗ trợ. Hỗ trợ: 8-13"
}
# Chỉ chấp nhận URL git an toàn. Chặn:
# - transport thực thi lệnh của git (ext::, fd::) => RCE
# - file:// (đọc đường dẫn nội bộ)
# - chuỗi bắt đầu bằng '-' (argument injection vào lệnh git)
# - khoảng trắng / ký tự điều khiển
validate_repo_url() {
local url="$1"
[[ -n "$url" ]] || die "URL git rỗng."
[[ "$url" != -* ]] || die "URL git không được bắt đầu bằng '-': $url"
[[ "$url" != *[[:space:][:cntrl:]]* ]] || die "URL git chứa khoảng trắng/ký tự điều khiển không hợp lệ."
case "$url" in
ext::*|fd::*|file://*)
die "Transport git bị cấm vì lý do bảo mật: $url" ;;
https://*|http://*|git://*|ssh://*)
: ;;
*@*:*)
: ;; # dạng scp: user@host:đường-dẫn
*)
die "URL git không hợp lệ: $url
Chỉ chấp nhận https://, http://, ssh://, git:// hoặc dạng user@host:path." ;;
esac
}
validate_compat() {
local laravel="$1" php="$2" required
required="$(compat_php_for_laravel "$laravel")"
if [[ -z "$required" ]]; then
die "Laravel $laravel yêu cầu phiên bản PHP mà công cụ này không cung cấp.
Laravel 9 cần PHP 8.0-8.2, Laravel 10 cần PHP 8.1-8.3,
nhưng công cụ này chỉ hỗ trợ PHP ${SUPPORTED_PHP[*]}.
Hãy chọn Laravel 8 (PHP 7.4) hoặc Laravel 11/12/13 (PHP 8.4)."
fi
[[ "$php" == "$required" ]] \
|| die "Laravel $laravel không tương thích với PHP $php. Hãy dùng PHP $required."
}
# Xác định user mà nginx worker đang chạy (thường là www-data)
nginx_user() {
local u
u=$(grep -E '^\s*user\s+' /etc/nginx/nginx.conf 2>/dev/null \
| head -1 | awk '{print $2}' | tr -d ';') || true
echo "${u:-www-data}"
}
check_environment() {
local php_version="${1:-}" errors=0
info "Đang kiểm tra môi trường máy chủ..."
# Hệ điều hành (hỗ trợ: Ubuntu 18.04 - 26.04 LTS)
if [[ -f /etc/os-release ]]; then
# shellcheck source=/dev/null
. /etc/os-release
if [[ "${ID:-}" == "ubuntu" ]]; then
local os_major="${VERSION_ID%%.*}"
if [[ "$os_major" =~ ^[0-9]+$ ]] && (( os_major >= 18 && os_major <= 26 )); then
ok "Hệ điều hành: ${PRETTY_NAME:-Ubuntu ${VERSION_ID:-?}}"
else
warn "Ubuntu ${VERSION_ID:-?} nằm ngoài phạm vi hỗ trợ (18.04 - 26.04 LTS)"
fi
elif [[ "${ID:-}" == "debian" || "${ID_LIKE:-}" == *debian* ]]; then
warn "Phát hiện hệ điều hành dạng Debian (${PRETTY_NAME:-không rõ}) - có thể chạy được, nhưng Ubuntu 18.04-26.04 mới là môi trường đã kiểm thử"
else
warn "Hệ điều hành chưa được kiểm thử: ${PRETTY_NAME:-không rõ} (công cụ này nhắm tới Ubuntu 18.04 - 26.04 LTS)"
fi
else
warn "Không xác định được hệ điều hành (thiếu file /etc/os-release)"
fi
# nginx
if command -v nginx >/dev/null 2>&1; then
local ngx_ver
ngx_ver=$(nginx -v 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
local ngx_major=${ngx_ver%%.*} ngx_minor; ngx_minor=$(echo "$ngx_ver" | cut -d. -f2)
if (( ngx_major > 1 || (ngx_major == 1 && ngx_minor >= 18) )); then
ok "nginx $ngx_ver"
else
warn "nginx $ngx_ver đã cũ (khuyến nghị >= 1.18)"
fi
[[ -d "$NGINX_AVAILABLE" && -d "$NGINX_ENABLED" ]] \
|| { warn "Thiếu cấu trúc thư mục $NGINX_AVAILABLE / $NGINX_ENABLED"; errors=$((errors + 1)); }
grep -rqs "sites-enabled" /etc/nginx/nginx.conf \
|| warn "nginx.conf có vẻ chưa include thư mục sites-enabled/*"
else
warn "nginx chưa được cài đặt. Cài bằng lệnh: apt install nginx"
errors=$((errors + 1))
fi
# certbot + plugin nginx (cần cho 'lara cert' phát hành/gia hạn/thu hồi SSL)
if command -v certbot >/dev/null 2>&1; then
local cb_ver cb_plugins
cb_ver=$(certbot --version 2>&1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)
# Lấy danh sách plugin một lần rồi đối chiếu bằng here-string - tránh đúng
# lỗi SIGPIPE+pipefail như 'php -m | grep -q' (báo nhầm thiếu plugin nginx).
cb_plugins=$(certbot plugins 2>/dev/null || true)
if grep -qi 'nginx' <<<"$cb_plugins"; then
ok "certbot ${cb_ver:-} (đã có plugin nginx)"
else
warn "certbot ${cb_ver:-} đã cài nhưng THIẾU plugin nginx ('lara cert' cần plugin này):"
warn " Khắc phục: apt install python3-certbot-nginx"
fi
else
warn "certbot chưa được cài (cần cho 'lara cert' / SSL): apt install certbot python3-certbot-nginx"
fi
# Phiên bản PHP
local versions=("${SUPPORTED_PHP[@]}")
[[ -n "$php_version" ]] && versions=("$php_version")
local v
for v in "${versions[@]}"; do
if [[ -x "/usr/sbin/php-fpm$v" ]] && [[ -d "/etc/php/$v/fpm/pool.d" ]]; then
ok "PHP $v FPM đã được cài đặt"
# Cần trình CLI php$v để đọc danh sách extension. Nếu thiếu, mọi
# extension sẽ bị báo "thiếu" oan -> kiểm tra và báo rõ trước.
if [[ ! -x "/usr/bin/php$v" ]]; then
warn "Thiếu trình CLI php$v nên không kiểm tra được extension."
warn " Khắc phục: apt install php$v-cli"
[[ -n "$php_version" ]] && errors=$((errors + 1))
continue
fi
# Lấy danh sách module MỘT lần vào biến rồi đối chiếu bằng here-string.
# TUYỆT ĐỐI không dùng 'php -m | grep -q': grep -q thoát ngay khi khớp
# -> php nhận SIGPIPE -> với 'set -o pipefail' pipeline trả non-zero
# -> báo NHẦM là thiếu một cách ngẫu nhiên (theo thứ tự module). Bug thật.
local installed_mods missing=() ext
installed_mods=$("/usr/bin/php$v" -m 2>/dev/null)
for ext in "${REQUIRED_EXTENSIONS[@]}"; do
grep -qix "$ext" <<<"$installed_mods" || missing+=("$ext")
done
if ((${#missing[@]})); then
warn "PHP $v thiếu các extension: ${missing[*]}"
# Quy đổi sang tên gói đúng và bỏ trùng (nhiều extension có thể
# cùng thuộc php-common).
local pkgs=() fix
for ext in "${missing[@]}"; do pkgs+=("$(ext_package "$v" "$ext")"); done
fix=$(printf '%s\n' "${pkgs[@]}" | sort -u | tr '\n' ' ')
warn " Khắc phục: apt install ${fix% }"
[[ -n "$php_version" ]] && errors=$((errors + 1))
else
ok "PHP $v đã có đủ các extension cần thiết"
fi
else
warn "PHP $v FPM chưa được cài đặt."
warn " Khắc phục: add-apt-repository ppa:ondrej/php && apt update"
warn " apt install php$v-fpm php$v-cli $(printf "php$v-%s " mbstring xml curl bcmath mysql zip gd intl)"
[[ -n "$php_version" ]] && errors=$((errors + 1))
fi
done
# composer
if command -v composer >/dev/null 2>&1; then
ok "composer $(composer --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
else
warn "composer chưa được cài đặt."
warn " Khắc phục: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer"
errors=$((errors + 1))
fi
# git (chỉ cần cho --repo / deploy)
command -v git >/dev/null 2>&1 || warn "git chưa được cài (cần khi deploy bằng --repo): apt install git"
# MySQL/MariaDB (không chặn - có thể dùng CSDL từ xa hoặc SQLite)
local db_ver=""
if systemctl is-active --quiet mariadb 2>/dev/null; then
db_ver=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true
ok "MariaDB ${db_ver:-} đang chạy"
elif systemctl is-active --quiet mysql 2>/dev/null; then
db_ver=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true
ok "MySQL ${db_ver:-} đang chạy"
elif command -v mysqld >/dev/null 2>&1 || command -v mariadbd >/dev/null 2>&1; then
warn "MySQL/MariaDB đã cài nhưng chưa chạy: systemctl start mariadb (hoặc mysql)"
else
warn "MySQL/MariaDB chưa cài (bỏ qua nếu dùng CSDL từ xa/SQLite): apt install mariadb-server"
fi
# Redis (không chặn - chỉ cần khi dùng cache/queue/session Redis)
if systemctl is-active --quiet redis-server 2>/dev/null || systemctl is-active --quiet redis 2>/dev/null; then
ok "Redis đang chạy"
elif command -v redis-server >/dev/null 2>&1; then
warn "Redis đã cài nhưng chưa chạy: systemctl start redis-server"
else
warn "Redis chưa cài (bỏ qua nếu không dùng cache/queue Redis): apt install redis-server"
fi
if ((errors > 0)); then
die "Kiểm tra môi trường thất bại với $errors lỗi nghiêm trọng. Hãy khắc phục các mục trên rồi thử lại."
fi
ok "Môi trường đạt yêu cầu."
}
#-------------------------------------------------------------------------------
# Hàm đặt tên
#-------------------------------------------------------------------------------
site_user_for() {
# example.com -> lv_example_com (username Linux: tối đa 32 ký tự, không có dấu chấm)
local slug
slug=$(echo "$1" | tr 'A-Z.-' 'a-z__' | tr -cd 'a-z0-9_')
echo "${USER_PREFIX}${slug}" | cut -c1-32
}
sock_path_for() { echo "$SOCK_DIR/${2}_${1}.sock"; } # $1=domain $2=php
pool_conf_for() { echo "/etc/php/$2/fpm/pool.d/$1.conf"; }
nginx_conf_for() { echo "$NGINX_AVAILABLE/$1.conf"; }
# Tìm phiên bản PHP của một domain dựa trên file pool
find_php_version() {
local domain="$1" v
for v in "${SUPPORTED_PHP[@]}"; do
[[ -f "$(pool_conf_for "$domain" "$v")" ]] && { echo "$v"; return 0; }
done
return 1
}
# Thực thi một lệnh THAY ĐỔI hệ thống - ở chế độ dry-run thì chỉ in ra.
# Các lệnh chỉ đọc (kiểm tra, validate) không đi qua hàm này.
run_cmd() {
if [[ -n "$DRY_RUN" ]]; then
echo -e "${C_YLW}[DRY-RUN]${C_RST} $*"
else
"$@"
fi
}
# Ghi file từ stdin - ở chế độ dry-run thì in nội dung sẽ ghi.
write_file() {
local path="$1"
if [[ -n "$DRY_RUN" ]]; then
echo -e "${C_YLW}[DRY-RUN]${C_RST} Sẽ ghi file $path với nội dung:"
sed 's/^/ | /'
else
cat > "$path"
fi
}
run_as() {
local user="$1"; shift
if [[ -n "$DRY_RUN" ]]; then
echo -e "${C_YLW}[DRY-RUN]${C_RST} (chạy bằng user $user) $*"
return 0
fi
sudo -u "$user" -H env COMPOSER_ALLOW_SUPERUSER=0 "$@"
}
# Khoá theo từng domain để hai tiến trình lara không thao tác đồng thời lên cùng
# một website (tránh đua tranh git/composer/migrate). Khoá tự nhả khi thoát.
acquire_lock() {
local domain="$1"
[[ -n "$DRY_RUN" ]] && return 0
command -v flock >/dev/null 2>&1 || { warn "Không có 'flock' - bỏ qua khoá chống chạy song song."; return 0; }
local lock_dir="/run/lock"
[[ -d "$lock_dir" && -w "$lock_dir" ]] || lock_dir="/tmp"
exec 9>"$lock_dir/lara-$domain.lock" 2>/dev/null || return 0
flock -n 9 \
|| die "Đang có một tiến trình lara khác thao tác trên '$domain'. Hãy đợi nó hoàn tất rồi thử lại."
}
#-------------------------------------------------------------------------------
# Database (tuỳ chọn) - mỗi website một database + user CSDL riêng
#-------------------------------------------------------------------------------
db_service_running() {
systemctl is-active --quiet mariadb 2>/dev/null \
|| systemctl is-active --quiet mysql 2>/dev/null
}
mysql_client() {
if command -v mysql >/dev/null 2>&1; then mysql "$@"
elif command -v mariadb >/dev/null 2>&1; then mariadb "$@"
else die "Không tìm thấy mysql/mariadb client."
fi
}
db_exists() {
[[ -n "$(mysql_client -N -e \
"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$1'" 2>/dev/null)" ]]
}
# Tạo database + user CSDL cùng tên slug của website, mật khẩu ngẫu nhiên.
# Kết quả đặt vào các biến toàn cục DB_NAME / DB_USER / DB_PASS.
create_site_database() {
local domain="$1"
DB_NAME=$(site_user_for "$domain")
DB_USER="$DB_NAME"
db_service_running \
|| die "MySQL/MariaDB chưa chạy - không thể tạo database. Hãy khởi động dịch vụ (systemctl start mariadb) hoặc bỏ tuỳ chọn --db."
# Kiểm tra kết nối quyền quản trị trước. Ubuntu mặc định cho root kết nối
# qua unix_socket khi chạy bằng sudo; nếu root CSDL có mật khẩu thì cần
# /root/.my.cnf. Báo lỗi rõ ràng thay vì để lệnh CREATE thất bại khó hiểu.
if [[ -z "$DRY_RUN" ]] && ! mysql_client -e "SELECT 1;" >/dev/null 2>&1; then
die "Không kết nối được MySQL/MariaDB bằng quyền quản trị.
Ubuntu mặc định cho phép root kết nối qua unix_socket (chạy lara bằng sudo).
Nếu root CSDL có mật khẩu: tạo file /root/.my.cnf với mục [client] user+password,
hoặc tự tạo database thủ công rồi tạo website mà KHÔNG dùng --db."
fi
db_exists "$DB_NAME" \
&& die "Database '$DB_NAME' đã tồn tại - không ghi đè. Hãy tự xử lý database này hoặc bỏ tuỳ chọn --db."
DB_PASS=$(openssl rand -hex 16 2>/dev/null) \
|| DB_PASS=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
if [[ -n "$DRY_RUN" ]]; then
echo -e "${C_YLW}[DRY-RUN]${C_RST} Sẽ chạy SQL:"
echo " | CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
echo " | CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '<mật khẩu ngẫu nhiên>';"
echo " | GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';"
return 0
fi
mysql_client <<SQL
CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';
FLUSH PRIVILEGES;
SQL
ok "Đã tạo database '$DB_NAME' và user CSDL '$DB_USER'@'localhost'"
}
# Ghi thông tin CSDL vào file .env (xử lý được cả dòng đang bị chú thích '# DB_...')
write_env_db() {
local site_user="$1" env_file="$2"
[[ -f "$env_file" ]] || return 0
run_as "$site_user" sed -i -E \
-e "s|^#? ?DB_CONNECTION=.*|DB_CONNECTION=mysql|" \
-e "s|^#? ?DB_HOST=.*|DB_HOST=127.0.0.1|" \
-e "s|^#? ?DB_PORT=.*|DB_PORT=3306|" \
-e "s|^#? ?DB_DATABASE=.*|DB_DATABASE=$DB_NAME|" \
-e "s|^#? ?DB_USERNAME=.*|DB_USERNAME=$DB_USER|" \
-e "s|^#? ?DB_PASSWORD=.*|DB_PASSWORD=$DB_PASS|" \
"$env_file"
ok "Đã ghi thông tin CSDL vào $env_file"
}
#-------------------------------------------------------------------------------
# Áp bộ quyền chuẩn cho một website (dùng chung cho 'create' và 'fixperms'):
# - chủ sở hữu = user riêng của site, group = user nginx (www-data)
# - thư mục 750, file 640; người ngoài không có quyền
# - storage/ và bootstrap/cache ghi được (owner + group)
# - artisan thực thi được (750); .env siết 600 (chứa bí mật)
# - đảm bảo có storage/tmp (thư mục tạm riêng theo cấu hình pool)
#-------------------------------------------------------------------------------
apply_site_permissions() {
local web_root="$1" site_user="$2" ngx_user="$3" d
run_cmd mkdir -p "$web_root/storage/tmp"
run_cmd chown -R "$site_user:$ngx_user" "$web_root"
run_cmd find "$web_root" -type d -exec chmod 750 {} +
run_cmd find "$web_root" -type f -exec chmod 640 {} +
for d in "$web_root/storage" "$web_root/bootstrap/cache"; do
[[ -d "$d" ]] && run_cmd chmod -R u+rwX,g+rwX "$d"
done
run_cmd chmod 750 "$web_root"
[[ -f "$web_root/artisan" ]] && run_cmd chmod 750 "$web_root/artisan"
[[ -f "$web_root/.env" ]] && run_cmd chmod 600 "$web_root/.env"
}
#-------------------------------------------------------------------------------
# Hoàn tác khi 'create' thất bại giữa chừng. Chỉ chạy khi RB_ACTIVE đang bật
# (tức đã qua assert_site_absent -> chắc chắn mọi tài nguyên vốn KHÔNG tồn tại,
# nên xoá đi là an toàn) và lệnh thoát với mã lỗi. An toàn để gọi nhiều lần.
#-------------------------------------------------------------------------------
_create_rollback() {
local code=$?
[[ -n "${RB_ACTIVE:-}" ]] || return 0
[[ -n "$DRY_RUN" ]] && return 0
(( code == 0 )) && return 0
warn "Tạo website thất bại - đang hoàn tác các thay đổi đã thực hiện..."
rm -f "$NGINX_ENABLED/$RB_DOMAIN.conf" "$(nginx_conf_for "$RB_DOMAIN")" 2>/dev/null || true
local v
for v in "${SUPPORTED_PHP[@]}"; do
rm -f "$(pool_conf_for "$RB_DOMAIN" "$v")" "$(sock_path_for "$RB_DOMAIN" "$v")" 2>/dev/null || true
done
[[ -n "$RB_PHP" ]] && systemctl restart "php$RB_PHP-fpm" 2>/dev/null || true
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
[[ -d "$WWW_ROOT/$RB_DOMAIN" ]] && { rm -rf "${WWW_ROOT:?}/$RB_DOMAIN" || true; }
if id "$RB_USER" &>/dev/null; then
userdel -r "$RB_USER" 2>/dev/null || userdel "$RB_USER" 2>/dev/null || true
fi
if [[ -n "$RB_DB" ]] && db_service_running && db_exists "$RB_USER"; then
mysql_client -e "DROP DATABASE IF EXISTS \`$RB_USER\`; DROP USER IF EXISTS '$RB_USER'@'localhost'; FLUSH PRIVILEGES;" 2>/dev/null || true
fi
warn "Đã hoàn tác. Hệ thống trở lại trạng thái trước khi tạo '$RB_DOMAIN'."
}
#-------------------------------------------------------------------------------
# Chống tạo trùng: kiểm tra TẤT CẢ tài nguyên của website TRƯỚC khi tạo bất cứ
# thứ gì. Chỉ cần một thành phần đã tồn tại là dừng ngay và liệt kê, để không
# bao giờ tạo website trùng hoặc chồng lên một website đang có.
#-------------------------------------------------------------------------------
assert_site_absent() {
local domain="$1" site_user="$2" want_db="$3"
local conflicts=() v
[[ -e "$WWW_ROOT/$domain" ]] && conflicts+=("thư mục mã nguồn: $WWW_ROOT/$domain")
[[ -e "$(nginx_conf_for "$domain")" ]] && conflicts+=("cấu hình nginx: $(nginx_conf_for "$domain")")
# -e theo symlink (link gãy sẽ trả false), nên kiểm tra thêm -L để bắt cả
# symlink trỏ tới đích đã bị xoá - đúng kiểu rác mà site xoá dở để lại.
{ [[ -e "$NGINX_ENABLED/$domain.conf" ]] || [[ -L "$NGINX_ENABLED/$domain.conf" ]]; } \
&& conflicts+=("symlink nginx: $NGINX_ENABLED/$domain.conf")
for v in "${SUPPORTED_PHP[@]}"; do
[[ -e "$(pool_conf_for "$domain" "$v")" ]] && conflicts+=("pool PHP $v: $(pool_conf_for "$domain" "$v")")
[[ -S "$(sock_path_for "$domain" "$v")" ]] && conflicts+=("socket PHP $v: $(sock_path_for "$domain" "$v")")
done
id "$site_user" &>/dev/null && conflicts+=("user hệ thống: $site_user")
if [[ -n "$want_db" ]] && db_service_running && db_exists "$site_user"; then
conflicts+=("database: $site_user")
fi
if (( ${#conflicts[@]} > 0 )); then
local msg="Website '$domain' (hoặc tài nguyên cùng tên) ĐÃ TỒN TẠI - không tạo trùng."
msg+=$'\n'" Các thành phần đã có:"
local c
for c in "${conflicts[@]}"; do msg+=$'\n'" - $c"; done
msg+=$'\n'" Muốn tạo lại? Hãy xoá website trước bằng: $0 remove $domain"
die "$msg"
fi
}
#-------------------------------------------------------------------------------
# create - tạo website mới
#-------------------------------------------------------------------------------
cmd_create() {
local domain="" php_version="" laravel_version="" repo_url="" want_db=""
DB_NAME=""; DB_USER=""; DB_PASS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--php) php_version="${2:-}"; shift 2 ;;
--laravel) laravel_version="${2:-}"; shift 2 ;;
--repo) repo_url="${2:-}"; shift 2 ;;
--db) want_db=1; shift ;;
-h|--help) usage ;;
-*) die "Tham số không hợp lệ: $1" ;;
*) if [[ -z "$domain" ]]; then domain="$1"; else die "Tham số thừa: $1"; fi; shift ;;
esac
done
[[ -n "$domain" ]] || die "Thiếu tên miền. Cách dùng: $0 create <domain> --php <7.4|8.4> --laravel <8-13>"
validate_domain "$domain"
# Hỏi tương tác nếu thiếu tham số
if [[ -z "$php_version" ]]; then
read -rp "Phiên bản PHP [${SUPPORTED_PHP[*]}]: " php_version
fi
validate_php_version "$php_version"
if [[ -z "$laravel_version" ]]; then
read -rp "Phiên bản Laravel [8-13]: " laravel_version
fi
validate_laravel_version "$laravel_version"
validate_compat "$laravel_version" "$php_version"
[[ -n "$repo_url" ]] && validate_repo_url "$repo_url"
local web_root="$WWW_ROOT/$domain"
local site_user; site_user=$(site_user_for "$domain")
local sock; sock=$(sock_path_for "$domain" "$php_version")
local pool_conf; pool_conf=$(pool_conf_for "$domain" "$php_version")
local ngx_conf; ngx_conf=$(nginx_conf_for "$domain")
local ngx_user; ngx_user=$(nginx_user)
# Chống tạo trùng: kiểm tra mọi tài nguyên TRƯỚC khi tạo bất cứ thứ gì.
assert_site_absent "$domain" "$site_user" "$want_db"
check_environment "$php_version"
# Khoá domain (chống chạy song song) và bật cơ chế hoàn tác: kể từ đây, nếu
# có lỗi giữa chừng, mọi thứ vừa tạo sẽ được dọn sạch (xem _create_rollback).
acquire_lock "$domain"
RB_DOMAIN="$domain"; RB_USER="$site_user"; RB_PHP="$php_version"; RB_DB="$want_db"; RB_ACTIVE=1
trap _create_rollback EXIT
# Tạo database trước tiên (nếu được yêu cầu) - lỗi ở bước này sẽ dừng sớm,
# trước khi bất kỳ thành phần nào của website được tạo ra.
if [[ -n "$want_db" ]]; then
create_site_database "$domain"
fi
info "Đang tạo website $domain (PHP $php_version, Laravel $laravel_version, user $site_user)"
#--- user hệ thống riêng, thuộc group của nginx -----------------------------
# assert_site_absent đã đảm bảo user chưa tồn tại. Nếu nó vừa xuất hiện
# (đua tranh hiếm gặp, hoặc trùng slug với site khác) thì DỪNG thay vì dùng
# lại - dùng nhầm user của site khác sẽ phá vỡ sự cô lập giữa các site.
id "$site_user" &>/dev/null \
&& die "User $site_user vừa xuất hiện (xung đột tên/đua tranh) - đã dừng để an toàn."
run_cmd useradd --system --create-home --home-dir "/home/$site_user" \
--shell /usr/sbin/nologin "$site_user"
ok "Đã tạo user hệ thống $site_user"
run_cmd usermod -aG "$ngx_user" "$site_user"
#--- thư mục mã nguồn + cài Laravel ------------------------------------------
run_cmd mkdir -p "$web_root"
run_cmd chown "$site_user:$ngx_user" "$web_root"
if [[ -n "$repo_url" ]]; then
info "Đang clone $repo_url ..."
run_as "$site_user" git clone "$repo_url" "$web_root"
info "Đang cài các gói composer..."
run_as "$site_user" composer install --working-dir="$web_root" \
--no-dev --optimize-autoloader --no-interaction
if [[ ! -f "$web_root/.env" ]]; then
run_as "$site_user" cp "$web_root/.env.example" "$web_root/.env"
run_as "$site_user" "php$php_version" "$web_root/artisan" key:generate --force
fi
else
info "Tạo thư mục trống (bạn sẽ tự đưa mã nguồn lên sau)..."
run_as "$site_user" mkdir -p "$web_root/public"
write_file "$web_root/public/index.php" <<EOF
<?php
// File tạm do lara tạo - hãy thay bằng mã nguồn Laravel của bạn.
echo 'Website ' . htmlspecialchars(\$_SERVER['HTTP_HOST'] ?? '$domain') . ' đang được thiết lập.';
EOF
run_cmd chown "$site_user:$ngx_user" "$web_root/public/index.php"
fi
# APP_URL (dùng http cho tới khi chạy certbot)
[[ -f "$web_root/.env" ]] && run_as "$site_user" \
sed -i "s|^APP_URL=.*|APP_URL=http://$domain|" "$web_root/.env"
# Ghi thông tin CSDL vào .env (nếu đã tạo database và có file .env)
if [[ -n "$want_db" ]]; then
write_env_db "$site_user" "$web_root/.env"
fi
#--- phân quyền (dùng chung với 'lara fixperms') ----------------------------
# storage/tmp + chủ sở hữu site_user:nginx, 750/640, storage ghi được, .env 600
apply_site_permissions "$web_root" "$site_user" "$ngx_user"
ok "Đã phân quyền ($site_user:$ngx_user, 750/640, .env 600, cho phép ghi vào storage)"
#--- pool php-fpm --------------------------------------------------------------
write_file "$pool_conf" <<EOF
; Managed by lara - site: $domain
[$domain]
user = $site_user
group = $site_user
listen = $sock
listen.owner = $ngx_user
listen.group = $ngx_user
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
pm.max_requests = 500
chdir = $web_root
php_admin_value[error_log] = /var/log/php/${php_version}_${domain}.error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[open_basedir] = $web_root:/usr/share/php
php_admin_value[sys_temp_dir] = $web_root/storage/tmp
php_admin_value[upload_tmp_dir] = $web_root/storage/tmp
php_admin_value[session.save_path] = $web_root/storage/framework/sessions
EOF
run_cmd mkdir -p /var/log/php
run_cmd "php-fpm$php_version" -t || die "Kiểm tra cấu hình php-fpm$php_version thất bại (xem chi tiết phía trên)."
run_cmd systemctl restart "php$php_version-fpm"
run_cmd systemctl enable -q "php$php_version-fpm" 2>/dev/null || true
ok "Đã tạo pool PHP-FPM -> $sock"
#--- vhost nginx (chỉ HTTP; certbot sẽ thêm SSL sau) --------------------------
write_file "$ngx_conf" <<EOF
# Managed by lara - site: $domain
# Chỉ HTTP. Chạy certbot để thêm SSL (certbot sẽ tự sửa file này).
server {
listen 80;
listen [::]:80;
server_name $domain www.$domain;
root $web_root/public;
index index.php;
charset utf-8;
access_log /var/log/nginx/${domain}.access.log;
error_log /var/log/nginx/${domain}.error.log;
client_max_body_size 64M;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
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; }
location ~ \.php\$ {
try_files \$uri =404;
fastcgi_pass unix:$sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
EOF
run_cmd ln -sf "$ngx_conf" "$NGINX_ENABLED/$domain.conf"
run_cmd nginx -t || die "Kiểm tra cấu hình nginx thất bại. Website CHƯA được kích hoạt. Hãy xem lại $ngx_conf"
run_cmd systemctl reload nginx
ok "Đã kích hoạt vhost nginx (chỉ HTTP)"
# Mọi bước đã xong - tắt cơ chế hoàn tác để website được giữ lại.
RB_ACTIVE=""; trap - EXIT
#--- tóm tắt -------------------------------------------------------------------
echo
echo "==============================================================="
ok "Tạo website thành công!"
echo " Tên miền : http://$domain"
echo " Mã nguồn : $web_root"
echo " Chạy bằng : $site_user (thuộc group $ngx_user)"
echo " PHP : $php_version (socket: $sock)"
echo " Laravel : $laravel_version"
echo " Pool FPM : $pool_conf"
echo " Cấu hình nginx : $ngx_conf"
if [[ -n "$DB_NAME" ]]; then
echo
echo " Database : $DB_NAME (CHARACTER SET utf8mb4)"
echo " User CSDL : $DB_USER@localhost"
if [[ -f "$web_root/.env" ]]; then
# Mật khẩu đã nằm an toàn trong .env (quyền 600) - không in ra
# màn hình để tránh lọt vào scrollback/log terminal/CI.
echo " Mật khẩu : đã ghi vào $web_root/.env (quyền 600), không in ra màn hình"
else
echo -e " Mật khẩu : ${C_YLW}$DB_PASS${C_RST}"
echo -e " ${C_YLW}HÃY LƯU MẬT KHẨU NÀY NGAY${C_RST} - nó chỉ hiển thị một lần duy nhất,"
echo " bạn sẽ cần điền vào .env khi đưa mã nguồn lên."
fi
fi
echo
echo " Các bước tiếp theo:"
echo " 1. Trỏ bản ghi DNS A của $domain (và www.$domain) về máy chủ này."
if [[ -n "$repo_url" ]]; then
echo " 2. Cấu hình cơ sở dữ liệu trong $web_root/.env, sau đó chạy:"
echo " sudo -u $site_user php$php_version $web_root/artisan migrate"
else
echo " 2. Đưa mã nguồn Laravel vào $web_root (đè lên public/index.php tạm),"
echo " sau đó cấp lại quyền sở hữu và chạy migrate:"
echo " sudo chown -R $site_user:$ngx_user $web_root"
echo " sudo -u $site_user composer install --working-dir=$web_root --no-dev"
echo " sudo -u $site_user php$php_version $web_root/artisan migrate"
fi
echo " 3. Kích hoạt SSL (sau khi DNS đã trỏ đúng) bằng một trong hai cách:"
echo
echo -e " ${C_GRN}sudo lara cert issue $domain${C_RST}"
echo -e " ${C_GRN}sudo certbot --nginx -d $domain -d www.$domain${C_RST}"
echo
echo " (Nếu www.$domain chưa có bản ghi DNS: dùng 'lara cert issue $domain --no-www'.)"
echo "==============================================================="
}
#-------------------------------------------------------------------------------
# Đưa site ra khỏi chế độ bảo trì khi 'deploy' thoát - kể cả khi lỗi giữa chừng -
# để site không bao giờ bị kẹt ở trang 503 "đang bảo trì".
#-------------------------------------------------------------------------------
_deploy_bringup() {
[[ -n "${DEP_ACTIVE:-}" ]] || return 0
[[ -n "$DRY_RUN" ]] && return 0
sudo -u "$DEP_USER" -H "php$DEP_PHP" "$WWW_ROOT/$DEP_DOMAIN/artisan" up >/dev/null 2>&1 || true
}
#-------------------------------------------------------------------------------
# deploy - cập nhật website đang chạy (git pull + composer + migrate + cache)
#-------------------------------------------------------------------------------
cmd_deploy() {
local domain="${1:-}"
[[ -n "$domain" ]] || die "Cách dùng: $0 deploy <domain>"
validate_domain "$domain"
acquire_lock "$domain"
local web_root="$WWW_ROOT/$domain"
[[ -d "$web_root" ]] || die "Không tìm thấy website: $web_root"
local php_version
php_version=$(find_php_version "$domain") || die "Không tìm thấy pool php-fpm nào của $domain."
local site_user; site_user=$(site_user_for "$domain")
local artisan=("php$php_version" "$web_root/artisan")
info "Đang deploy $domain (PHP $php_version, user $site_user)"
run_as "$site_user" "${artisan[@]}" down || warn "Không bật được chế độ bảo trì (vẫn tiếp tục)."
# Site đã vào bảo trì -> bật lưới an toàn: dù lỗi ở bước nào phía dưới, khi
# tiến trình thoát site vẫn được đưa trở lại online.
DEP_DOMAIN="$domain"; DEP_USER="$site_user"; DEP_PHP="$php_version"; DEP_ACTIVE=1
trap _deploy_bringup EXIT
if [[ -d "$web_root/.git" ]]; then
info "Đang kéo mã nguồn mới nhất..."
run_as "$site_user" git -C "$web_root" pull --ff-only
else
warn "$web_root không phải là git repository - bỏ qua bước kéo mã nguồn."
fi
info "Đang cài các gói composer..."
run_as "$site_user" composer install --working-dir="$web_root" \
--no-dev --optimize-autoloader --no-interaction
info "Đang chạy migration và tạo lại cache..."
run_as "$site_user" "${artisan[@]}" migrate --force
run_as "$site_user" "${artisan[@]}" config:cache
run_as "$site_user" "${artisan[@]}" route:cache
run_as "$site_user" "${artisan[@]}" view:cache
run_as "$site_user" "${artisan[@]}" up
run_cmd systemctl reload "php$php_version-fpm"
# Thành công - đã 'up' tường minh ở trên, tắt lưới an toàn.
DEP_ACTIVE=""; trap - EXIT
ok "Đã deploy $domain"
}
#-------------------------------------------------------------------------------
# fixperms - sửa lại quyền cho một website. Hữu ích sau khi upload code qua SFTP
# bằng tài khoản root (làm file thành root:root) khiến PHP-FPM mất quyền ghi
# storage/ và nginx mất quyền đọc. Áp đúng bộ quyền mà 'create' đặt ban đầu.
#-------------------------------------------------------------------------------
cmd_fixperms() {
local domain="${1:-}"
[[ -n "$domain" ]] || die "Cách dùng: $0 fixperms <domain>"
validate_domain "$domain"
acquire_lock "$domain"
local web_root="$WWW_ROOT/$domain"
[[ -d "$web_root" ]] || die "Không tìm thấy website: $web_root"
local site_user; site_user=$(site_user_for "$domain")
id "$site_user" &>/dev/null \
|| die "Không thấy user hệ thống '$site_user' của website này (website này có do lara tạo không?)."
local ngx_user; ngx_user=$(nginx_user)
local php_version; php_version=$(find_php_version "$domain") || php_version="<ver>"
info "Đang sửa quyền cho $domain (chủ sở hữu $site_user, group $ngx_user)..."
# Đảm bảo user của site vẫn thuộc group nginx để www-data đọc được tài nguyên tĩnh.
run_cmd usermod -aG "$ngx_user" "$site_user"
apply_site_permissions "$web_root" "$site_user" "$ngx_user"
ok "Đã sửa quyền cho $domain ($site_user:$ngx_user, 750/640, storage ghi được, .env 600)."
info "Nếu app vẫn lỗi, xoá cache rồi nạp lại:"
info " sudo -u $site_user php$php_version $web_root/artisan optimize:clear"
}
#-------------------------------------------------------------------------------
# remove - xoá website
#-------------------------------------------------------------------------------
cmd_remove() {
local domain="${1:-}"
[[ -n "$domain" ]] || die "Cách dùng: $0 remove <domain>"
validate_domain "$domain"
acquire_lock "$domain"
local web_root="$WWW_ROOT/$domain"
local site_user; site_user=$(site_user_for "$domain")
local php_version=""
php_version=$(find_php_version "$domain") || true
echo "Thao tác này sẽ xoá website '$domain':"
echo " - Cấu hình nginx + symlink"
[[ -n "$php_version" ]] && echo " - Pool FPM PHP $php_version"
echo " - (tuỳ chọn) thư mục mã nguồn $web_root và user $site_user"
read -rp "Tiếp tục? [y/N] " answer
[[ "$answer" =~ ^[Yy]$ ]] || die "Đã huỷ."
run_cmd rm -f "$NGINX_ENABLED/$domain.conf" "$(nginx_conf_for "$domain")"
if run_cmd nginx -t; then
run_cmd systemctl reload nginx
else
warn "Kiểm tra cấu hình nginx thất bại - hãy kiểm tra thủ công các cấu hình còn lại."
fi
ok "Đã xoá cấu hình nginx"
if [[ -n "$php_version" ]]; then
run_cmd rm -f "$(pool_conf_for "$domain" "$php_version")" \
"$(sock_path_for "$domain" "$php_version")"
run_cmd systemctl restart "php$php_version-fpm" || warn "Khởi động lại php$php_version-fpm thất bại."
ok "Đã xoá pool PHP-FPM"
fi
if [[ -d "$web_root" ]]; then
read -rp "XOÁ luôn toàn bộ dữ liệu trong $web_root? [y/N] " answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
run_cmd rm -rf "$web_root"
ok "Đã xoá $web_root"
else
warn "Đã giữ lại $web_root"
fi
fi
if id "$site_user" &>/dev/null; then
read -rp "Xoá luôn user hệ thống $site_user? [y/N] " answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
run_cmd userdel -r "$site_user" 2>/dev/null || run_cmd userdel "$site_user"
ok "Đã xoá user $site_user"
else
warn "Đã giữ lại user $site_user"
fi
fi
# Database cùng tên slug (nếu có và MySQL/MariaDB đang chạy)
local db_name="$site_user"
if db_service_running && db_exists "$db_name"; then
read -rp "Xoá luôn database '$db_name' và user CSDL cùng tên? (MẤT TOÀN BỘ DỮ LIỆU) [y/N] " answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
run_cmd mysql_client -e "DROP DATABASE \`$db_name\`; DROP USER IF EXISTS '$db_name'@'localhost'; FLUSH PRIVILEGES;"
ok "Đã xoá database và user CSDL '$db_name'"
else
warn "Đã giữ lại database '$db_name'"
fi
fi
ok "Đã xoá website $domain."
}
#-------------------------------------------------------------------------------
# list - liệt kê các website
#-------------------------------------------------------------------------------
cmd_list() {
printf "%-30s %-6s %-12s %s\n" "TÊN MIỀN" "PHP" "USER" "KÍCH HOẠT"
printf "%-30s %-6s %-12s %s\n" "--------" "---" "----" "---------"
local v f domain found=0
for v in "${SUPPORTED_PHP[@]}"; do
for f in /etc/php/"$v"/fpm/pool.d/*.conf; do
[[ -f "$f" ]] || continue
grep -q "Managed by lara" "$f" || continue
domain=$(basename "$f" .conf)
local enabled="chưa"
[[ -L "$NGINX_ENABLED/$domain.conf" ]] && enabled="rồi"
printf "%-30s %-6s %-12s %s\n" "$domain" "$v" "$(site_user_for "$domain")" "$enabled"
found=1
done
done
((found)) || echo "(chưa có website nào do lara quản lý)"
}
#-------------------------------------------------------------------------------
# cert - quản lý chứng chỉ SSL qua certbot
#-------------------------------------------------------------------------------
require_certbot() {
command -v certbot >/dev/null 2>&1 && return 0
# Khi chạy thử (dry-run) chỉ cảnh báo để vẫn xem trước được lệnh certbot.
[[ -n "$DRY_RUN" ]] && { warn "certbot chưa được cài (bỏ qua khi chạy thử)."; return 0; }
die "certbot chưa được cài. Cài bằng: apt install certbot python3-certbot-nginx"
}
# certbot certificates [--cert-name domain] (chỉ đọc -> chạy trực tiếp)
cmd_cert_status() {
local domain="${1:-}"
require_certbot
if [[ -n "$domain" ]]; then
validate_domain "$domain"
certbot certificates --cert-name "$domain"
else
certbot certificates
fi
}
cmd_cert_issue() {
local domain="" no_www=""
while [[ $# -gt 0 ]]; do
case "$1" in
--no-www) no_www=1; shift ;;
-*) die "Tham số không hợp lệ: $1" ;;
*) if [[ -z "$domain" ]]; then domain="$1"; else die "Tham số thừa: $1"; fi; shift ;;
esac
done
[[ -n "$domain" ]] || die "Cách dùng: $0 cert issue <domain> [--no-www]"
validate_domain "$domain"
require_certbot
local args=(--nginx -d "$domain")
[[ -z "$no_www" ]] && args+=(-d "www.$domain")
info "Đang phát hành chứng chỉ SSL cho $domain (certbot sẽ tự cập nhật nginx)..."
run_cmd certbot "${args[@]}"
ok "Hoàn tất. certbot đã cài chứng chỉ và tự lên lịch gia hạn (systemd timer)."
}
cmd_cert_renew() {
local domain="" force=""
while [[ $# -gt 0 ]]; do
case "$1" in
--force) force=1; shift ;;
-*) die "Tham số không hợp lệ: $1" ;;
*) if [[ -z "$domain" ]]; then domain="$1"; else die "Tham số thừa: $1"; fi; shift ;;
esac
done
require_certbot
local args=(renew)
[[ -n "$domain" ]] && { validate_domain "$domain"; args+=(--cert-name "$domain"); }
[[ -n "$force" ]] && args+=(--force-renewal)
if [[ -n "$domain" ]]; then
info "Đang gia hạn chứng chỉ cho $domain..."
else
info "Đang gia hạn tất cả chứng chỉ sắp hết hạn..."
fi
run_cmd certbot "${args[@]}"
ok "Hoàn tất gia hạn."
}
cmd_cert_revoke() {
local domain="${1:-}"
[[ -n "$domain" ]] || die "Cách dùng: $0 cert revoke <domain>"
validate_domain "$domain"
require_certbot
echo "Thao tác này sẽ THU HỒI và XOÁ chứng chỉ của '$domain'."
echo "Website sẽ không còn HTTPS hợp lệ cho tới khi bạn phát hành lại."
read -rp "Tiếp tục? [y/N] " answer
[[ "$answer" =~ ^[Yy]$ ]] || die "Đã huỷ."
run_cmd certbot revoke --cert-name "$domain" --delete-after-revoke --non-interactive
ok "Đã thu hồi và xoá chứng chỉ của $domain."
}
cmd_cert() {
local sub="${1:-list}"; shift || true
case "$sub" in
list|"") require_certbot; certbot certificates ;;
status) cmd_cert_status "$@" ;;
issue|generate) cmd_cert_issue "$@" ;;
renew) cmd_cert_renew "$@" ;;
revoke) cmd_cert_revoke "$@" ;;
*) die "Lệnh cert không hợp lệ: $sub (hỗ trợ: list, status, issue, renew, revoke)" ;;
esac
}
#-------------------------------------------------------------------------------
# version / changelog
#-------------------------------------------------------------------------------
cmd_version() {
echo "lara phiên bản $LARA_VERSION"
}
cmd_changelog() {
echo -e "${C_GRN}=== LARA - Lịch sử thay đổi (phiên bản hiện tại: $LARA_VERSION) ===${C_RST}"
cat <<'EOF'
v1.13.0 (2026-06-10)
- 'lara install' dựng thêm banner chào mừng SSH (/etc/update-motd.d/99-lara):
mỗi lần đăng nhập sẽ giới thiệu lệnh lara, hiện phiên bản + số website đang
quản lý. 'lara uninstall' gỡ banner này.
v1.12.0 (2026-06-10)
- Thêm 'lara fixperms <domain>' + mục menu: áp lại đúng bộ quyền cho một
website. Dùng khi upload code qua SFTP bằng root làm sai chủ sở hữu/quyền
(PHP-FPM mất quyền ghi storage/, nginx mất quyền đọc -> app lỗi).
- Gom logic phân quyền thành hàm dùng chung để 'create' và 'fixperms' luôn
đặt quyền giống hệt nhau.
v1.11.1 (2026-06-10)
- Cấu hình URL gist công khai cho 'lara update' (LARA_UPDATE_URL_DEFAULT) ->
'sudo lara update' chạy được ngay, không cần đặt biến môi trường.
v1.11.0 (2026-06-10)
- Thêm 'lara update': tự tải bản mới nhất từ gist công khai (chỉ chứa lara.sh)
qua HTTPS, KIỂM TRA cú pháp + đúng là lara rồi mới cài đè (không 'curl|bash').
Cấu hình URL gist qua LARA_UPDATE_URL_DEFAULT hoặc biến môi trường
LARA_UPDATE_URL. Có mục tương ứng trong menu.
v1.10.1 (2026-06-10)
- SỬA LỖI QUAN TRỌNG: 'check' báo nhầm extension bị thiếu một cách NGẪU NHIÊN
(đổi danh sách mỗi lần chạy) dù extension đã nạp. Nguyên nhân: 'php -m |
grep -q' + 'set -o pipefail' - grep -q thoát sớm làm php nhận SIGPIPE khiến
pipeline trả non-zero. Nay lấy 'php -m' một lần vào biến rồi đối chiếu bằng
here-string, hết báo nhầm. (Sửa cùng lỗi cho 'certbot plugins | grep -q'.)
- Sửa hướng dẫn cài extension: ctype/fileinfo/tokenizer thuộc php-common (không
có gói riêng), pdo_mysql thuộc php-mysql; quy đổi đúng tên gói và bỏ trùng.
- 'check' báo rõ khi thiếu trình CLI php<ver> (apt install php<ver>-cli) thay
vì báo nhầm là thiếu hàng loạt extension.
v1.10.0 (2026-06-10)
- Tự hoàn tác khi 'create' lỗi giữa chừng: dọn sạch user/pool/nginx/thư mục/
database vừa tạo, không để lại trạng thái dở dang.
- 'deploy' có lưới an toàn: dù lỗi ở bước nào, site luôn được đưa ra khỏi chế
độ bảo trì khi thoát (không còn kẹt trang 503).
- Khoá theo domain (flock) cho create/deploy/remove -> chống chạy song song.
- Mỗi site có thư mục tạm riêng (storage/tmp); bỏ /tmp dùng chung khỏi
open_basedir -> các site không đọc được file tạm/upload của nhau.
- Báo lỗi rõ ràng khi không kết nối được MySQL/MariaDB bằng quyền quản trị.
v1.9.3 (2026-06-10)
- Chống tạo website trùng triệt để: trước khi tạo, kiểm tra TẤT CẢ tài nguyên
(thư mục mã nguồn, cấu hình + symlink nginx, pool & socket của mọi phiên bản
PHP, user hệ thống, và database nếu dùng --db). Chỉ cần một thành phần đã
tồn tại là dừng ngay, liệt kê rõ và không tạo gì cả.
v1.9.2 (2026-06-10)
- Sửa lỗi tiềm ẩn với 'set -e': đổi ((errors++)) thành phép gán an toàn để
bộ đếm lỗi trong 'check' không vô tình làm thoát script trên một số bản bash.
v1.9.1 (2026-06-10)
- 'lara check' kiểm tra certbot kỹ hơn: hiển thị phiên bản và xác minh có
plugin nginx (thứ mà 'lara cert' cần để phát hành/gia hạn chứng chỉ).
v1.9.0 (2026-06-10)
- Thêm quản lý chứng chỉ SSL qua certbot (lệnh 'lara cert' + menu):
list/status (xem), issue (phát hành, có tuỳ chọn --no-www),
renew (gia hạn tất cả hoặc một site, có --force), revoke (thu hồi & xoá).
v1.8.0 (2026-06-10)
- Rà soát bảo mật theo OWASP ASVS 5.0:
- Kiểm tra URL git (--repo): chặn transport ext::/fd:: (RCE), file://
và chuỗi bắt đầu bằng '-' (argument injection).
- Siết quyền file .env về 600 (chỉ chủ sở hữu đọc được).
- Không in mật khẩu CSDL ra màn hình khi đã ghi vào .env.
- Hàm log dùng printf '%s' để tránh chèn mã ANSI/giả mạo dòng log.
v1.7.0 (2026-06-10)
- Thêm phiên bản cho script (lệnh 'lara version').
- Thêm changelog nhúng trong script (lệnh 'lara changelog' + mục menu).
v1.6.0 (2026-06-10)
- Thêm chế độ chạy thử --dry-run cho mọi lệnh: các bước kiểm tra vẫn chạy
thật, mọi thay đổi hệ thống chỉ được in ra (kèm nội dung file sẽ ghi).
- Dry-run không yêu cầu quyền root; menu có nút bật/tắt (mục 8).
v1.5.0 (2026-06-10)
- 'lara check' kiểm tra thêm MySQL/MariaDB và Redis (cảnh báo, không chặn).
- Tuỳ chọn --db: tạo database + user CSDL riêng cho website (mật khẩu
ngẫu nhiên, utf8mb4, GRANT chỉ trên database của chính site đó);
tự ghi thông tin vào .env nếu có.
- 'lara remove' hỏi xoá database cùng tên slug (mặc định giữ lại).
v1.4.0 (2026-06-10)
- Nguồn mã khi tạo website: chọn giữa "chỉ tạo thư mục trống" (kèm
public/index.php tạm) hoặc "clone từ git" (bỏ composer create-project).
v1.3.0 (2026-06-10)
- Menu tương tác: chạy 'lara' không tham số để mở; điều hướng bằng phím
mũi tên hoặc gõ số + Enter; menu đa cấp, nhập 0 để quay lại/thoát.
v1.2.0 (2026-06-10)
- Chuyển toàn bộ giao diện sang tiếng Việt.
v1.1.0 (2026-06-10)
- Đổi tên script thành lara.sh; thêm lệnh install/uninstall để tự cài
vào /usr/local/bin/lara.
- Mở rộng hỗ trợ Ubuntu 18.04 - 26.04 LTS.
v1.0.0 (2026-06-10)
- Bản đầu tiên: create / deploy / remove / list / check.
- PHP 7.4 hoặc 8.4, Laravel 8-13 (ràng buộc tương thích tự động).
- Mỗi website một user hệ thống riêng, pool PHP-FPM + socket riêng,
vhost nginx chỉ HTTP, in sẵn lệnh certbot để người dùng tự chạy.
EOF
}
#-------------------------------------------------------------------------------
# install / uninstall - tự cài script vào PATH
#-------------------------------------------------------------------------------
# Dựng banner MOTD giới thiệu lara, hiển thị mỗi khi đăng nhập SSH.
# Banner tự đọc phiên bản + đếm số website tại thời điểm đăng nhập.
install_welcome_banner() {
if [[ ! -d /etc/update-motd.d ]]; then
warn "Không có /etc/update-motd.d - bỏ qua banner chào mừng SSH."
return 0
fi
write_file "$MOTD_PATH" <<'BANNER'
#!/bin/bash
# Banner giới thiệu lara - tạo bởi 'lara install', gỡ bởi 'lara uninstall'.
command -v lara >/dev/null 2>&1 || exit 0
C_C='\033[1;36m'; C_Y='\033[1;33m'; C_D='\033[0;90m'; C_R='\033[0m'
ver=$(lara version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
sites=$(grep -lr 'Managed by lara' /etc/php/*/fpm/pool.d/ 2>/dev/null | wc -l | tr -d ' ')
printf '\n'
printf " ${C_C}lara${C_R} ${C_D}v%s${C_R} — Công cụ quản lý website PHP/Laravel\n" "${ver:-?}"
printf " ${C_D}%s website đang được lara quản lý${C_R}\n\n" "${sites:-0}"
printf " ${C_Y}sudo lara${C_R} mở menu quản lý (tạo / deploy / SSL / sửa quyền...)\n"
printf " ${C_Y}lara list${C_R} xem danh sách website\n"
printf " ${C_Y}sudo lara update${C_R} cập nhật lara lên bản mới nhất\n\n"
BANNER
run_cmd chmod 0755 "$MOTD_PATH"
ok "Đã cài banner chào mừng SSH ($MOTD_PATH)"
}
cmd_install() {
local src
src=$(readlink -f "$0" 2>/dev/null) || src="$0"
[[ -f "$src" ]] || die "Không xác định được vị trí file script ($0) - hãy chạy install trực tiếp từ file script."
if [[ "$src" == "$INSTALL_PATH" ]]; then
ok "lara đã được cài sẵn tại $INSTALL_PATH"
else
run_cmd install -m 0755 "$src" "$INSTALL_PATH"
ok "Đã cài vào $INSTALL_PATH"
fi
install_welcome_banner # luôn (cài lại) banner chào mừng SSH
info "Giờ bạn có thể chạy lara từ bất cứ đâu, ví dụ: sudo lara create example.com --php 8.4 --laravel 12"
}
cmd_uninstall() {
local removed=""
if [[ -f "$INSTALL_PATH" ]]; then
run_cmd rm -f "$INSTALL_PATH"
ok "Đã gỡ $INSTALL_PATH (các website, pool FPM và cấu hình nginx hiện có vẫn giữ nguyên)"
removed=1
fi
if [[ -f "$MOTD_PATH" ]]; then
run_cmd rm -f "$MOTD_PATH"
ok "Đã gỡ banner chào mừng SSH ($MOTD_PATH)"
removed=1
fi
[[ -n "$removed" ]] || warn "Không có gì để gỡ - $INSTALL_PATH không tồn tại."
}
#-------------------------------------------------------------------------------
# update - tự tải bản lara mới nhất từ gist công khai rồi cài đè.
# An toàn: tải về file tạm -> KIỂM TRA (cú pháp + đúng là lara) -> mới cài.
# Không dùng kiểu 'curl | bash'.
#-------------------------------------------------------------------------------
cmd_update() {
local url="${LARA_UPDATE_URL:-$LARA_UPDATE_URL_DEFAULT}"
case "$url" in
*REPLACE_WITH_GIST_ID*|"")
die "Chưa cấu hình nguồn cập nhật.
Hãy tạo gist công khai chứa lara.sh, rồi đặt URL raw 'mới nhất' của nó vào
LARA_UPDATE_URL_DEFAULT trong script, hoặc chạy:
sudo LARA_UPDATE_URL=\"https://gist.githubusercontent.com/<user>/<id>/raw/lara.sh\" lara update" ;;
https://*) : ;;
*) die "URL cập nhật phải dùng HTTPS: $url" ;;
esac
command -v curl >/dev/null 2>&1 || die "Cần 'curl' để cập nhật: apt install curl"
info "Đang tải bản mới nhất từ: $url"
local tmp; tmp=$(mktemp) || die "Không tạo được file tạm."
curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$tmp" \
|| { rm -f "$tmp"; die "Tải thất bại - kiểm tra URL hoặc kết nối mạng."; }
# Kiểm tra trước khi cài: cú pháp hợp lệ và đúng là file lara.
bash -n "$tmp" 2>/dev/null || { rm -f "$tmp"; die "File tải về lỗi cú pháp - KHÔNG cài (có thể tải dở/hỏng)."; }
grep -q '^LARA_VERSION=' "$tmp" || { rm -f "$tmp"; die "File tải về không giống lara.sh - KHÔNG cài."; }
local new_ver; new_ver=$(grep -m1 '^LARA_VERSION=' "$tmp" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
info "Bản đang chạy: $LARA_VERSION -> bản tải về: ${new_ver:-?}"
if [[ "$new_ver" == "$LARA_VERSION" ]]; then
warn "Đã là bản mới nhất ($LARA_VERSION); vẫn cài lại cho chắc."
fi
run_cmd install -m 0755 "$tmp" "$INSTALL_PATH"
rm -f "$tmp"
ok "Đã cập nhật lara: $LARA_VERSION -> ${new_ver:-?} ($INSTALL_PATH)"
}
#-------------------------------------------------------------------------------
# Menu tương tác
#-------------------------------------------------------------------------------
pause() {
echo
read -rp " Nhấn Enter để quay lại menu..." _
}
clear_screen() { printf '\033[2J\033[H'; }
# Đọc một phím bấm, kết quả đặt vào biến toàn cục KEY:
# up / down / enter / backspace / digit:<n> / other
read_key() {
local k rest=""
IFS= read -rsn1 k || { KEY=other; return; }
case "$k" in
$'\x1b')
if IFS= read -rsn2 -t 1 rest 2>/dev/null; then
case "$rest" in
'[A') KEY=up ;;
'[B') KEY=down ;;
*) KEY=other ;;
esac
else
KEY=other
fi
;;
"") KEY=enter ;;
$'\x7f') KEY=backspace ;;
[0-9]) KEY="digit:$k" ;;
*) KEY=other ;;
esac
}
# menu_select "Tiêu đề" "Nhãn mục 0" "mục 1" "mục 2" ...
# Điều khiển: phím ↑/↓ + Enter, hoặc gõ số + Enter. Mục 0 = quay lại/thoát.
# Kết quả đặt vào biến toàn cục MENU_CHOICE (0..N).
MENU_CHOICE=0
menu_select() {
local title="$1" back_label="$2"; shift 2
local items=("$@") n=$# sel=1 buf="" i choice
while true; do
clear_screen
echo -e "${C_GRN}=== $title ===${C_RST}"
echo
for ((i = 1; i <= n; i++)); do
if [[ -z "$buf" && $sel -eq $i ]]; then
echo -e " ${C_GRN}> $i. ${items[i-1]}${C_RST}"
else
echo " $i. ${items[i-1]}"
fi
done
if [[ -z "$buf" && $sel -eq 0 ]]; then
echo -e " ${C_GRN}> 0. $back_label${C_RST}"
else
echo " 0. $back_label"
fi
echo
echo " Hướng dẫn: dùng phím ↑/↓ rồi Enter, hoặc gõ số rồi Enter."
echo " Nhập 0 để: $back_label."
printf ' Lựa chọn: %s' "$buf"
read_key
case "$KEY" in
up) buf=""; sel=$((sel - 1)); (( sel < 0 )) && sel=$n ;;
down) buf=""; sel=$((sel + 1)); (( sel > n )) && sel=0 ;;
digit:*) buf+="${KEY#digit:}" ;;
backspace) buf="${buf%?}" ;;
enter)
if [[ -n "$buf" ]]; then
choice=$((10#$buf))
else
choice=$sel
fi
if (( choice >= 0 && choice <= n )); then
MENU_CHOICE=$choice
echo
return 0
fi
buf="" # số ngoài phạm vi -> nhập lại
;;
esac
done
}
# ask_text "Câu hỏi" -> kết quả vào REPLY_TEXT; trả về 1 nếu người dùng nhập 0
ask_text() {
echo
read -rp " $1 (nhập 0 để quay lại): " REPLY_TEXT
[[ "$REPLY_TEXT" != "0" ]]
}
# Liệt kê các domain do lara quản lý (mỗi dòng một domain)
managed_sites() {
local v f
for v in "${SUPPORTED_PHP[@]}"; do
for f in /etc/php/"$v"/fpm/pool.d/*.conf; do
[[ -f "$f" ]] || continue
grep -q "Managed by lara" "$f" || continue
basename "$f" .conf
done
done
}
# Chạy chính script này như một tiến trình con để lỗi (die/set -e)
# không làm thoát menu, sau đó dừng cho người dùng đọc kết quả.
run_cli() {
clear_screen
if [[ -n "$DRY_RUN" ]]; then
"$BASH" "$0" --dry-run "$@" || true
else
"$BASH" "$0" "$@" || true
fi
pause
}
menu_create() {
local laravel_version php_version repo_url=""
menu_select "Tạo website mới - chọn phiên bản Laravel" "Quay lại menu chính" \
"Laravel 8 (PHP 7.4)" \
"Laravel 11 (PHP 8.4)" \
"Laravel 12 (PHP 8.4)" \
"Laravel 13 (PHP 8.4)"
case $MENU_CHOICE in
0) return ;;
1) laravel_version=8; php_version=7.4 ;;
2) laravel_version=11; php_version=8.4 ;;
3) laravel_version=12; php_version=8.4 ;;
4) laravel_version=13; php_version=8.4 ;;
esac
menu_select "Tạo website mới - chọn nguồn mã" "Quay lại menu chính" \
"Chỉ tạo thư mục trống (tự đưa mã nguồn lên sau)" \
"Clone từ git repository"
case $MENU_CHOICE in
0) return ;;
2) ask_text "Nhập URL git repository" || return
repo_url="$REPLY_TEXT" ;;
esac
menu_select "Tạo database MySQL/MariaDB riêng cho website?" "Quay lại menu chính" \
"Có - tạo database + user CSDL riêng (mật khẩu ngẫu nhiên)" \
"Không - tự cấu hình cơ sở dữ liệu sau"
local want_db=""
case $MENU_CHOICE in
0) return ;;
1) want_db=1 ;;
esac
ask_text "Nhập tên miền (ví dụ: example.com)" || return
local domain="$REPLY_TEXT"
local args=(create "$domain" --php "$php_version" --laravel "$laravel_version")
[[ -n "$repo_url" ]] && args+=(--repo "$repo_url")
[[ -n "$want_db" ]] && args+=(--db)
run_cli "${args[@]}"
}
# Chọn một website do lara quản lý -> đặt vào PICKED_SITE.
# Trả về 1 (và để PICKED_SITE rỗng) nếu không có site nào hoặc người dùng quay lại.
PICKED_SITE=""
pick_managed_site() {
local title="$1"
PICKED_SITE=""
local sites=() d
while IFS= read -r d; do sites+=("$d"); done < <(managed_sites)
if (( ${#sites[@]} == 0 )); then
clear_screen
warn "Chưa có website nào do lara quản lý trên máy chủ này."
pause
return 1
fi
menu_select "$title" "Quay lại menu chính" "${sites[@]}"
(( MENU_CHOICE == 0 )) && return 1
PICKED_SITE="${sites[MENU_CHOICE-1]}"
return 0
}
# menu_pick_site <deploy|remove> "Tiêu đề"
menu_pick_site() {
local action="$1" title="$2"
pick_managed_site "$title" || return
run_cli "$action" "$PICKED_SITE"
}
#-------------------------------------------------------------------------------
# Menu chứng chỉ SSL
#-------------------------------------------------------------------------------
menu_cert_issue() {
pick_managed_site "Phát hành chứng chỉ - chọn website" || return
local domain="$PICKED_SITE"
menu_select "Bao gồm cả www.$domain trong chứng chỉ?" "Quay lại menu chính" \
"Có - $domain và www.$domain (www phải đã trỏ DNS)" \
"Không - chỉ $domain"
case $MENU_CHOICE in
0) return ;;
1) run_cli cert issue "$domain" ;;
2) run_cli cert issue "$domain" --no-www ;;
esac
}
menu_cert_renew() {
menu_select "Gia hạn chứng chỉ" "Quay lại menu chính" \
"Gia hạn TẤT CẢ chứng chỉ sắp hết hạn" \
"Gia hạn cho một website cụ thể"
case $MENU_CHOICE in
0) return ;;
1) run_cli cert renew ;;
2) pick_managed_site "Gia hạn chứng chỉ - chọn website" || return
run_cli cert renew "$PICKED_SITE" ;;
esac
}
menu_cert_revoke() {
pick_managed_site "Thu hồi & xoá chứng chỉ - chọn website" || return
run_cli cert revoke "$PICKED_SITE"
}
menu_cert() {
while true; do
menu_select "Quản lý chứng chỉ SSL (certbot)" "Quay lại menu chính" \
"Xem danh sách / trạng thái chứng chỉ" \
"Phát hành chứng chỉ mới cho website" \
"Gia hạn chứng chỉ" \
"Thu hồi & xoá chứng chỉ"
case $MENU_CHOICE in
0) return ;;
1) run_cli cert list ;;
2) menu_cert_issue ;;
3) menu_cert_renew ;;
4) menu_cert_revoke ;;
esac
done
}
menu_check() {
menu_select "Kiểm tra môi trường máy chủ" "Quay lại menu chính" \
"Kiểm tra tất cả các phiên bản PHP" \
"Chỉ kiểm tra PHP 7.4" \
"Chỉ kiểm tra PHP 8.4"
case $MENU_CHOICE in
0) return ;;
1) run_cli check ;;
2) run_cli check --php 7.4 ;;
3) run_cli check --php 8.4 ;;
esac
}
menu_main() {
local dry_label title
while true; do
if [[ -n "$DRY_RUN" ]]; then
dry_label="Chế độ chạy thử (dry-run): ĐANG BẬT - chọn để tắt"
title="LARA v$LARA_VERSION - Quản lý website PHP/Laravel [DRY-RUN]"
else
dry_label="Chế độ chạy thử (dry-run): đang tắt - chọn để bật"
title="LARA v$LARA_VERSION - Quản lý website PHP/Laravel"
fi
menu_select "$title" "Thoát" \
"Tạo website mới" \
"Deploy website (cập nhật mã nguồn)" \
"Sửa quyền website (sau khi upload code/SFTP)" \
"Xoá website" \
"Quản lý chứng chỉ SSL (certbot)" \
"Danh sách website" \
"Kiểm tra môi trường máy chủ" \
"Cài lara vào $INSTALL_PATH" \
"Cập nhật lara lên bản mới nhất" \
"Gỡ lara khỏi $INSTALL_PATH" \
"$dry_label" \
"Lịch sử thay đổi (changelog)"
case $MENU_CHOICE in
0) clear_screen; echo "Tạm biệt!"; exit 0 ;;
1) menu_create ;;
2) menu_pick_site deploy "Deploy website - chọn tên miền" ;;
3) menu_pick_site fixperms "Sửa quyền website - chọn tên miền" ;;
4) menu_pick_site remove "Xoá website - chọn tên miền" ;;
5) menu_cert ;;
6) clear_screen; cmd_list; pause ;;
7) menu_check ;;
8) run_cli install ;;
9) run_cli update ;;
10) run_cli uninstall ;;
11) if [[ -n "$DRY_RUN" ]]; then DRY_RUN=""; else DRY_RUN=1; fi ;;
12) clear_screen; cmd_changelog; pause ;;
esac
done
}
#-------------------------------------------------------------------------------
# main
#-------------------------------------------------------------------------------
# Ở chế độ dry-run không bắt buộc quyền root (không có gì được thay đổi thật)
maybe_require_root() {
[[ -n "$DRY_RUN" ]] || require_root
}
main() {
# Tách cờ toàn cục --dry-run ra khỏi danh sách tham số
local args=() a
for a in "$@"; do
if [[ "$a" == "--dry-run" ]]; then DRY_RUN=1; else args+=("$a"); fi
done
set -- "${args[@]+"${args[@]}"}"
local cmd="${1:-}"
if [[ -z "$cmd" ]]; then
if [[ -t 0 && -t 1 ]]; then
menu_main
else
usage 1 # không có terminal tương tác -> chỉ in hướng dẫn
fi
fi
shift || true
if [[ -n "$DRY_RUN" && "$cmd" != "menu" ]]; then
warn "Chế độ DRY-RUN: chỉ hiển thị các thay đổi, KHÔNG thực thi gì cả."
fi
case "$cmd" in
menu) menu_main ;;
install) maybe_require_root; cmd_install ;;
update) maybe_require_root; cmd_update ;;
uninstall) maybe_require_root; cmd_uninstall ;;
create) maybe_require_root; cmd_create "$@" ;;
deploy) maybe_require_root; cmd_deploy "$@" ;;
remove) maybe_require_root; cmd_remove "$@" ;;
fixperms) maybe_require_root; cmd_fixperms "$@" ;;
cert) maybe_require_root; cmd_cert "$@" ;;
list) cmd_list ;;
check)
local php=""
[[ "${1:-}" == "--php" ]] && php="${2:-}"
[[ -n "$php" ]] && validate_php_version "$php"
check_environment "$php"
;;
version|--version|-v) cmd_version ;;
changelog) cmd_changelog ;;
-h|--help|help) usage ;;
*) die "Lệnh không hợp lệ: $cmd (các lệnh hỗ trợ: install, update, create, deploy, remove, fixperms, cert, list, check, version, changelog, uninstall)" ;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment