#!/bin/bash # 이 서비스(웹메이커)를 다른 서버에 설치하고 192.168.20.66 의 DB 를 복사한다. # 복사 대상: data/users.json, data/sites/, data/git/(버전 저장소), data/billing.json, data/fx.json, # data/deploy-targets.json(접속 정보만), settings.json 의 비밀 아닌 값(모델·환율·마진), 사이트 파일(/var/www/sites) # 복사 제외(비밀값 — 새 서버 관리자 콘솔에서 다시 입력): settings.json 의 API 키/OAuth 토큰, session.secret, deploy-keys/(SSH 개인키), credentials.json(외부 연동 접속 정보, v1.3.1) # v1.3.17 (B29·B32): settings.json 은 "비밀이 아닌 항목" 목록(허용 목록)만 옮긴다 — 외부 서비스 키(serviceApiKey)·수집 키(backlogKey)· # 다른 AI 제공자 키(providers, session.secret 로 암호화되어 새 서버에서는 풀 수 없음)는 옮기지 않는다. data/site-keys/(사이트 원본 서버 접속 키)도 제외. # 사용: deploy/remote-install.sh [FQDN|-] [app-port] [sites-dir] # 예: deploy/remote-install.sh bear webmaker.ibit.kr 3080 /var/www/webmaker-sites # deploy/remote-install.sh newbox ← v1.5.11: FQDN 을 모르면 생략(또는 -). 서버 IP 로 설치하고 http://:8795/ 설정 페이지에서 나머지를 정한다 # 전제: 에 비밀번호 없는 SSH 접속과 sudo 가 가능. 대상 서버의 기존 서비스(Caddy/nginx 등)는 건드리지 않고 # FQDN(또는 IP) 전용 사이트 블록만 추가한다. # v1.5.11 (요구 1·2) 설치 중 입력을 받지 않는다. 처음 설치(FRESH=1 또는 FQDN 생략)는 이 PC 의 DB 를 복사하지 않고 빈 상태로 시작하며, # v1.6.31: 고정 관리자 비밀번호는 만들지 않는다 — 설정 페이지(접속 토큰)에서 최초 관리자를 만든다. 설치가 끝나면 웹(설정 페이지)에서 나머지 설정을 잇는다. set -euo pipefail HOST=${1:?ssh host}; FQDN=${2:-}; PORT=${3:-3080}; SITES=${4:-/var/www/webmaker-sites} [ "$FQDN" = "-" ] && FQDN="" SRC=$(cd "$(dirname "$0")/.." && pwd) APPDIR=/home/master/webmaker echo "== 1/7 대상 서버 점검 ($HOST)" ssh -o BatchMode=yes "$HOST" 'set -e; echo "user=$(id -un) host=$(hostname) os=$(. /etc/os-release; echo $PRETTY_NAME)"; sudo -n true && echo "sudo: ok"; command -v node && node --version || echo "node: missing"; command -v caddy && caddy version | head -1 || true; command -v nginx && nginx -v 2>&1 || true; (ss -tlnp 2>/dev/null | grep -E ":(80|443|'"$PORT"') " || true)' # FQDN 이 없으면 서버의 대표 IP(공인 IP 가 있으면 그것, 없으면 첫 사설 IP)를 접속 주소로 쓴다 SERVER_IP=$(ssh "$HOST" "ip -4 -o addr show scope global | awk '{print \$4}' | cut -d/ -f1 | grep -vE '^(172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)' | head -1; ip -4 -o addr show scope global | awk '{print \$4}' | cut -d/ -f1 | head -1" | head -1) if [ -z "$FQDN" ]; then FRESH=${FRESH:-1}; SITE_HOST=$SERVER_IP; PUBLIC_URL="http://$SERVER_IP"; echo " FQDN 없음 → 서버 IP $SERVER_IP 로 설치 (공개 주소 $PUBLIC_URL, 나중에 설정 페이지에서 바꿀 수 있음)"; else FRESH=${FRESH:-0}; SITE_HOST=$FQDN; PUBLIC_URL="https://$FQDN"; fi echo "== 2/7 앱 코드 전송 (main 작업 트리, data 제외)" ssh "$HOST" "mkdir -p $APPDIR" tar -C "$SRC" --exclude=./data --exclude=./.git --exclude=./node_modules -cf - . | ssh "$HOST" "tar -C $APPDIR -xf -" ssh "$HOST" "mkdir -p $APPDIR/data && sudo mkdir -p $SITES && sudo chown \$(id -un) $SITES && chmod 755 $SITES" # 사이트 폴더는 웹서버(www-data)가 읽어야 하므로 755 if [ "$FRESH" = "1" ]; then echo "== 3/7 처음 설치 — DB 를 복사하지 않습니다 (빈 상태 — 최초 관리자는 설정 페이지에서 만듭니다)" else echo "== 3/7 DB 복사 (비밀값 제외)" # v1.5.7: 설정 파일(config.json — 서버마다 다른 주소·연동 정보)과 초기 설정 페이지 토큰(setup.token)도 복사하지 않는다 (새 서버의 유닛·설정 페이지에서 새로 만듦) tar -C "$SRC" --exclude=./data/settings.json --exclude=./data/session.secret --exclude=./data/deploy-keys --exclude=./data/credentials.json --exclude=./data/site-keys --exclude=./data/user-connections.json --exclude=./data/config.json --exclude=./data/setup.token -cf - ./data | ssh "$HOST" "tar -C $APPDIR -xf -" # settings.json: 모델·환율·마진·LE 이메일만 복사, 인증 정보는 비움 python3 - "$SRC/data/settings.json" <<'PY' | ssh "$HOST" "cat > $APPDIR/data/settings.json && chmod 600 $APPDIR/data/settings.json" import json,sys try: s=json.load(open(sys.argv[1])) except Exception: s={} # 허용 목록: 비밀이 아닌 설정만 (v1.3.17) KEEP=['defaultModel','tiers','defaultTier','fx','marginPct','letsEncryptEmail','maxEdits','signupCredit','theme','mailSystem','uiLang','passwordPolicy','tierGuide'] out={k:s[k] for k in KEEP if k in s} m=s.get('mail') or {} out['mail']={k:m[k] for k in ('defaultDomain','fromName','formBase') if k in m} out['auth']={'mode':'server-login','apiKey':'','oauthToken':''} print(json.dumps(out,ensure_ascii=False,indent=2)) PY tar -C /var/www/sites -cf - . | ssh "$HOST" "tar -C $SITES -xf - && chmod -R a+rX $SITES" fi echo "== 4/7 node / claude CLI / 서비스 설치" ssh "$HOST" bash -s "$APPDIR" "$PORT" "$SITES" "$SITE_HOST" "$PUBLIC_URL" "$FRESH" <<'REMOTE' set -e; APPDIR=$1; PORT=$2; SITES=$3; FQDN=$4; PUBLIC_URL=$5; FRESH=$6 if ! command -v node >/dev/null 2>&1; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -n bash - >/dev/null && sudo -n apt-get install -y -q nodejs >/dev/null; fi sudo -n apt-get install -y -q python3-docx git tar >/dev/null 2>&1 || true if ! command -v codex >/dev/null 2>&1 && [ ! -x ~/.npm-global/bin/codex ]; then mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global && npm i -g @openai/codex >/dev/null 2>&1 && echo "codex CLI 설치됨"; fi if ! command -v claude >/dev/null 2>&1 && [ ! -x ~/.npm-global/bin/claude ]; then mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global && npm i -g @anthropic-ai/claude-code >/dev/null 2>&1 && echo "claude CLI 설치됨"; fi sudo -n cp "$APPDIR/deploy/nginx-wmstats.conf" /etc/nginx/conf.d/wmstats.conf 2>/dev/null || true # 접속 통계 로그 형식 sed -e "s#/home/master/work/WebHoting_AI_Agent#$APPDIR#g" -e "s#Environment=PORT=3080#Environment=PORT=$PORT#" -e "s#Environment=SITES_ROOT=/var/www/sites#Environment=SITES_ROOT=$SITES#" -e "s#Environment=PUBLIC_BASE_URL=http://192.168.20.66#Environment=PUBLIC_BASE_URL=$PUBLIC_URL#" -e "/^Environment=CLAUDE_BIN=/d" -e "s#^Environment=PATH=.*#Environment=PATH=$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin#" -e "s#^Environment=SITES_GIT_REMOTE=.*#Environment=SITES_GIT_REMOTE=#" -e "s#^Environment=HOME=.*#Environment=HOME=$HOME#" -e "s#^User=.*#User=$(id -un)#" -e "s#^Group=.*#Group=$(id -gn)#" "$APPDIR/deploy/webhosting-agent.service" | sudo -n tee /etc/systemd/system/webmaker.service >/dev/null sed -i "s#/home/master/work/WebHoting_AI_Agent/hooks/guard.sh#$APPDIR/hooks/guard.sh#" "$APPDIR/hooks/settings.json" sudo -n systemctl daemon-reload && sudo -n systemctl enable --now webmaker >/dev/null && sleep 1 && systemctl is-active webmaker # v1.5.7 (요구 1~3): 유닛의 환경 변수를 설정 파일(data/config.json)로 옮기고, 초기 설정 페이지 서비스(webmaker-setup, 기본 포트 3081)와 재시작 sudoers 규칙을 설치 (cd "$APPDIR" && node scripts/config.js import-env --unit /etc/systemd/system/webmaker.service >/dev/null && node scripts/config.js set service.systemdUnit webmaker >/dev/null) # v1.5.11: 본 서비스 유닛에 DATA_ROOT 가 있으면 설정 페이지 유닛에도 같은 값을 넣는다 (같은 설정 파일·관리자 계정·토큰) DR=$(grep -oP '^Environment=DATA_ROOT=\K.*' /etc/systemd/system/webmaker.service | head -1 || true) sed -e "s#/home/master/work/WebHoting_AI_Agent#$APPDIR#g" -e "s#^Environment=PATH=.*#Environment=PATH=$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin#" -e "s#^Environment=HOME=.*#Environment=HOME=$HOME#" -e "s#^User=.*#User=$(id -un)#" -e "s#^Group=.*#Group=$(id -gn)#" "$APPDIR/deploy/webmaker-setup.service" | { if [ -n "$DR" ]; then sed -e "s#^\# Environment=DATA_ROOT=.*#Environment=DATA_ROOT=$DR#"; else cat; fi; } | sudo -n tee /etc/systemd/system/webmaker-setup.service >/dev/null printf '%s ALL=(root) NOPASSWD: /usr/bin/systemctl restart webmaker, /usr/bin/systemctl restart webmaker-setup\n' "$(id -un)" | sudo -n tee /etc/sudoers.d/webmaker-setup >/dev/null && sudo -n chmod 440 /etc/sudoers.d/webmaker-setup sudo -n systemctl daemon-reload && sudo -n systemctl enable --now webmaker-setup >/dev/null && sudo -n systemctl restart webmaker-setup && echo "webmaker-setup: $(systemctl is-active webmaker-setup)" # v1.5.11 (요구 2): 설정 페이지 포트(기본 8795, 모든 인터페이스)를 방화벽(ufw)이 켜져 있으면 허용 — http://<서버 IP>:8795/ 가 기본 접속 위치. 접속은 토큰·관리자 로그인·시도 제한으로 보호 SP=$(cd "$APPDIR" && node scripts/config.js get service.setupPort); SP=${SP:-8795} if command -v ufw >/dev/null 2>&1 && sudo -n ufw status 2>/dev/null | grep -q '^Status: active'; then sudo -n ufw allow "$SP"/tcp comment 'AI Webmaker setup page' >/dev/null; fi # v1.6.31 (요구 6): 처음 설치해도 고정 비밀번호 관리자를 만들지 않는다 — 최초 관리자는 설정 페이지(접속 토큰, 1회)에서 운영자가 아이디·비밀번호를 정해 만든다 if [ "$FRESH" = "1" ]; then (cd "$APPDIR" && DATA_ROOT="$APPDIR/data" node scripts/admin.js bootstrap); fi echo "== 5/7 웹서버에 $FQDN 사이트 추가 (기존 서비스 유지, 오류는 일반 안내 페이지)" # FQDN 이 없으면 서버 IP 가 server_name ERR404='페이지를 찾을 수 없습니다

페이지를 찾을 수 없습니다

요청하신 페이지가 존재하지 않거나 이동되었습니다.

' # v1.5.27 (요구 5): 발행 취소 안내 페이지 — 영어 제목 + 한·일·중·서 안내 + "supported by <도메인>" (lib/maintpage.js 와 같은 문구; 도메인은 FQDN 의 상위 도메인) WM_DOM=$([ -n "$FQDN" ] && echo "$FQDN" | awk -F. '{ if (NF>2) { for (i=2;i<=NF;i++) printf "%s%s", $i, (iUpdating

We're updating our site. Please check back soon.

사이트 개정 작업 중입니다. 잠시 후에 다시 접속해주세요.

サイトの改訂作業中です。しばらくしてから再度アクセスしてください。

网站正在改版中,请稍后再访问。

El sitio está en proceso de revisión. Vuelva a intentarlo en unos minutos.

Webmaker is working .... supported by __WM_DOMAIN__

' | sed "s/__WM_DOMAIN__/$WM_DOM/g") ERR50X='일시적인 오류가 발생했습니다

일시적인 오류가 발생했습니다

잠시 후 다시 시도해 주세요.

' if command -v caddy >/dev/null 2>&1 && [ -f /etc/caddy/Caddyfile ]; then sudo -n mkdir -p /etc/caddy/sites # 공인 80/443 이 프런트 프록시를 거쳐 오므로 http:// 로 선언해 Caddy 의 자동 인증서 발급을 끈다(인증서는 프런트에서) # v1.3.17 (B05·B06): 생성 사이트(/<32자리ID>/)는 Caddy 가 파일을 직접 내보낸다 — 예전에는 모든 경로를 앱으로 넘겨 사이트 주소가 404 였다. # 발행 취소 표식(.unpublished)이 있으면 nginx 설정과 같이 "사이트 수정 중" 안내(503). 숨김 파일(.으로 시작)은 404. sudo -n tee /etc/caddy/sites/webmaker.caddy >/dev/null </dev/null sudo -n caddy validate --config /etc/caddy/Caddyfile >/dev/null && sudo -n systemctl reload caddy && echo "caddy: $FQDN 사이트 추가·reload" elif command -v nginx >/dev/null 2>&1; then printf 'server {\n listen 80;\n server_name %s;\n server_tokens off;\n client_max_body_size 200m;\n error_page 404 /__wh_404; error_page 500 502 503 504 /__wh_50x;\n location = /__wh_404 { internal; default_type text/html; return 404 '"'"'%s'"'"'; }\n location = /__wh_50x { internal; default_type text/html; return 503 '"'"'%s'"'"'; }\n location = /__wh_maint { internal; default_type text/html; add_header Cache-Control "no-store"; return 503 '"'"'%s'"'"'; }\n # 생성된 사이트: https://FQDN/<32자리ID>/ — 발행 취소 표식(.unpublished)이 있으면 안내 페이지 (v1.3.17 B06)\n location ~ "^/([a-f0-9]{32})(/|$)" { root %s; index index.html; if (-f $document_root/$1/.unpublished) { rewrite ^ /__wh_maint last; } try_files $uri $uri/ =404; add_header Cache-Control "no-cache"; access_log /var/log/nginx/webmaker-sites.log wmstats; location ~ /\\. { return 404; } }\n location / { proxy_pass http://127.0.0.1:%s; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 3600s; }\n}\n' "$FQDN" "$ERR404" "$ERR50X" "$MAINT" "$SITES" "$PORT" | sudo -n tee /etc/nginx/sites-available/webmaker.conf >/dev/null sudo -n ln -sf /etc/nginx/sites-available/webmaker.conf /etc/nginx/sites-enabled/webmaker.conf && sudo -n nginx -t && sudo -n systemctl reload nginx && echo "nginx: $FQDN 사이트 추가·reload" else echo "웹서버(caddy/nginx)를 찾지 못했습니다. 앱은 127.0.0.1:$PORT 에서 실행 중입니다."; fi if command -v ufw >/dev/null 2>&1 && sudo -n ufw status | grep -q '^Status: active'; then sudo -n ufw allow from 192.168.20.0/24 to any port 80 proto tcp >/dev/null; fi echo "== 6/7 설치 확인"; curl -s -o /dev/null -w "local app: %{http_code}\n" "http://127.0.0.1:$PORT/login?direct=1"; curl -s -o /dev/null -w "via webserver (Host $FQDN): %{http_code} (설치 점검 전에는 / 가 /setup 으로 302)\n" -H "Host: $FQDN" http://127.0.0.1/ REMOTE # v1.5.7 (요구 4): 연동 서비스 — Gitea 는 항상(docker 가 있으면), Arimail 은 ARIMAIL_HOST·ARIMAIL_MAILBOX 환경 변수를 주었을 때 함께 설치한다. # 예: ARIMAIL_HOST=mail.example.com ARIMAIL_MAILBOX=admin@example.com deploy/remote-install.sh bear webmaker.example.com # Arimail 소스는 이 PC 의 ~/work/mail-gateway 체크아웃을 git bundle 로 묶어 보낸다(서버에 저장소 자격 증명을 두지 않음). echo "== 7/7 연동 서비스 (Gitea · Arimail)" ssh "$HOST" "$APPDIR/deploy/install-services.sh --gitea --service webmaker" 2>&1 | sed 's/^/ /' || echo " Gitea 설치를 건너뛰었습니다 — 대상 서버에서 $APPDIR/deploy/install-gitea.sh --service webmaker 를 직접 실행하세요" if [ -n "${ARIMAIL_HOST:-}" ] && [ -n "${ARIMAIL_MAILBOX:-}" ]; then if [ -d "$HOME/work/mail-gateway/.git" ]; then git -C "$HOME/work/mail-gateway" bundle create /tmp/arimail-$$.bundle main >/dev/null 2>&1 && scp -q /tmp/arimail-$$.bundle "$HOST:/tmp/arimail.bundle" && rm -f /tmp/arimail-$$.bundle; BUNDLE_OPT="--bundle /tmp/arimail.bundle"; else BUNDLE_OPT=""; fi ssh "$HOST" "$APPDIR/deploy/install-arimail.sh --host $ARIMAIL_HOST --mailbox $ARIMAIL_MAILBOX $BUNDLE_OPT ${ARIMAIL_OPTS:-}" 2>&1 | sed 's/^/ /' || echo " Arimail 설치 실패 — 대상 서버에서 $APPDIR/deploy/install-arimail.sh --host $ARIMAIL_HOST --mailbox $ARIMAIL_MAILBOX 를 직접 실행하세요" else ssh "$HOST" "$APPDIR/deploy/install-arimail.sh --check" 2>&1 | sed 's/^/ /' || true; fi # v1.5.17 (요구 1): 대상 서버에 이미 *.<도메인> 와일드카드 인증서가 있으면 인지해 알린다 (서브 도메인 사이트에 자동 적용됨) echo "== 와일드카드 인증서"; ssh "$HOST" "cd $APPDIR && node scripts/ssl-wildcard.js" 2>&1 | sed 's/^/ /' || true echo "== 완료. 설치 중 더 입력할 것은 없습니다 — 나머지는 웹에서 이어집니다:" echo " 0) 초기 설정 페이지(설치 점검표: 관리자 비밀번호 · AI 엔진 인증 · 공개 주소 → 연동 정보)에서 설정하고 서비스를 재시작:" ssh "$HOST" "cd $APPDIR && node scripts/config.js setup-url" | sed 's/^/ /' echo " 서비스 첫 화면 $PUBLIC_URL/ 도 점검표가 끝나기 전에는 같은 설정 페이지(/setup)로 넘어갑니다. 최초 관리자는 설정 페이지의 "관리자 계정" 카드에서 만듭니다(고정 비밀번호 없음). 설치 점검이 끝나면 접속 토큰은 더 쓸 수 없습니다" echo " (같은 화면이 서비스 안 https://$FQDN/setup 과 관리자 콘솔 › 시스템 설정 탭에도 있습니다)" [ -n "$FQDN" ] && echo " 1) 대상 서버가 공인 80/443 을 직접 받으면 그 서버에서: sudo certbot --nginx -d $FQDN --redirect ; 다른 프런트 서버를 거치면 deploy/front-proxy-webmaker.conf 를 그 서버에 적용" || echo " 1) 도메인이 정해지면: 설정 페이지에서 서비스 공개 주소를 https://<도메인> 으로 바꾸고, 그 서버에서 nginx server_name 변경 후 sudo certbot --nginx -d <도메인> --redirect" echo " 2) Claude 연결(서버 로그인/OAuth 토큰/API 키)은 설정 페이지의 \"AI 엔진 인증\" 에서(관리자 콘솔 › 설정과 같은 값), 배포 서버 탭에서 SSH 키 재생성·등록" echo " 3) Gitea 를 건너뛰었다면 대상 서버에서 $APPDIR/deploy/install-gitea.sh --service webmaker [--migrate-from <옛 원격>]; Arimail 은 $APPDIR/deploy/install-arimail.sh --host mail.<도메인> --mailbox admin@<도메인>"