#!/bin/bash # ============================================================ # MORA Meet — BỘ CÀI MÁY CHỦ MEDIA (hình / tiếng) cho Ubuntu 22.04 / 24.04: thêm bao nhiêu máy chủ # media cũng được, mỗi máy cài bằng một script, đơn giản như cài máy chính. (Tên tệp, lệnh, thư mục # vẫn mang tên cũ "o3o-…" để máy đã cài chạy lại được — đổi thương hiệu MORA 29/09/2026 chỉ đổi chữ # hiển thị.) # # Trên MÁY CHÍNH: sudo o3o-ma-ghep → in ra MÃ GHÉP NỐI (giữ kín, chứa khoá API) # Trên MÁY MEDIA: curl -fsSL https://install.o3o.vn/meet/cai-o3o-media.sh \ # | sudo bash -s -- --domain media.congty.vn --ghep --email it@congty.vn # # Máy media chạy máy chủ media + Redis RIÊNG, dùng CÙNG khoá API với máy chính nên token do máy chính # cấp vào được; webhook trả về máy chính. Cách này đã chạy thật trên máy media đặt sau NAT ở một # trung tâm dữ liệu khác máy chính: không mở Redis ra internet, mỗi máy giữ phòng của mình; máy # chính chọn máy nào nhận phòng MỚI (admin › Hệ thống › Máy chủ media › Mặc định). # # Tuỳ chọn: # --domain BẮT BUỘC, đã trỏ DNS A về máy này (vd media.congty.vn) # --ghep BẮT BUỘC lần đầu (lần chạy lại không cần: đã cất ở /opt/o3o-media/.env) # --email Email nhận thông báo chứng chỉ Let's Encrypt # --sau-nat --ip Máy sau NAT/router: ghim IP công khai # --ghi-hinh Cài thêm bộ GHI HÌNH (egress) + KHO BẢN GHI trên máy này (18/09/2026). Máy chính # kéo bản ghi qua https:///ghi-noibo// (REC_REMOTE_BASE). Mặc định KHÔNG. # --thu-muc-ghi <đường> Thư mục chứa bản ghi (mặc định /data/recordings; NAS đã mount cũng được) # --ma-kho-ghi Mã 32 hex của đường kho bản ghi (mặc định sinh ngẫu nhiên; nhớ trong .env) # # Phiên bản ảnh GHIM CỨNG (đồng nhất máy chính và mọi máy media): media v1.13.5 · egress v1.14.0. # # Tệp này TỰ ĐỦ (không kéo kho mã): mọi mẫu cấu hình nằm ngay bên dưới. Chạy lại an toàn. # ============================================================ set -euo pipefail DOMAIN=""; EMAIL=""; GHEP=""; SAU_NAT=0; IP_CHI_DINH="" GHI_HINH=0; THU_MUC_GHI=""; MA_KHO_GHI="" ANH_MEDIA=livekit/livekit-server:v1.13.5 ANH_EGRESS=livekit/egress:v1.14.0 DICH=/opt/o3o-media ENV="$DICH/.env" while [ $# -gt 0 ]; do case "$1" in --domain) DOMAIN="$2"; shift 2;; --email) EMAIL="$2"; shift 2;; --ghep) GHEP="$2"; shift 2;; --sau-nat) SAU_NAT=1; shift;; --ip) IP_CHI_DINH="$2"; shift 2;; --ghi-hinh) GHI_HINH=1; shift;; --thu-muc-ghi) THU_MUC_GHI="$2"; shift 2;; --ma-kho-ghi) MA_KHO_GHI="$2"; shift 2;; *) 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' "$*"; } 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."' ERR [ "$(id -u)" = 0 ] || { do_ "Phải chạy bằng sudo/root."; exit 1; } [ -n "$DOMAIN" ] || { do_ "Thiếu --domain. Ví dụ: --domain media.congty.vn"; exit 2; } 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 if [ -z "$GHEP" ] && [ ! -f "$ENV" ]; then do_ "Thiếu --ghep . Lấy trên máy chính: sudo o3o-ma-ghep"; exit 2; fi . /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 " MORA Meet — máy chủ MEDIA $DOMAIN" echo "==============================================" # ---------- 1. Phần mềm nền ---------- echo; xanh "1/6 Cài phần mềm nền" export DEBIAN_FRONTEND=noninteractive APT_CHO="-o DPkg::Lock::Timeout=300" apt-get $APT_CHO update -qq apt-get $APT_CHO install -y -qq ca-certificates curl nginx certbot 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ã ghép nối → .env ---------- echo; xanh "2/6 Ghép nối với máy chính" mkdir -p "$DICH/config" if [ -n "$GHEP" ]; then # Mã = base64url của JSON {chinh, key, secret}. Không in secret ra màn hình. if ! GIAI=$(python3 -c 'import base64,json,sys s=sys.argv[1].strip(); s+="="*(-len(s)%4) d=json.loads(base64.urlsafe_b64decode(s)) print(d["chinh"]); print(d["key"]); print(d["secret"])' "$GHEP" 2>/dev/null); then do_ " Mã ghép nối không đọc được. Chép lại nguyên vẹn từ 'sudo o3o-ma-ghep' trên máy chính."; exit 2 fi CHINH=$(echo "$GIAI" | sed -n 1p); LK_KEY=$(echo "$GIAI" | sed -n 2p); LK_SECRET=$(echo "$GIAI" | sed -n 3p) umask 077 cat > "$ENV" </dev/null | cut -d= -f2- | tr -d '\r' || true; } CHINH=$(lay DOMAIN_CHINH); LK_KEY=$(lay MEDIA_API_KEY); LK_SECRET=$(lay MEDIA_API_SECRET) [ -n "$CHINH" ] && [ -n "$LK_KEY" ] && [ -n "$LK_SECRET" ] || { do_ " $ENV thiếu dòng — chạy lại với --ghep"; exit 3; } grep -qx "DOMAIN_MEDIA=$DOMAIN" "$ENV" || sed -i "s#^DOMAIN_MEDIA=.*#DOMAIN_MEDIA=$DOMAIN#" "$ENV" # Ghi hình (18/09/2026): cờ và mã nhớ trong .env để lần chạy lại không cần nhắc lại dat_env() { if grep -q "^$1=" "$ENV"; then sed -i "s#^$1=.*#$1=$2#" "$ENV"; else echo "$1=$2" >> "$ENV"; fi; } if [ $GHI_HINH = 1 ]; then dat_env GHI_HINH 1 [ -n "$THU_MUC_GHI" ] && dat_env THU_MUC_GHI "$THU_MUC_GHI" grep -q '^THU_MUC_GHI=' "$ENV" || dat_env THU_MUC_GHI /data/recordings if [ -n "$MA_KHO_GHI" ]; then [[ "$MA_KHO_GHI" =~ ^[a-f0-9]{32}$ ]] || { do_ " --ma-kho-ghi phải là 32 ký tự hex (a-f, 0-9)"; exit 2; } dat_env MA_KHO_GHI "$MA_KHO_GHI" fi grep -q '^MA_KHO_GHI=' "$ENV" || dat_env MA_KHO_GHI "$(openssl rand -hex 16)" fi [ "$(lay GHI_HINH)" = 1 ] && GHI_HINH=1 if [ $GHI_HINH = 1 ]; then THU_MUC_GHI=$(lay THU_MUC_GHI); MA_KHO_GHI=$(lay MA_KHO_GHI) [[ "$THU_MUC_GHI" =~ ^/[A-Za-z0-9._/-]+$ ]] || { do_ " THU_MUC_GHI trong $ENV không hợp lệ: $THU_MUC_GHI"; exit 3; } [[ "$MA_KHO_GHI" =~ ^[a-f0-9]{32}$ ]] || { do_ " MA_KHO_GHI trong $ENV không hợp lệ"; exit 3; } echo " ghi hình: BẬT — bản ghi vào $THU_MUC_GHI, máy chính kéo qua /ghi-noibo//" fi # ---------- 3. Cấu hình máy chủ media + compose ---------- echo; xanh "3/6 Sinh cấu hình" 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 LK_TRUOC=$(md5sum "$DICH/config/media.yaml" 2>/dev/null | cut -d' ' -f1 || true) cat > "$DICH/config/media.yaml.mau" <<'YAML' # Sinh bởi cai-o3o-media.sh — máy media chạy máy chủ media + Redis riêng, cùng khoá API với máy chính. port: 7880 bind_addresses: - "" log_level: info rtc: tcp_port: 7881 port_range_start: 50000 port_range_end: 60000 use_external_ip: __USE_EXTERNAL_IP__ node_ip: __NODE_IP__ stun_servers: - stun.l.google.com:19302 redis: address: 127.0.0.1:6379 keys: __MEDIA_API_KEY__: __MEDIA_API_SECRET__ room: auto_create: true empty_timeout: 300 departure_timeout: 30 max_participants: 50 enable_remote_unmute: true turn: enabled: true domain: __DOMAIN_MEDIA__ tls_port: 5349 udp_port: 3478 cert_file: /etc/letsencrypt/live/__DOMAIN_MEDIA__/fullchain.pem key_file: /etc/letsencrypt/live/__DOMAIN_MEDIA__/privkey.pem # Sự kiện phòng (vào/ra, ghi hình) trả về MÁY CHÍNH — nơi có web-app, điểm danh, bản ghi webhook: urls: - https://__DOMAIN_CHINH__/api/webhook/media api_key: __MEDIA_API_KEY__ YAML sed -e "s#__USE_EXTERNAL_IP__#$USE_EXT#; s#__NODE_IP__#$NODE_IP#; s#__MEDIA_API_KEY__#$LK_KEY#g; s#__MEDIA_API_SECRET__#$LK_SECRET#; s#__DOMAIN_MEDIA__#$DOMAIN#g; s#__DOMAIN_CHINH__#$CHINH#" \ "$DICH/config/media.yaml.mau" > "$DICH/config/media.yaml" rm -f "$DICH/config/media.yaml.mau" chmod 600 "$DICH/config/media.yaml" # máy chủ media chạy root trong ảnh → 600 đọc được LK_SAU=$(md5sum "$DICH/config/media.yaml" | cut -d' ' -f1) cat > "$DICH/docker-compose.yml" <<'YAML' # Máy chủ media — CHỈ máy chủ media + Redis riêng (sinh bởi cai-o3o-media.sh). Không sửa tay. services: redis: image: redis:7.2-alpine restart: always network_mode: host command: redis-server --bind 127.0.0.1 --save "" --appendonly no healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 media: image: livekit/livekit-server:v1.9.1 restart: always network_mode: host volumes: - ./config/media.yaml:/etc/media.yaml:ro - /etc/letsencrypt:/etc/letsencrypt:ro command: --config /etc/media.yaml depends_on: redis: condition: service_healthy logging: driver: json-file options: max-size: "20m" max-file: "3" YAML echo " máy chủ media node_ip=$NODE_IP use_external_ip=$USE_EXT" # Bộ đệm UDP 16 MB: mặc định 425 KB, máy chủ media cảnh báo quá nhỏ — với 80 người là mất gói giờ cao điểm cat > /etc/sysctl.d/99-o3o-media.conf <<'SYS' net.core.rmem_max=16777216 net.core.wmem_max=16777216 net.core.rmem_default=8388608 net.core.wmem_default=8388608 SYS sysctl -q --system >/dev/null 2>&1 || true sed -i "s#image: livekit/livekit-server:v1.9.1#image: $ANH_MEDIA#" "$DICH/docker-compose.yml" if [ $GHI_HINH = 1 ]; then # egress: chung Redis + máy chủ media với máy này (lệnh ghi hình đi qua Redis). ws_url PHẢI wss:// vì trang ghi # hình được nạp từ https máy chính — ws:// bị trình duyệt chặn (mixed content), bản ghi chỉ dài 3 giây (dính 08/2026). cat > "$DICH/config/egress.yaml" </dev/null || vang " không đổi được quyền $THU_MUC_GHI (NAS/ACL?) — egress (uid 1001) phải ghi được thư mục này" # Kho bản ghi: nginx nhỏ chạy ROOT liệt kê JSON + phát tệp (egress ghi 660 uid 1001 → worker thường 403) cat > "$DICH/config/nginx-ghi-main.conf" <<'NG' user root; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; } NG cat > "$DICH/config/nginx-ghi.conf" <<'NG' server { listen 8090; server_name _; root /recordings; location / { autoindex on; autoindex_format json; autoindex_exact_size on; add_header Accept-Ranges bytes; } } NG cat >> "$DICH/docker-compose.yml" </ kho-ghi: image: nginx:alpine restart: always user: root ports: - "127.0.0.1:8090:8090" volumes: - ./config/nginx-ghi-main.conf:/etc/nginx/nginx.conf:ro - ./config/nginx-ghi.conf:/etc/nginx/conf.d/default.conf:ro - $THU_MUC_GHI:/recordings:ro YAML echo " egress $ANH_EGRESS + kho bản ghi (127.0.0.1:8090)" else rm -f "$DICH/config/egress.yaml" "$DICH/config/nginx-ghi.conf" "$DICH/config/nginx-ghi-main.conf" fi # ---------- 4. nginx ---------- echo; xanh "4/6 nginx" cat > /etc/nginx/sites-available/o3o-media.mau <<'NGINX' # Máy chủ media __DOMAIN_MEDIA__ (sinh bởi cai-o3o-media.sh) server { listen 443 ssl http2; server_name __DOMAIN_MEDIA__; ssl_certificate /etc/letsencrypt/live/__DOMAIN_MEDIA__/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/__DOMAIN_MEDIA__/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Tín hiệu WebRTC của trình duyệt (wss://__DOMAIN_MEDIA__ → máy chủ media tự thêm /rtc) location /rtc { proxy_pass http://127.0.0.1:7880; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 86400s; } # API quản trị phòng (máy chính gọi RoomService qua https://__DOMAIN_MEDIA__) location /twirp/ { proxy_pass http://127.0.0.1:7880; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /.well-known/acme-challenge/ { root /var/www/html; } __KHO_GHI__ location / { default_type text/plain; return 200 "Media server __DOMAIN_MEDIA__\n"; } } server { listen 80; server_name __DOMAIN_MEDIA__; location /.well-known/acme-challenge/ { root /var/www/html; } location / { return 301 https://$host$request_uri; } } NGINX if [ $GHI_HINH = 1 ]; then # Kho bản ghi: đường có MÃ 32 hex thay mật khẩu — chỉ máy chính biết, người xem không bao giờ thấy địa chỉ này cat > /etc/nginx/sites-available/o3o-media.khoghi < /etc/nginx/sites-available/o3o-media rm -f /etc/nginx/sites-available/o3o-media.mau ln -sf /etc/nginx/sites-available/o3o-media /etc/nginx/sites-enabled/o3o-media rm -f /etc/nginx/sites-enabled/default mkdir -p /var/www/html # ---------- 5. DNS & chứng chỉ ---------- echo; xanh "5/6 DNS và chứng chỉ TLS" MAY_IP=$(curl -fsS4 --max-time 8 https://api.ipify.org 2>/dev/null || echo "$NODE_IP") 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; ip=$(tra_dns "$DOMAIN") if [ "$ip" = "$MAY_IP" ]; then echo " ✓ $DOMAIN → $ip"; else vang " ✗ $DOMAIN → ${ip:-(chưa có)} (máy này: $MAY_IP)"; DNS_OK=0; fi CO_CERT=0; [ -d "/etc/letsencrypt/live/$DOMAIN" ] && { CO_CERT=1; echo " chứng chỉ đã có"; } CO_CERT_TRUOC=$CO_CERT if [ $CO_CERT = 0 ]; then # Chưa có cert: cấu hình TẠM chỉ cổng 80 để xin cert (khối ssl trỏ cert chưa có là nginx -t fail) cp /etc/nginx/sites-available/o3o-media /etc/nginx/sites-available/o3o-media.day-du printf 'server {\n listen 80;\n server_name %s;\n location /.well-known/acme-challenge/ { root /var/www/html; }\n location / { default_type text/plain; return 200 "Media server dang cai dat\\n"; }\n}\n' "$DOMAIN" > /etc/nginx/sites-available/o3o-media nginx -t >/dev/null 2>&1 && systemctl enable --now nginx >/dev/null 2>&1 && systemctl reload nginx if [ $DNS_OK = 1 ]; then MAIL_ARG=$([ -n "$EMAIL" ] && echo "-m $EMAIL" || echo "--register-unsafely-without-email") if certbot certonly --webroot -w /var/www/html -n --agree-tos $MAIL_ARG -d "$DOMAIN"; then CO_CERT=1; else vang " ✗ certbot không cấp được cert (xem /var/log/letsencrypt/letsencrypt.log)"; fi fi fi if [ $CO_CERT = 1 ]; then # options-ssl-nginx.conf + ssl-dhparams.pem: certbot chỉ tạo khi dùng plugin --nginx; cấp bằng webroot thì # KHÔNG có → nginx -t hỏng, site HTTPS không lên (dính thật 18/09/2026 trên máy media VN). Lấy bản certbot tự # mang theo, không có thì viết bản chuẩn / sinh dhparam. if [ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]; then f=$(find /usr/lib/python3/dist-packages /usr/lib/python3*/site-packages /snap/certbot -name options-ssl-nginx.conf 2>/dev/null | head -1 || true) if [ -n "$f" ]; then cp "$f" /etc/letsencrypt/options-ssl-nginx.conf; else cat > /etc/letsencrypt/options-ssl-nginx.conf <<'SSL' ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; SSL fi fi if [ ! -f /etc/letsencrypt/ssl-dhparams.pem ]; then f=$(find /usr/lib/python3/dist-packages /usr/lib/python3*/site-packages /snap/certbot -name ssl-dhparams.pem 2>/dev/null | head -1 || true) if [ -n "$f" ]; then cp "$f" /etc/letsencrypt/ssl-dhparams.pem; else openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null; fi fi [ -f /etc/nginx/sites-available/o3o-media.day-du ] && mv -f /etc/nginx/sites-available/o3o-media.day-du /etc/nginx/sites-available/o3o-media mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/o3o-media.sh </dev/null 2>&1 || true H chmod +x /etc/letsencrypt/renewal-hooks/deploy/o3o-media.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ỏ đủ). máy chủ media sẽ khởi động lại liên tục 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." fi # ---------- 6. Dựng & kiểm ---------- echo; xanh "6/6 Dựng dịch vụ và kiểm tại chỗ" cd "$DICH" docker compose up -d --remove-orphans if { [ $CO_CERT_TRUOC = 0 ] && [ $CO_CERT = 1 ]; } || [ "$LK_TRUOC" != "$LK_SAU" ]; then docker compose restart media >/dev/null 2>&1 || true [ $GHI_HINH = 1 ] && { docker compose restart egress >/dev/null 2>&1 || true; } fi sleep 8 SO_HONG=0 kt() { printf ' %-36s ' "$1"; shift; if "$@" >/dev/null 2>&1; then echo "ĐẠT"; else echo "HỎNG"; SO_HONG=$((SO_HONG+1)); fi; } kt "Máy chủ media trả lời (7880)" curl -sf http://127.0.0.1:7880/ kt "TURN/TLS 5349 đang nghe" sh -c "ss -tln | grep -q ':5349 '" kt "TURN/UDP 3478 đang nghe" sh -c "ss -uln | grep -q ':3478 '" if [ $GHI_HINH = 1 ]; then kt "Egress (ghi hình) đang chạy" sh -c "docker compose ps --format '{{.Service}} {{.State}}' | grep -q '^egress running'" kt "Kho bản ghi trả danh sách JSON" sh -c "curl -sf http://127.0.0.1:8090/ | grep -q '^\['" kt "Egress ghi được thư mục bản ghi" sh -c "docker compose exec -T egress sh -c 'touch /recordings/.o3o-thu && rm -f /recordings/.o3o-thu'" fi if [ $CO_CERT = 1 ]; then ma_rtc() { [ "$(curl -s -o /dev/null -w '%{http_code}' "https://$DOMAIN/rtc/validate" 2>/dev/null)" = 401 ]; } kt "HTTPS /rtc tới máy chủ media" ma_rtc # twirp đòi Content-Type: application/json — thiếu là 404 "bad_route" (bản 1.13), có mà chưa ký là 401 = đúng đường kt "HTTPS /twirp tới máy chủ media" sh -c "[ \"\$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d '{}' https://$DOMAIN/twirp/livekit.RoomService/ListRooms)\" = 401 ]" fi [ $SO_HONG = 0 ] || { vang " $SO_HONG phép HỎNG — xem: cd $DICH && docker compose logs --tail 50 media"; } echo echo "==============================================" xanh " MÁY MEDIA $DOMAIN CÀI XONG." echo echo " Khai máy này vào MÁY CHÍNH: https://$CHINH/admin › Hệ thống › Máy chủ media › Thêm:" xanh " Tên: $(hostname) ($DOMAIN)" xanh " Địa chỉ cho người dùng: wss://$DOMAIN" xanh " API quản trị: https://$DOMAIN" echo " Đánh dấu 'Mặc định' nếu muốn mọi phòng MỚI mở trên máy này (phòng đang mở không bị chuyển)." if [ $GHI_HINH = 1 ]; then echo echo " GHI HÌNH: bật cờ « có ghi hình » cho máy này ở admin › Hệ thống › Máy chủ media, và cho máy chính kéo bản ghi:" xanh " REC_REMOTE_BASE=https://$DOMAIN/ghi-noibo/$MA_KHO_GHI" echo " (thêm vào .env máy chính rồi khởi động lại web-app; mã này thay mật khẩu — không dán nơi công khai)" echo " Bản ghi nằm ở $THU_MUC_GHI trên máy này (hình ảnh học sinh — sao lưu, không xoá tự động)." fi 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 " Khoá API dùng chung nằm trong $ENV — giữ như mật khẩu; đổi khoá trên máy chính là phải chạy lại với --ghep mới." echo "=============================================="