Files
incudal/scripts/install-docker.sh
T
2026-06-20 14:26:29 +08:00

760 lines
24 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# Incudal 面板一键部署脚本(Docker 模式)
#
# 功能:
# - 自动安装 Docker Engine 和 Docker Compose
# - 从 GitHub Container Registry (ghcr.io) 拉取预构建镜像
# - 自动配置 docker-compose.yml、环境变量
# - PostgreSQL 16 + Redis 7 全容器化运行
# - 支持 Nginx+Certbot / Cloudflare Tunnel / 纯端口 三种外部访问方案
# - 支持升级和卸载
#
# 用法:
# 安装: sudo bash install-docker.sh
# 升级: sudo bash install-docker.sh --upgrade
# 卸载: sudo bash install-docker.sh --uninstall
#
# 项目地址: https://github.com/0xdabiaoge/incudal
# ============================================================================
set -euo pipefail
# ========================== 全局常量 ==========================
readonly SCRIPT_VERSION="1.0.0"
readonly GITHUB_REPO="0xdabiaoge/incudal"
readonly DOCKER_IMAGE="ghcr.io/${GITHUB_REPO}"
readonly INSTALL_DIR="/opt/incudal"
readonly ENV_FILE="${INSTALL_DIR}/.env"
readonly COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
readonly DEFAULT_PORT=3000
# ========================== 颜色定义 ==========================
readonly RED='\033[1;31m'
readonly GREEN='\033[1;32m'
readonly YELLOW='\033[1;33m'
readonly CYAN='\033[1;36m'
readonly BOLD='\033[1m'
readonly DIM='\033[2m'
readonly NC='\033[0m'
# ========================== 工具函数 ==========================
# 所有日志输出到 stderr,避免在 $() 子 shell 中被捕获
log() { echo -e "${GREEN}[✓]${NC} $1" >&2; }
info() { echo -e "${CYAN}[i]${NC} $1" >&2; }
warn() { echo -e "${YELLOW}[!]${NC} $1" >&2; }
error() { echo -e "${RED}[✗]${NC} $1" >&2; }
step() { echo -e "\n${CYAN}[▶]${NC} ${BOLD}$1${NC}" >&2; }
divider() {
echo -e "${DIM}────────────────────────────────────────────────────${NC}" >&2
}
# 生成随机密码
gen_password() {
openssl rand -hex 64 | cut -c "1-${1:-24}"
}
gen_secret() {
printf 'A1!%s' "$(openssl rand -hex 64)" | cut -c "1-${1:-48}"
}
get_env_value() {
local key="$1"
if [[ ! -f "$ENV_FILE" ]]; then
return 0
fi
grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | tail -n 1 | cut -d'=' -f2- || true
}
set_env_if_missing() {
local key="$1"
local value="$2"
local label="$3"
local current
current="$(get_env_value "$key")"
if [[ -n "$current" ]]; then
return 0
fi
if grep -qE "^${key}=" "$ENV_FILE" 2>/dev/null; then
local tmp_file
tmp_file="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^" key "=" && replaced == 0 {
print key "=" value
replaced = 1
next
}
{ print }
' "$ENV_FILE" > "$tmp_file"
cat "$tmp_file" > "$ENV_FILE"
rm -f "$tmp_file"
else
printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE"
fi
log "已自动补充 ${label}: ${key}"
}
ensure_env_keys() {
if [[ ! -f "$ENV_FILE" ]]; then
return 0
fi
set_env_if_missing "POSTGRES_PASSWORD" "$(gen_password 24)" "PostgreSQL 密码"
set_env_if_missing "REDIS_PASSWORD" "$(gen_password 24)" "Redis 密码"
set_env_if_missing "JWT_SECRET" "$(gen_secret 48)" "JWT 密钥"
set_env_if_missing "COOKIE_SECRET" "$(gen_secret 48)" "Cookie 密钥"
set_env_if_missing "ENCRYPTION_KEY" "$(openssl rand -base64 32)" "敏感数据加密密钥"
set_env_if_missing "ADMIN_PASSWORD" "$(gen_password 16)" "管理员初始密码"
chmod 600 "$ENV_FILE"
}
# ========================== 系统检查 ==========================
check_root() {
if [[ "$EUID" -ne 0 ]]; then
error "请以 root 权限运行此部署脚本!"
error "用法: sudo bash $0"
exit 1
fi
}
check_os() {
if [[ ! -f /etc/os-release ]]; then
error "无法检测操作系统(/etc/os-release 不存在)"
exit 1
fi
source /etc/os-release
OS_ID="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m)
# 仅支持 Ubuntu 和 Debian
if [[ "$OS_ID" != "ubuntu" && "$OS_ID" != "debian" ]]; then
error "不支持的操作系统: ${OS_ID}"
error "本脚本仅支持 Ubuntu 和 Debian 系统"
exit 1
fi
# 架构检查
case "$ARCH" in
amd64|x86_64) ARCH="amd64" ;;
arm64|aarch64) ARCH="arm64" ;;
*)
error "不支持的系统架构: ${ARCH}"
error "仅支持 amd64 (x86_64) 和 arm64 (aarch64)"
exit 1
;;
esac
log "系统检测通过: ${OS_ID} ${OS_VERSION} (${ARCH})"
}
# ========================== 显示横幅 ==========================
show_banner() {
echo -e "${CYAN}" >&2
echo "╔══════════════════════════════════════════════════╗" >&2
echo "║ ║" >&2
echo "║ Incudal 面板一键部署脚本 ║" >&2
echo "║ Docker Compose Deploy ║" >&2
echo "║ ║" >&2
echo "╚══════════════════════════════════════════════════╝" >&2
echo -e "${NC}" >&2
echo -e " 版本: ${BOLD}${SCRIPT_VERSION}${NC} | 仓库: ${DIM}${GITHUB_REPO}${NC}" >&2
echo "" >&2
}
# ========================== 安装 Docker ==========================
install_docker() {
step "安装 Docker..."
if command -v docker &>/dev/null; then
local docker_version
docker_version=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',')
log "Docker ${docker_version} 已安装,跳过"
else
info "正在安装 Docker Engine..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null 2>&1
# 安装前置依赖
apt-get install -y -qq ca-certificates curl gnupg >/dev/null 2>&1
# 添加 Docker 官方 GPG 密钥
install -m 0755 -d /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# 添加 Docker 仓库
echo "deb [arch=${ARCH} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${OS_ID} ${VERSION_CODENAME:-$(. /etc/os-release && echo "$VERSION_CODENAME")} stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq >/dev/null 2>&1
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
# 确保 Docker 启动
systemctl enable docker >/dev/null 2>&1
systemctl start docker
log "Docker 安装完成"
fi
# 检查 Docker Compose
if docker compose version &>/dev/null; then
log "Docker Compose 已可用"
else
error "Docker Compose 不可用,请手动安装 docker-compose-plugin"
exit 1
fi
}
# ========================== 生成面板客户端证书 ==========================
generate_panel_cert() {
local cert_dir="${INSTALL_DIR}/server/certs"
local cert_file="${cert_dir}/client.crt"
local key_file="${cert_dir}/client.key"
step "配置面板客户端证书..."
# 幂等性:证书已存在则跳过
if [[ -f "$cert_file" && -f "$key_file" ]]; then
log "面板客户端证书已存在,跳过生成"
return 0
fi
mkdir -p "$cert_dir"
# 生成自签名客户端证书(用于面板与 Incus API 的 mTLS 通信)
info "生成面板客户端证书(RSA 4096 位,有效期 10 年)..."
openssl req -x509 -newkey rsa:4096 \
-keyout "$key_file" \
-out "$cert_file" \
-days 3650 -nodes \
-subj "/CN=incudal-panel/O=Incudal" \
2>/dev/null
chmod 644 "$cert_file" "$key_file"
log "面板客户端证书生成完成"
}
# ========================== 生成 .env 文件 ==========================
generate_env() {
step "生成环境配置..."
if [[ -f "$ENV_FILE" ]]; then
info ".env 文件已存在,检查并补齐缺失的密钥配置"
ensure_env_keys
return 0
fi
local pg_password
pg_password=$(gen_password 24)
local redis_password
redis_password=$(gen_password 24)
local jwt_secret
jwt_secret=$(gen_secret 48)
local cookie_secret
cookie_secret=$(gen_secret 48)
local encryption_key
encryption_key=$(openssl rand -base64 32)
local admin_password
admin_password=$(gen_password 16)
cat > "$ENV_FILE" << EOF
# ============================================================================
# Incudal Docker 部署环境配置
# 由安装脚本自动生成于 $(date '+%Y-%m-%d %H:%M:%S')
# ============================================================================
# ============ 数据库配置 ============
POSTGRES_USER=incudal
POSTGRES_PASSWORD=${pg_password}
POSTGRES_DB=incudal
# ============ Redis 配置 ============
REDIS_PASSWORD=${redis_password}
# ============ 安全配置(请勿泄露!)============
JWT_SECRET=${jwt_secret}
COOKIE_SECRET=${cookie_secret}
ENCRYPTION_KEY=${encryption_key}
# ============ 应用配置 ============
APP_PORT=${DEFAULT_PORT}
ADMIN_PASSWORD=${admin_password}
LOG_LEVEL=info
DISABLE_REQUEST_LOG=true
# ============ 面板访问地址 ============
# 节点安装脚本、支付回调等功能依赖此地址
FRONTEND_URL=${DETECTED_FRONTEND_URL:-}
EOF
chmod 600 "$ENV_FILE"
log "环境配置文件生成完成: ${ENV_FILE}"
info "管理员密码: ${admin_password}"
info "PostgreSQL 密码: ${pg_password}"
}
# ========================== 生成 docker-compose.yml ==========================
generate_compose() {
step "生成 Docker Compose 配置..."
if [[ -f "$COMPOSE_FILE" ]]; then
info "docker-compose.yml 已存在,跳过生成"
return 0
fi
cat > "$COMPOSE_FILE" << 'COMPOSEFILE'
services:
app:
image: ghcr.io/0xdabiaoge/incudal:latest
ports:
- "${APP_PORT:-3000}:3000"
environment:
- NODE_ENV=production
- HOST=0.0.0.0
- PORT=3000
- DATABASE_URL=postgresql://${POSTGRES_USER:-incudal}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-incudal}
- REDIS_URL=redis://:${REDIS_PASSWORD:-}@redis:6379
- JWT_SECRET=${JWT_SECRET}
- COOKIE_SECRET=${COOKIE_SECRET:-}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- FRONTEND_URL=${FRONTEND_URL:-}
- SITE_URL=${SITE_URL:-}
- LOG_LEVEL=${LOG_LEVEL:-info}
- DISABLE_REQUEST_LOG=${DISABLE_REQUEST_LOG:-true}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
volumes:
- ./server/certs:/app/server/certs:ro
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER:-incudal}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB:-incudal}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-incudal}"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD:-}
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
default:
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: "172.31.0.0/16"
- subnet: "fd42:dead:beef:10::/64"
COMPOSEFILE
log "Docker Compose 配置生成完成: ${COMPOSE_FILE}"
}
# ========================== 拉取镜像并启动 ==========================
start_docker() {
step "拉取镜像并启动服务..."
cd "$INSTALL_DIR"
info "拉取最新镜像..."
docker compose pull 2>&1 | tail -5
info "启动容器..."
docker compose up -d 2>&1
# 等待服务就绪
info "等待服务启动..."
local retries=0
local max_retries=30
while [[ $retries -lt $max_retries ]]; do
if curl -sf "http://127.0.0.1:${DEFAULT_PORT}/api/health" &>/dev/null 2>&1; then
log "服务启动成功!"
return 0
fi
retries=$((retries + 1))
sleep 2
done
warn "服务尚未就绪,请查看日志确认状态:"
warn " docker compose -f ${COMPOSE_FILE} logs -f app"
}
# ========================== Nginx + Certbot ==========================
setup_nginx_certbot() {
info "准备配置 Nginx 反代及 Let's Encrypt SSL 自动证书"
echo -ne " ${BOLD}请输入你要绑定的域名 (例如 panel.yourdomain.com): ${NC}"
read -r DOMAIN
if [[ -z "$DOMAIN" ]]; then
error "域名不能为空!"
return 1
fi
echo -ne " ${BOLD}请输入你的邮箱 (用于证书过期通知,可留空): ${NC}"
read -r EMAIL
info "安装 Nginx 与 Certbot..."
apt-get install -y -qq nginx certbot python3-certbot-nginx >/dev/null 2>&1
# 更新 FRONTEND_URL
sed -i "s|^FRONTEND_URL=.*|FRONTEND_URL=https://${DOMAIN}|" "$ENV_FILE"
log "配置 Nginx 站点..."
cat > /etc/nginx/sites-available/incudal.conf <<NGINX
server {
listen 80;
listen [::]:80;
server_name ${DOMAIN};
# 安全头
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
location / {
proxy_pass http://127.0.0.1:${DEFAULT_PORT};
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_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
# WebSocket 超时
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
NGINX
# 启用站点
ln -sf /etc/nginx/sites-available/incudal.conf /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
# 测试并重载 Nginx
nginx -t >/dev/null 2>&1
systemctl restart nginx
# 申请 SSL 证书
info "申请 Let's Encrypt SSL 证书..."
if [[ -n "$EMAIL" ]]; then
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos -m "$EMAIL" --redirect
else
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --register-unsafely-without-email --redirect
fi
log "Nginx + HTTPS 配置完成"
info "面板地址: https://${DOMAIN}"
# 重启容器使新 FRONTEND_URL 生效
cd "$INSTALL_DIR"
docker compose up -d --force-recreate app 2>/dev/null || true
}
# ========================== Cloudflare Tunnel ==========================
setup_cf_tunnel() {
info "请前往 Cloudflare Zero Trust 控制台创建 Tunnel"
info "配置 Tunnel 时,将 Public Hostname 指向 http://127.0.0.1:${DEFAULT_PORT}"
echo ""
echo -ne " ${BOLD}请输入你绑定的域名 (用于更新 FRONTEND_URL): ${NC}"
read -r CF_DOMAIN
if [[ -n "$CF_DOMAIN" ]]; then
sed -i "s|^FRONTEND_URL=.*|FRONTEND_URL=https://${CF_DOMAIN}|" "$ENV_FILE"
log "已更新 FRONTEND_URL 为 https://${CF_DOMAIN}"
# 重启容器以应用新环境变量
cd "$INSTALL_DIR"
docker compose up -d 2>/dev/null
fi
}
# ========================== 显示结果 ==========================
show_result() {
# 从 .env 中提取信息
local admin_pass
admin_pass=$(grep '^ADMIN_PASSWORD=' "$ENV_FILE" | cut -d'=' -f2-)
local frontend_url
frontend_url=$(grep '^FRONTEND_URL=' "$ENV_FILE" | cut -d'=' -f2-)
local app_port
app_port=$(grep '^APP_PORT=' "$ENV_FILE" | cut -d'=' -f2- || echo "$DEFAULT_PORT")
echo "" >&2
echo -e "${GREEN}" >&2
echo "╔══════════════════════════════════════════════════╗" >&2
echo "║ ║" >&2
echo "║ ✅ Incudal 部署完成! ║" >&2
echo "║ ║" >&2
echo "╚══════════════════════════════════════════════════╝" >&2
echo -e "${NC}" >&2
divider
echo -e " ${BOLD}部署模式${NC} Docker Compose" >&2
echo -e " ${BOLD}访问地址${NC} ${frontend_url:-http://服务器IP:${app_port}}" >&2
echo -e " ${BOLD}管理员账号${NC} admin" >&2
echo -e " ${BOLD}管理员密码${NC} ${admin_pass}" >&2
divider
echo "" >&2
echo -e " ${YELLOW}常用运维命令:${NC}" >&2
echo -e " ${DIM}查看日志${NC} docker compose -f ${COMPOSE_FILE} logs -f app" >&2
echo -e " ${DIM}重启服务${NC} docker compose -f ${COMPOSE_FILE} restart" >&2
echo -e " ${DIM}停止服务${NC} docker compose -f ${COMPOSE_FILE} down" >&2
echo -e " ${DIM}更新镜像${NC} sudo bash $0 --upgrade" >&2
divider
echo "" >&2
if [[ -z "$frontend_url" ]]; then
warn "FRONTEND_URL 尚未配置,面板部分功能(如节点注册)将不可用"
warn "请编辑 ${ENV_FILE} 设置 FRONTEND_URL 字段"
fi
}
# ========================== 升级 ==========================
do_upgrade() {
show_banner
check_os
if [[ ! -f "$COMPOSE_FILE" ]]; then
error "未检测到 Docker 部署,无法升级"
error "请先运行安装: sudo bash $0"
exit 1
fi
step "升级 Incudal..."
cd "$INSTALL_DIR"
info "拉取最新镜像..."
docker compose pull 2>&1 | tail -5
info "重建容器(使用新镜像)..."
docker compose up -d --force-recreate app 2>&1
# 等待服务就绪
info "等待服务启动..."
local retries=0
while [[ $retries -lt 20 ]]; do
if curl -sf "http://127.0.0.1:${DEFAULT_PORT}/api/health" &>/dev/null 2>&1; then
break
fi
retries=$((retries + 1))
sleep 2
done
log "升级完成!"
info "当前镜像: $(docker compose images app --format '{{.Repository}}:{{.Tag}}' 2>/dev/null || echo 'unknown')"
}
# ========================== 卸载 ==========================
do_uninstall() {
show_banner
echo -e " ${RED}${BOLD}⚠ 警告:卸载将删除所有 Incudal 数据!${NC}" >&2
echo "" >&2
echo -ne " ${BOLD}确认卸载?输入 'yes' 继续: ${NC}"
read -r confirm
if [[ "$confirm" != "yes" ]]; then
info "已取消卸载"
exit 0
fi
step "卸载 Incudal..."
if [[ -f "$COMPOSE_FILE" ]]; then
cd "$INSTALL_DIR"
info "停止并移除容器..."
docker compose down -v 2>/dev/null || true
fi
# 移除 Nginx 配置
if [[ -f /etc/nginx/sites-enabled/incudal.conf ]]; then
info "移除 Nginx 配置..."
rm -f /etc/nginx/sites-enabled/incudal.conf
rm -f /etc/nginx/sites-available/incudal.conf
systemctl reload nginx 2>/dev/null || true
fi
# 删除安装目录
if [[ -d "$INSTALL_DIR" ]]; then
info "删除安装目录 ${INSTALL_DIR}..."
rm -rf "$INSTALL_DIR"
fi
log "卸载完成"
info "Docker Engine 未被移除,如需卸载请手动执行: apt-get remove docker-ce"
}
# ========================== 安装 ==========================
do_install() {
show_banner
check_os
# 检查是否已安装
if [[ -f "$COMPOSE_FILE" ]]; then
warn "检测到已有安装 (${INSTALL_DIR})"
echo -ne " ${BOLD}覆盖安装?[y/N]: ${NC}"
read -r overwrite
if [[ "${overwrite,,}" != "y" ]]; then
info "已取消安装。如需升级请使用: sudo bash $0 --upgrade"
exit 0
fi
fi
# 更新系统包
step "更新系统包索引..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null 2>&1
# 安装基础工具
apt-get install -y -qq curl openssl >/dev/null 2>&1
# 安装 Docker
install_docker
# 创建安装目录
mkdir -p "${INSTALL_DIR}/server/certs"
# 生成面板客户端证书
generate_panel_cert
# 生成 .env
generate_env
# 生成 docker-compose.yml
generate_compose
# 登录 GHCR(私有仓库需要)
step "配置镜像仓库访问..."
if docker pull "${DOCKER_IMAGE}:latest" &>/dev/null 2>&1; then
log "镜像仓库可访问"
else
warn "无法拉取镜像(仓库可能是私有的)"
info "如果仓库是私有的,请先登录:"
info " echo 'YOUR_GITHUB_TOKEN' | docker login ghcr.io -u USERNAME --password-stdin"
echo ""
echo -ne " ${BOLD}是否已登录或仓库为公开?继续安装?[y/N]: ${NC}"
read -r cont
if [[ "${cont,,}" != "y" ]]; then
info "请登录后重新运行安装脚本"
exit 0
fi
fi
# ---- 先选择网络方案,确保 FRONTEND_URL 在容器启动前就已写入 .env ----
echo ""
divider
echo -e " ${BOLD}请选择外部访问方案:${NC}" >&2
divider
echo -e " ${CYAN}[1]${NC} Nginx + Certbot ${YELLOW}(推荐:自动 HTTPS,需要公网 IP 和域名)${NC}" >&2
echo -e " ${CYAN}[2]${NC} Cloudflare Tunnel ${YELLOW}(适合无公网 IP 或隐藏源站 IP${NC}" >&2
echo -e " ${CYAN}[3]${NC} 仅启动服务 ${DIM}(手动配置反代,稍后输入面板地址)${NC}" >&2
echo ""
echo -ne " ${BOLD}请选择 [1-3]: ${NC}"
read -r net_opt
# 方案 3 或无效选项:需要在启动前确定面板访问地址
case "${net_opt:-3}" in
1|2)
;; # Nginx/CF 方案会在后续步骤中自动写入 FRONTEND_URL
*)
# 自动探测面板公网地址,用于写入 FRONTEND_URL
local detected_ip=""
detected_ip=$(curl -4sf --connect-timeout 5 https://api.ipify.org 2>/dev/null || \
curl -4sf --connect-timeout 5 https://ifconfig.me 2>/dev/null || \
curl -6sf --connect-timeout 5 https://api6.ipify.org 2>/dev/null || echo "")
echo ""
if [[ -n "$detected_ip" ]]; then
# 检测到公网 IP,构建默认地址
local default_url="http://${detected_ip}:${DEFAULT_PORT}"
info "检测到公网 IP: ${detected_ip}"
echo -ne " ${BOLD}请输入面板访问地址 [默认 ${default_url}]: ${NC}"
read -r manual_url
manual_url=${manual_url:-${default_url}}
else
echo -ne " ${BOLD}请输入面板访问地址 (例如 https://panel.example.com 或 http://IP:${DEFAULT_PORT}): ${NC}"
read -r manual_url
fi
if [[ -n "$manual_url" ]]; then
# 去掉末尾斜杠
manual_url="${manual_url%/}"
sed -i "s|^FRONTEND_URL=.*|FRONTEND_URL=${manual_url}|" "$ENV_FILE"
log "面板访问地址已设置: ${manual_url}"
else
warn "未设置面板访问地址,节点注册功能将不可用"
warn "请后续编辑 ${ENV_FILE} 手动设置 FRONTEND_URL"
fi
;;
esac
# 启动服务(此时 .env 中 FRONTEND_URL 已有值)
start_docker
# 方案 1/2 在服务启动后配置外部访问并更新 FRONTEND_URL
case "${net_opt:-3}" in
1) setup_nginx_certbot ;;
2) setup_cf_tunnel ;;
esac
# 显示结果
show_result
}
# ========================== 主入口 ==========================
main() {
check_root
case "${1:-}" in
--upgrade|-u)
do_upgrade
;;
--uninstall|--remove)
do_uninstall
;;
--help|-h)
echo "Incudal 面板部署脚本(Docker 模式)v${SCRIPT_VERSION}"
echo ""
echo "用法: sudo bash $0 [选项]"
echo ""
echo "选项:"
echo " (无参数) 全新安装"
echo " --upgrade 拉取最新镜像并重建"
echo " --uninstall 卸载 Incudal(包含数据)"
echo " --help 显示帮助"
;;
*)
do_install
;;
esac
}
main "$@"