2편 전체 목차
🐧 Linux 기본 설정 & 홈랩 추천 설정
① 서버 첫 부팅 후 필수 초기 설정
# 1. 시스템 업데이트 & 필수 패키지 apt update && apt upgrade -y apt install -y curl wget git vim htop tmux tree net-tools build-essential ca-certificates gnupg lsb-release software-properties-common apt-transport-https unzip p7zip-full rsync ncdu duf # 2. 타임존 설정 timedatectl set-timezone Asia/Seoul timedatectl status # 확인 # 3. 로케일 설정 locale-gen ko_KR.UTF-8 update-locale LANG=ko_KR.UTF-8 echo 'LANG=ko_KR.UTF-8' >> /etc/environment # 4. 스왑 설정 (RAM 128GB면 불필요하지만 안전망으로) fallocate -l 8G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab # 5. 커널 파라미터 최적화 (AI 서버 권장) cat >> /etc/sysctl.conf << 'EOF' # 네트워크 최적화 net.core.rmem_max = 134217728 net.core.wmem_max = 134217728 net.ipv4.tcp_rmem = 4096 87380 134217728 net.ipv4.tcp_wmem = 4096 65536 134217728 net.core.netdev_max_backlog = 5000 net.ipv4.tcp_congestion_control = bbr # 파일 디스크립터 fs.file-max = 2097152 # 메모리 과할당 허용 (Docker 컨테이너용) vm.overcommit_memory = 1 vm.max_map_count = 262144 EOF sysctl -p
② 사용자 & SSH 보안 초기 설정
# 1. 관리자 계정 생성 (root 직접 사용 금지) adduser evan # 비밀번호 설정 usermod -aG sudo evan usermod -aG docker evan # Docker 그룹 추가 # 2. SSH 키 기반 인증 설정 (클라이언트 PC에서) ssh-keygen -t ed25519 -C "homelab-r730" ssh-copy-id [email protected] # 서버 IP # 3. SSH 보안 강화 (/etc/ssh/sshd_config) sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config sed -i 's/#PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/#PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config # SSH 포트 변경 권장 (기본 22 → 비표준 포트) sed -i 's/#Port 22/Port 22222/' /etc/ssh/sshd_config systemctl restart sshd # 4. sudo 타임아웃 조정 (기본 15분 → 30분) echo 'Defaults timestamp_timeout=30' >> /etc/sudoers.d/custom
③ 스토리지 마운트 설정 (/mnt/data)
# 추가 HDD/SSD 파티션 생성 (예: /dev/sdb)
fdisk /dev/sdb # n → p → Enter × 3 → w
mkfs.ext4 /dev/sdb1
blkid /dev/sdb1 # UUID 확인
# /etc/fstab에 영구 마운트 등록
echo 'UUID=xxxx-xxxx /mnt/data ext4 defaults,noatime 0 2' >> /etc/fstab
mount -a
df -h /mnt/data # 확인
# AI 서버 표준 디렉토리 구조
mkdir -p /mnt/data/{01_ai,02_automation,03_database,04_web,05_monitor}
mkdir -p /mnt/data/01_ai/{ollama,openwebui,comfyui,dify,whisper,kokoro}
mkdir -p /mnt/data/02_automation/{n8n,n8n-db}
mkdir -p /mnt/data/03_database/{qdrant,postgres,redis,meilisearch}
mkdir -p /mnt/data/05_monitor/{grafana,prometheus}
# 소유권 설정
chown -R evan:evan /mnt/data
chmod -R 755 /mnt/data
④ 홈랩 추천 툴 설치
# lazydocker — 터미널 Docker TUI curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_lzd.sh | bash # ctop — 컨테이너 리소스 모니터링 wget https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-linux-amd64 -O /usr/local/bin/ctop chmod +x /usr/local/bin/ctop # duf — 디스크 사용량 시각화 apt install duf -y # btop — htop 대체 리소스 모니터 apt install btop -y # bat — cat 대체 (신택스 하이라이팅) apt install bat -y && ln -s /usr/bin/batcat /usr/local/bin/bat # fd — find 대체 apt install fd-find -y && ln -s /usr/bin/fdfind /usr/local/bin/fd # ripgrep — grep 대체 apt install ripgrep -y # tmux 설정 (~/.tmux.conf) cat > ~/.tmux.conf << 'EOF' set -g default-terminal "screen-256color" set -g mouse on set -g history-limit 10000 bind r source-file ~/.tmux.conf \; display "Reloaded!" set -g prefix C-a unbind C-b bind C-a send-prefix EOF
⑤ 시스템 자동화 & 유지보수 스크립트
# /usr/local/bin/homelab-maintenance.sh
cat > /usr/local/bin/homelab-maintenance.sh << 'EOF'
#!/bin/bash
LOG=/var/log/homelab-maintenance.log
echo "=== $(date) ===" >> $LOG
# Docker 이미지·컨테이너 정리
docker system prune -f >> $LOG 2>&1
# apt 자동 업데이트 (보안 패치만)
apt-get update -qq && apt-get upgrade -y --only-upgrade >> $LOG 2>&1
# 디스크 사용량 리포트
df -h /mnt/data >> $LOG
# 컨테이너 상태 확인
docker ps --format "table {{.Names}}\t{{.Status}}" >> $LOG
echo "유지보수 완료" >> $LOG
EOF
chmod +x /usr/local/bin/homelab-maintenance.sh
# Cron 등록 — 매주 일요일 새벽 3시
echo '0 3 * * 0 /usr/local/bin/homelab-maintenance.sh' | crontab -
# 로그 로테이션 설정
cat > /etc/logrotate.d/homelab << 'EOF'
/var/log/homelab-*.log {
weekly
rotate 4
compress
missingok
notifempty
}
EOF
① root 직접 로그인 금지 — sudo 계정만 사용
② SSH 키 인증만 허용 — 비밀번호 인증 비활성화
③ /mnt/data/ 아래 모든 데이터 집중 — 디스크 교체 시 재마운트만으로 복구
④ tmux 세션 유지 — SSH 끊겨도 작업 유지
⑤ lazydocker로 컨테이너 상태 빠른 확인
AI 서버 보안 아키텍처 설계 원칙
AI 서버는 LLM API, 이미지 생성 엔진, 자동화 파이프라인 등 고가의 연산 자원이 집중된 시스템입니다. 보안 레이어를 단계별로 설계하지 않으면 한 번의 침입으로 모든 서비스가 무력화됩니다. 핵심 원칙은 외부에는 최소한만 노출하고, 내부에서는 서비스 간 격리를 유지하는 것입니다.
🏗️ AI 서버 보안 레이어 구조
| 레이어 | 도구 | 역할 | 위치 |
|---|---|---|---|
| L1. 네트워크 경계 | UFW / iptables | 포트 기반 접근 제어 | OS 수준 |
| L2. 리버스 프록시 | Nginx / Caddy | SSL 종료, 도메인 라우팅, Rate Limit | Docker 컨테이너 |
| L3. CDN / 터널 | Cloudflare Tunnel | DDoS 방어, IP 숨김, 글로벌 캐시 | 클라우드 엣지 |
| L4. VPN | Tailscale / WireGuard | 신뢰 디바이스 전용 내부망 | OS 수준 |
| L5. 애플리케이션 | Open WebUI Auth | 사용자 인증, 역할 분리 | 앱 수준 |
| L6. 침입 탐지 | Fail2ban | 브루트포스 자동 차단 | OS 수준 |
| L7. 감사 로그 | auditd / syslog | 이상 행동 추적 | OS 수준 |
🌐 접근 방식별 보안 설계 선택
| 시나리오 | 추천 구성 | 특징 |
|---|---|---|
| 개인 전용 (내부망만) | Tailscale VPN만 | 공인 IP 불필요. 가장 안전. 외부 접근은 VPN으로만 |
| 팀 공유 (소규모) | Cloudflare Tunnel + Zero Trust | Google/Microsoft 계정 인증. 포트포워딩 불필요 |
| 도메인 공개 서비스 | Nginx + Let's Encrypt + Cloudflare | SSL 자동 갱신. DDoS 방어. Rate Limiting 필수 |
| 기업/멀티유저 | Nginx + LDAP/SSO + Cloudflare + VPN | 전사 계정 연동. 완전한 감사 로그 |
UFW 방화벽 완전 설정
UFW(Uncomplicated Firewall)는 Ubuntu에서 가장 많이 사용하는 방화벽 관리 도구입니다. AI 서버에서 주의해야 할 점은 Docker가 UFW를 우회해 직접 iptables를 수정한다는 것입니다. 이를 방지하지 않으면 UFW에서 포트를 막아도 Docker 서비스는 외부에 노출됩니다.
기본 설정에서 docker run -p 8080:8080을 실행하면 UFW가 포트 8080을 막아도 외부에서 접근됩니다. Docker 데몬이 iptables를 직접 수정하기 때문입니다. 아래 설정으로 반드시 차단해야 합니다.
## ── Step 1: Docker UFW 우회 차단 ───────────────── # /etc/docker/daemon.json 수정 sudo tee /etc/docker/daemon.json << 'EOF' { "iptables": false, "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } } EOF # Docker 재시작 sudo systemctl restart docker ## ── Step 2: UFW 기본 정책 설정 ─────────────────── sudo ufw default deny incoming sudo ufw default allow outgoing ## ── Step 3: 허용 포트 설정 ─────────────────────── # SSH (변경한 포트로) sudo ufw allow 2222/tcp comment 'SSH' # HTTP/HTTPS (Nginx 리버스 프록시) sudo ufw allow 80/tcp comment 'HTTP' sudo ufw allow 443/tcp comment 'HTTPS' # 내부 네트워크에서만 직접 접근 허용 (예: 192.168.1.0/24) sudo ufw allow from 192.168.1.0/24 to any port 3000 comment 'Open WebUI LAN' sudo ufw allow from 192.168.1.0/24 to any port 11434 comment 'Ollama API LAN' sudo ufw allow from 192.168.1.0/24 to any port 5678 comment 'n8n LAN' sudo ufw allow from 192.168.1.0/24 to any port 8188 comment 'ComfyUI LAN' # Tailscale VPN 네트워크에서 전체 허용 sudo ufw allow in on tailscale0 ## ── Step 4: UFW 활성화 ─────────────────────────── sudo ufw enable sudo ufw status verbose ## ── Step 5: Docker 네트워크용 iptables 규칙 추가 ─ # /etc/ufw/after.rules 파일 끝에 추가 sudo tee -a /etc/ufw/after.rules << 'EOF' # Docker 컨테이너 간 통신 허용 *filter :DOCKER-USER - [0:0] -A DOCKER-USER -i docker0 -j ACCEPT -A DOCKER-USER -o docker0 -j ACCEPT -A DOCKER-USER -i br-+ -j ACCEPT -A DOCKER-USER -o br-+ -j ACCEPT COMMIT EOF sudo ufw reload
📊 AI 서버 포트 보안 정책표
| 포트 | 서비스 | 외부 허용 | LAN 허용 | VPN 허용 |
|---|---|---|---|---|
| 2222 | SSH | ✅ (Key 인증만) | ✅ | ✅ |
| 80/443 | Nginx HTTPS | ✅ | ✅ | ✅ |
| 3000 | Open WebUI | ❌ (Nginx 통해서만) | ✅ | ✅ |
| 11434 | Ollama API | ❌ | ✅ | ✅ |
| 5678 | n8n | ❌ | ✅ | ✅ |
| 8188 | ComfyUI | ❌ | ✅ | ✅ |
| 6333 | Qdrant | ❌ | ❌ | ✅ (관리자만) |
| 5432 | PostgreSQL | ❌ | ❌ | ✅ (관리자만) |
Nginx 리버스 프록시 + SSL 완전 설정
Nginx 리버스 프록시는 모든 외부 요청을 443(HTTPS)으로 받아 내부 Docker 서비스로 라우팅합니다. 이 구조의 핵심 장점은 내부 포트를 완전히 숨기면서 서비스별 서브도메인, Rate Limiting, 인증 레이어를 중앙에서 관리할 수 있다는 것입니다.
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80" # HTTP → HTTPS 리다이렉트
- "443:443" # HTTPS
- "81:81" # 관리 UI (내부망만 허용)
volumes:
- nginx_data:/data
- nginx_letsencrypt:/etc/letsencrypt
networks:
- ai_net
volumes:
nginx_data:
nginx_letsencrypt:
networks:
ai_net:
external: true
name: ai_network⚙️ 수동 Nginx 설정 (고급 — Rate Limiting 포함)
# Rate Limiting 정의 limit_req_zone $binary_remote_addr zone=ai_api:10m rate=30r/m; limit_req_zone $binary_remote_addr zone=webui:10m rate=60r/m; # Open WebUI (ai.yourdomain.com) server { listen 443 ssl http2; server_name ai.yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; # 보안 헤더 add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; add_header Strict-Transport-Security "max-age=31536000" always; # Rate Limiting 적용 limit_req zone=webui burst=20 nodelay; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # WebSocket 지원 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_read_timeout 300s; # LLM 긴 응답 타임아웃 proxy_send_timeout 300s; client_max_body_size 50M; # 파일 업로드 크기 } } # n8n (n8n.yourdomain.com) server { listen 443 ssl http2; server_name n8n.yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; location / { proxy_pass http://localhost:5678; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 86400s; # 웹훅 롱폴링 } } # HTTP → HTTPS 리다이렉트 server { listen 80; server_name _; return 301 https://$host$request_uri; }
🔑 Let's Encrypt SSL 자동 발급 & 갱신
# Certbot 설치 sudo apt install -y certbot python3-certbot-nginx # 와일드카드 인증서 발급 (Cloudflare DNS 사용 시) sudo apt install -y python3-certbot-dns-cloudflare # Cloudflare API 토큰 설정 sudo tee /root/.cloudflare.ini << 'EOF' dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN EOF sudo chmod 600 /root/.cloudflare.ini # 와일드카드 인증서 발급 (*.yourdomain.com) sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /root/.cloudflare.ini \ -d yourdomain.com \ -d '*.yourdomain.com' # 자동 갱신 확인 sudo certbot renew --dry-run # cron 자동 갱신 등록 (이미 자동 설정됨, 확인만) sudo systemctl status certbot.timer
Cloudflare Tunnel — 공인 IP 없이 HTTPS 공개
Cloudflare Tunnel은 포트 포워딩도, 공인 IP도 필요 없이 AI 서버를 안전하게 인터넷에 공개하는 가장 강력한 방법입니다. 서버가 Cloudflare 엣지와 아웃바운드 터널을 유지하기 때문에 DDoS 공격으로부터 서버 IP가 완전히 숨겨집니다.
# cloudflared 설치 curl -L --output cloudflared.deb \ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb sudo dpkg -i cloudflared.deb # Cloudflare 계정 인증 cloudflared tunnel login # 터널 생성 cloudflared tunnel create ai-server # → UUID가 출력됩니다. 예: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx # 터널 설정 파일 작성 mkdir -p ~/.cloudflared tee ~/.cloudflared/config.yml << 'EOF' tunnel: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx credentials-file: /root/.cloudflared/a1b2c3d4-xxxx.json ingress: - hostname: ai.yourdomain.com service: http://localhost:3000 originRequest: connectTimeout: 30s noTLSVerify: false - hostname: n8n.yourdomain.com service: http://localhost:5678 - hostname: comfy.yourdomain.com service: http://localhost:8188 - service: http_status:404 EOF # DNS 레코드 자동 등록 cloudflared tunnel route dns ai-server ai.yourdomain.com cloudflared tunnel route dns ai-server n8n.yourdomain.com # 시스템 서비스로 등록 (부팅 시 자동 시작) sudo cloudflared service install sudo systemctl start cloudflared sudo systemctl status cloudflared
🔐 Cloudflare Zero Trust Access — 추가 인증 레이어
Cloudflare Zero Trust를 사용하면 도메인에 접근하기 전에 Google/Microsoft/GitHub 계정 인증을 추가할 수 있습니다. Open WebUI의 자체 로그인 전에 한 번 더 인증하는 구조로 최강의 보안을 제공합니다.
ai.yourdomain.com 입력Tailscale VPN — 프라이빗 AI 전용 내부망
Tailscale은 WireGuard 기반의 메시 VPN으로, 어디서든 내 AI 서버에 안전하게 접근할 수 있는 가장 간단한 솔루션입니다. 설정 시간 5분, 공인 IP 불필요, 포트 포워딩 불필요. 특히 신뢰하는 디바이스만 AI 서버 내부 포트에 직접 접근해야 할 때 Cloudflare Tunnel과 병행해서 사용합니다.
# Tailscale 설치 curl -fsSL https://tailscale.com/install.sh | sh # 로그인 (브라우저 인증 링크 출력) sudo tailscale up --advertise-exit-node # Tailscale IP 확인 (100.x.x.x 형태) tailscale ip -4 # Docker 컨테이너에서 Tailscale 통해 접근 허용 # UFW에 tailscale0 인터페이스 전체 허용 sudo ufw allow in on tailscale0 sudo ufw allow out on tailscale0 # 상태 확인 tailscale status
📱 모바일 접근 설정
- ✓iOS/Android에 Tailscale 앱 설치 → 같은 계정으로 로그인
- ✓앱에서 AI 서버 Tailscale IP(100.x.x.x)로 접속 → 내부망과 동일하게 동작
- ✓모바일 브라우저에서
http://100.x.x.x:3000→ Open WebUI 접속 - iTailscale Magic DNS 활성화 시
http://ai-server로도 접속 가능
- Cloudflare Tunnel — 팀원·외부 협력사에게 AI 서비스 공개 시. 브라우저만 있으면 접근 가능
- Tailscale — 본인만의 완전한 프라이빗 접근. Qdrant, PostgreSQL 같은 DB 직접 접근 시
- 두 가지 병행 — Open WebUI는 Cloudflare로 팀 공개, Ollama API는 Tailscale로 개발자 전용
Fail2ban · SSH 강화 · 서버 해딩 보안
🛡️ SSH 키 인증 강제화 (비밀번호 로그인 차단)
# SSH 키 생성 (클라이언트에서 실행) ssh-keygen -t ed25519 -C "ai-server-access" ssh-copy-id -p 2222 user@ai-server-ip # SSH 보안 설정 강화 (/etc/ssh/sshd_config) sudo tee -a /etc/ssh/sshd_config << 'EOF' # 비밀번호 인증 완전 차단 PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin no # 연결 타임아웃 ClientAliveInterval 300 ClientAliveCountMax 3 LoginGraceTime 30 # MaxAuth 시도 제한 MaxAuthTries 3 MaxSessions 5 # 특정 사용자만 허용 AllowUsers your_username EOF sudo systemctl restart ssh # 설정 테스트 (반드시 새 터미널에서 확인 후 기존 세션 닫기) ssh -p 2222 your_username@ai-server-ip
🚫 Fail2ban 설치 & AI 서버 특화 설정
sudo apt install -y fail2ban
# AI 서버 특화 Fail2ban 설정
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600 # 차단 시간 (초)
findtime = 600 # 탐지 시간 윈도우
maxretry = 5 # 허용 실패 횟수
backend = systemd
banaction = ufw
[sshd]
enabled = true
port = 2222
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400 # SSH는 24시간 차단
[nginx-http-auth]
enabled = true
logpath = /var/log/nginx/error.log
[nginx-limit-req]
enabled = true
logpath = /var/log/nginx/error.log
[open-webui]
enabled = true
port = 3000,443
logpath = /var/lib/docker/containers/*/open-webui*.log
maxretry = 10
bantime = 1800
filter = open-webui
EOF
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status🔄 자동 보안 업데이트 설정
sudo apt install -y unattended-upgrades sudo dpkg-reconfigure --priority=low unattended-upgrades # 보안 업데이트만 자동 적용 설정 sudo tee /etc/apt/apt.conf.d/20auto-upgrades << 'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::AutocleanInterval "7"; APT::Periodic::Unattended-Upgrade "1"; EOF # 업데이트 로그 확인 sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
✅ 보안 설정 최종 체크리스트
- ✓UFW 활성화 + Docker UFW 우회 차단 완료
- ✓SSH 포트 변경 + 키 인증만 허용 + 비밀번호 로그인 차단
- ✓Nginx HTTPS + WebSocket + Rate Limiting 설정 완료
- ✓Let's Encrypt SSL 와일드카드 인증서 발급 + 자동 갱신
- ✓Cloudflare Tunnel 설정 + Zero Trust Access 인증 추가
- ✓Tailscale VPN 설치 + 내부 서비스 전용 접근 구성
- ✓Fail2ban 브루트포스 자동 차단 활성화
- ✓자동 보안 업데이트 활성화
- !Open WebUI Admin Panel → Default User Role → Pending 설정 (외부 공개 시 필수)
- !정기적으로
sudo fail2ban-client status sshd로 차단 현황 모니터링
✅ 글 2 완료! AI 서버 보안 레이어가 완성됐습니다. 글 3에서는 ComfyUI와 Stable Diffusion A1111을 설치해 무제한 AI 이미지 생성 환경을 구축합니다. FLUX.1, SDXL, Lora, ControlNet까지 전부 다룹니다.
3-2-1 백업 전략 설계 & 복구 목표 설정
백업은 "있으면 좋은 것"이 아니라 반드시 테스트된 복구 계획이어야 합니다. "백업이 있다"는 것과 "복구가 된다"는 것은 다릅니다. 많은 팀이 백업은 하지만 복구 테스트를 하지 않아 실제 장애 시 데이터를 잃습니다.
📐 RTO & RPO — 복구 목표 설정
| 지표 | 의미 | 개인 홈랩 | 팀 운영 | 기업 서비스 |
|---|---|---|---|---|
| RTO (Recovery Time Objective) | 장애 발생 후 서비스 재개까지 허용 시간 | 24시간 | 4시간 | 1시간 이하 |
| RPO (Recovery Point Objective) | 데이터 손실을 허용하는 최대 시간 범위 | 24시간 | 6시간 | 1시간 이하 |
📦 3-2-1 백업 원칙 적용
| 원칙 | 의미 | AI 서버 적용 방법 |
|---|---|---|
| 3개의 복사본 | 원본 + 백업 2개 | 운영 데이터 + 로컬 NAS + 클라우드 |
| 2가지 미디어 | 서로 다른 저장 매체 | NVMe SSD (운영) + HDD NAS (백업) |
| 1개 오프사이트 | 물리적으로 분리된 장소 | Cloudflare R2 / Backblaze B2 (클라우드) |
📊 AI 서버 데이터 중요도 분류
| 데이터 | 위치 | 중요도 | 백업 주기 | 보존 기간 |
|---|---|---|---|---|
| Qdrant 벡터 DB | qdrant_storage 볼륨 | 최고 | 6시간마다 | 30일 |
| Open WebUI 대화·설정 | open_webui_data 볼륨 | 최고 | 매일 | 90일 |
| PostgreSQL (n8n·Dify) | postgres_data 볼륨 | 최고 | 매일 (WAL 연속) | 30일 |
| Ollama 모델 파일 | ollama_data 볼륨 | 높음 | 주간 | 영구 |
| n8n 워크플로우 JSON | n8n_data 볼륨 | 높음 | 매일 | 90일 |
| ComfyUI 워크플로우 | comfyui_models 볼륨 | 보통 | 주간 | 90일 |
| 이미지 생성 결과물 | comfyui_output 볼륨 | 낮음 | 월간 | 1년 |
Docker 볼륨 자동 백업 완전 구현
#!/bin/bash # AI 서버 완전 자동 백업 스크립트 # cron: 0 2 * * * /home/ubuntu/ai-server/scripts/backup.sh >> /var/log/ai-backup.log 2>&1 set -euo pipefail BACKUP_BASE="/mnt/nas/ai-server-backup" # NAS 마운트 경로 DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="$BACKUP_BASE/$DATE" LOG_FILE="/var/log/ai-backup.log" SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK" RETENTION_DAYS=30 # 30일 이전 백업 자동 삭제 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; } notify_slack() { curl -s -X POST "$SLACK_WEBHOOK" \ -H 'Content-type: application/json' \ -d "{\"text\": \"$1\"}" > /dev/null } log "=== AI 서버 백업 시작 ===" mkdir -p "$BACKUP_DIR" ## ── 핵심 볼륨 목록 ─────────────────────────────── declare -A CRITICAL_VOLUMES=( ["qdrant_storage"]="Qdrant 벡터 DB" ["open_webui_data"]="Open WebUI 데이터" ["postgres_data"]="PostgreSQL" ["n8n_data"]="n8n 워크플로우" ) declare -A NORMAL_VOLUMES=( ["ollama_data"]="Ollama 모델" ["grafana_data"]="Grafana 대시보드" ["pipelines_data"]="Open WebUI 파이프라인" ) TOTAL_SIZE=0 BACKUP_COUNT=0 ERRORS=0 backup_volume() { local VOL_NAME=$1 local VOL_DESC=$2 local COMPRESS=${3:-"gz"} # gz=빠름, bz2=작음 log " 백업: $VOL_DESC ($VOL_NAME)..." # 볼륨 존재 확인 if ! docker volume inspect "$VOL_NAME" &>/dev/null; then log " ⚠️ 볼륨 없음, 건너뜀: $VOL_NAME" return 0 fi local OUTPUT_FILE="$BACKUP_DIR/${VOL_NAME}_${DATE}.tar.${COMPRESS}" # Alpine 컨테이너로 볼륨 압축 백업 docker run --rm \ -v "${VOL_NAME}:/data:ro" \ -v "${BACKUP_DIR}:/backup" \ alpine:latest \ sh -c "cd /data && tar c${COMPRESS}f /backup/$(basename $OUTPUT_FILE) . 2>/dev/null" \ && { local SIZE=$(du -sh "$OUTPUT_FILE" | cut -f1) log " ✅ 완료: $VOL_DESC → $SIZE" BACKUP_COUNT=$((BACKUP_COUNT + 1)) } || { log " ❌ 실패: $VOL_DESC" ERRORS=$((ERRORS + 1)) } } ## ── PostgreSQL 핫 덤프 (서비스 중단 없이) ──────── backup_postgres() { log " 백업: PostgreSQL (pg_dump)..." local PG_BACKUP="$BACKUP_DIR/postgres_dump_${DATE}.sql.gz" docker exec n8n_postgres pg_dumpall \ -U aiserver 2>/dev/null \ | gzip > "$PG_BACKUP" \ && log " ✅ PostgreSQL 덤프 완료: $(du -sh $PG_BACKUP | cut -f1)" \ || { log " ❌ PostgreSQL 덤프 실패"; ERRORS=$((ERRORS + 1)); } } ## ── Qdrant 스냅샷 (API 사용) ───────────────────── backup_qdrant_snapshots() { log " 백업: Qdrant 스냅샷..." local QDRANT_URL="http://localhost:6333" local SNAP_DIR="$BACKUP_DIR/qdrant_snapshots" mkdir -p "$SNAP_DIR" # 컬렉션 목록 조회 COLLECTIONS=$(curl -s "${QDRANT_URL}/collections" \ -H "api-key: your_qdrant_key" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print('\n'.join([c['name'] for c in d['result']['collections']]))" 2>/dev/null) for COL in $COLLECTIONS; do # 스냅샷 생성 SNAP_INFO=$(curl -s -X POST \ "${QDRANT_URL}/collections/${COL}/snapshots" \ -H "api-key: your_qdrant_key" \ -H "Content-Type: application/json") SNAP_NAME=$(echo "$SNAP_INFO" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d['result']['name'])" 2>/dev/null) if [ -n "$SNAP_NAME" ]; then # 스냅샷 파일 다운로드 curl -s "${QDRANT_URL}/collections/${COL}/snapshots/${SNAP_NAME}" \ -H "api-key: your_qdrant_key" \ -o "$SNAP_DIR/${COL}_${DATE}.snapshot" \ && log " ✅ Qdrant $COL 스냅샷 완료" fi done } ## ── 설정 파일 백업 ──────────────────────────────── backup_configs() { log " 백업: 설정 파일..." local CONFIG_DIR="$BACKUP_DIR/configs" mkdir -p "$CONFIG_DIR" # 중요 설정 파일 모음 for f in \ ~/ai-server/core/docker-compose.yml \ ~/ai-server/security/docker-compose.yml \ ~/ai-server/monitoring/docker-compose.yml \ ~/ai-server/monitoring/prometheus.yml \ ~/ai-server/monitoring/alert_rules.yml \ /etc/nginx/conf.d/ \ ~/.cloudflared/config.yml; do [ -e "$f" ] && cp -r "$f" "$CONFIG_DIR/" 2>/dev/null done log " ✅ 설정 파일 백업 완료" } ## ── 실행 ────────────────────────────────────────── log "핵심 볼륨 백업..." for VOL in "${!CRITICAL_VOLUMES[@]}"; do backup_volume "$VOL" "${CRITICAL_VOLUMES[$VOL]}" "gz" done # 일요일만 일반 볼륨 백업 (대용량 절약) if [ "$(date +%u)" = "7" ]; then log "일반 볼륨 백업 (주간)..." for VOL in "${!NORMAL_VOLUMES[@]}"; do backup_volume "$VOL" "${NORMAL_VOLUMES[$VOL]}" "gz" done fi backup_postgres backup_qdrant_snapshots backup_configs ## ── 백업 검증 ───────────────────────────────────── log "백업 파일 검증..." for f in "$BACKUP_DIR"/*.tar.gz; do [ -f "$f" ] || continue if ! gzip -t "$f" 2>/dev/null; then log " ❌ 손상된 백업: $(basename $f)" ERRORS=$((ERRORS + 1)) fi done ## ── 오래된 백업 삭제 ────────────────────────────── log "오래된 백업 정리 (${RETENTION_DAYS}일 이전)..." find "$BACKUP_BASE" -maxdepth 1 -type d -mtime "+${RETENTION_DAYS}" -exec rm -rf {} \; 2>/dev/null || true ## ── 클라우드 오프사이트 동기화 ─────────────────── if command -v rclone &>/dev/null; then log "클라우드 동기화 (Backblaze B2)..." rclone sync "$BACKUP_DIR" "b2:ai-server-backup/$DATE" \ --transfers=4 --quiet \ && log " ✅ 클라우드 동기화 완료" \ || log " ⚠️ 클라우드 동기화 실패 (로컬 백업은 유지)" fi ## ── 완료 알림 ───────────────────────────────────── TOTAL_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1) STATUS_EMOJI=$( [ "$ERRORS" -eq 0 ] && echo "✅" || echo "⚠️" ) MSG="${STATUS_EMOJI} AI 서버 백업 완료\n날짜: ${DATE}\n성공: ${BACKUP_COUNT}건\n오류: ${ERRORS}건\n크기: ${TOTAL_SIZE}\n경로: ${BACKUP_DIR}" notify_slack "$MSG" log "=== 백업 완료: ${BACKUP_COUNT}건 성공, ${ERRORS}건 오류, ${TOTAL_SIZE} ===" [ "$ERRORS" -gt 0 ] && exit 1 || exit 0
데이터베이스 핫 백업 — PostgreSQL & Qdrant
Docker 볼륨 전체를 복사하는 방법은 DB가 실행 중일 때 파일이 불완전 상태일 수 있습니다. 데이터베이스는 반드시 DB 자체 백업 도구를 사용해야 데이터 무결성이 보장됩니다.
## ── Primary DB 설정 (주 서버) ───────────────────── # docker-compose 환경변수에 추가 services: postgresql: image: pgvector/pgvector:pg16 command: > postgres -c wal_level=replica -c max_wal_senders=3 -c wal_keep_size=1GB -c archive_mode=on -c archive_command='cp %p /var/lib/postgresql/wal_archive/%f' environment: - POSTGRES_USER=aiserver - POSTGRES_PASSWORD=secure_password - POSTGRES_DB=aiserver_main # 복제 사용자 생성 (컨테이너 내부에서 실행) docker exec -it postgresql psql -U aiserver -c " CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'repl_password'; " ## ── 간단한 Point-in-Time 복구 설정 ─────────────── # 기본 백업 (베이스라인) docker exec postgresql pg_basebackup \ -U aiserver \ -D /var/lib/postgresql/base_backup \ -Ft -z -P # 특정 시점으로 복구 (장애 발생 시) docker exec postgresql psql -U aiserver -c " SELECT pg_wal_replay_resume(); " ## ── Barman으로 PostgreSQL 자동 백업 관리 ───────── docker run -d \ --name barman \ --network ai_network \ -v barman_data:/var/lib/barman \ -e BARMAN_SERVER_NAME=aiserver_pg \ -e BARMAN_CONNSTRING="host=postgresql user=barman" \ ubazzio/barman:latest
import httpx, asyncio, os
from datetime import datetime
from pathlib import Path
QDRANT_URL = "http://192.168.1.253:6333"
QDRANT_KEY = "your_qdrant_key"
SNAP_DIR = Path("/mnt/nas/ai-server-backup/qdrant")
async def create_all_snapshots() -> dict:
"""모든 컬렉션 스냅샷 생성"""
headers = {"api-key": QDRANT_KEY}
async with httpx.AsyncClient(timeout=300) as client:
# 컬렉션 목록
cols = await client.get(f"{QDRANT_URL}/collections", headers=headers)
collections = [c["name"] for c in cols.json()["result"]["collections"]]
results = {}
for col in collections:
print(f" 스냅샷 생성: {col}")
snap = await client.post(
f"{QDRANT_URL}/collections/{col}/snapshots",
headers=headers
)
snap_name = snap.json()["result"]["name"]
# 스냅샷 다운로드
SNAP_DIR.mkdir(parents=True, exist_ok=True)
output = SNAP_DIR / f"{col}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.snapshot"
download = await client.get(
f"{QDRANT_URL}/collections/{col}/snapshots/{snap_name}",
headers=headers
)
output.write_bytes(download.content)
file_size = output.stat().st_size / 1024 / 1024
results[col] = {"file": str(output), "size_mb": round(file_size, 2)}
print(f" ✅ {col}: {file_size:.1f}MB")
return results
async def restore_from_snapshot(collection: str, snapshot_file: str):
"""스냅샷에서 컬렉션 복구"""
headers = {"api-key": QDRANT_KEY}
async with httpx.AsyncClient(timeout=600) as client:
# 기존 컬렉션 삭제 (있을 경우)
await client.delete(
f"{QDRANT_URL}/collections/{collection}",
headers=headers
)
# 스냅샷 업로드 및 복구
with open(snapshot_file, "rb") as f:
resp = await client.post(
f"{QDRANT_URL}/collections/{collection}/snapshots/upload",
headers=headers,
files={"snapshot": f}
)
if resp.status_code == 200:
print(f"✅ {collection} 복구 완료")
else:
print(f"❌ 복구 실패: {resp.text}")
# 전체 스냅샷 생성
results = asyncio.run(create_all_snapshots())
print(f"\n✅ 총 {len(results)}개 컬렉션 백업 완료")
# 복구 예시
# asyncio.run(restore_from_snapshot("ai_knowledge_base", "/mnt/nas/.../snapshot"))재해복구 플레이북 & 자동 복구 스크립트
🚨 장애 시나리오별 대응 타임라인
ssh admin@ai-server 접속 시도~/ai-server/scripts/health_check.sh 실행 → 증상 파악. 하드웨어 vs 소프트웨어 vs 데이터 문제 구분#!/bin/bash
# AI 서버 재해복구 스크립트
# 사용법: ./disaster_recovery.sh [scenario]
# scenario: full | services | database | qdrant | configs
BACKUP_BASE="/mnt/nas/ai-server-backup"
LOG_FILE="/var/log/ai-recovery.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
## 최신 백업 자동 탐색
find_latest_backup() {
ls -td "$BACKUP_BASE"/[0-9]* 2>/dev/null | head -1
}
LATEST_BACKUP=$(find_latest_backup)
if [ -z "$LATEST_BACKUP" ]; then
log "❌ 백업을 찾을 수 없습니다: $BACKUP_BASE"
exit 1
fi
log "사용할 백업: $LATEST_BACKUP"
## 시나리오별 복구 함수
recover_services() {
log "🔄 서비스 재시작 복구..."
# 모든 AI 서버 서비스 중지 후 재시작
for svc in core security automation database monitoring; do
COMPOSE="$HOME/ai-server/$svc/docker-compose.yml"
[ -f "$COMPOSE" ] && {
docker compose -f "$COMPOSE" down --remove-orphans
docker compose -f "$COMPOSE" up -d
log " ✅ $svc 재시작"
}
done
}
recover_volume() {
local VOL_NAME=$1
local BACKUP_FILE=$(ls "$LATEST_BACKUP"/${VOL_NAME}_*.tar.gz 2>/dev/null | head -1)
[ -z "$BACKUP_FILE" ] && { log " ❌ 백업 없음: $VOL_NAME"; return 1; }
log " 복구: $VOL_NAME from $(basename $BACKUP_FILE)"
# 볼륨 재생성
docker volume rm "$VOL_NAME" 2>/dev/null || true
docker volume create "$VOL_NAME"
# 압축 복구
docker run --rm \
-v "${VOL_NAME}:/data" \
-v "${LATEST_BACKUP}:/backup:ro" \
alpine:latest \
sh -c "cd /data && tar xzf /backup/$(basename $BACKUP_FILE) ." \
&& log " ✅ $VOL_NAME 복구 완료" \
|| { log " ❌ $VOL_NAME 복구 실패"; return 1; }
}
recover_database() {
log "🔄 PostgreSQL 복구..."
DUMP_FILE=$(ls "$LATEST_BACKUP"/postgres_dump_*.sql.gz 2>/dev/null | head -1)
[ -z "$DUMP_FILE" ] && { log " ❌ DB 덤프 없음"; return 1; }
recover_volume "postgres_data"
# 컨테이너 재시작 후 덤프 복구
docker compose -f ~/ai-server/automation/docker-compose.yml up -d postgresql
sleep 10
zcat "$DUMP_FILE" | docker exec -i postgresql psql -U aiserver
log " ✅ PostgreSQL 복구 완료"
}
## 복구 시나리오 실행
SCENARIO=${1:-"full"}
log "=== 재해복구 시작: $SCENARIO ==="
case "$SCENARIO" in
"services")
recover_services
;;
"database")
recover_database
;;
"qdrant")
recover_volume "qdrant_storage"
docker compose -f ~/ai-server/database/docker-compose.yml restart qdrant
;;
"full")
log "전체 복구 시작..."
for VOL in open_webui_data n8n_data postgres_data qdrant_storage grafana_data; do
recover_volume "$VOL" || true
done
recover_services
log "✅ 전체 복구 완료"
;;
*)
log "알 수 없는 시나리오: $SCENARIO"
echo "사용법: $0 [full|services|database|qdrant|configs]"
exit 1
;;
esac
# 복구 후 헬스체크
log "헬스체크 실행..."
sleep 30
~/ai-server/scripts/health_check.sh
log "=== 재해복구 완료 ==="고가용성 구성 — Nginx HA & 자동 페일오버
단일 서버 장애 시 서비스 중단 없이 자동으로 백업 서버로 전환하는 구조입니다. 두 대의 서버에 Keepalived와 Nginx를 설정해 가상 IP(VIP)가 항상 살아있는 서버로 자동 이동합니다.
sudo apt install -y keepalived # Primary 서버 설정 sudo tee /etc/keepalived/keepalived.conf << 'EOF' global_defs { router_id AI_SERVER_PRIMARY enable_script_security } vrrp_script check_nginx { script "/bin/curl -sf http://localhost:3000/health || exit 1" interval 5 # 5초마다 헬스체크 weight -20 # 실패 시 우선순위 20 감소 fall 2 # 2번 실패 시 FAULT rise 1 # 1번 성공 시 복구 } vrrp_instance VI_1 { state MASTER # 백업 서버는 BACKUP으로 변경 interface ens3 # 네트워크 인터페이스명 virtual_router_id 51 # 두 서버 동일 값 priority 110 # 백업 서버는 100으로 낮게 advert_int 1 authentication { auth_type PASS auth_pass ai_server_ha_secret } virtual_ipaddress { 192.168.1.200/24 # 가상 IP (어느 서버가 살아도 이 IP로 접속) } track_script { check_nginx } notify_master "/etc/keepalived/notify.sh MASTER" notify_backup "/etc/keepalived/notify.sh BACKUP" notify_fault "/etc/keepalived/notify.sh FAULT" } EOF # 페일오버 알림 스크립트 sudo tee /etc/keepalived/notify.sh << 'SCRIPT' #!/bin/bash STATE=$1 WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK" curl -s -X POST "$WEBHOOK" \ -H 'Content-type: application/json' \ -d "{\"text\": \"🔄 AI 서버 HA 상태 변경: $STATE\n서버: $(hostname)\n시간: $(date)\"}" SCRIPT sudo chmod +x /etc/keepalived/notify.sh sudo systemctl enable keepalived sudo systemctl start keepalived
완전한 HA는 하드웨어 2배 비용이 듭니다. 현실적 대안: 주 서버는 단일 구성, 백업 서버는 NAS나 저사양 미니PC로 Open WebUI + 외부 API 연결만 유지하는 Warm Standby 구성이 비용 효율적입니다. 주 서버 장애 시 Cloudflare Tunnel 대상을 백업으로 변경하면 10분 이내 서비스 재개 가능합니다.
서버 이전 & 마이그레이션 완전 가이드
GPU 업그레이드, 새 서버 도입, 클라우드 이전 등 서버를 바꿔야 하는 상황을 위한 무중단 마이그레이션 절차입니다. 올바른 순서를 지키면 데이터 손실 없이 수 시간 이내 이전이 완료됩니다.
#!/bin/bash
# AI 서버 이전 절차 스크립트
# 기존 서버: OLD_SERVER=192.168.1.100
# 신규 서버: NEW_SERVER=192.168.1.200
OLD_SERVER="192.168.1.100"
NEW_SERVER="192.168.1.200"
TRANSFER_DIR="/tmp/ai_migration_$(date +%Y%m%d)"
SSH_KEY="~/.ssh/ai_server_key"
log() { echo "[$(date '+%H:%M:%S')] $1"; }
## ── Phase 1: 신규 서버 준비 확인 ─────────────────
log "=== Phase 1: 신규 서버 사전 확인 ==="
ssh -i "$SSH_KEY" "ubuntu@$NEW_SERVER" << 'REMOTE'
docker --version || { echo "Docker 미설치"; exit 1; }
nvidia-smi || echo "GPU 없음 (정상이면 계속)"
df -h /
free -h
echo "✅ 신규 서버 준비 확인"
REMOTE
## ── Phase 2: 기존 서버에서 최종 백업 ────────────
log "=== Phase 2: 최종 전체 백업 ==="
ssh -i "$SSH_KEY" "ubuntu@$OLD_SERVER" << 'REMOTE'
mkdir -p /tmp/ai_migration
# 핵심 볼륨 내보내기
for VOL in open_webui_data n8n_data postgres_data qdrant_storage ollama_data grafana_data; do
if docker volume inspect "$VOL" &>/dev/null; then
echo "내보내기: $VOL"
docker run --rm \
-v "${VOL}:/data:ro" \
-v "/tmp/ai_migration:/backup" \
alpine:latest \
tar czf "/backup/${VOL}.tar.gz" -C /data .
echo " 완료: $(du -sh /tmp/ai_migration/${VOL}.tar.gz | cut -f1)"
fi
done
# 설정 파일 패키징
tar czf /tmp/ai_migration/configs.tar.gz \
~/ai-server \
~/.cloudflared 2>/dev/null || true
echo "✅ 백업 완료: $(du -sh /tmp/ai_migration)"
REMOTE
## ── Phase 3: 신규 서버로 데이터 전송 ────────────
log "=== Phase 3: 데이터 전송 (rsync) ==="
rsync -avzP --progress \
-e "ssh -i $SSH_KEY" \
"ubuntu@${OLD_SERVER}:/tmp/ai_migration/" \
"ubuntu@${NEW_SERVER}:${TRANSFER_DIR}/"
## ── Phase 4: 신규 서버에서 복원 ─────────────────
log "=== Phase 4: 신규 서버 복원 ==="
ssh -i "$SSH_KEY" "ubuntu@$NEW_SERVER" << REMOTE
# 설정 파일 복원
mkdir -p ~/ai-server ~/.cloudflared
tar xzf ${TRANSFER_DIR}/configs.tar.gz -C / 2>/dev/null || true
# Docker 볼륨 복원
for ARCHIVE in ${TRANSFER_DIR}/*.tar.gz; do
VOL_NAME=\$(basename "\$ARCHIVE" .tar.gz)
[[ "\$VOL_NAME" == "configs" ]] && continue
echo "복원: \$VOL_NAME"
docker volume create "\$VOL_NAME"
docker run --rm \
-v "\${VOL_NAME}:/data" \
-v "${TRANSFER_DIR}:/backup:ro" \
alpine:latest \
tar xzf "/backup/\${VOL_NAME}.tar.gz" -C /data
echo " 완료"
done
# 서비스 시작
cd ~/ai-server
for svc in core security automation database monitoring; do
[ -f "\$svc/docker-compose.yml" ] && \
docker compose -f "\$svc/docker-compose.yml" up -d
sleep 3
done
echo "✅ 복원 완료"
REMOTE
## ── Phase 5: 검증 ─────────────────────────────────
log "=== Phase 5: 서비스 검증 ==="
sleep 30
ssh -i "$SSH_KEY" "ubuntu@$NEW_SERVER" \
"~/ai-server/scripts/health_check.sh"
## ── Phase 6: Cloudflare Tunnel 대상 변경 ────────
log "=== Phase 6: DNS/Tunnel 전환 ==="
log "⚡ 수동 작업 필요:"
log " Cloudflare Zero Trust → Networks → Tunnels"
log " 기존 터널 비활성화 → 신규 서버 터널 활성화"
log " 또는 Tailscale IP 업데이트"
log "=== ✅ 마이그레이션 완료! ==="
log "다음 작업:"
log " 1. 신규 서버에서 모든 서비스 정상 확인"
log " 2. 사용자 테스트 완료 후 기존 서버 종료"
log " 3. 기존 서버 데이터 30일 후 삭제"- Ollama 모델 재다운로드 vs 이전 — 모델 파일이 수십 GB라 전송보다 신규 서버에서 다시 pull이 빠를 수 있음. 인터넷 속도와 전송 속도 비교 후 결정.
- 환경변수 및 시크릿 분리 확인 — docker-compose.yml에 API Key나 비밀번호가 하드코딩되어 있으면 이전 과정에서 노출 위험. .env 파일로 분리 관리 필수.
- Qdrant 인덱스 재구축 필요 — 볼륨 복원 후 대용량 컬렉션은 최적화 필요:
POST /collections/{name}/index - TailScale/Cloudflare 설정 갱신 — 신규 서버의 새 IP나 터널 ID로 반드시 업데이트. 기존 클라이언트의 접속 설정도 함께 변경.
- 매일 새벽 2시 자동 백업 → NAS + Backblaze B2 오프사이트 동시 저장
- 장애 발생 시 30분 이내 원클릭 복구 스크립트로 서비스 재개
- PostgreSQL WAL 연속 아카이빙으로 어느 시점으로도 복구 가능
- Qdrant 컬렉션별 스냅샷으로 벡터 DB 안전하게 보호
- Keepalived HA 구성으로 주 서버 장애 시 자동 페일오버
- 마이그레이션 스크립트로 새 서버 이전 시 4시간 이내 완료
🎉 추가편 F까지 완성으로 AI 서버 시리즈 전체 완결!
기본 5편 + 추가 6편 총 11편의 콘텐츠로 최종 5편으로 통합하여 AI 서버 구축·운영·활용·최적화·보안·백업의 모든 것을 커버했습니다. 각 글을 시리즈로 연결하고 실제 URL을 업데이트하면 완성된 마스터 가이드가 됩니다.
