From a3ef921a4f9741847326459600d543990299845f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 24 Feb 2026 01:56:58 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=9A=B4=EC=98=81/CI/CD=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=85=8B=ED=8C=85=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영서버(211.117.60.189) 전체 설치 완료 문서화 - OS, MySQL 8.4.8, Redis 7.0.15, Nginx 1.24.0, PHP 8.4.18 - 7개 도메인 SSL (develop@codebridge-x.com), PM2 cluster - Supervisor queue worker, node_exporter, 보안 설정 - CI/CD 서버(110.10.147.46) 셋팅 가이드 추가 Co-Authored-By: Claude Opus 4.6 --- deploys/cicd-server-setup.md | 1172 ++++++++++++++++++++++++++++ deploys/production-server-setup.md | 1110 ++++++++++++++++++++++++++ 2 files changed, 2282 insertions(+) create mode 100644 deploys/cicd-server-setup.md create mode 100644 deploys/production-server-setup.md diff --git a/deploys/cicd-server-setup.md b/deploys/cicd-server-setup.md new file mode 100644 index 0000000..39beca5 --- /dev/null +++ b/deploys/cicd-server-setup.md @@ -0,0 +1,1172 @@ +# CI/CD 서버 셋팅 가이드 + +> 작성일: 2026-02-23 | 최종 수정: 2026-02-24 +> 상태: 설치 완료 (Jenkinsfile 작성 + 실제 배포 테스트 남음) + +--- + +## 1. 서버 구성 개요 + +### 인프라 구조 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ CI/CD서버 (2 vCPU / 8GB) │ +│ Ubuntu 24.04 / IDC 클라우드 │ +│ IP: 110.10.147.46 │ +│ │ +│ ┌──────────┐ ┌───────────┐ ┌───────────────────────────┐ │ +│ │ Nginx │ │ Certbot │ │ UFW (22,80,443) │ │ +│ │ (Proxy) │ │ (SSL) │ │ │ │ +│ └────┬─────┘ └───────────┘ └───────────────────────────┘ │ +│ │ │ +│ ┌────┴───────────────────────────────────────────────────┐ │ +│ │ Virtual Hosts │ │ +│ │ │ │ +│ │ git.sam.it.kr ──────────→ Gitea (:3000) │ │ +│ │ ci.sam.it.kr ───────────→ Jenkins (:8080) │ │ +│ │ monitor.sam.it.kr ──────→ Grafana (:3100) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ +│ │ Gitea │ │ Jenkins │ │ MySQL 8.4 │ │ +│ │ (운영 Git) │ │ (CI/CD) │ │ (Gitea DB + 백업) │ │ +│ └────────────┘ └────────────┘ └────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Prometheus │ │ Grafana │ │ +│ │ (:9090) │ │ (:3100) │ │ +│ └──────────────┘ └──────────────┘ │ +└───────────────────────────────────────────────────────────────┘ +``` + +### 도메인 매핑 + +| 도메인 | 서비스 | 포트 | SSL | +|--------|--------|------|-----| +| git.sam.it.kr | Gitea | 3000 | Let's Encrypt | +| ci.sam.it.kr | Jenkins | 8080 | Let's Encrypt | +| monitor.sam.it.kr | Grafana | 3100 | Let's Encrypt | + +### Git 동기화 전략 + +**방침**: 개발서버 Gitea(origin) 유지 + CI/CD Gitea에 **선택적 브랜치 push** (post-receive hook) + +> Gitea Push Mirror는 전체 브랜치를 미러링하므로 사용하지 않음. +> 대신 개발서버 Gitea의 **post-receive hook**으로 필요한 브랜치만 CI/CD Gitea에 push. + +``` +개발자 로컬 + │ git push origin (develop/stage/main) + ▼ +개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자의 origin + │ + ├─ develop push 시 + │ ├─ api/mng/sales: 기존 post-update hook (개발서버 pull) ← 현행 유지 + │ └─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 개발서버 배포 + │ + ├─ stage push 시 + │ ├─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 운영서버 Stage 배포 + │ └─ api: hook → CI/CD Gitea push → Jenkins → 운영서버 Stage pull + │ + └─ main push 시 (react/mng/api) + └─ ❌ CI/CD Gitea에 자동 push 안함 + → 배포관리자가 수동으로 CI/CD Gitea에 push + → Jenkins 자동 배포 + +별도 처리: + sales/www(landing): Push Mirror 또는 hook → CI/CD Gitea → Jenkins → 운영서버 pull +``` + +### 브랜치별 배포 정책 상세 + +| 브랜치 | 저장소 | CI/CD Gitea 동기화 | Jenkins 배포 | 배포 대상 | +|--------|--------|-------------------|-------------|----------| +| **stage** | react | 자동 (hook) | 빌드 + rsync | 운영서버 Stage | +| **stage** | api | 자동 (hook) | SSH pull | 운영서버 Stage | +| **main** | react | 수동 (배포관리자) | 빌드 + rsync | 운영서버 Production | +| **main** | mng | 수동 (배포관리자) | SSH deploy | 운영서버 Production | +| **main** | api | 수동 (배포관리자) | SSH deploy | 운영서버 Production | +| **main** | sales | 자동 (hook/mirror) | SSH pull | 운영서버 Production | +| **main** | www | 자동 (hook/mirror) | SSH pull | 운영서버 Production | +| **develop** | react | 자동 (hook) | 빌드 → 개발서버 배포 | 개발서버 | +| **develop** | api | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | +| **develop** | mng | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | +| **develop** | sales | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | + +### 배포관리자 운영 배포 워크플로우 + +```bash +# 배포관리자 로컬에서 (react/mng/api 저장소) +# CI/CD Gitea를 별도 remote로 등록 (1회) +git remote add production https://git.sam.it.kr/SamProject/sam-api.git + +# 운영 배포 시: main 브랜치를 CI/CD Gitea에 push +git push production main +# → CI/CD Gitea webhook → Jenkins → 운영서버 자동 배포 +``` + +현재 Git remote 현황 (개발서버): +``` +sam-api: http://114.203.209.83:3000/SamProject/sam-api.git +sam-manage: http://114.203.209.83:3000/SamProject/sam-manage.git +sam-react-prod: http://114.203.209.83:3000/SamProject/sam-react-prod.git +sam-sales: http://114.203.209.83:3000/SamProject/sam-sales.git +sam-docs: http://114.203.209.83:3000/SamProject/sam-docs.git +sam-design: http://114.203.209.83:3000/SamProject/sam-design.git +sam-planning: http://114.203.209.83:3000/SamProject/sam-planning.git +``` + +--- + +## 2. 메모리 배분 계획 (8GB) + +| 서비스 | 할당 | 설정 | 비고 | +|--------|------|------|------| +| Jenkins | ~2.0GB | -Xmx2048m | Java 기반, 빌드 시 메모리 소모 큼 | +| MySQL 8.4 | ~1.5GB | innodb_buffer_pool_size=1536M | Gitea DB + 운영 백업 | +| Gitea | ~0.5GB | - | Go 기반, 가벼움 | +| Prometheus | ~0.5GB | --storage.tsdb.retention.time=30d | 메트릭 저장 30일 | +| Grafana | ~0.3GB | - | 대시보드 시각화 | +| Nginx | ~0.1GB | - | 리버스 프록시 | +| node_exporter | ~0.01GB | - | 자체 모니터링 | +| OS + 여유 | ~3.1GB | 스왑 4GB | 안전 마진 | +| **합계** | **~8GB** | | **스왑 4GB 백업** | + +--- + +## 3. 설치 순서 + +### ① OS 기본 셋팅 ✅ + +```bash +# 시스템 업데이트 +sudo apt update && sudo apt upgrade -y + +# 기본 패키지 +sudo apt install -y curl wget git unzip vim htop net-tools software-properties-common + +# 타임존 +sudo timedatectl set-timezone Asia/Seoul + +# 스왑 4GB 설정 +sudo fallocate -l 4G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab + +# 스왑 최적화 +echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf +sudo sysctl -p + +# UFW 방화벽 +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable + +# webservice 사용자 그룹 (배포 스크립트용) +sudo groupadd -f webservice +sudo usermod -aG webservice hskwon +``` + +### ② MySQL 8.4 ✅ + +> **설치 완료**: MySQL 8.4.8 (GPG 키 만료 이슈로 Ubuntu keyserver 경유 설치) +> **주의**: `validate_password.policy` 설정은 플러그인 미로딩 시 MySQL 시작 실패 — 사용하지 않음 + +```bash +# 1. mysql-apt-config deb로 repo 등록 +sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb +sudo DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.33-1_all.deb + +# 2. GPG 키 만료 시 — Ubuntu keyserver에서 갱신 +sudo gpg --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C +sudo gpg --export B7B3B788A8D3785C | sudo tee /usr/share/keyrings/mysql-apt-config.gpg > /dev/null + +# 3. 설치 +sudo apt update +sudo apt install -y mysql-server +``` + +**성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`): + +```ini +[mysqld] +innodb_buffer_pool_size = 1536M +innodb_redo_log_capacity = 536870912 +innodb_flush_log_at_trx_commit = 2 +max_connections = 50 +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +slow_query_log = 1 +slow_query_log_file = /var/log/mysql/slow.log +long_query_time = 2 + +# Replication (Slave) — 운영서버 Master 설정 완료 후 활성화 +# server-id = 2 +# relay-log = /var/log/mysql/mysql-relay-bin +# read-only = 1 ← Gitea 로컬 DB 쓰기를 차단하므로 반드시 주석 유지 +``` + +```bash +sudo systemctl restart mysql +``` + +**DB 및 사용자:** + +```sql +-- Gitea DB +CREATE DATABASE gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'gitea'@'localhost' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'localhost'; + +-- 관리자 (hskwon) - auth_socket 인증 +CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; +GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; + +FLUSH PRIVILEGES; +``` + +> **참고**: root는 auth_socket 인증 (비밀번호 없이 `sudo mysql`로 접근) + +### ②-slave MySQL Replication 설정 (운영 DB 백업) + +> 운영서버 MySQL Master → CI/CD 서버 MySQL Slave + +**운영서버(Master)에서 실행:** + +```sql +CREATE USER 'repl_user'@'110.10.147.46' IDENTIFIED BY '<복제_비밀번호>'; +GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'110.10.147.46'; +FLUSH PRIVILEGES; +SHOW MASTER STATUS; +-- → File: mysql-bin.XXXXXX, Position: XXXX 기록 +``` + +**운영서버 my.cnf 추가:** +```ini +[mysqld] +server-id = 1 +log-bin = /var/log/mysql/mysql-bin +binlog-do-db = sam_production +binlog-do-db = chandj +``` + +**CI/CD서버(Slave)에서 실행:** + +```sql +CHANGE REPLICATION SOURCE TO + SOURCE_HOST='211.117.60.189', + SOURCE_USER='repl_user', + SOURCE_PASSWORD='<복제_비밀번호>', + SOURCE_LOG_FILE='mysql-bin.XXXXXX', + SOURCE_LOG_POS=XXXX; + +START REPLICA; +SHOW REPLICA STATUS\G +-- Slave_IO_Running: Yes, Slave_SQL_Running: Yes 확인 +``` + +### ③ Redis → 운영서버에 설치 + +> **Redis는 운영서버(211.117.60.189)에 설치.** +> 상세 설정: [production-server-setup.md](./production-server-setup.md) Redis 섹션 참조 + +### ④ Java (Jenkins 의존) ✅ + +> **설치 완료**: OpenJDK 17.0.18 + +```bash +sudo apt install -y openjdk-17-jre-headless +java -version +``` + +### ⑤ Gitea ✅ + +> **설치 완료**: Gitea 1.22.6 +> - 관리자: hskwon / kent@codebridge-x.com +> - Organization: SamProject +> - 저장소: sam-api, sam-manage, sam-react-prod, sam-sales, sam-landing +> - 회원가입 비활성화, API 토큰 생성 완료 + +```bash +GITEA_VERSION="1.22.6" +wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64 +sudo mv /tmp/gitea /usr/local/bin/gitea +sudo chmod +x /usr/local/bin/gitea + +sudo adduser --system --group --disabled-password --shell /bin/bash --home /home/git git + +sudo mkdir -p /var/lib/gitea/{custom,data,log} +sudo mkdir -p /etc/gitea +sudo chown -R git:git /var/lib/gitea +sudo chown git:git /etc/gitea +sudo chmod 750 /etc/gitea +``` + +**systemd 서비스 등록:** + +```bash +sudo tee /etc/systemd/system/gitea.service > /dev/null << 'EOF' +[Unit] +Description=Gitea (Git with a cup of tea) +After=syslog.target network.target mysql.service + +[Service] +Type=simple +User=git +Group=git +WorkingDirectory=/var/lib/gitea/ +ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini +Restart=always +Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable gitea +``` + +**Gitea 설정** (`/etc/gitea/app.ini`): + +```ini +[server] +DOMAIN = git.sam.it.kr +HTTP_PORT = 3000 +ROOT_URL = https://git.sam.it.kr/ +DISABLE_SSH = false +SSH_PORT = 22 +LFS_START_SERVER = true + +[database] +DB_TYPE = mysql +HOST = 127.0.0.1:3306 +NAME = gitea +USER = gitea +PASSWD = +CHARSET = utf8mb4 + +[repository] +ROOT = /var/lib/gitea/data/repositories + +[log] +ROOT_PATH = /var/lib/gitea/log +MODE = file +LEVEL = info + +[service] +DISABLE_REGISTRATION = true +REQUIRE_SIGNIN_VIEW = true +``` + +```bash +sudo systemctl start gitea +# 확인: curl http://localhost:3000 +``` + +**CI/CD Gitea 초기 설정:** + +``` +1. https://git.sam.it.kr 웹 설치 마법사 완료 +2. 관리자 계정 생성 +3. Organization "SamProject" 생성 +4. 빈 저장소 생성: + - sam-api, sam-manage, sam-react-prod + - sam-sales, sam-landing (www) +5. Jenkins Webhook용 API 토큰 생성 +``` + +### ⑤-hook 개발서버 post-receive hook 설정 (선택적 브랜치 동기화) ✅ + +> **설치 완료**: 개발서버 Gitea bare repository에 post-receive hook 추가 +> stage/develop(react만) push 시 CI/CD Gitea에 해당 브랜치만 push. +> 기존 post-update hook (개발서버 pull)은 **그대로 유지**. + +**개발서버 Gitea bare repo 경로:** + +``` +/data/GIT/samproject/.git/hooks/post-receive.d/push-to-cicd +``` + +**토큰 보안 (환경변수 파일):** + +```bash +# /data/GIT/.cicd-env (chmod 600, owner: git) +CICD_GITEA_TOKEN=<토큰> +CICD_GITEA_USER=hskwon +CICD_GITEA_HOST=git.sam.it.kr +``` + +**hook 스크립트 구조 (공통 패턴):** + +```bash +#!/bin/bash +source /data/GIT/.cicd-env +LOGFILE=/home/webservice/logs/cicd_push_.log +CICD_REMOTE="https://${CICD_GITEA_USER}:${CICD_GITEA_TOKEN}@${CICD_GITEA_HOST}/SamProject/.git" + +mkdir -p /home/webservice/logs + +while read oldrev newrev refname; do + BRANCH=$(echo "$refname" | sed 's|refs/heads/||') + if [ "$BRANCH" = "" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S'): Pushing ${BRANCH} to CI/CD Gitea" >> $LOGFILE + git push $CICD_REMOTE ${BRANCH}:${BRANCH} >> $LOGFILE 2>&1 + echo "$(date '+%Y-%m-%d %H:%M:%S'): Done (exit: $?)" >> $LOGFILE + fi +done +``` + +> **참고**: Gitea는 `hooks/post-receive.d/` 디렉토리의 스크립트를 자동 실행. +> 기존 post-update hook (pull 스크립트)과 별도로 동작. + +**동기화 요약:** + +| 저장소 | hook 대상 브랜치 | 동작 | +|--------|-----------------|------| +| sam-react-prod | stage, develop | CI/CD Gitea에 push | +| sam-api | stage | CI/CD Gitea에 push | +| sam-sales | main | CI/CD Gitea에 push | +| sam-landing | main | CI/CD Gitea에 push | +| sam-manage | ❌ 없음 | main만 사용, 배포관리자 수동 push | + +### ⑥ Jenkins ✅ + +> **설치 완료**: Jenkins 2.541.2 +> - 관리자: hskwon +> - 플러그인: Gitea, SSH Agent, Pipeline Stage View, Workflow Aggregator, Blue Ocean, NodeJS +> - Gitea 서버 연동 완료 (https://git.sam.it.kr) +> - SSH Credential 등록: `deploy-ssh-key` (운영/개발 서버 모두 접속 확인) +> - Gitea API Token Credential 등록: `gitea-api-token` +> - Node.js 22.22.0 설치 완료 + +```bash +# Jenkins GPG 키 + APT Repository +# 주의: 공식 2023 키가 유효하지 않을 수 있음 → Ubuntu keyserver에서 획득 +sudo gpg --keyserver keyserver.ubuntu.com --recv-keys 7198F4B714ABFC68 +sudo gpg --export 7198F4B714ABFC68 | sudo tee /usr/share/keyrings/jenkins-keyring.gpg > /dev/null +echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.gpg]" \ + https://pkg.jenkins.io/debian-stable binary/ | sudo tee \ + /etc/apt/sources.list.d/jenkins.list > /dev/null + +sudo apt update +sudo apt install -y jenkins + +# Jenkins JVM 메모리 제한 +sudo mkdir -p /etc/systemd/system/jenkins.service.d +sudo tee /etc/systemd/system/jenkins.service.d/override.conf > /dev/null << 'EOF' +[Service] +Environment="JAVA_OPTS=-Xmx2048m -Xms512m -Djava.awt.headless=true" +EOF + +sudo systemctl daemon-reload +sudo systemctl enable jenkins +sudo systemctl start jenkins + +# 초기 관리자 비밀번호 +sudo cat /var/lib/jenkins/secrets/initialAdminPassword +``` + +**Jenkins에 필요한 도구 설치:** + +```bash +# Node.js 22 (react 빌드용) +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs + +# PHP 8.4 + Composer (Laravel 테스트/린트용 — 선택사항) +sudo add-apt-repository ppa:ondrej/php -y +sudo apt update +sudo apt install -y php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-mysql php8.4-bcmath +curl -sS https://getcomposer.org/installer | php +sudo mv composer.phar /usr/local/bin/composer +``` + +**Jenkins 초기 설정 (웹 UI):** + +1. `https://ci.sam.it.kr` 접속 +2. 초기 비밀번호 입력 +3. 추천 플러그인 설치 + 추가 플러그인: + - **Gitea Plugin** (Gitea webhook 연동) + - **SSH Agent Plugin** (운영/개발서버 SSH 배포) + - **Pipeline** (Jenkinsfile 지원) + - **Blue Ocean** (모던 UI, 선택) + +4. Jenkins SSH 키 설정: +```bash +# Jenkins 사용자로 SSH 키 생성 +sudo -u jenkins ssh-keygen -t ed25519 -C "jenkins@sam-cicd" -f /var/lib/jenkins/.ssh/id_ed25519 -N "" + +# 운영서버에 공개키 등록 +ssh sam-prod "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" + +# 개발서버에도 공개키 등록 (develop react 빌드 배포용) +ssh sam-dev "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" + +# SSH 호스트 키 등록 (known_hosts) +sudo -u jenkins ssh-keyscan -H 211.117.60.189 >> /var/lib/jenkins/.ssh/known_hosts +sudo -u jenkins ssh-keyscan -H 114.203.209.83 >> /var/lib/jenkins/.ssh/known_hosts +``` + +5. Jenkins Credentials 설정 (CLI로 완료): + - `deploy-ssh-key`: SSH 키 (hskwon@운영/개발 서버 공용) + - `gitea-api-token`: Gitea API 토큰 + +### ⑦ Nginx + Certbot ✅ + +> **설치 완료**: Nginx + Let's Encrypt SSL (git/ci/monitor.sam.it.kr) + +```bash +sudo apt install -y nginx certbot python3-certbot-nginx +``` + +**Gitea 리버스 프록시** (`/etc/nginx/sites-available/git.sam.it.kr`): + +```nginx +server { + listen 80; + server_name git.sam.it.kr; + client_max_body_size 100M; + + location / { + proxy_pass http://127.0.0.1:3000; + 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; + } +} +``` + +**Jenkins 리버스 프록시** (`/etc/nginx/sites-available/ci.sam.it.kr`): + +```nginx +server { + listen 80; + server_name ci.sam.it.kr; + + location / { + proxy_pass http://127.0.0.1:8080; + 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_set_header X-Forwarded-Port $server_port; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 90; + proxy_buffering off; + } +} +``` + +**Grafana 리버스 프록시** (`/etc/nginx/sites-available/monitor.sam.it.kr`): + +```nginx +server { + listen 80; + server_name monitor.sam.it.kr; + + location / { + proxy_pass http://127.0.0.1:3100; + 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_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +```bash +# 사이트 활성화 + SSL +sudo ln -s /etc/nginx/sites-available/git.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/ci.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/monitor.sam.it.kr /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +sudo certbot --nginx -d git.sam.it.kr +sudo certbot --nginx -d ci.sam.it.kr +sudo certbot --nginx -d monitor.sam.it.kr +sudo certbot renew --dry-run +``` + +### ⑧ Prometheus + node_exporter ✅ + +> **설치 완료**: Prometheus 2.51.0, node_exporter 1.8.2 + +```bash +# Prometheus +PROM_VERSION="2.51.0" +cd /tmp +wget https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz +tar xzf prometheus-${PROM_VERSION}.linux-amd64.tar.gz +sudo mv prometheus-${PROM_VERSION}.linux-amd64/prometheus /usr/local/bin/ +sudo mv prometheus-${PROM_VERSION}.linux-amd64/promtool /usr/local/bin/ +sudo mkdir -p /etc/prometheus /var/lib/prometheus +sudo mv prometheus-${PROM_VERSION}.linux-amd64/consoles /etc/prometheus/ +sudo mv prometheus-${PROM_VERSION}.linux-amd64/console_libraries /etc/prometheus/ +rm -rf prometheus-${PROM_VERSION}* +sudo useradd --no-create-home --shell /bin/false prometheus +sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus +``` + +**Prometheus 설정** (`/etc/prometheus/prometheus.yml`): + +```yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'sam-prod' + static_configs: + - targets: ['211.117.60.189:9100'] + labels: + server: 'production' + + - job_name: 'sam-cicd' + static_configs: + - targets: ['localhost:9100'] + labels: + server: 'cicd' +``` + +```bash +# Prometheus systemd +sudo tee /etc/systemd/system/prometheus.service > /dev/null << 'EOF' +[Unit] +Description=Prometheus +After=network-online.target + +[Service] +User=prometheus +Group=prometheus +Type=simple +ExecStart=/usr/local/bin/prometheus \ + --config.file=/etc/prometheus/prometheus.yml \ + --storage.tsdb.path=/var/lib/prometheus/ \ + --storage.tsdb.retention.time=30d \ + --web.listen-address=:9090 +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +# node_exporter +cd /tmp +wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz +tar xzf node_exporter-1.8.2.linux-amd64.tar.gz +sudo mv node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/ +rm -rf node_exporter-1.8.2* + +sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'EOF' +[Unit] +Description=Node Exporter +After=network.target + +[Service] +User=nobody +ExecStart=/usr/local/bin/node_exporter +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable prometheus node_exporter +sudo systemctl start prometheus node_exporter +``` + +### ⑨ Grafana ✅ + +> **설치 완료**: Grafana (포트 3100) + +```bash +sudo mkdir -p /etc/apt/keyrings/ +wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null +echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list +sudo apt update +sudo apt install -y grafana +``` + +**Grafana 설정** (`/etc/grafana/grafana.ini`): + +```ini +[server] +http_port = 3100 +domain = monitor.sam.it.kr +root_url = https://monitor.sam.it.kr/ + +[security] +admin_password = + +[users] +allow_sign_up = false +``` + +```bash +sudo systemctl enable grafana-server +sudo systemctl start grafana-server +``` + +**Grafana 초기 설정 (웹 UI):** + +1. Data Source: Prometheus → `http://localhost:9090` +2. 대시보드 임포트: **Node Exporter Full** (ID: 1860) + +--- + +## 4. 배포 파이프라인 설계 + +### Webhook 설정 (CI/CD Gitea → Jenkins) + +각 저장소에 Webhook 추가 (CI/CD Gitea 웹 UI): + +``` +Repository Settings → Webhooks → Add Webhook (Gitea) +- URL: https://ci.sam.it.kr/gitea-webhook/post +- Content Type: application/json +- Secret: +- Events: Push events +``` + +### 파이프라인: Laravel API (api/) + +**Jenkinsfile** (`api/Jenkinsfile`): + +```groovy +pipeline { + agent any + + environment { + DEPLOY_USER = 'hskwon' + APP_NAME = 'api' + RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + // ── main → 운영서버 (배포관리자 수동 push 후 트리거) ── + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 ' + cd /home/webservice/api/releases && + git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && + ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/${RELEASE_ID}/storage && + ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/${RELEASE_ID}/.env && + cd /home/webservice/api/releases/${RELEASE_ID} && + composer install --no-dev --optimize-autoloader --no-interaction && + php artisan config:cache && + php artisan route:cache && + php artisan view:cache && + php artisan migrate --force && + ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current && + sudo systemctl reload php8.4-fpm && + sudo supervisorctl restart sam-queue-worker:* && + cd /home/webservice/api/releases && + ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // ── stage → 운영서버 Stage ── + stage('Deploy Stage') { + when { branch 'stage' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 ' + cd /home/webservice/api-stage/releases && + git clone --depth 1 --branch stage https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && + ln -sfn /home/webservice/api-stage/shared/storage /home/webservice/api-stage/releases/${RELEASE_ID}/storage && + ln -sfn /home/webservice/api-stage/shared/.env /home/webservice/api-stage/releases/${RELEASE_ID}/.env && + cd /home/webservice/api-stage/releases/${RELEASE_ID} && + composer install --no-dev --optimize-autoloader --no-interaction && + php artisan config:cache && + php artisan route:cache && + php artisan view:cache && + php artisan migrate --force && + ln -sfn /home/webservice/api-stage/releases/${RELEASE_ID} /home/webservice/api-stage/current && + sudo systemctl reload php8.4-fpm && + cd /home/webservice/api-stage/releases && + ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // develop → Jenkins 관여 안함 (기존 post-update hook 유지) + } + + post { + success { echo "✅ api 배포 완료 (${env.BRANCH_NAME})" } + failure { + echo "❌ api 배포 실패 (${env.BRANCH_NAME})" + // 운영/Stage 실패 시 이전 릴리즈로 롤백 + script { + if (env.BRANCH_NAME in ['main', 'stage']) { + def baseDir = env.BRANCH_NAME == 'main' + ? '/home/webservice/api' + : '/home/webservice/api-stage' + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 ' + PREV=\$(ls -1dt ${baseDir}/releases/*/ | sed -n "2p" | xargs basename) && + [ -n "\$PREV" ] && ln -sfn ${baseDir}/releases/\$PREV ${baseDir}/current && + sudo systemctl reload php8.4-fpm + ' + """ + } + } + } + } + } +} +``` + +### 파이프라인: Laravel Admin (mng/) + +> API와 동일 구조. 차이점: +> - main만 (stage/develop 없음 — develop은 기존 post-update hook) +> - Queue Worker 재시작 불필요 +> - npm run build 추가 (Blade + Vite) + +### 파이프라인: Next.js React (react/) + +**Jenkinsfile** (`react/Jenkinsfile`): + +```groovy +pipeline { + agent any + + environment { + DEPLOY_USER = 'hskwon' + RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') + } + + stages { + stage('Checkout') { + steps { checkout scm } + } + + stage('Build') { + steps { + sh 'npm ci && npm run build' + } + } + + // ── main → 운영서버 (배포관리자 수동 push 후 트리거) ── + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' + + rsync -az --delete \ + .next/ package.json package-lock.json next.config.* public/ node_modules/ \ + ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ + + ssh ${DEPLOY_USER}@211.117.60.189 ' + ln -sfn /home/webservice/react/shared/.env.local /home/webservice/react/releases/${RELEASE_ID}/.env.local && + ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && + cd /home/webservice && pm2 reload sam-front && + cd /home/webservice/react/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // ── stage → 운영서버 Stage ── + stage('Deploy Stage') { + when { branch 'stage' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' + + rsync -az --delete \ + .next/ package.json package-lock.json next.config.* public/ node_modules/ \ + ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ + + ssh ${DEPLOY_USER}@211.117.60.189 ' + ln -sfn /home/webservice/react-stage/shared/.env.local /home/webservice/react-stage/releases/${RELEASE_ID}/.env.local && + ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && + cd /home/webservice && pm2 reload sam-front-stage && + cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +3 | xargs rm -rf 2>/dev/null || true + ' + """ + } + } + } + + // ── develop → 개발서버 (CI/CD에서 빌드 후 배포) ── + stage('Deploy Development') { + when { branch 'develop' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh """ + # 빌드 결과물을 개발서버로 전송 + rsync -az --delete \ + .next/ package.json package-lock.json next.config.* public/ node_modules/ \ + ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ + + ssh ${DEPLOY_USER}@114.203.209.83 ' + cd /home/webservice/react && + pm2 restart sam-front + ' + """ + } + } + } + } + + post { + success { echo "✅ react 배포 완료 (${env.BRANCH_NAME})" } + failure { echo "❌ react 배포 실패 (${env.BRANCH_NAME})" } + } +} +``` + +### 파이프라인: Sales (레거시 PHP) + +```groovy +pipeline { + agent any + environment { DEPLOY_USER = 'hskwon' } + + stages { + // main → 운영서버 (hook 자동) + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/sales && git pull origin main'" + } + } + } + // develop → 개발서버는 기존 post-update hook 유지 + } +} +``` + +### 파이프라인: Landing (www) + +```groovy +pipeline { + agent any + environment { DEPLOY_USER = 'hskwon' } + + stages { + stage('Deploy Production') { + when { branch 'main' } + steps { + sshagent(credentials: ['deploy-ssh-key']) { + sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/landing && git pull origin main'" + } + } + } + } +} +``` + +--- + +## 5. 배포 흐름도 + +``` +개발자 로컬 + │ git push origin (develop / stage / main) + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ 개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자 origin │ +│ │ +│ post-receive hooks: │ +│ │ +│ ┌─ develop push ────────────────────────────────────────┐ │ +│ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │ +│ │ → 빌드 결과 rsync → 개발서버 배포 │ │ +│ │ api → 기존 post-update hook (pull + migrate) │ │ +│ │ mng → 기존 post-update hook (pull + build) │ │ +│ │ sales → 기존 post-update hook (pull) │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ stage push ──────────────────────────────────────────┐ │ +│ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │ +│ │ → rsync → 운영서버 Stage + PM2 reload │ │ +│ │ api → hook: CI/CD Gitea push ──→ Jenkins │ │ +│ │ → 운영서버 Stage Release + 심링크 │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ main push (sales/www만 자동) ────────────────────────┐ │ +│ │ sales → hook: CI/CD Gitea push ──→ Jenkins │ │ +│ │ → 운영서버 pull │ │ +│ │ www → hook: CI/CD Gitea push ──→ Jenkins │ │ +│ │ → 운영서버 pull │ │ +│ │ react/mng/api → ❌ 자동 push 안함 │ │ +│ └───────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────┘ + +┌─ 운영 배포 (main - react/mng/api) ──────────────────────────┐ +│ │ +│ 배포관리자 로컬 │ +│ │ git push production main (CI/CD Gitea remote) │ +│ ▼ │ +│ CI/CD Gitea (git.sam.it.kr) │ +│ │ Webhook │ +│ ▼ │ +│ Jenkins → 운영서버 배포 │ +│ react: CI/CD 빌드 → rsync → PM2 reload │ +│ api: Release + 심링크 → PHP-FPM reload │ +│ mng: Release + 심링크 → PHP-FPM reload │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +### 환경별 배포 비교 + +| 항목 | 운영 (main) | Stage (stage) | 개발 (develop) | +|------|------------|---------------|----------------| +| **트리거** | 배포관리자 수동 push | 자동 (hook) | react만 자동 (hook), 나머지 기존 hook | +| **react 전략** | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | +| **api 전략** | Release + 심링크 | Release + 심링크 | 기존 post-update (pull) | +| **mng 전략** | Release + 심링크 | - | 기존 post-update (pull + build) | +| **롤백** | 이전 릴리즈 심링크 | 이전 릴리즈 심링크 | git revert | +| **릴리즈 보관** | 최근 5개 | 최근 3개 | - | + +--- + +## 6. 백업 자동화 + +### DB 일일 백업 (CI/CD 서버 crontab) + +```bash +# /home/hskwon/scripts/backup-db.sh +#!/bin/bash +set -e + +BACKUP_DIR="/home/hskwon/backups/mysql" +DATE=$(date +%Y%m%d_%H%M%S) +RETENTION_DAYS=14 + +mkdir -p $BACKUP_DIR + +mysqldump --single-transaction --routines --triggers \ + sam_production > $BACKUP_DIR/sam_production_$DATE.sql +mysqldump --single-transaction --routines --triggers \ + chandj > $BACKUP_DIR/chandj_$DATE.sql + +gzip $BACKUP_DIR/sam_production_$DATE.sql +gzip $BACKUP_DIR/chandj_$DATE.sql +find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete + +echo "$(date): Backup completed" >> $BACKUP_DIR/backup.log +``` + +```bash +chmod +x /home/hskwon/scripts/backup-db.sh +# crontab (매일 새벽 3시) +(crontab -l 2>/dev/null; echo "0 3 * * * /home/hskwon/scripts/backup-db.sh") | crontab - +``` + +--- + +## 7. 최종 점검 + +```bash +# 서비스 상태 +sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter + +# 방화벽 / 메모리 / 디스크 / 포트 +sudo ufw status verbose +free -h +df -h +sudo ss -tlnp + +# 웹 접속 테스트 +curl -sI https://ci.sam.it.kr +curl -sI https://git.sam.it.kr +curl -sI https://monitor.sam.it.kr + +# SSL +sudo certbot certificates +``` + +--- + +## 8. 보안 체크리스트 + +- [x] SSH 키 인증만 허용 (PasswordAuthentication no) +- [x] root SSH 로그인 비활성화 (PermitRootLogin no) +- [x] UFW 방화벽 활성화 (22, 80, 443만 허용) +- [x] Jenkins 관리자 계정 변경 (hskwon) +- [ ] Jenkins CSRF 보호 활성화 +- [x] Gitea 회원가입 비활성화 (DISABLE_REGISTRATION = true) +- [x] Grafana 익명 접근 비활성화 (allow_sign_up = false) +- [x] Prometheus 외부 접근 차단 (127.0.0.1:9090 바인딩) +- [x] MySQL root 원격 접근 차단 (auth_socket 인증) +- [x] fail2ban 설치 (sshd jail 활성) +- [x] Certbot 자동 갱신 (certbot.timer 활성) +- [x] Jenkins SSH 키 ed25519 생성 + Credential 등록 +- [x] Webhook Secret 설정 (Gitea → Jenkins) +- [x] post-receive hook 토큰 보안 (/data/GIT/.cicd-env 파일 참조, 600 권한) + +--- + +## 9. 설치 순서 요약 + +| 순서 | 항목 | 예상 시간 | 의존성 | +|------|------|----------|--------| +| ① | OS 기본 셋팅 | 15분 | - | +| ② | MySQL 8.4 | 20분 | ① | +| ②-s | MySQL Replication | 30분 | ② + 운영서버 MySQL | +| ④ | Java 17 | 5분 | ① | +| ⑤ | Gitea 설치 + 초기 설정 | 30분 | ② | +| ⑤-h | 개발서버 post-receive hook 설정 | 30분 | ⑤ | +| ⑥ | Jenkins | 20분 | ④ | +| ⑦ | Nginx + SSL | 20분 | ⑤⑥ | +| ⑧ | Prometheus + node_exporter | 15분 | ① | +| ⑨ | Grafana | 15분 | ⑧ | +| ⑩ | Jenkins 파이프라인 + Webhook 설정 | 1시간 | ⑥⑦ | +| ⑪ | 백업 자동화 | 15분 | ②-s | +| ⑫ | 최종 점검 + 보안 | 30분 | 전체 | + +**총 예상 시간: 5~6시간** + +--- + +## 10. 결정 필요 사항 + +- [x] ~~Gitea 이전 여부~~ → 선택적 브랜치 동기화 (post-receive hook) +- [x] ~~브랜치 전략~~ → main(수동/운영), stage(자동/Stage), develop(react만 자동) +- [x] ~~도메인 확정~~ → git.sam.it.kr, ci.sam.it.kr, monitor.sam.it.kr (SSL 발급 완료) +- [ ] **Jenkins 테스트 실행 여부**: CI에서 phpunit/lint 실행 vs 배포만 +- [ ] **알림 채널**: Slack, 이메일, 카카오톡 등 +- [ ] **개발서버 Gitea bare repo 경로 확인** (hook 설정을 위해) \ No newline at end of file diff --git a/deploys/production-server-setup.md b/deploys/production-server-setup.md new file mode 100644 index 0000000..108a23d --- /dev/null +++ b/deploys/production-server-setup.md @@ -0,0 +1,1110 @@ +# 운영서버 셋팅 가이드 + +> 작성일: 2026-02-23 | 최종 수정: 2026-02-24 +> 상태: ✅ 완료 (커널 6.8.0-100, 전체 서비스 가동 중) + +--- + +## 1. 서버 구성 개요 + +### 인프라 구조 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 운영서버 (2 vCPU / 8GB) │ +│ Ubuntu 24.04 / IDC 클라우드 │ +│ IP: 211.117.60.189 │ +│ Kernel: 6.8.0-100-generic │ +│ │ +│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │ +│ │ Nginx │ │ Certbot │ │ UFW (22,80,443,9100) │ │ +│ │ (Proxy) │ │ (SSL) │ │ │ │ +│ └────┬─────┘ └───────────┘ └───────────────────────┘ │ +│ │ │ +│ ┌────┴───────────────────────────────────────────────┐ │ +│ │ Virtual Hosts (운영) │ │ +│ │ │ │ +│ │ sam.it.kr ──────────→ Next.js (PM2 cluster, :3000)│ │ +│ │ api.sam.it.kr ──────→ PHP-FPM (api pool) │ │ +│ │ admin.codebridge-x.com → PHP-FPM (admin pool) │ │ +│ │ sales.codebridge-x.com → PHP-FPM (sales pool) │ │ +│ │ codebridge-x.com ──→ 정적 랜딩페이지 │ │ +│ │ │ │ +│ │ Virtual Hosts (Stage) │ │ +│ │ │ │ +│ │ stage.sam.it.kr ────→ Next.js (PM2 fork, :3100) │ │ +│ │ stage-api.sam.it.kr → PHP-FPM (api-stage pool) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────┐ ┌────────────┐ ┌─────────────────┐ │ +│ │ MySQL 8.4 │ │ Redis │ │ Supervisor │ │ +│ │ (Master) │ │ (캐시/큐) │ │ (Queue Worker) │ │ +│ └────────────┘ └────────────┘ └─────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ node_exporter (:9100) → CI/CD Prometheus │ │ +│ └─────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────┐ +│ CI/CD서버 (2 vCPU / 8GB) │ +│ Ubuntu 24.04 / IDC 클라우드 │ +│ IP: 110.10.147.46 │ +│ │ +│ ┌──────────┐ ┌───────────┐ ┌────────────────────┐ │ +│ │ Jenkins │ │ Gitea │ │ MySQL 8.4 │ │ +│ │ │ │ │ │ (백업/슬레이브) │ │ +│ └──────────┘ └───────────┘ └────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Prometheus │ │ Grafana │ │ +│ │ (수집) │ │ (시각화) │ │ +│ └──────────────┘ └──────────────┘ │ +└───────────────────────────────────────────────────────────┘ +``` + +### 도메인 매핑 + +| 도메인 | 서비스 | 앱 | SSL | +|--------|--------|-----|-----| +| sam.it.kr | Next.js 15 (PM2 cluster) | react/ | Let's Encrypt | +| api.sam.it.kr | Laravel 12 API | api/ | Let's Encrypt | +| admin.codebridge-x.com | Laravel 12 Admin | mng/ | Let's Encrypt | +| sales.codebridge-x.com | Plain PHP (레거시) | sales/ | Let's Encrypt | +| codebridge-x.com (+ www) | 정적 랜딩페이지 | landing/ | Let's Encrypt | +| stage.sam.it.kr | Next.js 15 (PM2 fork) | react/ (stage) | Let's Encrypt | +| stage-api.sam.it.kr | Laravel 12 API (stage) | api/ (stage) | Let's Encrypt | + +### 도메인 환경 분리 + +| 서비스 | 운영 | Stage | 개발 | +|--------|------|-------|------| +| Front | sam.it.kr | stage.sam.it.kr | dev.codebridge-x.com | +| API | api.sam.it.kr | stage-api.sam.it.kr | api.codebridge-x.com | +| Admin | admin.codebridge-x.com | - | mng.codebridge-x.com | +| Sales | sales.codebridge-x.com | - | salesdev.codebridge-x.com | +| Landing | codebridge-x.com | - | - | + +### SSH 접속 정보 + +| 별칭 | IP | 용도 | 설정완료 | +|------|-----|------|---------| +| sam-prod | 211.117.60.189 | 운영서버 | ✅ | +| sam-cicd | 110.10.147.46 | CI/CD서버 | ✅ | +| sam-dev | 114.203.209.83 | 개발서버 | ✅ | + +보안: root SSH 차단, 비밀번호 로그인 차단, hskwon 키 인증 + sudo NOPASSWD + +--- + +## 2. 메모리 배분 계획 (8GB) + +| 서비스 | 할당 | 설정 | 비고 | +|--------|------|------|------| +| MySQL 8.4 | ~2GB | innodb_buffer_pool_size=2G | 메인 DB | +| Redis | ~0.5GB | maxmemory 512mb | 캐시/세션/큐 | +| PHP-FPM (API) | ~0.8GB | pm.max_children=10 | 트래픽 주력 | +| PHP-FPM (Admin) | ~0.3GB | pm.max_children=5 | 관리자용 | +| PHP-FPM (Sales) | ~0.2GB | pm.max_children=3 | 영업자용 | +| PHP-FPM (API-Stage) | ~0.2GB | pm.max_children=3 | Stage | +| Next.js 운영 (PM2 cluster×2) | ~0.6GB | max-old-space-size=256 | 무중단 배포 | +| Next.js Stage (PM2 fork×1) | ~0.15GB | max-old-space-size=128 | 트래픽 적음 | +| Supervisor (Queue Worker) | ~0.1GB | numprocs=2 | Laravel 큐 | +| Nginx | ~0.1GB | worker_connections 1024 | 리버스 프록시 | +| node_exporter | ~0.01GB | - | 모니터링 | +| OS + 여유 | ~2.9GB | 스왑 4GB | 안전 마진 | +| **합계** | **~8GB** | | **스왑 4GB 백업** | + +--- + +## 3. 설치 순서 + +### ① OS 기본 셋팅 ✅ (2026-02-23) + +```bash +# 시스템 업데이트 +sudo apt update && sudo apt upgrade -y + +# 기본 패키지 +sudo apt install -y curl wget git unzip vim htop net-tools + +# 타임존 +sudo timedatectl set-timezone Asia/Seoul + +# 스왑 4GB 설정 +sudo fallocate -l 4G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab + +# 스왑 최적화 +echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf +sudo sysctl -p + +# UFW 방화벽 +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw allow from 110.10.147.46 to any port 9100 # node_exporter (CI/CD만) +sudo ufw allow from 110.10.147.46 to any port 3306 # MySQL (CI/CD 백업용) +sudo ufw enable + +# webservice 사용자 그룹 생성 +sudo groupadd -f webservice +sudo usermod -aG webservice hskwon +sudo usermod -aG webservice www-data +sudo mkdir -p /home/webservice +sudo chown hskwon:webservice /home/webservice +sudo chmod 2775 /home/webservice # setgid +``` + +### ② MySQL 8.4 ✅ (2026-02-23) + +> **설치 완료**: MySQL 8.4.8 (GPG 키 만료 이슈로 Ubuntu keyserver 경유 설치) + +**성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`): + +```ini +[mysqld] +innodb_buffer_pool_size = 2048M +innodb_log_file_size = 512M +innodb_flush_log_at_trx_commit = 2 +max_connections = 100 +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +slow_query_log = 1 +slow_query_log_file = /var/log/mysql/slow.log +long_query_time = 2 +validate_password.policy = LOW +``` + +**DB 및 사용자:** + +```sql +-- 데이터베이스 (4개) +CREATE DATABASE sam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 앱 사용자 (codebridge) +CREATE USER 'codebridge'@'localhost' IDENTIFIED BY '<비밀번호>'; +GRANT ALL PRIVILEGES ON sam.* TO 'codebridge'@'localhost'; +GRANT ALL PRIVILEGES ON sam_stage.* TO 'codebridge'@'localhost'; +GRANT ALL PRIVILEGES ON sam_stat.* TO 'codebridge'@'localhost'; +GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; + +-- 관리자 (hskwon) - auth_socket + 패스워드 인증 +CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; +GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; + +-- CI/CD 서버 백업용 +CREATE USER 'sam_backup'@'110.10.147.46' IDENTIFIED BY '<백업용_비밀번호>'; +GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam.* TO 'sam_backup'@'110.10.147.46'; +GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam_stat.* TO 'sam_backup'@'110.10.147.46'; + +FLUSH PRIVILEGES; +``` + +> **참고**: root는 auth_socket 인증 (비밀번호 없이 `sudo mysql`로 접근) +> **MySQL 업그레이드 전략**: 운영(8.4) 안정화 확인 후 → 개발서버(8.0→8.4) 업그레이드 + +### ③ Redis 7.0.15 ✅ (2026-02-24) + +```bash +# Redis 설치 +sudo apt install -y redis-server + +# 설정 (/etc/redis/redis.conf) +# bind 127.0.0.1 ::1 +# maxmemory 512mb +# maxmemory-policy allkeys-lru +# supervised systemd + +sudo systemctl enable redis-server +sudo systemctl restart redis-server + +# 연결 테스트 +redis-cli ping # → PONG +``` + +**Laravel .env 설정:** +``` +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +QUEUE_CONNECTION=redis +CACHE_STORE=redis +SESSION_DRIVER=redis +``` + +### ④ Nginx 1.24.0 + Certbot 2.9.0 ✅ (2026-02-24) + +```bash +# Nginx 설치 +sudo apt install -y nginx + +# Certbot 설치 +sudo apt install -y certbot python3-certbot-nginx + +# 보안 스니펫 생성 (개발서버와 동일) +sudo tee /etc/nginx/snippets/security.conf > /dev/null << 'SECEOF' +# 숨김 파일 차단 (.env, .git 등) +location ~ /\. { + deny all; + access_log off; + log_not_found off; +} + +# 환경설정/백업/로그 파일 차단 +location ~* \.(env|ini|log|conf|bak|sql)$ { + deny all; + access_log off; + log_not_found off; +} + +# Git 폴더 차단 +location ~ ^/\.git { + deny all; + access_log off; + log_not_found off; +} + +# Composer, Node 패키지 등 민감 파일 차단 +location ~* /(composer\.(json|lock)|package\.json|yarn\.lock|phpunit\.xml|artisan|server\.php)$ { + deny all; + access_log off; + log_not_found off; +} +SECEOF +``` + +**기본 설정 최적화** (`/etc/nginx/nginx.conf`): + +```nginx +worker_processes auto; +events { + worker_connections 1024; +} +http { + keepalive_timeout 65; + client_max_body_size 50M; + gzip on; + gzip_types text/plain application/json application/javascript text/css; +} +``` + +### ⑤ PHP 8.4.18 + Composer 2.9.5 ✅ (2026-02-24) + +```bash +# PHP 8.4 PPA +sudo add-apt-repository ppa:ondrej/php -y +sudo apt update + +# PHP 8.4 + 확장 +sudo apt install -y \ + php8.4-fpm \ + php8.4-mysql \ + php8.4-mbstring \ + php8.4-xml \ + php8.4-curl \ + php8.4-zip \ + php8.4-gd \ + php8.4-bcmath \ + php8.4-intl \ + php8.4-redis \ + php8.4-opcache + +# Composer 설치 +curl -sS https://getcomposer.org/installer | php +sudo mv composer.phar /usr/local/bin/composer +``` + +**PHP-FPM Pool 분리 (4개):** + +```ini +# /etc/php/8.4/fpm/pool.d/api.conf +[api] +user = www-data +group = webservice +listen = /run/php/php8.4-fpm-api.sock +listen.owner = www-data +listen.group = www-data +pm = dynamic +pm.max_children = 10 +pm.start_servers = 4 +pm.min_spare_servers = 2 +pm.max_spare_servers = 6 +pm.max_requests = 500 +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[post_max_size] = 50M +php_admin_value[display_errors] = Off +``` + +```ini +# /etc/php/8.4/fpm/pool.d/admin.conf +[admin] +user = www-data +group = webservice +listen = /run/php/php8.4-fpm-admin.sock +listen.owner = www-data +listen.group = www-data +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 500 +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[post_max_size] = 50M +php_admin_value[display_errors] = Off +``` + +```ini +# /etc/php/8.4/fpm/pool.d/sales.conf +[sales] +user = www-data +group = webservice +listen = /run/php/php8.4-fpm-sales.sock +listen.owner = www-data +listen.group = www-data +pm = dynamic +pm.max_children = 3 +pm.start_servers = 1 +pm.min_spare_servers = 1 +pm.max_spare_servers = 2 +pm.max_requests = 500 +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[post_max_size] = 50M +php_admin_value[display_errors] = Off +``` + +```ini +# /etc/php/8.4/fpm/pool.d/api-stage.conf +[api-stage] +user = www-data +group = webservice +listen = /run/php/php8.4-fpm-api-stage.sock +listen.owner = www-data +listen.group = www-data +pm = dynamic +pm.max_children = 3 +pm.start_servers = 1 +pm.min_spare_servers = 1 +pm.max_spare_servers = 2 +pm.max_requests = 500 +php_admin_value[memory_limit] = 128M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[post_max_size] = 50M +php_admin_value[display_errors] = Off +``` + +```bash +# 기본 pool 제거, 분리된 pool 사용 +sudo rm /etc/php/8.4/fpm/pool.d/www.conf +sudo systemctl restart php8.4-fpm +``` + +### ⑥ Supervisor ✅ (2026-02-24) + +```bash +sudo apt install -y supervisor + +# Laravel Queue Worker 설정 +sudo tee /etc/supervisor/conf.d/sam-queue.conf > /dev/null << 'EOF' +[program:sam-queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=2 +redirect_stderr=true +stdout_logfile=/home/webservice/api/shared/storage/logs/queue-worker.log +stopwaitsecs=3600 +EOF + +sudo supervisorctl reread +sudo supervisorctl update +``` + +### ⑦ Laravel API 배포 (api.sam.it.kr) ✅ (2026-02-24) + +> **초기 배포**: 개발서버에서 tar pipe로 전체 소스 전송 (이미지/업로드 파일 포함) +> **이후 배포**: CI/CD Jenkins → Git pull + composer install + +```bash +# 디렉토리 구조 +sudo mkdir -p /home/webservice/api/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} +sudo chown -R hskwon:webservice /home/webservice/api +sudo chmod -R 2775 /home/webservice/api + +# 초기 배포 (개발서버 → 운영서버, tar pipe) +ssh sam-dev "cd /home/webservice && tar cf - --exclude='vendor' --exclude='node_modules' --exclude='.env' api/" | \ + ssh sam-prod "cd /home/webservice && tar xf -" +# 이후: CI/CD에서 git clone + releases 구조 사용 +ln -sfn /home/webservice/api/releases/20260223 /home/webservice/api/current + +# shared 심링크 +ln -sfn /home/webservice/api/shared/storage /home/webservice/api/current/storage +ln -sfn /home/webservice/api/shared/.env /home/webservice/api/current/.env + +# 의존성 설치 + 최적화 +cd /home/webservice/api/current +composer install --no-dev --optimize-autoloader +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan migrate --force +``` + +**Nginx 설정 (api.sam.it.kr):** + +```nginx +# /etc/nginx/sites-available/api.sam.it.kr +server { + listen 80; + server_name api.sam.it.kr; + + root /home/webservice/api/current/public; + index index.php; + + access_log /var/log/nginx/api.sam.it.kr.access.log; + error_log /var/log/nginx/api.sam.it.kr.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-api.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + client_max_body_size 50M; +} +``` + +### ⑦-stage Laravel API Stage (stage-api.sam.it.kr) ✅ (2026-02-24) + +```bash +# 디렉토리 (API와 동일 구조) +sudo mkdir -p /home/webservice/api-stage/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} +sudo chown -R hskwon:webservice /home/webservice/api-stage +sudo chmod -R 2775 /home/webservice/api-stage + +# 배포 절차는 API와 동일, DB는 sam_stage 사용 +``` + +**Nginx 설정 (stage-api.sam.it.kr):** + +```nginx +# /etc/nginx/sites-available/stage-api.sam.it.kr +server { + listen 80; + server_name stage-api.sam.it.kr; + + root /home/webservice/api-stage/current/public; + index index.php; + + access_log /var/log/nginx/stage-api.sam.it.kr.access.log; + error_log /var/log/nginx/stage-api.sam.it.kr.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-api-stage.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + client_max_body_size 50M; +} +``` + +### ⑧ Laravel Admin 배포 (admin.codebridge-x.com) ✅ (2026-02-24) + +```bash +sudo mkdir -p /home/webservice/mng/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}} +sudo chown -R hskwon:webservice /home/webservice/mng +sudo chmod -R 2775 /home/webservice/mng + +# 배포 절차는 API와 동일 (mng/ 저장소 기준) +``` + +**Nginx 설정 (admin.codebridge-x.com):** + +```nginx +# /etc/nginx/sites-available/admin.codebridge-x.com +server { + listen 80; + server_name admin.codebridge-x.com; + + root /home/webservice/mng/current/public; + index index.php; + + access_log /var/log/nginx/admin.codebridge-x.com.access.log; + error_log /var/log/nginx/admin.codebridge-x.com.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-admin.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + client_max_body_size 50M; +} +``` + +### ⑨ Sales 배포 (sales.codebridge-x.com) ✅ (2026-02-24) + +```bash +# 디렉토리 (레거시이므로 단순 구조) +sudo mkdir -p /home/webservice/sales +sudo chown -R hskwon:webservice /home/webservice/sales + +# 코드 배포 +cd /home/webservice +git clone sales + +# .env 설정 +cp /home/webservice/sales/.env.example /home/webservice/sales/.env +# .env: DB_HOST=localhost, DB_NAME=codebridge, DB_USER=codebridge, DB_PASS=<비밀번호> +chmod 600 /home/webservice/sales/.env +chmod 775 /home/webservice/sales/uploads +``` + +> **참고**: sales는 순수 PHP 앱. session.php → .env 로드 → lib/mydb.php DB 연결. + +**Nginx 설정 (sales.codebridge-x.com):** + +```nginx +# /etc/nginx/sites-available/sales.codebridge-x.com +server { + listen 80; + server_name sales.codebridge-x.com; + + root /home/webservice/sales; + index index.php index.html; + + access_log /var/log/nginx/sales.codebridge-x.com.access.log; + error_log /var/log/nginx/sales.codebridge-x.com.error.log; + + include /etc/nginx/snippets/security.conf; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-sales.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + # uploads PHP 실행 차단 (보안) + location ~* /uploads/.*\.php$ { + deny all; + } + + client_max_body_size 50M; +} +``` + +### ⑩ 랜딩페이지 (codebridge-x.com) ✅ (2026-02-24) + +```bash +sudo mkdir -p /home/webservice/landing +sudo chown -R hskwon:webservice /home/webservice/landing + +# 간단한 index.html 배치 +``` + +**Nginx 설정 (codebridge-x.com):** + +```nginx +# /etc/nginx/sites-available/codebridge-x.com +server { + listen 80; + server_name codebridge-x.com www.codebridge-x.com; + + root /home/webservice/landing; + index index.html; + + access_log /var/log/nginx/codebridge-x.com.access.log; + error_log /var/log/nginx/codebridge-x.com.error.log; + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +### ⑪ Node.js 22.17.1 + Next.js 15.5.12 + PM2 6.0.14 ✅ (2026-02-24) + +```bash +# Node.js 22 LTS +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs + +# PM2 설치 +sudo npm install -g pm2 + +# 운영 디렉토리 +sudo mkdir -p /home/webservice/react/{releases,shared} +sudo chown -R hskwon:webservice /home/webservice/react + +# Stage 디렉토리 +sudo mkdir -p /home/webservice/react-stage/{releases,shared} +sudo chown -R hskwon:webservice /home/webservice/react-stage + +# 초기 배포 (개발서버에서 빌드된 소스 tar pipe 전송) +ssh sam-dev "cd /home/webservice && tar cf - --exclude='node_modules' react/" | \ + ssh sam-prod "cd /home/webservice && tar xf -" +cd /home/webservice/react/current +npm install # 의존성 설치 (빌드 결과물은 이미 포함) + +# ⚠️ 빌드는 CI/CD 서버(Jenkins)에서 수행 → 운영서버로 rsync +# 운영서버에서 직접 npm run build 하지 않음 +``` + +**PM2 설정:** + +```javascript +// /home/webservice/ecosystem.config.js +module.exports = { + apps: [ + { + // 운영: cluster 모드 (무중단 배포 지원) + name: 'sam-front', + cwd: '/home/webservice/react/current', + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3000', + instances: 2, + exec_mode: 'cluster', + max_memory_restart: '300M', + env: { + NODE_ENV: 'production', + NODE_OPTIONS: '--max-old-space-size=256' + } + }, + { + // Stage: fork 모드 (리소스 절약) + name: 'sam-front-stage', + cwd: '/home/webservice/react-stage/current', + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3100', + instances: 1, + exec_mode: 'fork', + max_memory_restart: '200M', + env: { + NODE_ENV: 'production', + NODE_OPTIONS: '--max-old-space-size=128' + } + } + ] +}; +``` + +```bash +# PM2 시작 + 부팅 시 자동시작 +cd /home/webservice +pm2 start ecosystem.config.js +pm2 save +pm2 startup + +# 무중단 배포 (운영) +pm2 reload sam-front # cluster 모드에서 zero-downtime reload +``` + +**Nginx 설정 (sam.it.kr):** + +```nginx +# /etc/nginx/sites-available/sam.it.kr +upstream nextjs_prod { + server 127.0.0.1:3000; + keepalive 32; +} + +server { + listen 80; + server_name sam.it.kr; + + access_log /var/log/nginx/sam.it.kr.access.log; + error_log /var/log/nginx/sam.it.kr.error.log; + + location /_next/static/ { + alias /home/webservice/react/current/.next/static/; + expires 365d; + add_header Cache-Control "public, immutable"; + } + + location / { + proxy_pass http://nextjs_prod; + 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; + } +} +``` + +**Nginx 설정 (stage.sam.it.kr):** + +```nginx +# /etc/nginx/sites-available/stage.sam.it.kr +upstream nextjs_stage { + server 127.0.0.1:3100; +} + +server { + listen 80; + server_name stage.sam.it.kr; + + access_log /var/log/nginx/stage.sam.it.kr.access.log; + error_log /var/log/nginx/stage.sam.it.kr.error.log; + + location / { + proxy_pass http://nextjs_stage; + 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; + } +} +``` + +### ⑫ SSL 인증서 (Let's Encrypt) ✅ (2026-02-24) + +```bash +# Nginx 사이트 활성화 +sudo ln -s /etc/nginx/sites-available/sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/api.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/admin.codebridge-x.com /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/sales.codebridge-x.com /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/codebridge-x.com /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/stage.sam.it.kr /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/stage-api.sam.it.kr /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +# SSL 인증서 발급 (전체 develop@codebridge-x.com) +sudo certbot --nginx -d sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d api.sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d stage.sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d stage-api.sam.it.kr --email develop@codebridge-x.com +sudo certbot --nginx -d admin.codebridge-x.com --email develop@codebridge-x.com +sudo certbot --nginx -d sales.codebridge-x.com --email develop@codebridge-x.com +sudo certbot --nginx -d codebridge-x.com -d www.codebridge-x.com --email develop@codebridge-x.com + +# 자동 갱신 확인 +sudo certbot renew --dry-run # 7/7 success +sudo systemctl status certbot.timer +``` + +**인증서 현황 (만료일: 2026-05-24, 알림: develop@codebridge-x.com):** + +| 도메인 | 상태 | +|--------|------| +| sam.it.kr | ✅ | +| api.sam.it.kr | ✅ | +| stage.sam.it.kr | ✅ | +| stage-api.sam.it.kr | ✅ | +| admin.codebridge-x.com | ✅ | +| sales.codebridge-x.com | ✅ | +| codebridge-x.com + www | ✅ | + +**인증서 현황 (만료일: 2026-05-24):** + +| 도메인 | 알림 이메일 | +|--------|------------| +| sam.it.kr | admin@codebridge-x.com | +| api.sam.it.kr | admin@codebridge-x.com | +| stage.sam.it.kr | admin@codebridge-x.com | +| stage-api.sam.it.kr | admin@codebridge-x.com | +| admin.codebridge-x.com | develop@codebridge-x.com | +| sales.codebridge-x.com | develop@codebridge-x.com | +| codebridge-x.com + www | develop@codebridge-x.com | + +### ⑬ 모니터링 에이전트 (node_exporter 1.8.2) ✅ (2026-02-24) + +```bash +# node_exporter 설치 (Prometheus 메트릭 수집) +cd /tmp +wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz +tar xzf node_exporter-1.8.2.linux-amd64.tar.gz +sudo mv node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/ +rm -rf node_exporter-1.8.2* + +# systemd 서비스 등록 +sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'EOF' +[Unit] +Description=Node Exporter +After=network.target + +[Service] +User=nobody +ExecStart=/usr/local/bin/node_exporter +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable node_exporter +sudo systemctl start node_exporter + +# 확인: http://localhost:9100/metrics +curl -s localhost:9100/metrics | head -5 +``` + +> CI/CD 서버의 Prometheus가 `211.117.60.189:9100`을 스크래핑. +> UFW에서 CI/CD IP만 9100 포트 허용. + +### ⑭ 최종 점검 ✅ (2026-02-24) + +> 커널 리부트 완료 (6.8.0-41 → 6.8.0-100), 전체 서비스 자동 복구 확인 + +```bash +# 서비스 상태 + sudo systemctl status nginx php8.4-fpm mysql redis-server supervisor node_exporter +pm2 status + +# 방화벽 +sudo ufw status verbose + +# 메모리 +free -h + +# 디스크 +df -h + +# 포트 +sudo ss -tlnp + +# Redis +redis-cli ping + +# SSL 인증서 +sudo certbot certificates +``` + +--- + +## 4. 디렉토리 구조 + +``` +/home/webservice/ +├── api/ # Laravel API (운영) +│ ├── current → releases/20260223/ +│ ├── releases/ +│ │ └── 20260223/ +│ └── shared/ +│ ├── .env +│ └── storage/ +│ ├── app/public/ +│ ├── framework/{cache,sessions,views}/ +│ └── logs/ +│ +├── api-stage/ # Laravel API (Stage) +│ ├── current → releases/... +│ ├── releases/ +│ └── shared/ (.env, storage/) +│ +├── mng/ # Laravel Admin +│ ├── current → releases/... +│ ├── releases/ +│ └── shared/ (.env, storage/) +│ +├── sales/ # Plain PHP (레거시) +│ ├── .env +│ ├── index.php +│ ├── lib/ +│ ├── uploads/ +│ └── ... +│ +├── react/ # Next.js (운영) +│ ├── current → releases/... +│ ├── releases/ +│ └── shared/ (.env.local) +│ +├── react-stage/ # Next.js (Stage) +│ ├── current → releases/... +│ ├── releases/ +│ └── shared/ (.env.local) +│ +├── landing/ # 정적 랜딩페이지 +│ └── index.html +│ +└── ecosystem.config.js # PM2 설정 (운영 + Stage) +``` + +--- + +## 5. DB 마이그레이션 ✅ (2026-02-24) + +### SAM 메인 DB (sam) + +```bash +# 개발서버에서 덤프 +ssh sam-dev "mysqldump -u codebridge -p sam > /tmp/sam_dump.sql" +scp sam-dev:/tmp/sam_dump.sql /tmp/ + +# 운영서버로 전송 + 임포트 +scp /tmp/sam_dump.sql sam-prod:/tmp/ +ssh sam-prod "sudo mysql sam < /tmp/sam_dump.sql" +ssh sam-prod "rm /tmp/sam_dump.sql" +``` + +### 통계 DB (sam_stat) + +```bash +ssh sam-dev "mysqldump -u codebridge -p sam_stat > /tmp/sam_stat_dump.sql" +scp sam-dev:/tmp/sam_stat_dump.sql /tmp/ +scp /tmp/sam_stat_dump.sql sam-prod:/tmp/ +ssh sam-prod "sudo mysql sam_stat < /tmp/sam_stat_dump.sql" +ssh sam-prod "rm /tmp/sam_stat_dump.sql" +``` + +> **참고**: chandj DB는 운영서버로 이관하지 않음. Sales는 codebridge DB 사용. + +--- + +## 6. 개발서버 현황 (참고) + +| 항목 | 개발서버 | 운영서버 | +|------|----------|---------| +| OS | Ubuntu 24.04.2 | Ubuntu 24.04 (kernel 6.8.0-100) | +| CPU/RAM | 2C / 3.8GB (스왑 없음) | 2C / 8GB + 스왑 4GB | +| PHP | 8.4.15 (+ 5.6, 7.3) | 8.4.18 | +| MySQL | **8.0.45** | **8.4.8** | +| Node.js | 22.17.1 | 22.17.1 | +| Nginx | 1.24.0 | 1.24.0 | +| Redis | - | 7.0.15 (로컬, 512mb) | +| Composer | - | 2.9.5 | +| PHP-FPM | 단일 www pool | 4개 분리 (api/admin/sales/stage) | +| PM2 | fork ×1 (:3001) | 6.0.14 cluster ×2 (:3000) | +| Supervisor | - | queue worker ×2 (redis) | +| UFW | **비활성** | 활성 (22/80/443/9100/3306) | +| fail2ban | - | ✅ | +| 디렉토리 | /home/webservice/ | /home/webservice/ | + +--- + +## 7. Claude Code SSH 원격 셋팅 + +### 완료 항목 ✅ + +- [x] SSH 키 인증 (sam-prod, sam-cicd, sam-dev) +- [x] sudo NOPASSWD (3개 서버 모두) +- [x] ~/.ssh/config 등록 (sam-prod, sam-cicd, sam-dev) +- [x] root SSH 차단 + 비밀번호 로그인 차단 (sam-prod, sam-cicd) + +### Claude Code 실행 방식 + +```bash +# 단일 명령 +ssh sam-prod "sudo apt update" + +# 파일 전송 +scp local_file sam-prod:/remote/path/ + +# 설정 파일 생성 (tee + heredoc) +ssh sam-prod "sudo tee /etc/nginx/sites-available/sam.it.kr > /dev/null << 'EOF' +...설정 내용... +EOF" +``` + +### 제약사항 +- 인터랙티브 명령 불가 (vim, mysql_secure_installation 등) +- 명령 타임아웃: 최대 10분 +- `DEBIAN_FRONTEND=noninteractive` 사용 + +--- + +## 8. 보안 체크리스트 + +- [x] SSH 키 인증만 허용 (비밀번호 로그인 비활성화) +- [x] root SSH 로그인 비활성화 +- [x] UFW 방화벽 활성화 +- [x] MySQL root 원격 접근 차단 (auth_socket) +- [x] MySQL 앱 사용자 최소 권한 (codebridge) +- [x] .env 파일 권한 600 (api, admin, sales 모두) +- [x] storage/ 디렉토리 권한 775 +- [x] Nginx security.conf 스니펫 적용 +- [x] PHP display_errors = Off (모든 pool) +- [x] Laravel APP_DEBUG=false, APP_ENV=production +- [x] Sales uploads/ PHP 실행 차단 +- [x] Certbot 자동 갱신 확인 (7/7 dry-run success) +- [x] fail2ban 설치 (SSH 브루트포스 방지) +- [x] Redis bind 127.0.0.1 (외부 접근 차단) +- [x] node_exporter CI/CD IP만 허용 (UFW) + +--- + +## 9. 모니터링 아키텍처 + +``` +운영서버 CI/CD서버 +┌─────────────────┐ ┌────────────────────┐ +│ node_exporter │──(:9100)──→ │ Prometheus │ +│ (CPU/RAM/Disk) │ │ (수집 + 알림) │ +└─────────────────┘ └────────┬───────────┘ + │ + ┌────────▼───────────┐ + │ Grafana │ + │ (대시보드 시각화) │ + └────────────────────┘ +``` + +| 서버 | 프로그램 | 역할 | 메모리 | +|------|----------|------|--------| +| 운영 | node_exporter | 메트릭 수집 (CPU/RAM/디스크/네트워크) | ~10MB | +| CI/CD | Prometheus | 메트릭 저장 + 알림 규칙 | ~200MB | +| CI/CD | Grafana | 대시보드 시각화 | ~100MB | + +--- + +## 10. 향후 계획 + +- [ ] CI/CD 서버 셋팅 (Jenkins + Gitea + Prometheus + Grafana) +- [ ] 자동 배포 파이프라인 (Jenkins → 운영서버) +- [ ] Stage React 배포 (react-stage, PM2 fork :3100) +- [ ] API CORS/Sanctum 설정 (운영 도메인 반영) +- [ ] DB 복제 (Master → Slave) +- [ ] 개발서버 MySQL 8.0 → 8.4 업그레이드 (운영 안정화 후) +- [ ] Docker 기반 전환 검토 +- [ ] 백업 자동화 (DB 일일 백업 → CI/CD 서버) +- [ ] React → Vercel 이전 검토 \ No newline at end of file