Files
sam-docs/deploys/production-server-setup.md
권혁성 a3ef921a4f docs: 운영/CI/CD 서버 셋팅 가이드 추가
- 운영서버(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 <noreply@anthropic.com>
2026-02-24 10:02:48 +09:00

1110 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 운영서버 셋팅 가이드
> 작성일: 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 <repo_url> 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 이전 검토