#!/bin/bash # ============================================================ # o3o Meet — BỘ CÀI MỘT DÒNG cho Ubuntu 22.04 / 24.04 (anh Huy 10/09/2026: # "người dùng chỉ cần 1 dòng ssh là có thể cài được toàn bộ meet o3o") # # curl -fsSL https://install.o3o.vn/meet/cai-o3o-meet.sh \ # | sudo bash -s -- --domain meet.congty.vn --email it@congty.vn # # (Hướng dẫn đầy đủ: https://install.o3o.vn/meet — bản script này cũng nằm trong kho # gitlab.apps.vn/khahuyvu/o3o-meet tại cai-dat/cai-o3o-meet.sh) # # Tuỳ chọn: # --domain BẮT BUỘC. Mặc định chế độ MỘT TÊN MIỀN: chỉ cần một bản ghi DNS A # trỏ về máy này; LiveKit và máy chủ nét vẽ đi chung tên miền theo # đường dẫn (/rtc, /ws). Dùng --tach-ten-mien nếu muốn bốn tên miền # con livekit./ws./turn. (cần thêm 3 bản ghi DNS). # --email Email nhận thông báo chứng chỉ Let's Encrypt (khuyên dùng) # --keycloak Cài kèm Keycloak tại id. khi chưa có máy chủ đăng nhập # --repo Mặc định https://gitlab.apps.vn/khahuyvu/o3o-meet.git # --nhanh Repo riêng: token đọc (GitLab "read_repository") # --nguon Cài từ MÃ ĐÃ CÓ SẴN trên máy (không kéo git) — dùng khi kho chưa # công khai hoặc máy không ra được internet tới kho # --sau-nat Máy đứng sau NAT/router: không tự dò IP, ghim IP công khai # --ip Dùng kèm --sau-nat # # Bộ cài LÀM: cài Docker + nginx + certbot, kéo mã, sinh mọi bí mật, dựng dịch vụ, xin # chứng chỉ, cài bộ cập nhật (nút "Cập nhật ngay" trong trang admin), và in đường vào TRANG # CHÀO MỪNG kèm khoá một lần để khách tự cấu hình (logo, tên tổ chức, SSO, giấy phép, máy # chủ media thêm). # Bộ cài KHÔNG LÀM: trỏ DNS, mở tường lửa nhà cung cấp — hai việc đó nó kiểm và nhắc. # # Chạy lại an toàn: đã có .env thì GIỮ NGUYÊN bí mật cũ (không sinh lại). # ============================================================ set -euo pipefail DOMAIN=""; EMAIL=""; KEYCLOAK=0; SAU_NAT=0; IP_CHI_DINH=""; TACH=0; NGUON="" REPO="https://gitlab.apps.vn/khahuyvu/o3o-meet.git"; TOKEN="" DICH=/opt/o3o-meet while [ $# -gt 0 ]; do case "$1" in --domain) DOMAIN="$2"; shift 2;; --email) EMAIL="$2"; shift 2;; --keycloak) KEYCLOAK=1; shift;; --repo) REPO="$2"; shift 2;; --nhanh) TOKEN="$2"; shift 2;; --nguon) NGUON="$2"; shift 2;; --sau-nat) SAU_NAT=1; shift;; --ip) IP_CHI_DINH="$2"; shift 2;; --tach-ten-mien) TACH=1; shift;; *) echo "Tham số lạ: $1"; exit 2;; esac done xanh() { printf '\033[1;32m%s\033[0m\n' "$*"; } vang() { printf '\033[1;33m%s\033[0m\n' "$*"; } do_() { printf '\033[1;31m%s\033[0m\n' "$*"; } # set -e dừng bộ cài ở bất kỳ lệnh hỏng nào — phải NÓI dừng ở đâu, không thì màn hình chỉ im # lặng sau một dòng tiêu đề bước (rà soát lần 2, 11/09/2026) trap 'do_ "✗ Bộ cài DỪNG ở dòng $LINENO: $BASH_COMMAND"; do_ " Sửa nguyên nhân rồi chạy lại đúng lệnh này — chạy lại an toàn (bí mật, cert giữ nguyên)."' ERR [ "$(id -u)" = 0 ] || { do_ "Phải chạy bằng sudo/root."; exit 1; } if [ -z "$DOMAIN" ]; then do_ "Thiếu --domain. Ví dụ: --domain meet.congty.vn"; exit 2 fi DOMAIN=$(echo "$DOMAIN" | tr 'A-Z' 'a-z') if ! [[ "$DOMAIN" =~ ^[a-z0-9.-]+\.[a-z]{2,}$ ]]; then do_ "Tên miền không hợp lệ: $DOMAIN"; exit 2; fi # Tên miền con nằm DƯỚI tên miền chính (livekit.meet.congty.vn) — khách tạo một wildcard là đủ, # và không đụng vào bản ghi sẵn có ở gốc congty.vn. if [ $TACH = 1 ]; then DOMAIN_LIVEKIT="livekit.$DOMAIN"; DOMAIN_WS="ws.$DOMAIN"; DOMAIN_TURN="turn.$DOMAIN" URL_LIVEKIT="wss://$DOMAIN_LIVEKIT"; URL_WS="wss://$DOMAIN_WS" else DOMAIN_LIVEKIT="$DOMAIN"; DOMAIN_WS="$DOMAIN"; DOMAIN_TURN="$DOMAIN" URL_LIVEKIT="wss://$DOMAIN"; URL_WS="wss://$DOMAIN" fi DOMAIN_ID="id.$DOMAIN" . /etc/os-release case "${VERSION_ID:-}" in 22.04|24.04) ;; *) vang "Chỉ thử trên Ubuntu 22.04/24.04 — máy này là ${PRETTY_NAME:-?}. Vẫn tiếp tục.";; esac echo "==============================================" xanh " o3o Meet — cài lên $DOMAIN" if [ $TACH = 1 ]; then echo " bốn tên miền: $DOMAIN · $DOMAIN_LIVEKIT · $DOMAIN_WS · $DOMAIN_TURN"; else echo " một tên miền (LiveKit ở /rtc, nét vẽ ở /ws)"; fi [ $KEYCLOAK = 1 ] && echo " keycloak: $DOMAIN_ID" echo "==============================================" # ---------- 1. Phần mềm nền ---------- echo; xanh "1/7 Cài phần mềm nền" export DEBIAN_FRONTEND=noninteractive # Ubuntu vừa khởi động chạy unattended-upgrades giữ khoá dpkg vài phút; apt-get (khác apt) # không chờ khoá khi stdin không phải tty → chết ngay dòng đầu. Chờ tối đa 5 phút. APT_CHO="-o DPkg::Lock::Timeout=300" apt-get $APT_CHO update -qq apt-get $APT_CHO install -y -qq ca-certificates curl git rsync nginx certbot python3-certbot-nginx openssl dnsutils python3 >/dev/null if ! command -v docker >/dev/null 2>&1; then curl -fsSL https://get.docker.com | sh >/dev/null fi docker compose version >/dev/null 2>&1 || { do_ "Docker compose plugin không có — cài docker thất bại?"; exit 1; } echo " docker $(docker --version | cut -d' ' -f3 | tr -d ,) · nginx $(nginx -v 2>&1 | cut -d/ -f2)" # ---------- 2. Mã nguồn ---------- echo; xanh "2/7 Mã nguồn" if [ -n "$NGUON" ]; then [ -f "$NGUON/cai-dat/cai-o3o-meet.sh" ] || { do_ "--nguon $NGUON không phải cây mã o3o Meet"; exit 2; } mkdir -p "$DICH" # Không mang theo bí mật, node_modules, gói dựng của máy nguồn rsync -a --delete \ --exclude node_modules --exclude .git --exclude '.env' --exclude '.ssh' \ --exclude 'config/livekit.yaml' --exclude 'config/egress.yaml' --exclude 'config-may-vn' \ --exclude 'extension/.key' --exclude 'app-windows/goi*' --exclude 'app-cloud-win' \ --exclude 'web-app/dist' --exclude 'extension/test' --exclude 'cai-dat/config' \ --exclude 'cai-dat/.env' --exclude '.claude' --exclude 'cai-dat/khoa-rieng-giay-phep*' \ --exclude '*.log' --exclude 'recordings' \ "$NGUON/" "$DICH/" # Cây nguồn chép từ máy khác mang uid lạ (tar/rsync -a giữ chủ sở hữu) → git (chạy root) # từ chối "dubious ownership" và set -e dừng cả bộ cài — dính thật lần cài đầu 11/09/2026. chown -R root:root "$DICH" git config --global --add safe.directory "$DICH" >/dev/null 2>&1 || true if [ ! -d "$DICH/.git" ]; then # Bộ cập nhật đọc lịch sử bằng git — không có kho từ xa thì ít nhất cũng có commit tại chỗ git -C "$DICH" init -q -b main fi # Ghi lại cây mã vừa chép thành một commit (lần đầu lẫn lần chạy lại): cây luôn "sạch" so với # HEAD → sau này `o3o-cap-nhat noi-kho` chuyển sang nhánh của kho không bị git từ chối vì # "local changes would be overwritten" git -C "$DICH" add -A >/dev/null 2>&1 || true git -C "$DICH" -c user.name=o3o -c user.email=cai-dat@o3o.vn commit -qm "Cài từ mã sẵn có ($(hostname)) $(date -I)" >/dev/null 2>&1 || true echo " chép từ $NGUON → $DICH (chưa có kho từ xa: nút Cập nhật sẽ báo 'chưa nối kho')" elif [ -d "$DICH/.git" ] && git -C "$DICH" remote get-url origin >/dev/null 2>&1; then git -C "$DICH" pull -q --ff-only || vang " không pull được (sửa tay trong $DICH?) — dùng bản đang có" echo " $DICH — $(git -C "$DICH" log -1 --format='%h %ad' --date=short)" else URL="$REPO" if [ -n "$TOKEN" ]; then URL=$(echo "$REPO" | sed "s#https://#https://oauth2:${TOKEN}@#"); fi git clone -q --depth 1 "$URL" "$DICH" # Không để token nằm lại trong .git/config git -C "$DICH" remote set-url origin "$REPO" # Kho RIÊNG vẫn cần token để bộ cập nhật kéo được về sau → cất vào credential store riêng # của root (600), không nằm trong .git/config (644). Rà soát 11/09/2026. if [ -n "$TOKEN" ]; then KHO_HOST=$(echo "$REPO" | sed -E 's#https://([^/]+)/.*#\1#') umask 077; echo "https://oauth2:${TOKEN}@${KHO_HOST}" > /root/.o3o-git-credentials; umask 022 git -C "$DICH" config credential.helper 'store --file /root/.o3o-git-credentials' fi echo " $DICH — $(git -C "$DICH" log -1 --format='%h %ad' --date=short)" fi # ---------- 3. Bí mật & .env ---------- echo; xanh "3/7 Sinh cấu hình" mkdir -p /data/recordings /data/attendance /data/rooms /data/config /data/tai-app chgrp 0 /data/recordings && chmod 2775 /data/recordings # egress chạy uid 1001 gid 0 ENV="$DICH/cai-dat/.env" if [ -f "$ENV" ]; then echo " .env đã có — giữ nguyên bí mật cũ" # Chạy lại với --domain KHÁC (đổi tên miền, hoặc cài thử bằng tên tạm rồi đổi sang tên # thật) HOẶC đổi chế độ một/bốn tên miền: chỉ cập nhật các dòng tên miền/địa chỉ, KHÔNG # đụng bí mật. Không có đoạn này thì .env giữ tên miền cũ mãi và bản dựng web-app nhúng # địa chỉ cũ. So ĐỦ BỘ (rà soát lần 2): chỉ so DOMAIN_MEET thì --tach-ten-mien thêm vào # sau bị .env cũ ghi đè lặng lẽ trong khi màn hình in "bốn tên miền". if ! grep -qx "DOMAIN_MEET=$DOMAIN" "$ENV" || ! grep -qx "DOMAIN_LIVEKIT=$DOMAIN_LIVEKIT" "$ENV" || ! grep -qx "URL_WS=$URL_WS" "$ENV"; then sed -i -e "s#^DOMAIN_MEET=.*#DOMAIN_MEET=$DOMAIN#" -e "s#^DOMAIN_LIVEKIT=.*#DOMAIN_LIVEKIT=$DOMAIN_LIVEKIT#" \ -e "s#^DOMAIN_WS=.*#DOMAIN_WS=$DOMAIN_WS#" -e "s#^DOMAIN_TURN=.*#DOMAIN_TURN=$DOMAIN_TURN#" \ -e "s#^DOMAIN_ID=.*#DOMAIN_ID=$DOMAIN_ID#" -e "s#^URL_LIVEKIT=.*#URL_LIVEKIT=$URL_LIVEKIT#" \ -e "s#^URL_WS=.*#URL_WS=$URL_WS#" "$ENV" vang " tên miền đổi → đã cập nhật DOMAIN_*/URL_* trong .env (bí mật giữ nguyên); web-app sẽ dựng lại" fi else rand() { openssl rand -hex "$1"; } # Mã chủ trì dễ đọc: 8 chữ số, không phải chuỗi hex 48 ký tự MA_CHU_TRI=$(printf '%08d' $(( $(od -An -N4 -tu4 /dev/urandom) % 100000000 ))) cat > "$ENV" </dev/null | cut -d= -f2- | tr -d '\r' || true; } for k in DOMAIN_MEET DOMAIN_LIVEKIT DOMAIN_WS DOMAIN_TURN DOMAIN_ID LIVEKIT_API_KEY LIVEKIT_API_SECRET TEACHER_CODE; do v=$(lay "$k") [ -n "$v" ] || { do_ " $ENV thiếu dòng $k= (tệp bị sửa tay hỏng?). Sửa lại hoặc đổi tên tệp để bộ cài sinh mới (mất bí mật cũ!)."; exit 3; } printf -v "$k" '%s' "$v" done unset v # IP node if [ -n "$IP_CHI_DINH" ]; then NODE_IP="$IP_CHI_DINH"; else NODE_IP=$(curl -fsS4 --max-time 8 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}'); fi USE_EXT=true; [ $SAU_NAT = 1 ] && USE_EXT=false mkdir -p "$DICH/cai-dat/config" # Nhớ dấu vân cấu hình LiveKit TRƯỚC khi sinh lại: đổi tên miền/IP là phải khởi động lại # container (compose `up -d` không thấy tệp mount đổi nên không tự làm) LK_TRUOC=$(md5sum "$DICH/cai-dat/config/livekit.yaml" 2>/dev/null | cut -d' ' -f1 || true) sed -e "s#__USE_EXTERNAL_IP__#$USE_EXT#; s#__NODE_IP__#$NODE_IP#; s#__LIVEKIT_API_KEY__#$LIVEKIT_API_KEY#g; s#__LIVEKIT_API_SECRET__#$LIVEKIT_API_SECRET#; s#__DOMAIN_TURN__#$DOMAIN_TURN#; s#__DOMAIN_MEET__#$DOMAIN_MEET#g" \ "$DICH/cai-dat/mau/livekit.yaml" > "$DICH/cai-dat/config/livekit.yaml" sed -e "s#__LIVEKIT_API_KEY__#$LIVEKIT_API_KEY#; s#__LIVEKIT_API_SECRET__#$LIVEKIT_API_SECRET#" \ "$DICH/cai-dat/mau/egress.yaml" > "$DICH/cai-dat/config/egress.yaml" # livekit-server chạy root trong ảnh → 600 root được. egress chạy `USER egress` = uid 1001 # gid 0 → 600 là nó KHÔNG ĐỌC ĐƯỢC cấu hình, khởi động lại mãi, phép "egress ghi được /data" # HỎNG (dính thật lần cài 11/09/2026). 640 nhóm root: egress đọc được, người dùng thường vẫn không. chmod 600 "$DICH/cai-dat/config/livekit.yaml" chgrp 0 "$DICH/cai-dat/config/egress.yaml"; chmod 640 "$DICH/cai-dat/config/egress.yaml" LK_SAU=$(md5sum "$DICH/cai-dat/config/livekit.yaml" | cut -d' ' -f1) echo " livekit node_ip=$NODE_IP use_external_ip=$USE_EXT" # ---------- 4. nginx ---------- echo; xanh "4/7 nginx" if [ "$DOMAIN_LIVEKIT" = "$DOMAIN_MEET" ]; then MAU_NGINX="$DICH/cai-dat/mau/nginx-mot-ten-mien.conf"; else MAU_NGINX="$DICH/cai-dat/mau/nginx.conf"; fi sed -e "s#__DOMAIN_MEET__#$DOMAIN_MEET#g; s#__DOMAIN_LIVEKIT__#$DOMAIN_LIVEKIT#g; s#__DOMAIN_WS__#$DOMAIN_WS#g; s#__DOMAIN_TURN__#$DOMAIN_TURN#g" \ "$MAU_NGINX" > /etc/nginx/sites-available/o3o-meet ln -sf /etc/nginx/sites-available/o3o-meet /etc/nginx/sites-enabled/o3o-meet rm -f /etc/nginx/sites-enabled/default cp "$DICH/config/nginx-nen.conf" /etc/nginx/conf.d/o3o-nen.conf 2>/dev/null || true cp "$DICH/config/logrotate-docker.conf" /etc/logrotate.d/docker-container 2>/dev/null || true if [ $KEYCLOAK = 1 ]; then sed -e "s#__DOMAIN_ID__#$DOMAIN_ID#g" "$DICH/cai-dat/mau/nginx-id.conf" > /etc/nginx/sites-available/o3o-id ln -sf /etc/nginx/sites-available/o3o-id /etc/nginx/sites-enabled/o3o-id fi mkdir -p /var/www/html # ---------- 5. DNS & chứng chỉ ---------- echo; xanh "5/7 DNS và chứng chỉ TLS" MAY_IP=$(curl -fsS4 --max-time 8 https://api.ipify.org 2>/dev/null || echo "$NODE_IP") TEN_CERT="$DOMAIN_MEET" [ "$DOMAIN_LIVEKIT" != "$DOMAIN_MEET" ] && TEN_CERT="$TEN_CERT $DOMAIN_LIVEKIT $DOMAIN_WS $DOMAIN_TURN" # dig: `|| true` vì máy bị chặn UDP 53 ra ngoài thì dig trả mã 9 → pipefail + set -e giết bộ # cài giữa bước 5 không một lời (rà soát lần 2). Không tới 1.1.1.1 thì hỏi resolver hệ thống. tra_dns() { local ip; ip=$(dig +short A "$1" @1.1.1.1 2>/dev/null | grep -E '^[0-9.]+$' | tail -1 || true) [ -n "$ip" ] || ip=$(dig +short A "$1" 2>/dev/null | grep -E '^[0-9.]+$' | tail -1 || true) echo "$ip" } DNS_OK=1; DNS_ID_OK=1 for d in $TEN_CERT; do ip=$(tra_dns "$d") if [ "$ip" = "$MAY_IP" ]; then echo " ✓ $d → $ip"; else vang " ✗ $d → ${ip:-(chưa có)} (máy này: $MAY_IP)"; DNS_OK=0; fi done if [ $KEYCLOAK = 1 ]; then ip=$(tra_dns "$DOMAIN_ID") if [ "$ip" = "$MAY_IP" ]; then echo " ✓ $DOMAIN_ID → $ip"; else vang " ✗ $DOMAIN_ID → ${ip:-(chưa có)} (máy này: $MAY_IP)"; DNS_ID_OK=0; fi fi CO_CERT=0; CO_CERT_ID=1 if [ -d "/etc/letsencrypt/live/$DOMAIN_MEET" ]; then CO_CERT=1; echo " chứng chỉ $DOMAIN_MEET đã có"; fi if [ $KEYCLOAK = 1 ] && [ ! -d "/etc/letsencrypt/live/$DOMAIN_ID" ]; then CO_CERT_ID=0; fi CO_CERT_TRUOC=$CO_CERT # Cấu hình đầy đủ (443) đã ghi ở bước 4 — nhưng chưa có cert thì nginx không nạp được nó. # Cất tạm sang .day-du, đặt cấu hình CHỈ CỔNG 80 để xin cert, xong mới trả lại. (Rà soát # 11/09/2026: bản đầu chỉ comment dòng listen 443 → khối đó rơi về cổng 80 mặc định và nuốt # luôn /.well-known/acme-challenge/, lần cài đầu không bao giờ xin được cert.) # Cert id. (Keycloak) xin và bật ĐỘC LẬP với cert chính: rà soát lần 2 — bản đầu # cert id hỏng một lần là o3o-id vẫn được bật → nginx -t fail mọi lần chạy lại và không bao # giờ xin lại (điều kiện gộp CO_CERT=0 đã sai). cat_http_tam() { sed -e "s#__TEN_MIEN__#$TEN_CERT $([ $KEYCLOAK = 1 ] && echo "$DOMAIN_ID")#" "$DICH/cai-dat/mau/nginx-http-tam.conf" > /etc/nginx/sites-available/o3o-meet rm -f /etc/nginx/sites-enabled/o3o-id nginx -t >/dev/null 2>&1 && systemctl enable --now nginx >/dev/null 2>&1 && systemctl reload nginx } if [ $CO_CERT = 0 ] || [ $CO_CERT_ID = 0 ]; then cp /etc/nginx/sites-available/o3o-meet /etc/nginx/sites-available/o3o-meet.day-du [ $KEYCLOAK = 1 ] && cp /etc/nginx/sites-available/o3o-id /etc/nginx/sites-available/o3o-id.day-du cat_http_tam fi MAIL_ARG=$([ -n "$EMAIL" ] && echo "-m $EMAIL" || echo "--register-unsafely-without-email") if [ $CO_CERT = 0 ] && [ $DNS_OK = 1 ]; then D_ARGS=""; for d in $TEN_CERT; do D_ARGS="$D_ARGS -d $d"; done if certbot certonly --webroot -w /var/www/html -n --agree-tos $MAIL_ARG $D_ARGS; then CO_CERT=1; else vang " ✗ certbot không cấp được cert cho $TEN_CERT (xem /var/log/letsencrypt/letsencrypt.log)"; fi fi if [ $KEYCLOAK = 1 ] && [ $CO_CERT_ID = 0 ] && [ $DNS_ID_OK = 1 ]; then if certbot certonly --webroot -w /var/www/html -n --agree-tos $MAIL_ARG -d "$DOMAIN_ID"; then CO_CERT_ID=1; else vang " ✗ certbot không cấp được cert cho $DOMAIN_ID — Keycloak chưa lên; chạy lại bộ cài sau"; fi fi if [ $CO_CERT = 1 ]; then # Trả lại cấu hình đầy đủ (nếu vừa xin cert lần đầu) [ -f /etc/nginx/sites-available/o3o-meet.day-du ] && mv -f /etc/nginx/sites-available/o3o-meet.day-du /etc/nginx/sites-available/o3o-meet if [ $KEYCLOAK = 1 ]; then [ -f /etc/nginx/sites-available/o3o-id.day-du ] && mv -f /etc/nginx/sites-available/o3o-id.day-du /etc/nginx/sites-available/o3o-id if [ $CO_CERT_ID = 1 ]; then ln -sf /etc/nginx/sites-available/o3o-id /etc/nginx/sites-enabled/o3o-id else # Không bật khối ssl trỏ tới cert chưa có: nginx -t sẽ fail và kéo cả tên miền chính chết theo rm -f /etc/nginx/sites-enabled/o3o-id vang " $DOMAIN_ID chưa có cert → chưa bật Keycloak trên nginx; DNS đúng rồi thì chạy lại bộ cài" fi fi # certbot gia hạn: (1) xoá http2 → khôi phục; (2) LiveKit giữ cert cũ cho TURN/TLS 5349 tới # khi khởi động lại → khởi động lại container livekit (vài giây, chỉ lúc gia hạn ~60 ngày/lần) mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/o3o-http2.sh </dev/null 2>&1 || true H chmod +x /etc/letsencrypt/renewal-hooks/deploy/o3o-http2.sh nginx -t && systemctl enable --now nginx >/dev/null 2>&1 && systemctl reload nginx && echo " ✓ nginx HTTPS đã chạy" else vang " Chưa có chứng chỉ (DNS chưa trỏ đủ). nginx đang chạy cấu hình tạm cổng 80; dịch vụ vẫn dựng," vang " nhưng LiveKit/egress sẽ khởi động lại liên tục cho tới khi có cert (TURN/TLS cần cert)." vang " Sau khi DNS đúng, chạy lại đúng lệnh cài này — nó sẽ xin cert, trả cấu hình đầy đủ và khởi động lại LiveKit." fi # ---------- 6. Dựng dịch vụ ---------- echo; xanh "6/7 Dựng và chạy dịch vụ (lần đầu mất vài phút)" cd "$DICH/cai-dat" PROFILE=""; [ $KEYCLOAK = 1 ] && PROFILE="--profile sso" docker compose --env-file "$ENV" $PROFILE build -q docker compose --env-file "$ENV" $PROFILE up -d # Vừa có cert lần đầu, hoặc livekit.yaml đổi (tên miền/IP) → `up -d` không tự khởi động lại # container đang chạy tệp mount cũ; LiveKit chỉ sống lại nhờ backoff của Docker (tới 1 phút) # → phép "LiveKit trả lời" ở bước 7 HỎNG oan (rà soát lần 2) if { [ $CO_CERT_TRUOC = 0 ] && [ $CO_CERT = 1 ]; } || [ "$LK_TRUOC" != "$LK_SAU" ]; then echo " cấu hình LiveKit/cert đổi → khởi động lại livekit + egress" docker compose --env-file "$ENV" restart livekit egress >/dev/null 2>&1 || true fi sleep 10 docker compose --env-file "$ENV" $PROFILE ps --format 'table {{.Service}}\t{{.Status}}' # Mốc "bản đang CHẠY" cho bộ cập nhật (khác HEAD của cây mã khi cập nhật hỏng giữa chừng) mkdir -p /data/config git -C "$DICH" rev-parse --short HEAD 2>/dev/null > /data/config/ban-dang-chay || true # ---------- 6b. Bộ cập nhật (trang admin: "Kiểm bản mới" / "Cập nhật ngay") ---------- install -m 755 "$DICH/cai-dat/cap-nhat.sh" /usr/local/bin/o3o-cap-nhat cp "$DICH/cai-dat/mau/o3o-cap-nhat.service" /etc/systemd/system/o3o-cap-nhat.service systemctl daemon-reload systemctl enable --now o3o-cap-nhat.service >/dev/null 2>&1 || vang " không bật được dịch vụ o3o-cap-nhat (xem: journalctl -u o3o-cap-nhat)" O3O_DICH="$DICH" O3O_CONFIG=/data/config /usr/local/bin/o3o-cap-nhat kiem || true echo " bộ cập nhật: $(systemctl is-active o3o-cap-nhat.service 2>/dev/null || echo '?') · $(python3 -c 'import json;d=json.load(open("/data/config/phien-ban.json"));print(d["hienTai"]["commit"], d["hienTai"]["ngay"])' 2>/dev/null)" # Khoá một lần cho trang chào mừng. GIỮ khoá cũ nếu còn (rà soát lần 2: mỗi lần chạy lại # sinh khoá mới thì URL lần trước in "sau khi DNS đúng… mở" đã vô hiệu). Web-app xoá tệp khi # chào mừng xong nên không sinh lại nhầm; đã cài xong thì không cần khoá nữa. DA_CAI_XONG=$(python3 -c 'import json;print("1" if json.load(open("/data/config/cau-hinh.json")).get("daCaiDat") is True else "0")' 2>/dev/null || echo 0) if [ "$DA_CAI_XONG" = 1 ]; then KHOA_CD="" elif [ -s /data/config/khoa-cai-dat.txt ]; then KHOA_CD=$(cat /data/config/khoa-cai-dat.txt) else KHOA_CD=$(openssl rand -hex 16) echo -n "$KHOA_CD" > /data/config/khoa-cai-dat.txt chmod 600 /data/config/khoa-cai-dat.txt fi # ---------- 7. Kiểm tại chỗ ---------- echo; xanh "7/7 Kiểm tại chỗ" SO_HONG=0 kt() { printf ' %-36s ' "$1"; shift; if "$@" >/dev/null 2>&1; then echo "ĐẠT"; KT_CUOI=0; else echo "HỎNG"; KT_CUOI=1; SO_HONG=$((SO_HONG+1)); fi; } kt "web-app trả lời" curl -sf http://127.0.0.1:3000/ kt "annotation-server sống" curl -sf http://127.0.0.1:8080/health kt "LiveKit trả lời" curl -sf http://127.0.0.1:7880/ [ $KT_CUOI = 1 ] && { docker compose --env-file "$ENV" logs --no-color --tail 4 livekit 2>/dev/null | sed 's/^/ /' || true; } kt "egress ghi được /data" docker compose --env-file "$ENV" exec -T egress sh -c 'touch /recordings/probe && rm /recordings/probe' # HỎNG mà không nói lý do là admin mò cả buổi: in đuôi log egress ngay tại đây [ $KT_CUOI = 1 ] && { docker compose --env-file "$ENV" logs --no-color --tail 4 egress 2>/dev/null | grep -v pulseaudio | sed 's/^/ /' || true; } kt "bộ cập nhật đang chạy" systemctl is-active --quiet o3o-cap-nhat.service if [ $CO_CERT = 1 ]; then kt "HTTPS $DOMAIN_MEET" curl -sfI "https://$DOMAIN_MEET/" kt "HTTPS /api/tinh-nang" curl -sf "https://$DOMAIN_MEET/api/tinh-nang" # LiveKit không token trả 401 "invalid authorization token" — nghĩa là nginx đã đưa tới đúng # LiveKit (502/404 mới là hỏng đường dẫn /rtc). Kiểm đúng mã đó, không dùng -f. ma_rtc() { [ "$(curl -s -o /dev/null -w '%{http_code}' "https://$DOMAIN_LIVEKIT/rtc/validate" 2>/dev/null)" = 401 ]; } kt "LiveKit qua HTTPS /rtc" ma_rtc fi [ $SO_HONG = 0 ] || vang " $SO_HONG phép HỎNG — xem lý do ở trên; sửa rồi chạy lại bộ cài (an toàn) hoặc: cd $DICH/cai-dat && docker compose --env-file .env logs --tail 50" echo echo "==============================================" xanh " CÀI XONG." echo if [ -z "$KHOA_CD" ]; then echo " Hệ thống đã cấu hình xong từ trước. Quản trị tại:" xanh " https://$DOMAIN_MEET/admin" elif [ $CO_CERT = 1 ]; then echo " Mở TRANG CHÀO MỪNG để cấu hình logo, tên tổ chức, đăng nhập SSO, giấy phép:" xanh " https://$DOMAIN_MEET/chao-mung?khoa=$KHOA_CD" echo " (khoá dùng MỘT lần; mất thì đọc lại ở /data/config/khoa-cai-dat.txt)" else echo " Sau khi DNS trỏ đúng và chạy lại bộ cài, mở:" xanh " https://$DOMAIN_MEET/chao-mung?khoa=$KHOA_CD" echo " (khoá giữ nguyên qua các lần chạy lại; mất thì đọc lại ở /data/config/khoa-cai-dat.txt)" fi echo echo " Mã chủ trì dùng chung (khi chưa có SSO): $TEACHER_CODE" [ $KEYCLOAK = 1 ] && echo " Keycloak: https://$DOMAIN_ID — tài khoản admin / mật khẩu trong $ENV (KC_ADMIN_PASSWORD)" echo vang " TƯỜNG LỬA nhà cung cấp phải mở: 22,80,443,7881/tcp · 5349/tcp · 3478/udp · 50000-60000/udp" vang " (thiếu dải UDP là vào họp được nhưng không ai nghe thấy ai)" echo " Mọi bí mật nằm trong $ENV — sao lưu tệp này và /data/." echo "=============================================="