deploy.sh 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. #!/usr/bin/env bash
  2. set -e
  3. # Colors for output
  4. RED='\033[0;31m'
  5. GREEN='\033[0;32m'
  6. YELLOW='\033[1;33m'
  7. BLUE='\033[0;34m'
  8. NC='\033[0m' # No Color
  9. # Script version
  10. VERSION="1.1.0"
  11. # Logging functions (defined early for use in parse_args)
  12. log_info() {
  13. echo -e "${BLUE}[INFO]${NC} $1"
  14. }
  15. log_success() {
  16. echo -e "${GREEN}[SUCCESS]${NC} $1"
  17. }
  18. log_warning() {
  19. echo -e "${YELLOW}[WARNING]${NC} $1"
  20. }
  21. log_error() {
  22. echo -e "${RED}[ERROR]${NC} $1"
  23. }
  24. # Global variables
  25. SUFFIX=""
  26. ADMIN_TOKEN=""
  27. DB_PASSWORD=""
  28. DEPLOY_DIR=""
  29. OS_TYPE=""
  30. IMAGE_TAG="latest"
  31. BRANCH_NAME="main"
  32. APP_PORT="23000"
  33. UPDATE_MODE=false
  34. FORCE_NEW=false
  35. # CLI argument variables
  36. BRANCH_ARG=""
  37. PORT_ARG=""
  38. TOKEN_ARG=""
  39. DIR_ARG=""
  40. DOMAIN_ARG=""
  41. ENABLE_CADDY=false
  42. NON_INTERACTIVE=false
  43. show_help() {
  44. cat << EOF
  45. Claude Code Hub - One-Click Deployment Script v${VERSION}
  46. Usage: $0 [OPTIONS]
  47. Options:
  48. -b, --branch <name> Branch to deploy: main (default) or dev
  49. -p, --port <port> App external port (default: 23000)
  50. -t, --admin-token <token> Custom admin token (default: auto-generated)
  51. -d, --deploy-dir <path> Custom deployment directory
  52. --domain <domain> Domain for Caddy HTTPS (enables Caddy automatically)
  53. --enable-caddy Enable Caddy reverse proxy without HTTPS (HTTP only)
  54. --force-new Force fresh installation (ignore existing deployment)
  55. -y, --yes Non-interactive mode (skip prompts, use defaults)
  56. -h, --help Show this help message
  57. Examples:
  58. $0 # Interactive deployment
  59. $0 -y # Non-interactive with defaults
  60. $0 -b dev -p 8080 -y # Deploy dev branch on port 8080
  61. $0 -t "my-secure-token" -y # Use custom admin token
  62. $0 --domain hub.example.com -y # Deploy with Caddy HTTPS
  63. $0 --enable-caddy -y # Deploy with Caddy HTTP-only
  64. $0 -y # Update existing deployment (auto-detected)
  65. $0 --force-new -y # Force fresh install even if deployment exists
  66. For more information, visit: https://github.com/ding113/claude-code-hub
  67. EOF
  68. }
  69. parse_args() {
  70. while [[ $# -gt 0 ]]; do
  71. case $1 in
  72. -b|--branch)
  73. if [[ -z "${2:-}" ]] || [[ "$2" == -* ]]; then
  74. log_error "Option $1 requires an argument"
  75. exit 1
  76. fi
  77. BRANCH_ARG="$2"
  78. shift 2
  79. ;;
  80. -p|--port)
  81. if [[ -z "${2:-}" ]] || [[ "$2" == -* ]]; then
  82. log_error "Option $1 requires an argument"
  83. exit 1
  84. fi
  85. PORT_ARG="$2"
  86. shift 2
  87. ;;
  88. -t|--admin-token)
  89. if [[ -z "${2:-}" ]] || [[ "$2" == -* ]]; then
  90. log_error "Option $1 requires an argument"
  91. exit 1
  92. fi
  93. TOKEN_ARG="$2"
  94. shift 2
  95. ;;
  96. -d|--deploy-dir)
  97. if [[ -z "${2:-}" ]] || [[ "$2" == -* ]]; then
  98. log_error "Option $1 requires an argument"
  99. exit 1
  100. fi
  101. DIR_ARG="$2"
  102. shift 2
  103. ;;
  104. --domain)
  105. if [[ -z "${2:-}" ]] || [[ "$2" == -* ]]; then
  106. log_error "Option $1 requires an argument"
  107. exit 1
  108. fi
  109. DOMAIN_ARG="$2"
  110. ENABLE_CADDY=true
  111. shift 2
  112. ;;
  113. --enable-caddy)
  114. ENABLE_CADDY=true
  115. shift
  116. ;;
  117. --force-new)
  118. FORCE_NEW=true
  119. shift
  120. ;;
  121. -y|--yes)
  122. NON_INTERACTIVE=true
  123. shift
  124. ;;
  125. -h|--help)
  126. show_help
  127. exit 0
  128. ;;
  129. *)
  130. log_error "Unknown option: $1"
  131. echo ""
  132. show_help
  133. exit 1
  134. ;;
  135. esac
  136. done
  137. }
  138. validate_inputs() {
  139. # Validate port
  140. if [[ -n "$PORT_ARG" ]]; then
  141. if ! [[ "$PORT_ARG" =~ ^[0-9]+$ ]] || [[ "$PORT_ARG" -lt 1 ]] || [[ "$PORT_ARG" -gt 65535 ]]; then
  142. log_error "Invalid port number: $PORT_ARG (must be 1-65535)"
  143. exit 1
  144. fi
  145. APP_PORT="$PORT_ARG"
  146. fi
  147. # Validate admin token length
  148. if [[ -n "$TOKEN_ARG" ]]; then
  149. if [[ ${#TOKEN_ARG} -lt 16 ]]; then
  150. log_error "Admin token too short: minimum 16 characters required"
  151. exit 1
  152. fi
  153. ADMIN_TOKEN="$TOKEN_ARG"
  154. fi
  155. # Validate branch
  156. if [[ -n "$BRANCH_ARG" ]]; then
  157. case "$BRANCH_ARG" in
  158. main)
  159. IMAGE_TAG="latest"
  160. BRANCH_NAME="main"
  161. ;;
  162. dev)
  163. IMAGE_TAG="dev"
  164. BRANCH_NAME="dev"
  165. ;;
  166. *)
  167. log_error "Invalid branch: $BRANCH_ARG (must be 'main' or 'dev')"
  168. exit 1
  169. ;;
  170. esac
  171. fi
  172. # Apply custom deploy directory
  173. if [[ -n "$DIR_ARG" ]]; then
  174. DEPLOY_DIR="$DIR_ARG"
  175. fi
  176. # Validate domain format if provided
  177. if [[ -n "$DOMAIN_ARG" ]]; then
  178. if ! [[ "$DOMAIN_ARG" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
  179. log_error "Invalid domain format: $DOMAIN_ARG"
  180. exit 1
  181. fi
  182. fi
  183. }
  184. print_header() {
  185. echo -e "${BLUE}"
  186. echo "+=================================================================+"
  187. echo "| |"
  188. echo "| Claude Code Hub - One-Click Deployment |"
  189. echo "| Version ${VERSION} |"
  190. echo "| |"
  191. echo "+=================================================================+"
  192. echo -e "${NC}"
  193. }
  194. detect_os() {
  195. if [[ "$OSTYPE" == "linux-gnu"* ]]; then
  196. OS_TYPE="linux"
  197. DEPLOY_DIR="/www/compose/claude-code-hub"
  198. elif [[ "$OSTYPE" == "darwin"* ]]; then
  199. OS_TYPE="macos"
  200. DEPLOY_DIR="$HOME/Applications/claude-code-hub"
  201. else
  202. log_error "Unsupported operating system: $OSTYPE"
  203. exit 1
  204. fi
  205. log_info "Detected OS: $OS_TYPE"
  206. }
  207. select_branch() {
  208. # Skip if branch already set via CLI or non-interactive mode
  209. if [[ -n "$BRANCH_ARG" ]]; then
  210. log_info "Using branch from CLI argument: $BRANCH_NAME"
  211. return
  212. fi
  213. if [[ "$NON_INTERACTIVE" == true ]]; then
  214. log_info "Non-interactive mode: using default branch (main)"
  215. return
  216. fi
  217. echo ""
  218. echo -e "${BLUE}Please select the branch to deploy:${NC}"
  219. echo -e " ${GREEN}1)${NC} main (Stable release - recommended for production)"
  220. echo -e " ${YELLOW}2)${NC} dev (Latest features - for testing)"
  221. echo ""
  222. local choice
  223. while true; do
  224. read -p "Enter your choice [1]: " choice
  225. choice=${choice:-1}
  226. case $choice in
  227. 1)
  228. IMAGE_TAG="latest"
  229. BRANCH_NAME="main"
  230. log_success "Selected branch: main (image tag: latest)"
  231. break
  232. ;;
  233. 2)
  234. IMAGE_TAG="dev"
  235. BRANCH_NAME="dev"
  236. log_success "Selected branch: dev (image tag: dev)"
  237. break
  238. ;;
  239. *)
  240. log_error "Invalid choice. Please enter 1 or 2."
  241. ;;
  242. esac
  243. done
  244. }
  245. check_docker() {
  246. log_info "Checking Docker installation..."
  247. if ! command -v docker &> /dev/null; then
  248. log_warning "Docker is not installed"
  249. return 1
  250. fi
  251. if ! docker compose version &> /dev/null && ! docker-compose --version &> /dev/null; then
  252. log_warning "Docker Compose is not installed"
  253. return 1
  254. fi
  255. log_success "Docker and Docker Compose are installed"
  256. docker --version
  257. docker compose version 2>/dev/null || docker-compose --version
  258. return 0
  259. }
  260. install_docker() {
  261. log_info "Installing Docker..."
  262. if [[ "$OS_TYPE" == "linux" ]]; then
  263. if [[ $EUID -ne 0 ]]; then
  264. log_error "Docker installation requires root privileges on Linux"
  265. log_info "Please run: sudo $0"
  266. exit 1
  267. fi
  268. fi
  269. log_info "Downloading Docker installation script from get.docker.com..."
  270. local temp_script
  271. temp_script=$(mktemp)
  272. if curl -fsSL https://get.docker.com -o "$temp_script"; then
  273. log_info "Running Docker installation script..."
  274. sh "$temp_script"
  275. rm -f "$temp_script"
  276. if [[ "$OS_TYPE" == "linux" ]]; then
  277. log_info "Starting Docker service..."
  278. systemctl start docker
  279. systemctl enable docker
  280. if [[ -n "$SUDO_USER" ]]; then
  281. log_info "Adding user $SUDO_USER to docker group..."
  282. usermod -aG docker "$SUDO_USER"
  283. log_warning "Please log out and log back in for group changes to take effect"
  284. fi
  285. fi
  286. log_success "Docker installed successfully"
  287. else
  288. log_error "Failed to download Docker installation script"
  289. exit 1
  290. fi
  291. }
  292. generate_random_suffix() {
  293. SUFFIX=$(tr -dc 'a-z0-9' < /dev/urandom | head -c 4)
  294. log_info "Generated random suffix: $SUFFIX"
  295. }
  296. generate_admin_token() {
  297. # Skip if token already set via CLI
  298. if [[ -n "$ADMIN_TOKEN" ]]; then
  299. log_info "Using admin token from CLI argument"
  300. return
  301. fi
  302. if command -v openssl &> /dev/null; then
  303. ADMIN_TOKEN=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
  304. else
  305. ADMIN_TOKEN=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)
  306. fi
  307. log_info "Generated secure admin token"
  308. }
  309. generate_db_password() {
  310. if command -v openssl &> /dev/null; then
  311. DB_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
  312. else
  313. DB_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 24)
  314. fi
  315. log_info "Generated secure database password"
  316. }
  317. detect_existing_deployment() {
  318. if [[ "$FORCE_NEW" == true ]]; then
  319. log_info "Force-new flag set, skipping existing deployment detection"
  320. return 1
  321. fi
  322. if [[ -f "$DEPLOY_DIR/.env" ]] && [[ -f "$DEPLOY_DIR/docker-compose.yaml" ]]; then
  323. log_info "Detected existing deployment in $DEPLOY_DIR"
  324. UPDATE_MODE=true
  325. return 0
  326. fi
  327. return 1
  328. }
  329. extract_suffix_from_compose() {
  330. local compose_file="$DEPLOY_DIR/docker-compose.yaml"
  331. SUFFIX=$(sed -n 's/.*container_name: claude-code-hub-db-\([a-z0-9]*\)/\1/p' "$compose_file" | head -1)
  332. if [[ -z "$SUFFIX" ]]; then
  333. log_warning "Could not extract suffix from docker-compose.yaml, generating new one"
  334. generate_random_suffix
  335. return
  336. fi
  337. log_info "Using existing suffix: $SUFFIX"
  338. }
  339. load_existing_env() {
  340. local env_file="$DEPLOY_DIR/.env"
  341. # Load DB_PASSWORD
  342. local existing_db_pw
  343. existing_db_pw=$(grep '^DB_PASSWORD=' "$env_file" | head -1 | cut -d'=' -f2-)
  344. if [[ -n "$existing_db_pw" ]]; then
  345. DB_PASSWORD="$existing_db_pw"
  346. log_info "Preserved existing database password"
  347. else
  348. log_warning "DB_PASSWORD not found in existing .env, generating new one"
  349. generate_db_password
  350. fi
  351. # Load ADMIN_TOKEN (CLI argument takes priority)
  352. if [[ -z "$ADMIN_TOKEN" ]]; then
  353. local existing_token
  354. existing_token=$(grep '^ADMIN_TOKEN=' "$env_file" | head -1 | cut -d'=' -f2-)
  355. if [[ -n "$existing_token" ]]; then
  356. ADMIN_TOKEN="$existing_token"
  357. log_info "Preserved existing admin token"
  358. else
  359. log_warning "ADMIN_TOKEN not found in existing .env, generating new one"
  360. generate_admin_token
  361. fi
  362. fi
  363. # Load APP_PORT (CLI argument takes priority)
  364. if [[ -z "$PORT_ARG" ]]; then
  365. local existing_port
  366. existing_port=$(grep '^APP_PORT=' "$env_file" | head -1 | cut -d'=' -f2-)
  367. if [[ -n "$existing_port" ]]; then
  368. APP_PORT="$existing_port"
  369. fi
  370. fi
  371. }
  372. create_deployment_dir() {
  373. log_info "Creating deployment directory: $DEPLOY_DIR"
  374. if [[ "$OS_TYPE" == "linux" ]] && [[ ! -d "/www" ]]; then
  375. if [[ $EUID -ne 0 ]]; then
  376. log_error "Creating /www directory requires root privileges"
  377. log_info "Please run: sudo $0"
  378. exit 1
  379. fi
  380. mkdir -p "$DEPLOY_DIR"
  381. if [[ -n "$SUDO_USER" ]]; then
  382. chown -R "$SUDO_USER:$SUDO_USER" /www
  383. fi
  384. else
  385. mkdir -p "$DEPLOY_DIR"
  386. fi
  387. mkdir -p "$DEPLOY_DIR/data/postgres"
  388. mkdir -p "$DEPLOY_DIR/data/redis"
  389. log_success "Deployment directory created"
  390. }
  391. write_compose_file() {
  392. log_info "Writing docker-compose.yaml..."
  393. # Determine app ports configuration
  394. local app_ports_config
  395. if [[ "$ENABLE_CADDY" == true ]]; then
  396. # When Caddy is enabled, don't expose app port externally
  397. app_ports_config=""
  398. else
  399. app_ports_config="ports:
  400. - \"\${APP_PORT:-${APP_PORT}}:\${APP_PORT:-${APP_PORT}}\""
  401. fi
  402. cat > "$DEPLOY_DIR/docker-compose.yaml" << EOF
  403. services:
  404. postgres:
  405. image: postgres:18
  406. container_name: claude-code-hub-db-${SUFFIX}
  407. restart: unless-stopped
  408. ports:
  409. - "127.0.0.1:35432:5432"
  410. env_file:
  411. - ./.env
  412. environment:
  413. POSTGRES_USER: \${DB_USER:-postgres}
  414. POSTGRES_PASSWORD: \${DB_PASSWORD:-postgres}
  415. POSTGRES_DB: \${DB_NAME:-claude_code_hub}
  416. PGDATA: /data/pgdata
  417. TZ: Asia/Shanghai
  418. PGTZ: Asia/Shanghai
  419. volumes:
  420. - ./data/postgres:/data
  421. networks:
  422. - claude-code-hub-net-${SUFFIX}
  423. healthcheck:
  424. test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-postgres} -d \${DB_NAME:-claude_code_hub}"]
  425. interval: 5s
  426. timeout: 5s
  427. retries: 10
  428. start_period: 10s
  429. redis:
  430. image: redis:7-alpine
  431. container_name: claude-code-hub-redis-${SUFFIX}
  432. restart: unless-stopped
  433. volumes:
  434. - ./data/redis:/data
  435. command: redis-server --appendonly yes
  436. networks:
  437. - claude-code-hub-net-${SUFFIX}
  438. healthcheck:
  439. test: ["CMD", "redis-cli", "ping"]
  440. interval: 5s
  441. timeout: 3s
  442. retries: 5
  443. start_period: 5s
  444. app:
  445. image: ghcr.io/ding113/claude-code-hub:${IMAGE_TAG}
  446. container_name: claude-code-hub-app-${SUFFIX}
  447. depends_on:
  448. postgres:
  449. condition: service_healthy
  450. redis:
  451. condition: service_healthy
  452. env_file:
  453. - ./.env
  454. environment:
  455. NODE_ENV: production
  456. PORT: \${APP_PORT:-${APP_PORT}}
  457. DSN: postgresql://\${DB_USER:-postgres}:\${DB_PASSWORD:-postgres}@claude-code-hub-db-${SUFFIX}:5432/\${DB_NAME:-claude_code_hub}
  458. REDIS_URL: redis://claude-code-hub-redis-${SUFFIX}:6379
  459. AUTO_MIGRATE: \${AUTO_MIGRATE:-true}
  460. ENABLE_RATE_LIMIT: \${ENABLE_RATE_LIMIT:-true}
  461. SESSION_TTL: \${SESSION_TTL:-300}
  462. TZ: Asia/Shanghai
  463. EOF
  464. # Add app ports only if Caddy is not enabled
  465. if [[ "$ENABLE_CADDY" != true ]]; then
  466. cat >> "$DEPLOY_DIR/docker-compose.yaml" << EOF
  467. ports:
  468. - "\${APP_PORT:-${APP_PORT}}:\${APP_PORT:-${APP_PORT}}"
  469. EOF
  470. fi
  471. cat >> "$DEPLOY_DIR/docker-compose.yaml" << EOF
  472. restart: unless-stopped
  473. networks:
  474. - claude-code-hub-net-${SUFFIX}
  475. healthcheck:
  476. test: ["CMD-SHELL", "curl -f http://localhost:\${APP_PORT:-${APP_PORT}}/api/actions/health || exit 1"]
  477. interval: 30s
  478. timeout: 5s
  479. retries: 3
  480. start_period: 30s
  481. EOF
  482. # Add Caddy service if enabled
  483. if [[ "$ENABLE_CADDY" == true ]]; then
  484. cat >> "$DEPLOY_DIR/docker-compose.yaml" << EOF
  485. caddy:
  486. image: caddy:2-alpine
  487. container_name: claude-code-hub-caddy-${SUFFIX}
  488. restart: unless-stopped
  489. ports:
  490. - "80:80"
  491. - "443:443"
  492. volumes:
  493. - ./Caddyfile:/etc/caddy/Caddyfile:ro
  494. - caddy_data:/data
  495. - caddy_config:/config
  496. depends_on:
  497. app:
  498. condition: service_healthy
  499. networks:
  500. - claude-code-hub-net-${SUFFIX}
  501. EOF
  502. fi
  503. cat >> "$DEPLOY_DIR/docker-compose.yaml" << EOF
  504. networks:
  505. claude-code-hub-net-${SUFFIX}:
  506. driver: bridge
  507. name: claude-code-hub-net-${SUFFIX}
  508. EOF
  509. # Add Caddy volumes if enabled
  510. if [[ "$ENABLE_CADDY" == true ]]; then
  511. cat >> "$DEPLOY_DIR/docker-compose.yaml" << EOF
  512. volumes:
  513. caddy_data:
  514. caddy_config:
  515. EOF
  516. fi
  517. log_success "docker-compose.yaml created"
  518. }
  519. write_caddyfile() {
  520. if [[ "$ENABLE_CADDY" != true ]]; then
  521. return
  522. fi
  523. log_info "Writing Caddyfile..."
  524. if [[ -n "$DOMAIN_ARG" ]]; then
  525. # HTTPS mode with domain (Let's Encrypt automatic)
  526. cat > "$DEPLOY_DIR/Caddyfile" << EOF
  527. ${DOMAIN_ARG} {
  528. reverse_proxy app:${APP_PORT}
  529. encode gzip
  530. }
  531. EOF
  532. log_success "Caddyfile created (HTTPS mode with domain: $DOMAIN_ARG)"
  533. else
  534. # HTTP-only mode
  535. cat > "$DEPLOY_DIR/Caddyfile" << EOF
  536. :80 {
  537. reverse_proxy app:${APP_PORT}
  538. encode gzip
  539. }
  540. EOF
  541. log_success "Caddyfile created (HTTP-only mode)"
  542. fi
  543. }
  544. write_env_file() {
  545. log_info "Writing .env file..."
  546. # Update mode: backup existing .env, then restore custom variables after writing
  547. local backup_file=""
  548. if [[ "$UPDATE_MODE" == true ]] && [[ -f "$DEPLOY_DIR/.env" ]]; then
  549. backup_file="$DEPLOY_DIR/.env.bak"
  550. cp "$DEPLOY_DIR/.env" "$backup_file"
  551. log_info "Backed up existing .env to .env.bak"
  552. fi
  553. # Determine secure cookies setting based on Caddy and domain
  554. local secure_cookies="true"
  555. if [[ "$ENABLE_CADDY" == true ]] && [[ -z "$DOMAIN_ARG" ]]; then
  556. # HTTP-only Caddy mode - disable secure cookies
  557. secure_cookies="false"
  558. fi
  559. # If domain is set, APP_URL should use https
  560. local app_url=""
  561. if [[ -n "$DOMAIN_ARG" ]]; then
  562. app_url="https://${DOMAIN_ARG}"
  563. fi
  564. cat > "$DEPLOY_DIR/.env" << EOF
  565. # Admin Token (KEEP THIS SECRET!)
  566. ADMIN_TOKEN=${ADMIN_TOKEN}
  567. # Database Configuration
  568. DB_USER=postgres
  569. DB_PASSWORD=${DB_PASSWORD}
  570. DB_NAME=claude_code_hub
  571. # Application Configuration
  572. APP_PORT=${APP_PORT}
  573. APP_URL=${app_url}
  574. # Auto Migration (enabled for first-time setup)
  575. AUTO_MIGRATE=true
  576. # Redis Configuration
  577. ENABLE_RATE_LIMIT=true
  578. # Session Configuration
  579. SESSION_TTL=300
  580. STORE_SESSION_MESSAGES=false
  581. STORE_SESSION_RESPONSE_BODY=true
  582. # Cookie Security
  583. ENABLE_SECURE_COOKIES=${secure_cookies}
  584. # Circuit Breaker Configuration
  585. ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false
  586. ENABLE_ENDPOINT_CIRCUIT_BREAKER=false
  587. # Environment
  588. NODE_ENV=production
  589. TZ=Asia/Shanghai
  590. LOG_LEVEL=info
  591. EOF
  592. # Restore user custom variables from backup (variables not managed by this script)
  593. if [[ -n "$backup_file" ]] && [[ -f "$backup_file" ]]; then
  594. local managed_keys="ADMIN_TOKEN|DB_USER|DB_PASSWORD|DB_NAME|APP_PORT|APP_URL|AUTO_MIGRATE|ENABLE_RATE_LIMIT|SESSION_TTL|STORE_SESSION_MESSAGES|STORE_SESSION_RESPONSE_BODY|ENABLE_SECURE_COOKIES|ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS|ENABLE_ENDPOINT_CIRCUIT_BREAKER|NODE_ENV|TZ|LOG_LEVEL"
  595. local custom_vars
  596. custom_vars=$(grep -v '^\s*#' "$backup_file" | grep -v '^\s*$' | grep -vE "^($managed_keys)=" || true)
  597. if [[ -n "$custom_vars" ]]; then
  598. echo "" >> "$DEPLOY_DIR/.env"
  599. echo "# User Custom Configuration (preserved from previous deployment)" >> "$DEPLOY_DIR/.env"
  600. echo "$custom_vars" >> "$DEPLOY_DIR/.env"
  601. log_info "Preserved $(echo "$custom_vars" | wc -l | tr -d ' ') custom environment variables"
  602. fi
  603. fi
  604. # W-015: restrict .env file permissions to prevent sensitive data leaks
  605. chmod 600 "$DEPLOY_DIR/.env"
  606. log_success ".env file created"
  607. }
  608. start_services() {
  609. log_info "Starting Docker services..."
  610. cd "$DEPLOY_DIR"
  611. if docker compose version &> /dev/null; then
  612. docker compose pull
  613. docker compose up -d
  614. else
  615. docker-compose pull
  616. docker-compose up -d
  617. fi
  618. log_success "Docker services started"
  619. }
  620. wait_for_health() {
  621. log_info "Waiting for services to become healthy (max 60 seconds)..."
  622. cd "$DEPLOY_DIR"
  623. local max_attempts=12
  624. local attempt=0
  625. while [ $attempt -lt $max_attempts ]; do
  626. attempt=$((attempt + 1))
  627. local postgres_health=$(docker inspect --format='{{.State.Health.Status}}' "claude-code-hub-db-${SUFFIX}" 2>/dev/null || echo "unknown")
  628. local redis_health=$(docker inspect --format='{{.State.Health.Status}}' "claude-code-hub-redis-${SUFFIX}" 2>/dev/null || echo "unknown")
  629. local app_health=$(docker inspect --format='{{.State.Health.Status}}' "claude-code-hub-app-${SUFFIX}" 2>/dev/null || echo "unknown")
  630. log_info "Health status - Postgres: $postgres_health, Redis: $redis_health, App: $app_health"
  631. if [[ "$postgres_health" == "healthy" ]] && [[ "$redis_health" == "healthy" ]] && [[ "$app_health" == "healthy" ]]; then
  632. log_success "All services are healthy!"
  633. return 0
  634. fi
  635. if [ $attempt -lt $max_attempts ]; then
  636. sleep 5
  637. fi
  638. done
  639. log_warning "Services did not become healthy within 60 seconds"
  640. log_info "You can check the logs with: cd $DEPLOY_DIR && docker compose logs -f"
  641. return 1
  642. }
  643. get_network_addresses() {
  644. local addresses=()
  645. if [[ "$OS_TYPE" == "linux" ]]; then
  646. if command -v ip &> /dev/null; then
  647. while IFS= read -r line; do
  648. addresses+=("$line")
  649. done < <(ip addr show 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '^127\.' | grep -v '^172\.17\.' | grep -v '^169\.254\.')
  650. elif command -v ifconfig &> /dev/null; then
  651. while IFS= read -r line; do
  652. addresses+=("$line")
  653. done < <(ifconfig 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '^127\.' | grep -v '^172\.17\.' | grep -v '^169\.254\.')
  654. fi
  655. elif [[ "$OS_TYPE" == "macos" ]]; then
  656. while IFS= read -r line; do
  657. addresses+=("$line")
  658. done < <(ifconfig 2>/dev/null | grep 'inet ' | awk '{print $2}' | grep -v '^127\.' | grep -v '^169\.254\.')
  659. fi
  660. addresses+=("localhost")
  661. printf '%s\n' "${addresses[@]}"
  662. }
  663. print_success_message() {
  664. local addresses=($(get_network_addresses))
  665. echo ""
  666. echo -e "${GREEN}+================================================================+${NC}"
  667. echo -e "${GREEN}| |${NC}"
  668. if [[ "$UPDATE_MODE" == true ]]; then
  669. echo -e "${GREEN}| Claude Code Hub Updated Successfully! |${NC}"
  670. else
  671. echo -e "${GREEN}| Claude Code Hub Deployed Successfully! |${NC}"
  672. fi
  673. echo -e "${GREEN}| |${NC}"
  674. echo -e "${GREEN}+================================================================+${NC}"
  675. echo ""
  676. echo -e "${BLUE}Deployment Directory:${NC}"
  677. echo -e " $DEPLOY_DIR"
  678. echo ""
  679. echo -e "${BLUE}Access URLs:${NC}"
  680. if [[ "$ENABLE_CADDY" == true ]]; then
  681. if [[ -n "$DOMAIN_ARG" ]]; then
  682. # HTTPS mode with domain
  683. echo -e " ${GREEN}https://${DOMAIN_ARG}${NC}"
  684. else
  685. # HTTP-only Caddy mode
  686. for addr in "${addresses[@]}"; do
  687. echo -e " ${GREEN}http://${addr}${NC}"
  688. done
  689. fi
  690. else
  691. # Direct app access
  692. for addr in "${addresses[@]}"; do
  693. echo -e " ${GREEN}http://${addr}:${APP_PORT}${NC}"
  694. done
  695. fi
  696. echo ""
  697. # In update mode, skip printing the admin token (user already knows it)
  698. if [[ "$UPDATE_MODE" != true ]]; then
  699. echo -e "${BLUE}Admin Token (KEEP THIS SECRET!):${NC}"
  700. echo -e " ${YELLOW}${ADMIN_TOKEN}${NC}"
  701. echo ""
  702. fi
  703. echo -e "${BLUE}Usage Documentation:${NC}"
  704. if [[ "$ENABLE_CADDY" == true ]] && [[ -n "$DOMAIN_ARG" ]]; then
  705. echo -e " Chinese: ${GREEN}https://${DOMAIN_ARG}/zh-CN/usage-doc${NC}"
  706. echo -e " English: ${GREEN}https://${DOMAIN_ARG}/en-US/usage-doc${NC}"
  707. else
  708. local first_addr="${addresses[0]}"
  709. local port_suffix=""
  710. if [[ "$ENABLE_CADDY" != true ]]; then
  711. port_suffix=":${APP_PORT}"
  712. fi
  713. echo -e " Chinese: ${GREEN}http://${first_addr}${port_suffix}/zh-CN/usage-doc${NC}"
  714. echo -e " English: ${GREEN}http://${first_addr}${port_suffix}/en-US/usage-doc${NC}"
  715. fi
  716. echo ""
  717. echo -e "${BLUE}Useful Commands:${NC}"
  718. echo -e " View logs: ${YELLOW}cd $DEPLOY_DIR && docker compose logs -f${NC}"
  719. echo -e " Stop services: ${YELLOW}cd $DEPLOY_DIR && docker compose down${NC}"
  720. echo -e " Restart: ${YELLOW}cd $DEPLOY_DIR && docker compose restart${NC}"
  721. if [[ "$ENABLE_CADDY" == true ]]; then
  722. echo ""
  723. echo -e "${BLUE}Caddy Configuration:${NC}"
  724. if [[ -n "$DOMAIN_ARG" ]]; then
  725. echo -e " Mode: HTTPS with Let's Encrypt (domain: $DOMAIN_ARG)"
  726. echo -e " Ports: 80 (HTTP redirect), 443 (HTTPS)"
  727. else
  728. echo -e " Mode: HTTP-only reverse proxy"
  729. echo -e " Port: 80"
  730. fi
  731. fi
  732. echo ""
  733. if [[ "$UPDATE_MODE" != true ]]; then
  734. echo -e "${RED}IMPORTANT: Please save the admin token in a secure location!${NC}"
  735. else
  736. echo -e "${BLUE}NOTE: Your existing secrets and custom configuration have been preserved.${NC}"
  737. fi
  738. echo ""
  739. }
  740. main() {
  741. # Parse CLI arguments first
  742. parse_args "$@"
  743. print_header
  744. detect_os
  745. # Apply CLI overrides after OS detection (for deploy dir)
  746. validate_inputs
  747. if ! check_docker; then
  748. log_warning "Docker is not installed. Attempting to install..."
  749. install_docker
  750. if ! check_docker; then
  751. log_error "Docker installation failed. Please install Docker manually."
  752. exit 1
  753. fi
  754. fi
  755. select_branch
  756. # Key branch: detect existing deployment
  757. if detect_existing_deployment; then
  758. log_info "=== UPDATE MODE ==="
  759. log_info "Updating existing deployment (secrets and custom config will be preserved)"
  760. extract_suffix_from_compose
  761. load_existing_env
  762. else
  763. log_info "=== FRESH INSTALL MODE ==="
  764. generate_random_suffix
  765. generate_admin_token
  766. generate_db_password
  767. fi
  768. create_deployment_dir
  769. write_compose_file
  770. write_caddyfile
  771. write_env_file
  772. start_services
  773. if wait_for_health; then
  774. print_success_message
  775. else
  776. if [[ "$UPDATE_MODE" == true ]]; then
  777. log_warning "Update completed but some services may not be fully healthy yet"
  778. else
  779. log_warning "Deployment completed but some services may not be fully healthy yet"
  780. fi
  781. log_info "Please check the logs: cd $DEPLOY_DIR && docker compose logs -f"
  782. print_success_message
  783. fi
  784. }
  785. main "$@"