AI 서버 활용 및 튜닝 가이드 2편: 철통 보안 아키텍처 및 3-2-1 백업전략

-

편수2편 / 5편
난이도⭐⭐ 중급
다루는 내용Linux 기본설정 · UFW · Nginx · Cloudflare · Tailscale · 백업 · DR
섹션 수13개 섹션
🐧 Linux 초기설정 🔒 보안 아키텍처 🌐 Nginx · Cloudflare 🛡️ Tailscale VPN 💾 3-2-1 백업 재해복구 · HA · 서버이전
2편은 서버를 외부 위협으로부터 지키고, 장애로부터 빠르게 복구하는 방법을 다룹니다. Ubuntu 서버 첫 부팅 직후 해야 할 Linux 기본 설정과 홈랩 추천 설정부터 시작해, UFW 방화벽·Nginx SSL·Cloudflare Tunnel·Tailscale VPN으로 보안 레이어를 쌓습니다. 이어서 3-2-1 백업 전략, Docker 볼륨 자동 백업, PostgreSQL·Qdrant 핫 백업, 재해복구 플레이북까지 — 보안과 안정성을 한 편에 완전히 정복합니다.
SECTION01

🐧 Linux 기본 설정 & 홈랩 추천 설정

① 서버 첫 부팅 후 필수 초기 설정

Ubuntu 24.04 초기 설정 (root 또는 sudo)
# 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 보안 초기 설정

관리자 계정 & 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

⑤ 시스템 자동화 & 유지보수 스크립트

Cron 기반 자동 유지보수 설정
# /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로 컨테이너 상태 빠른 확인

SECTION02

AI 서버 보안 아키텍처 설계 원칙

AI 서버는 LLM API, 이미지 생성 엔진, 자동화 파이프라인 등 고가의 연산 자원이 집중된 시스템입니다. 보안 레이어를 단계별로 설계하지 않으면 한 번의 침입으로 모든 서비스가 무력화됩니다. 핵심 원칙은 외부에는 최소한만 노출하고, 내부에서는 서비스 간 격리를 유지하는 것입니다.

🏗️ AI 서버 보안 레이어 구조

레이어도구역할위치
L1. 네트워크 경계UFW / iptables포트 기반 접근 제어OS 수준
L2. 리버스 프록시Nginx / CaddySSL 종료, 도메인 라우팅, Rate LimitDocker 컨테이너
L3. CDN / 터널Cloudflare TunnelDDoS 방어, IP 숨김, 글로벌 캐시클라우드 엣지
L4. VPNTailscale / WireGuard신뢰 디바이스 전용 내부망OS 수준
L5. 애플리케이션Open WebUI Auth사용자 인증, 역할 분리앱 수준
L6. 침입 탐지Fail2ban브루트포스 자동 차단OS 수준
L7. 감사 로그auditd / syslog이상 행동 추적OS 수준

🌐 접근 방식별 보안 설계 선택

시나리오추천 구성특징
개인 전용 (내부망만)Tailscale VPN만공인 IP 불필요. 가장 안전. 외부 접근은 VPN으로만
팀 공유 (소규모)Cloudflare Tunnel + Zero TrustGoogle/Microsoft 계정 인증. 포트포워딩 불필요
도메인 공개 서비스Nginx + Let's Encrypt + CloudflareSSL 자동 갱신. DDoS 방어. Rate Limiting 필수
기업/멀티유저Nginx + LDAP/SSO + Cloudflare + VPN전사 계정 연동. 완전한 감사 로그
SECTION03

UFW 방화벽 완전 설정

UFW(Uncomplicated Firewall)는 Ubuntu에서 가장 많이 사용하는 방화벽 관리 도구입니다. AI 서버에서 주의해야 할 점은 Docker가 UFW를 우회해 직접 iptables를 수정한다는 것입니다. 이를 방지하지 않으면 UFW에서 포트를 막아도 Docker 서비스는 외부에 노출됩니다.

🚨
Docker + UFW 치명적 보안 구멍 — 반드시 패치 필요

기본 설정에서 docker run -p 8080:8080을 실행하면 UFW가 포트 8080을 막아도 외부에서 접근됩니다. Docker 데몬이 iptables를 직접 수정하기 때문입니다.

⚠️ 검증 전 — daemon.json의 "iptables": false 설정

이 방식(Docker의 iptables 자동 관리를 완전히 끄는 것)은 더 강력하지만, Docker의 자동 NAT·포트포워딩 규칙까지 직접 관리해야 해서 더 복잡합니다. 실제로 검증한 더 간단한 방법은 Docker의 기본 동작은 그대로 두고, UFW에 Docker 브리지 대역(172.16.0.0/12)에 대한 규칙을 추가하는 것입니다 — 바로 아래에서 다룹니다.

bash — 검증된 방화벽 설정 (마스터 가이드 기준)
## ── 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 허용
22SSH✅ (Key 인증 권장)
3001Open WebUI❌ (Cloudflare Tunnel 통해서만)
11434Ollama API (네이티브)✅ + Docker 172.16.0.0/12
5678n8n❌ (Cloudflare Tunnel 통해서만)
8188ComfyUI
3002/8443Dify (nginx)❌ (Cloudflare Tunnel 통해서만)
3003Firecrawl
15432~15434, 16333, 16379~16380각 DB (127.0.0.1 바인딩)❌ (로컬호스트만)
SECTION04

Nginx 리버스 프록시 + SSL 완전 설정 ⚠️ 검증 전

💡
실제로는 Cloudflare Tunnel로 대체했습니다

직접 Nginx + Let's Encrypt를 운영하는 대신, Cloudflare Tunnel을 써서 공인 IP·포트 개방·인증서 관리 없이 HTTPS를 적용했습니다. 다음 섹션(s04)에서 실제 검증된 방법을 다룹니다. 아래는 직접 Nginx를 운영하고 싶을 때 참고용입니다.

Nginx 리버스 프록시는 모든 외부 요청을 443(HTTPS)으로 받아 내부 Docker 서비스로 라우팅합니다. 이 구조의 핵심 장점은 내부 포트를 완전히 숨기면서 서비스별 서브도메인, Rate Limiting, 인증 레이어를 중앙에서 관리할 수 있다는 것입니다.

📁 ~/ai-server/security/docker-compose.yml
yaml — Nginx Proxy Manager (노코드 관리 UI)
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 포함)

nginx — /etc/nginx/conf.d/ai-server.conf
# 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 자동 발급 & 갱신

bash — Certbot 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
SECTION05

Cloudflare Tunnel — 공인 IP 없이 HTTPS 공개

Cloudflare Tunnel은 포트 포워딩도, 공인 IP도 필요 없이 AI 서버를 안전하게 인터넷에 공개하는 가장 강력한 방법입니다. 서버가 Cloudflare 엣지와 아웃바운드 터널을 유지하기 때문에 DDoS 공격으로부터 서버 IP가 완전히 숨겨집니다.

💡
더 간단한 방법 — Cloudflare 대시보드에서 UI로 설정

아래는 cloudflared CLI로 config.yml을 직접 작성하는 전통적인 방법입니다. 실제로는 Cloudflare 대시보드 → Networks → Tunnels → Public Hostname 추가 UI에서 클릭만으로 같은 결과를 얻을 수 있어 더 간단합니다 (Service Type: HTTP, URL: localhost:포트 입력).

bash — Cloudflare Tunnel 설치 및 설정 (CLI 방식)
# 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
🚨
실제로 겪은 문제 — Cloudflare가 자체적으로 403 차단 (텔레그램 웹훅 등)

n8n의 Telegram Webhook처럼 브라우저가 아닌 자동화 트래픽이 들어오는 경로는, Tunnel을 제대로 설정해도 Cloudflare의 봇 차단 기능(Bot Fight Mode)에 막혀 403 Forbidden이 날 수 있습니다. 증상은 텔레그램의 getWebhookInfo에서 "Wrong response from the webhook: 403 Forbidden"로 나타납니다.

해결 — Cloudflare 대시보드
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의 자체 로그인 전에 한 번 더 인증하는 구조로 최강의 보안을 제공합니다.

1
Cloudflare Zero Trust 대시보드 접속
one.dash.cloudflare.com → Access → Applications → Add an Application
2
Self-hosted Application 선택
Application domain: ai.yourdomain.com 입력
3
Policy 설정
Allow → Emails → 허용할 이메일 주소 추가. 또는 Google Workspace 도메인 전체 허용.
4
Identity Provider 연결
Settings → Authentication → Add new → Google 또는 GitHub 선택 후 OAuth 설정.
SECTION06

Tailscale VPN — 프라이빗 AI 전용 내부망 ⚠️ 검증 전

Tailscale은 WireGuard 기반의 메시 VPN으로, 어디서든 내 AI 서버에 안전하게 접근할 수 있는 가장 간단한 솔루션입니다. 설정 시간 5분, 공인 IP 불필요, 포트 포워딩 불필요. 특히 신뢰하는 디바이스만 AI 서버 내부 포트에 직접 접근해야 할 때 Cloudflare Tunnel과 병행해서 사용합니다.

bash — Tailscale 설치 및 AI 서버 전용 네트워크 설정
# 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 vs Tailscale — 사용 구분
  • Cloudflare Tunnel — 팀원·외부 협력사에게 AI 서비스 공개 시. 브라우저만 있으면 접근 가능
  • Tailscale — 본인만의 완전한 프라이빗 접근. Qdrant, PostgreSQL 같은 DB 직접 접근 시
  • 두 가지 병행 — Open WebUI는 Cloudflare로 팀 공개, Ollama API는 Tailscale로 개발자 전용
SECTION07

Fail2ban · SSH 강화 · 서버 해딩 보안 ⚠️ 검증 전

🛡️ SSH 키 인증 강제화 (비밀번호 로그인 차단)

bash — 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 서버 특화 설정

bash — Fail2ban 완전 설정
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

🔄 자동 보안 업데이트 설정

bash — unattended-upgrades 보안 패치 자동화
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까지 전부 다룹니다.

SECTIONF1

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 벡터 DBqdrant_storage 볼륨최고6시간마다30일
Open WebUI 대화·설정open_webui_data 볼륨최고매일90일
PostgreSQL (n8n·Dify)postgres_data 볼륨최고매일 (WAL 연속)30일
Ollama 모델 파일ollama_data 볼륨높음주간영구
n8n 워크플로우 JSONn8n_data 볼륨높음매일90일
ComfyUI 워크플로우comfyui_models 볼륨보통주간90일
이미지 생성 결과물comfyui_output 볼륨낮음월간1년
SECTIONF2

실전 백업 스크립트 — Postgres 3종 + 설정 + 볼륨

실제로 R730에서 매일 새벽 cron으로 돌리고 있는 백업 스크립트입니다. 한 단계가 실패해도 나머지는 계속 진행되도록 설계했고, 파이프(|) 사용 시 앞단 실패를 놓치지 않도록 pipefail도 챙겼습니다 (실제로 이 둘을 안 챙겼다가 디버깅에 시간을 많이 썼습니다).

bash — /mnt/data/backups/backup.sh (검증된 실제 운영 스크립트)
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.sh
⚠️
실제로 겪은 함정 — pipefail 없이 파이프를 쓰면 실패가 숨겨집니다

docker exec ... pg_dump | gzip > file 형태의 파이프는 기본적으로 마지막 명령어(gzip)의 종료코드만 봅니다. pg_dump가 실패해도 gzip은 빈 입력을 받아 "성공"으로 끝나버려서, 실패를 못 감지하고 빈 백업 파일만 쌓이는 사고가 날 수 있습니다. set -o pipefail을 반드시 같이 쓰세요.

💡
2단계 백업 — R730은 "최신 상태"만, 버전 관리는 NAS가

위 스크립트는 NAS로 rsync --delete(최신 상태 1개만 유지)하고, 버전별 보관(일/주/월)은 Synology NAS의 Hyper Backup이 그 NAS 폴더를 대상으로 별도 스케줄로 처리합니다. R730 스크립트와 NAS Hyper Backup, 2단계로 나눈 이유는 "최신 동기화"와 "버전 보관"의 책임을 분리하기 위해서입니다.

SECTIONF4

재해복구 플레이북 & 자동 복구 스크립트 ⚠️ 검증 전

실제 검증된 복구 방법 — 훨씬 단순합니다

아래의 정교한 자동 복구 시나리오 대신, 실제로는 ① NAS Hyper Backup에서 원하는 날짜 버전 복원 → ② R730에서 NAS의 해당 폴더를 rsync로 다시 가져오기 → ③ docker exec -i [컨테이너] psql ... < 덤프파일로 DB 복원 3단계로 충분했습니다. 복잡한 자동화보다 수동 3단계가 더 안전하고 명확했습니다.

🚨 장애 시나리오별 대응 타임라인

T+0
장애 감지 — Grafana 알림 수신
GPU 온도 급등 / 서비스 다운 / 디스크 오류 알림 → 즉시 확인 시작. ssh admin@ai-server 접속 시도
T+5분
초기 진단
~/ai-server/scripts/health_check.sh 실행 → 증상 파악. 하드웨어 vs 소프트웨어 vs 데이터 문제 구분
T+15분
격리 & 영향 범위 확인
어떤 서비스가 영향받는가? 데이터 손실 여부? 최신 백업 타임스탬프 확인 → 복구 계획 결정
T+30분
복구 실행
시나리오별 플레이북 실행. 소프트웨어 문제는 컨테이너 재시작. 데이터 문제는 백업 복구 스크립트 실행
T+1~4시간
서비스 검증 & 모니터링 강화
헬스체크 통과 확인 → 사용자 통보 → 1시간 집중 모니터링 → 원인 분석 보고서 작성
bash — ~/ai-server/scripts/disaster_recovery.sh (원클릭 복구)
#!/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 "=== 재해복구 완료 ==="
SECTIONF5

고가용성 구성 — Nginx HA & 자동 페일오버 ⚠️ 검증 전

단일 서버 장애 시 서비스 중단 없이 자동으로 백업 서버로 전환하는 구조입니다. 두 대의 서버에 Keepalived와 Nginx를 설정해 가상 IP(VIP)가 항상 살아있는 서버로 자동 이동합니다.

bash — Keepalived 설치 및 HA 설정 (Primary 서버)
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 구성 팁 — 어디까지 투자할 것인가

완전한 HA는 하드웨어 2배 비용이 듭니다. 현실적 대안: 주 서버는 단일 구성, 백업 서버는 NAS나 저사양 미니PC로 Open WebUI + 외부 API 연결만 유지하는 Warm Standby 구성이 비용 효율적입니다. 주 서버 장애 시 Cloudflare Tunnel 대상을 백업으로 변경하면 10분 이내 서비스 재개 가능합니다.

SECTIONF6

서버 이전 & 마이그레이션 완전 가이드 ⚠️ 검증 전

GPU 업그레이드, 새 서버 도입, 클라우드 이전 등 서버를 바꿔야 하는 상황을 위한 무중단 마이그레이션 절차입니다. 올바른 순서를 지키면 데이터 손실 없이 수 시간 이내 이전이 완료됩니다.

bash — 서버 이전 완전 절차 스크립트
#!/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로 반드시 업데이트. 기존 클라이언트의 접속 설정도 함께 변경.
✅ 추가편 F 적용으로 달성하는 운영 안정성
  • 매일 새벽 2시 자동 백업 → NAS + Backblaze B2 오프사이트 동시 저장
  • 장애 발생 시 30분 이내 원클릭 복구 스크립트로 서비스 재개
  • PostgreSQL WAL 연속 아카이빙으로 어느 시점으로도 복구 가능
  • Qdrant 컬렉션별 스냅샷으로 벡터 DB 안전하게 보호
  • Keepalived HA 구성으로 주 서버 장애 시 자동 페일오버
  • 마이그레이션 스크립트로 새 서버 이전 시 4시간 이내 완료

🎉 추가편 F까지 완성으로 AI 서버 시리즈 전체 완결!
기본 5편 + 추가 6편 총 11편의 콘텐츠로 AI 서버 구축·운영·활용·최적화·보안·백업의 모든 것을 커버했습니다. 각 글을 시리즈로 연결하고 실제 URL을 업데이트하면 완성된 마스터 가이드가 됩니다.

← 이전 편
1편 — 인프라·OS·Docker·LLM 최적화 구축
다음 편 →
3편 — 이미지·음성·비전 AI 완전 구현

Leave A Reply

Please enter your comment!
Please enter your name here

Related Stories