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 agibop # 비밀번호 설정 usermod -aG sudo agibop usermod -aG docker agibop # 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 agibop:agibop /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를 직접 수정하기 때문입니다.
이 방식(Docker의 iptables 자동 관리를 완전히 끄는 것)은 더 강력하지만, Docker의 자동 NAT·포트포워딩 규칙까지 직접 관리해야 해서 더 복잡합니다. 실제로 검증한 더 간단한 방법은 Docker의 기본 동작은 그대로 두고, UFW에 Docker 브리지 대역(172.16.0.0/12)에 대한 규칙을 추가하는 것입니다 — 바로 아래에서 다룹니다.
## ── UFW 기본 정책 설정 ─────────────────── sudo ufw default deny incoming sudo ufw default allow outgoing ## ── 허용 포트 설정 ─────────────────────── sudo ufw allow 22/tcp comment 'SSH' # 내부 네트워크 전체 허용 sudo ufw allow from 192.168.1.0/24 comment 'LAN' ## ── 🚨 가장 중요한 규칙 — 실제 트러블슈팅에서 확인 ── # Ollama는 네이티브 설치라 호스트에서 직접 돕니다. # Dify·Firecrawl 같은 Docker 컨테이너가 호스트의 Ollama(11434)에 # 접근하려면, Docker 기본 브리지 대역(172.16.0.0/12) 전체를 허용해야 합니다. # LAN(192.168.1.0/24)만 허용하면 Docker 컨테이너→Ollama 연결이 # 타임아웃됩니다 — 실제로 이 문제로 여러 시간 디버깅했던 부분입니다. sudo ufw allow from 172.16.0.0/12 to any port 11434 comment 'Docker → Ollama' sudo ufw --force enable sudo ufw status verbose
📊 AI 서버 포트 보안 정책표 (검증된 실제 포트 기준)
| 포트 | 서비스 | 외부 허용 | LAN 허용 |
|---|---|---|---|
| 22 | SSH | ✅ (Key 인증 권장) | ✅ |
| 3001 | Open WebUI | ❌ (Cloudflare Tunnel 통해서만) | ✅ |
| 11434 | Ollama API (네이티브) | ❌ | ✅ + Docker 172.16.0.0/12 |
| 5678 | n8n | ❌ (Cloudflare Tunnel 통해서만) | ✅ |
| 8188 | ComfyUI | ❌ | ✅ |
| 3002/8443 | Dify (nginx) | ❌ (Cloudflare Tunnel 통해서만) | ✅ |
| 3003 | Firecrawl | ❌ | ✅ |
| 15432~15434, 16333, 16379~16380 | 각 DB (127.0.0.1 바인딩) | ❌ | ❌ (로컬호스트만) |
Nginx 리버스 프록시 + SSL 완전 설정 ⚠️ 검증 전
직접 Nginx + Let's Encrypt를 운영하는 대신, Cloudflare Tunnel을 써서 공인 IP·포트 개방·인증서 관리 없이 HTTPS를 적용했습니다. 다음 섹션(s04)에서 실제 검증된 방법을 다룹니다. 아래는 직접 Nginx를 운영하고 싶을 때 참고용입니다.
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 CLI로 config.yml을 직접 작성하는 전통적인 방법입니다. 실제로는 Cloudflare 대시보드 → Networks → Tunnels → Public Hostname 추가 UI에서 클릭만으로 같은 결과를 얻을 수 있어 더 간단합니다 (Service Type: HTTP, URL: localhost:포트 입력).
# 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:3001 originRequest: connectTimeout: 30s noTLSVerify: false - hostname: n8n.yourdomain.com service: http://localhost:5678 - hostname: dify.yourdomain.com service: http://localhost:3002 - 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
n8n의 Telegram Webhook처럼 브라우저가 아닌 자동화 트래픽이 들어오는 경로는, Tunnel을 제대로 설정해도 Cloudflare의 봇 차단 기능(Bot Fight Mode)에 막혀 403 Forbidden이 날 수 있습니다. 증상은 텔레그램의 getWebhookInfo에서 "Wrong response from the webhook: 403 Forbidden"로 나타납니다.
1. Security → Bots → "Bot Fight Mode" 끄기 2. 그래도 안 되면 Security → WAF → Custom rules → Create rule 조건: URI Path contains "/webhook/" 동작: Skip (Bot Fight Mode 포함 전부 체크) 3. ⭐ 가장 흔히 놓치는 부분 — 만든 규칙을 우선순위 1번으로 올리기 (규칙을 추가만 하고 순서를 안 올리면 다른 규칙이 먼저 평가되어 계속 막힘)
🔐 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년 |
실전 백업 스크립트 — Postgres 3종 + 설정 + 볼륨
실제로 R730에서 매일 새벽 cron으로 돌리고 있는 백업 스크립트입니다. 한 단계가 실패해도 나머지는 계속 진행되도록 설계했고, 파이프(|) 사용 시 앞단 실패를 놓치지 않도록 pipefail도 챙겼습니다 (실제로 이 둘을 안 챙겼다가 디버깅에 시간을 많이 썼습니다).
cat > /mnt/data/backups/backup.sh << 'EOF'
#!/bin/bash
# set -e를 안 씁니다 — 한 단계(예: firecrawl) 실패해도
# 나머지(n8n·dify·qdrant 등) 백업은 계속 진행되도록 의도적으로 뺐습니다.
STAGE=/mnt/data/backups/staging
FAILED=()
mkdir -p "$STAGE"/{postgres,configs,volumes}
echo "[$(date)] 백업 시작..."
# 실패해도 다음 줄로 넘어가게 하는 헬퍼 함수
run_step() {
local desc="$1"; shift
if ! "$@"; then
echo "[$(date)] ⚠️ 실패: $desc"
FAILED+=("$desc")
fi
}
# ── ① Postgres 3종 덤프 (매번 덮어쓰기, 하나 실패해도 나머지 계속) ──
# set -o pipefail: pg_dump가 실패해도 gzip만 성공하면 "성공"으로 오판하는 것 방지
run_step "n8n DB" bash -c 'set -o pipefail; docker exec n8n-pg-db pg_dump -U n8n n8n | gzip > "'"$STAGE"'/postgres/n8n.sql.gz"'
run_step "dify DB" bash -c 'set -o pipefail; docker exec dify-pg-db pg_dump -U postgres dify | gzip > "'"$STAGE"'/postgres/dify.sql.gz"'
run_step "firecrawl DB" bash -c 'set -o pipefail; docker exec firecrawl-pg-db pg_dump -U firecrawl firecrawl | gzip > "'"$STAGE"'/postgres/firecrawl.sql.gz"'
# ── ② 핵심 설정 파일 (.env 4종 + n8n 암호화 키) ─────────
run_step ".env 파일들" tar -czf "$STAGE/configs/env-files.tar.gz" \
/mnt/data/.env \
/mnt/data/01_ai/dify/docker/.env \
/mnt/data/02_automation/firecrawl/.env
run_step "n8n 데이터" tar -czf "$STAGE/configs/n8n-data.tar.gz" /mnt/data/02_automation/n8n/data
# ── ③ Qdrant 벡터DB + Open WebUI + ComfyUI 워크플로우 ──
run_step "Qdrant" tar -czf "$STAGE/volumes/dify-qdrant.tar.gz" \
/mnt/data/01_ai/dify/docker/volumes/qdrant
run_step "Open WebUI" tar -czf "$STAGE/volumes/open-webui.tar.gz" /mnt/data/01_ai/open-webui/data
run_step "ComfyUI 워크플로우" tar -czf "$STAGE/volumes/comfyui-workflows.tar.gz" /mnt/data/01_ai/comfyui/workflows
# ── ④ NAS로 "최신 상태" 동기화 (버전관리는 안 함 — 하이퍼백업이 함) ──
run_step "NAS 동기화" rsync -az --delete "$STAGE/" [email protected]:/volume1/backups/r730/latest/
if [ ${#FAILED[@]} -eq 0 ]; then
echo "[$(date)] ✅ 백업 완료 (전체 성공) → NAS /volume1/backups/r730/latest/"
else
echo "[$(date)] ⚠️ 백업 완료 (일부 실패: ${FAILED[*]}) — 위 항목들 따로 점검 필요"
fi
EOF
chmod +x /mnt/data/backups/backup.shdocker exec ... pg_dump | gzip > file 형태의 파이프는 기본적으로 마지막 명령어(gzip)의 종료코드만 봅니다. pg_dump가 실패해도 gzip은 빈 입력을 받아 "성공"으로 끝나버려서, 실패를 못 감지하고 빈 백업 파일만 쌓이는 사고가 날 수 있습니다. set -o pipefail을 반드시 같이 쓰세요.
위 스크립트는 NAS로 rsync --delete(최신 상태 1개만 유지)하고, 버전별 보관(일/주/월)은 Synology NAS의 Hyper Backup이 그 NAS 폴더를 대상으로 별도 스케줄로 처리합니다. R730 스크립트와 NAS Hyper Backup, 2단계로 나눈 이유는 "최신 동기화"와 "버전 보관"의 책임을 분리하기 위해서입니다.
재해복구 플레이북 & 자동 복구 스크립트 ⚠️ 검증 전
아래의 정교한 자동 복구 시나리오 대신, 실제로는 ① NAS Hyper Backup에서 원하는 날짜 버전 복원 → ② R730에서 NAS의 해당 폴더를 rsync로 다시 가져오기 → ③ docker exec -i [컨테이너] psql ... < 덤프파일로 DB 복원 3단계로 충분했습니다. 복잡한 자동화보다 수동 3단계가 더 안전하고 명확했습니다.
🚨 장애 시나리오별 대응 타임라인
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.250/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.250
OLD_SERVER="192.168.1.100"
NEW_SERVER="192.168.1.250"
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편의 콘텐츠로 AI 서버 구축·운영·활용·최적화·보안·백업의 모든 것을 커버했습니다. 각 글을 시리즈로 연결하고 실제 URL을 업데이트하면 완성된 마스터 가이드가 됩니다.



