| 장비 | IP | 접속 계정 |
|---|---|---|
| R730 서버 (Ubuntu, RTX 3060) | 192.168.1.100 | agibop |
| Synology NAS (DS925+) | 192.168.1.250 | agibop 또는 admin |
본문 곳곳의 IP 옆에 (R730)/(NAS) 라벨을 달아뒀지만, 실제 실행하는 명령어 줄 안에는 복붙 시 깨지지 않도록 라벨을 넣지 않았습니다 — 헷갈리면 이 표로 돌아오세요.
R730의 OS 계정과 NAS의 사용자 계정이 둘 다 agibop으로 이름이 같습니다. n8n 등에서 SSH 자격증명을 등록할 때 Host 필드를 반드시 한 번 더 확인하세요 — 계정명이 같다는 이유로 R730용 자격증명을 NAS 자격증명으로 착각해서 쓰는 실수가 실제로 발생했습니다 (Section 07④ NAS-SSH 자격증명 참고).
Synology DS925+ NAS의 WordPress · MariaDB · Redis · Nginx는 삭제하지 않고 그대로 유지합니다. NAS 섹션에서는 Synology Drive만 추가로 설정합니다.
전체 목차
전체 아키텍처 & 서비스 배치 전략
① Ollama → 네이티브 systemd: Docker 레이어 없이 GPU 직접 접근. 추론 속도 10~15% 향상. RTX 3060 12GB 환경에서 VRAM 효율 극대화.
② Dify · Firecrawl → 공식 git clone: 각 서비스가 자체 내부 스택(8개·2개 서비스)을 보유. 공식 팀이 관리하는 compose 파일을 그대로 사용해 업데이트 부담 없음.
③ 나머지 7개 → 단일 docker-compose.yml: 단순 단일 서비스들을 하나의 파일로 통합. docker compose up -d 한 줄로 전체 시작.
┌──────────────────────────────────────────────────────────────┐
│ Dell R730 (192.168.1.100) │
│ Ubuntu 24.04 LTS · RTX 3060 12GB · 128GB RAM │
│ │
│ ① 네이티브 systemd │
│ └─ Ollama :11434 (GPU 직접 접근, 최고 성능) │
│ │
│ ② 단일 docker-compose.yml (/mnt/data/docker-compose.yml) │
│ ├─ Portainer :9000 / :9443 │
│ ├─ Watchtower (포트 없음, 자동 업데이트) │
│ ├─ Open WebUI :3001 │
│ ├─ ComfyUI :8188 (RTX 3060 최적화) │
│ ├─ n8n :5678 │
│ ├─ Meilisearch :7700 │
│ └─ Cloudflared (포트 없음, 외부 터널) │
│ │
│ ③ 공식 단독 docker compose │
│ ├─ Dify :3002 (/mnt/data/01_ai/dify/) │
│ └─ Firecrawl :3003 (/mnt/data/02_automation/firecrawl/)│
│ │
│ 모두 ai-common-net (172.20.0.0/16) 공유 │
└──────────────────────────────────────────────────────────────┘
↕ 내부망 (192.168.1.x)
┌──────────────────────────────────────────────────────────────┐
│ Synology DS925+ (192.168.1.250) │
│ 유지: WordPress · MariaDB · Redis · Nginx │
│ 추가: Synology Drive Server (Obsidian vault 동기화) │
└──────────────────────────────────────────────────────────────┘
📁 /mnt/data/ 전체 폴더 구조
/mnt/data/ ├── docker-compose.yml ← ② 통합 compose (7개 서비스) ├── .env ← 공통 환경변수 (chmod 600) │ ├── 00_infra/ │ └── portainer/data/ ← Portainer DB │ ├── 01_ai/ │ ├── ollama/models/ ← ① Ollama 모델 (네이티브) │ ├── open-webui/data/ ← Open WebUI 대화 기록 │ ├── dify/ ← ③ Dify 공식 (git clone) │ │ ├── docker-compose.yml ← Dify 공식 파일 │ │ ├── .env │ │ └── volumes/ │ └── comfyui/ │ ├── models/ ← 체크포인트·LoRA·VAE │ ├── workflows/ ← 저장된 워크플로우 │ ├── output/ ← 생성된 이미지 │ ├── input/ ← 입력 이미지 │ ├── temp/ ← 임시 파일 │ ├── custom_nodes/ ← 플러그인 │ ├── user/ ← 설정 파일 │ └── dot-cache/ ← pip·HF 캐시 │ ├── 02_automation/ │ ├── n8n/data/ ← 워크플로우·자격증명 │ ├── meilisearch/data/ ← 검색 인덱스 │ └── firecrawl/ ← ③ Firecrawl 공식 (git clone) │ ├── docker-compose.yml │ └── .env │ └── backups/ ← 백업 디렉토리
🌐 포트 계획표
| 서비스 | 포트 | 접속 방법 | 설치 방식 |
|---|---|---|---|
| Portainer | 9000 / 9443 | 내부망 직접 | ② 통합 compose |
| Ollama | 11434 | 내부망 / Docker 내부 | ① 네이티브 systemd |
| Open WebUI | 3001 | Cloudflare Tunnel | ② 통합 compose |
| ComfyUI | 8188 | 내부망 직접 | ② 통합 compose |
| n8n | 5678 | Cloudflare Tunnel | ② 통합 compose |
| Meilisearch | 7700 | 내부망 (n8n·Dify 호출) | ② 통합 compose |
| Dify (nginx) | 3002 / 8443 | Cloudflare Tunnel | ③ 공식 단독 |
| Firecrawl (api) | 3003 | 내부망 (Dify·n8n 호출) | ③ 공식 단독 |
| Cloudflared | 없음 | 외부 HTTPS 터널 | ② 통합 compose |
↓ DB 전용 포트 — 전부 127.0.0.1 바인딩, 외부 노출 금지 (Section 07·08③·09③) | |||
| n8n-pg-db | 15434 | localhost만 (SSH 터널) | ② 통합 compose 내부 |
| dify-pg-db | 15432 | localhost만 (SSH 터널) | ③ Dify 내부 |
| dify-redis-db | 16379 | localhost만 (SSH 터널) | ③ Dify 내부 |
| dify-qdrant-db | 16333 | localhost만 (SSH 터널) | ③ Dify 내부 |
| firecrawl-pg-db | 15433 | localhost만 (SSH 터널) | ③ Firecrawl 내부 |
| firecrawl-redis-db | 16380 | localhost만 (SSH 터널) | ③ Firecrawl 내부 |
| ↓ 별도 격리 서버 — R730과 네트워크 분리 (Section 10) | |||
| OpenClaw Gateway | 18789 | 127.0.0.1만 (SSH 터널) · R730 아닌 별도 서버 | 단독 격리 설치 |
Ubuntu 24.04 초기 설정
이 섹션의 모든 명령어는 Ubuntu 설치 시 생성한 관리자 계정(예: agibop)으로 실행합니다. root 직접 로그인은 금지합니다. sudo가 붙은 명령만 관리자 권한으로 실행됩니다.
① 시스템 업데이트 & 기본 패키지
sudo apt update && sudo apt upgrade -y sudo apt install -y curl wget git vim htop net-tools build-essential software-properties-common ca-certificates gnupg lsb-release apt-transport-https unzip jq ufw fail2ban # 타임존 설정 (한국) sudo timedatectl set-timezone Asia/Seoul timedatectl status
② SSH 보안 강화
# 아래 항목 찾아서 변경 (# 제거 후 값 수정) PermitRootLogin no # root 직접 로그인 금지 MaxAuthTries 3 # 로그인 시도 3회 제한 ClientAliveInterval 300 # 5분 비활성 시 연결 확인 # 저장 후 SSH 재시작 sudo systemctl restart sshd
③ UFW 방화벽 설정
sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp # SSH sudo ufw allow from 192.168.1.0/24 # 내부망 전체 허용 sudo ufw --force enable sudo ufw status verbose
④ 스왑 설정 (128GB RAM 환경)
sudo fallocate -l 16G /swapfile sudo chmod 600 /swapfile # 루트만 읽기/쓰기 가능 (보안) sudo mkswap /swapfile sudo swapon /swapfile # 재부팅 시 자동 마운트 echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 스왑 사용 우선순위 낮춤 (RAM 우선) echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf sudo sysctl -p free -h # 확인
⑤ 데이터 디스크 마운트 (/mnt/data)
# 디스크 목록 확인 (데이터용 디스크 찾기) lsblk # 예) /dev/sdb 가 데이터 디스크인 경우 # 포맷 (처음 사용 시만 — 기존 데이터 있으면 skip) sudo mkfs.ext4 /dev/sdb # 마운트 포인트 생성 및 권한 설정 sudo mkdir -p /mnt/data sudo chown $USER:$USER /mnt/data # 현재 사용자 소유로 설정 # UUID로 fstab 등록 (재부팅 시 자동 마운트) UUID=$(sudo blkid -s UUID -o value /dev/sdb) echo "UUID=$UUID /mnt/data ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab sudo mount -a df -h /mnt/data # 마운트 확인
NVIDIA 드라이버 + CUDA 설치 (RTX 3060)
모든 명령어는 agibop@ai-server:~$ 에서 실행합니다. NVIDIA 드라이버 설치 후 반드시 재부팅이 필요합니다.
① NVIDIA 드라이버 설치
# 추천 드라이버 확인 (RTX 3060 → 보통 nvidia-driver-570 추천) ubuntu-drivers devices # 자동 설치 (추천 드라이버 자동 선택) sudo ubuntu-drivers install # 재부팅 (필수) sudo reboot
② 재부팅 후 드라이버 확인
nvidia-smi # 정상 출력 예시: # +-------------------------+ # | NVIDIA-SMI 570.xx | # | RTX 3060 42C 12288MiB | # +-------------------------+ # RTX 3060 확인 포인트: # - 드라이버 버전: 570.xx 이상 # - CUDA 버전: 12.8 이상 # - Memory: 12288MiB (12GB)
③ CUDA Toolkit 12.8 설치
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb sudo dpkg -i cuda-keyring_1.1-1_all.deb sudo apt update sudo apt install -y cuda-toolkit-12-8 # PATH 등록 echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc echo 'export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc source ~/.bashrc # 확인 nvcc --version # release 12.8 출력 확인 # 임시 다운로드 파일 제거 rm cuda-keyring_1.1-1_all.deb
④ GPU Persistence Mode (서버 운영 최적화)
# Persistence Mode 활성화 (모델 로딩 시간 단축) sudo nvidia-smi -pm 1 # 재부팅 시 자동 실행 등록 sudo tee /etc/systemd/system/nvidia-pm.service << 'EOF' [Unit] Description=NVIDIA Persistence Mode After=multi-user.target [Service] Type=oneshot ExecStart=/usr/bin/nvidia-smi -pm 1 RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF sudo systemctl enable --now nvidia-pm.service sudo systemctl status nvidia-pm.service
Docker + NVIDIA Container Toolkit 설치
Docker 설치 후 newgrp docker 또는 로그아웃 후 재로그인해야 sudo 없이 docker 명령을 사용할 수 있습니다. 재로그인 전까지는 sudo docker를 사용하세요.
① Docker CE 설치
# 기존 Docker 완전 제거 for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt remove -y $pkg 2>/dev/null || true done # 공식 GPG 키 & 저장소 추가 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Docker Engine 설치 sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 현재 사용자를 docker 그룹에 추가 sudo usermod -aG docker $USER # Docker 자동 시작 sudo systemctl enable --now docker # 그룹 적용 (재로그인 없이 현재 세션에 적용) newgrp docker << 'NEWGRP' docker --version docker compose version NEWGRP
② NVIDIA Container Toolkit 설치
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt update sudo apt install -y nvidia-container-toolkit # Docker와 통합 sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker # GPU 컨테이너 동작 확인 docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi # RTX 3060 정보가 출력되면 성공
③ Docker 로깅 & 스토리지 최적화
sudo tee /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
},
"storage-driver": "overlay2"
}
EOF
sudo systemctl restart docker
docker info | grep -E "Storage Driver|Default Runtime"
# Storage Driver: overlay2
# Default Runtime: nvidia폴더 구조 & 권한 완전 설계
폴더 소유자·권한을 잘못 설정하면 Docker 컨테이너가 파일을 읽거나 쓰지 못해 서비스가 시작되지 않습니다. 아래 명령어를 순서대로 정확히 실행하세요.
① 전체 폴더 한 번에 생성
mkdir -p /mnt/data/00_infra/portainer/data
mkdir -p /mnt/data/01_ai/ollama/models
mkdir -p /mnt/data/01_ai/open-webui/data
mkdir -p /mnt/data/01_ai/comfyui/{models,workflows,output,input,temp,custom_nodes,user,dot-cache}
mkdir -p /mnt/data/02_automation/n8n/data
mkdir -p /mnt/data/02_automation/n8n-postgres/data
mkdir -p /mnt/data/02_automation/meilisearch/data
mkdir -p /mnt/data/backups/staging
# 구조 확인
find /mnt/data -type d | sort② 기본 권한 설정
# 전체 소유자를 현재 사용자로 설정 sudo chown -R $USER:$USER /mnt/data chmod -R 755 /mnt/data
③ 서비스별 특수 권한 설정
| 서비스 | 이유 | 필요 권한 |
|---|---|---|
| Ollama | 설치 시 ollama 시스템 계정 생성 | ollama:ollama 소유 |
| n8n | 컨테이너 내부에서 node 유저(uid 1000)로 실행 | uid 1000 쓰기 가능 |
| Portainer | docker.sock 접근 필요 | root 실행 (기본) |
| ComfyUI | yanwk 이미지는 root로 실행 | 별도 설정 불필요 |
| Meilisearch | 컨테이너 내 meili 유저(uid 1000) | uid 1000 쓰기 가능 |
| n8n-postgres | postgres 공식 이미지가 초기화 시 자체 처리 | 별도 chown 불필요 |
# n8n: node 유저(uid:1000)가 쓸 수 있도록 sudo chown -R 1000:1000 /mnt/data/02_automation/n8n/data chmod 700 /mnt/data/02_automation/n8n/data # Meilisearch: uid 1000 sudo chown -R 1000:1000 /mnt/data/02_automation/meilisearch/data # n8n-postgres: 공식 postgres 이미지는 컨테이너 내부에서 initdb 시 # 자체적으로 권한을 설정하므로 별도 chown 불필요 (기본 755로 생성된 상태 그대로 둠) # Ollama 모델 경로 (Ollama 설치 완료 후 실행) # → Section 06 완료 후 아래 명령 실행 # sudo chown -R ollama:ollama /mnt/data/01_ai/ollama/
④ 환경변수 파일 생성 (.env)
cat > /mnt/data/.env << 'EOF' # ── 서버 정보 ──────────────────────────────── HOST_IP=192.168.1.100 TZ=Asia/Seoul # ── API 키 ─────────────────────────────────── ANTHROPIC_API_KEY=sk-ant-여기에입력 GEMINI_API_KEY=AIzaSy여기에입력 # ── Open WebUI ─────────────────────────────── WEBUI_SECRET_KEY= # ── n8n ────────────────────────────────────── N8N_ENCRYPTION_KEY= N8N_DB_PASSWORD= # ── Meilisearch ────────────────────────────── MEILI_MASTER_KEY= # ── Cloudflared ────────────────────────────── CLOUDFLARE_TOKEN=여기에터널토큰입력 EOF # 키 자동 생성 (위 빈 값들 채우기) WEBUI_KEY=$(openssl rand -hex 32) N8N_KEY=$(openssl rand -hex 24) N8N_DB_PASS=$(openssl rand -hex 16) MEILI_KEY=$(openssl rand -hex 24) sed -i "s/^WEBUI_SECRET_KEY=$/WEBUI_SECRET_KEY=$WEBUI_KEY/" /mnt/data/.env sed -i "s/^N8N_ENCRYPTION_KEY=$/N8N_ENCRYPTION_KEY=$N8N_KEY/" /mnt/data/.env sed -i "s/^N8N_DB_PASSWORD=$/N8N_DB_PASSWORD=$N8N_DB_PASS/" /mnt/data/.env sed -i "s/^MEILI_MASTER_KEY=$/MEILI_MASTER_KEY=$MEILI_KEY/" /mnt/data/.env # 보안 설정 (소유자만 읽기/쓰기) chmod 600 /mnt/data/.env echo "생성된 N8N_ENCRYPTION_KEY: $N8N_KEY" echo "→ 이 값은 절대 분실 금지! 변경 시 모든 자격증명 재입력 필요"
Ollama — 네이티브 systemd 설치 (GPU 최고 성능)
Docker Ollama: 요청 → Docker 네트워크 → 컨테이너 → NVIDIA Toolkit → GPU 드라이버 → GPU
Native Ollama: 요청 → GPU 드라이버 → GPU
레이어가 줄어 추론 속도 10~15% 향상, RTX 3060 12GB VRAM을 온전히 활용합니다. Docker 서비스들은 http://host.docker.internal:11434로 접근합니다.
① Ollama 설치
# 공식 설치 스크립트 (ollama 시스템 계정 자동 생성) curl -fsSL https://ollama.com/install.sh | sh # 설치 확인 ollama --version systemctl status ollama
② systemd 서비스 최적화 (RTX 3060 12GB 맞춤)
sudo mkdir -p /etc/systemd/system/ollama.service.d sudo tee /etc/systemd/system/ollama.service.d/override.conf << 'EOF' [Service] # 내부망 모든 IP에서 접근 허용 (Docker 컨테이너 포함) Environment="OLLAMA_HOST=0.0.0.0:11434" # 모델 저장 경로 변경 (/mnt/data 로 통합) Environment="OLLAMA_MODELS=/mnt/data/01_ai/ollama/models" # RTX 3060 12GB 기준 # NUM_PARALLEL=2: 동시 추론 2개 (12GB 기준 적정) # MAX_LOADED_MODELS=1: 동시 로드 모델 1개 (VRAM 보호) # KEEP_ALIVE=30m: 30분 미사용 시 언로드 Environment="OLLAMA_NUM_PARALLEL=2" Environment="OLLAMA_MAX_LOADED_MODELS=1" Environment="OLLAMA_KEEP_ALIVE=30m" # RTX 3060 지정 (GPU 0) Environment="CUDA_VISIBLE_DEVICES=0" EOF sudo systemctl daemon-reload sudo systemctl restart ollama sudo systemctl status ollama
③ 방화벽 열기 — Docker 컨테이너가 11434에 접근하도록 (필수, 실제 트러블슈팅에서 확인)
Section 02에서 설정한 UFW는 192.168.1.0/24(내부망)만 허용했지, Docker 컨테이너 → 호스트로 가는 요청은 막혀있는 상태입니다. OLLAMA_HOST=0.0.0.0으로 모든 인터페이스에서 듣게 해도, 방화벽이 그 앞에서 막으면 소용없습니다.
# Docker는 기본적으로 172.16.0.0/12 대역(172.16.x.x ~ 172.31.x.x)을 사용합니다. # ai-common-net(172.20.0.0/16)뿐 아니라 Dify·Firecrawl처럼 별도 compose # 프로젝트가 임의로 할당받는 172.18.x, 172.19.x 등도 전부 이 안에 포함되므로, # 대역 하나로 통째로 열어두면 앞으로 어떤 서비스가 추가돼도 다시 설정할 필요가 없습니다. sudo ufw allow from 172.16.0.0/12 to any port 11434 sudo ufw status # 11434 ALLOW 172.16.0.0/12 ← 이 한 줄만 있으면 충분
④ Ollama 모델 경로 권한 설정
# Ollama 설치 시 생성된 ollama 시스템 계정이 모델 폴더를 소유해야 함 sudo chown -R ollama:ollama /mnt/data/01_ai/ollama/ sudo chmod -R 755 /mnt/data/01_ai/ollama/ # 확인 ls -la /mnt/data/01_ai/ollama/ # drwxr-xr-x ollama ollama models/
⑤ RTX 3060 12GB 추천 모델 & 설치
| 모델 | VRAM | 용도 | 우선순위 |
|---|---|---|---|
| gemma4:12b | ~6.6GB | 멀티모달·256K·한국어·최신 (6/4) | ⭐ 1순위 |
| qwen3:14b | ~9GB | 12GB 최고 품질 dense | ⭐ 2순위 |
| qwen2.5:14b | ~8.7GB | 한국어 블로그 생성 | 3순위 |
| gemma4:e4b | ~3GB | 툴 콜링·이미지 입력·n8n 에이전트 | 4순위 |
| deepseek-r1:7b | ~5GB | CoT 추론·수학·분석 | 5순위 |
| nomic-embed-text | ~274MB | Dify RAG 임베딩 (필수) | 필수 |
| bge-m3 | ~1.2GB | 한국어 임베딩 고품질 | 권장 |
# 임베딩 먼저 (작은 파일, 빠름) ollama pull nomic-embed-text ollama pull bge-m3 # 주력 LLM (큰 파일, 시간 소요) ollama pull gemma4:12b # 6.6GB ollama pull qwen3:14b # 9GB (선택) # 설치 확인 ollama list # 빠른 테스트 ollama run gemma4:12b "안녕! 간단히 자기소개 해줘" --verbose
⑥ GPU 추론 정상 동작 검증 — 속도·VRAM 실측
# ① --verbose로 토큰 생성 속도 확인
ollama run gemma4:12b "1부터 10까지 세어줘" --verbose
# 마지막에 나오는 "eval rate" 항목 확인
# GPU 정상: eval rate: 40~80 tokens/s (RTX 3060 12GB 기준)
# CPU로 폴백: eval rate: 3~8 tokens/s → 비정상! GPU 인식 실패
# ② 추론 중 실제 GPU 사용률 확인 (별도 터미널에서 동시 실행)
watch -n 1 nvidia-smi --query-gpu=utilization.gpu,memory.used --format=csv
# 추론 중 utilization.gpu가 50~100%로 튀어야 정상
# 0%에 머물러 있으면 CPU로만 돌고 있다는 뜻 → OLLAMA_NUM_GPU 설정 확인
# ③ 동시 요청 처리 확인 (OLLAMA_NUM_PARALLEL=2 적용 확인)
for i in 1 2; do
curl -s http://localhost:11434/api/generate -d "{\"model\":\"gemma4:12b\",\"prompt\":\"테스트 $i\",\"stream\":false}" &
done
wait
# 두 응답이 모두 정상적으로 돌아오면 병렬 처리 정상 동작통합 docker-compose.yml — 7개 서비스 완전체
7개 서비스를 /mnt/data/docker-compose.yml 하나로 관리합니다. 이 파일을 먼저 실행해야 ai-common-net 네트워크가 생성되고, 이후 Dify·Firecrawl이 해당 네트워크를 사용할 수 있습니다.
① docker-compose.yml 파일 생성
# /mnt/data/docker-compose.yml
# 통합 서비스: Portainer · Watchtower · Open WebUI · ComfyUI · n8n · Meilisearch · Cloudflared
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
# ────────────────────────────────────────────────────────────
# Portainer — 컨테이너 관리 Web UI
# ────────────────────────────────────────────────────────────
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: unless-stopped
ports:
- "9000:9000"
- "9443:9443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /mnt/data/00_infra/portainer/data:/data
networks:
- ai-common-net
logging: *default-logging
labels:
# Portainer는 Watchtower 자동 업데이트 제외 (수동 관리)
- "com.centurylinklabs.watchtower.enable=false"
# ────────────────────────────────────────────────────────────
# Watchtower — 컨테이너 자동 업데이트 (매일 새벽 4시)
# ────────────────────────────────────────────────────────────
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
# Docker API 버전 명시 (불일치 오류 방지)
- DOCKER_API_VERSION=1.45
# 업데이트 후 구 이미지 자동 삭제
- WATCHTOWER_CLEANUP=true
# 중지된 컨테이너는 업데이트 제외
- WATCHTOWER_INCLUDE_STOPPED=false
# 매일 새벽 4시 실행 (cron 형식)
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- TZ=Asia/Seoul
networks:
- ai-common-net
logging: *default-logging
# ────────────────────────────────────────────────────────────
# Open WebUI — Ollama Web UI (ChatGPT 스타일)
# Ollama는 네이티브 설치 → host.docker.internal:11434 으로 접근
# ────────────────────────────────────────────────────────────
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
restart: unless-stopped
ports:
- "3001:8080"
volumes:
- /mnt/data/01_ai/open-webui/data:/app/backend/data
environment:
# 네이티브 Ollama 접근 (host.docker.internal 사용)
- OLLAMA_BASE_URL=http://host.docker.internal:11434
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
- DEFAULT_LOCALE=ko-KR
- ENABLE_RAG_WEB_SEARCH=true
- ENABLE_IMAGE_GENERATION=true
# ComfyUI 이미지 생성 연동
- COMFYUI_BASE_URL=http://comfyui:8188
- TZ=Asia/Seoul
# 네이티브 Ollama 접근을 위한 host 추가
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- ai-common-net
logging: *default-logging
labels:
- "com.centurylinklabs.watchtower.enable=true"
# ────────────────────────────────────────────────────────────
# ComfyUI — AI 이미지 생성 (RTX 3060 12GB 최적화)
# ────────────────────────────────────────────────────────────
comfyui:
image: yanwk/comfyui-boot:cu128-slim
# cu128: 설치된 CUDA 12.8에 정확히 일치
container_name: comfyui
runtime: nvidia
restart: unless-stopped
ports:
- "8188:8188"
environment:
# ── GPU 설정 ─────────────────────────────────────────────
- NVIDIA_VISIBLE_DEVICES=0
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
# RTX 3060 Ampere 아키텍처 명시 (compute 8.6)
- TORCH_CUDA_ARCH_LIST=8.6
# CUDA 모듈 지연 로딩 (시작 2~3초 단축)
- CUDA_MODULE_LOADING=LAZY
# ── ComfyUI 실행 옵션 ────────────────────────────────────
# --cuda-malloc RTX 30xx VRAM 단편화 30% 감소
# --fast 불필요한 안전 검사 제거
# --highvram 12GB: SDXL(6.5GB)·FLUX GGUF Q4(7~8GB) 상주
# --preview-method latent2rgb VAE 없이 빠른 프리뷰
# --cache-none 워크플로우 전환 시 VRAM 즉시 해제 (OOM 방지)
- CLI_ARGS=--listen 0.0.0.0 --port 8188 --cuda-malloc --fast --preview-method latent2rgb --highvram --cache-none
# ── PyTorch 메모리 최적화 (RTX 3060 12GB 기준) ──────────
# gc threshold 0.85: VRAM 85%(10.2GB) 시 GC 실행
# max_split_size 256: 12GB 기준 최적 블록 크기
# expandable_segments: PyTorch 2.x 동적 세그먼트 (단편화 감소)
- PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.85,max_split_size_mb:256,expandable_segments:True
- GIT_CONFIG_GLOBAL_SAFE_DIRECTORY=/root/ComfyUI
- TZ=Asia/Seoul
- PYTHONUNBUFFERED=1
volumes:
# yanwk cu128-slim 내부 경로: /root/ComfyUI
- /mnt/data/01_ai/comfyui/models:/root/ComfyUI/models
- /mnt/data/01_ai/comfyui/workflows:/root/ComfyUI/user/default/workflows
- /mnt/data/01_ai/comfyui/output:/root/ComfyUI/output
- /mnt/data/01_ai/comfyui/input:/root/ComfyUI/input
- /mnt/data/01_ai/comfyui/temp:/root/ComfyUI/temp
- /mnt/data/01_ai/comfyui/custom_nodes:/root/ComfyUI/custom_nodes
- /mnt/data/01_ai/comfyui/user:/root/ComfyUI/user
# pip·HuggingFace 캐시 영속화 (재생성 시 재다운로드 방지)
- /mnt/data/01_ai/comfyui/dot-cache:/root/.cache
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ['0'] # --gpus all 보다 명시적
capabilities: [gpu]
limits:
cpus: '16.0'
memory: 64G
# R730 128GB 기준: Wan 2.2 오프로딩 + FLUX GGUF 처리 여유
# Wan 2.2 비디오 생성 시 공유 메모리 OOM 방지
shm_size: '16g'
ulimits:
memlock:
soft: -1
hard: -1 # GPU 메모리 전송 최적화
nofile:
soft: 65536
hard: 65536 # 대량 모델 파일 처리
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8188/system_stats > /dev/null || exit 1"]
interval: 30s
timeout: 15s
retries: 5
# 첫 실행: Manager 설치 + custom_nodes 의존성 설치 여유
start_period: 180s
networks:
- ai-common-net
logging: *default-logging
labels:
# ComfyUI는 자동 업데이트 제외 (워크플로우 호환성 관리)
- "com.centurylinklabs.watchtower.enable=false"
# ────────────────────────────────────────────────────────────
# n8n-postgres — n8n 전용 DB (SQLite 기본값 대신 Postgres 권장)
# 동시 워크플로우 실행이 많아지면 SQLite는 "database is locked" 발생 가능
# ────────────────────────────────────────────────────────────
n8n-postgres:
image: postgres:15-alpine
container_name: n8n-pg-db
restart: unless-stopped
ports:
- "127.0.0.1:15434:5432" # 외부 접속용, 로컬호스트로만 제한 (Dify 15432·Firecrawl 15433과 겹치지 않음)
volumes:
- /mnt/data/02_automation/n8n-postgres/data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
- POSTGRES_DB=n8n
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
timeout: 5s
retries: 5
networks:
- ai-common-net
logging: *default-logging
labels:
- "com.centurylinklabs.watchtower.enable=true"
# ────────────────────────────────────────────────────────────
# n8n — 워크플로우 자동화
# ────────────────────────────────────────────────────────────
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
depends_on:
n8n-postgres:
condition: service_healthy
ports:
- "5678:5678"
volumes:
# n8n은 내부에서 node 유저(uid:1000)로 실행
# → 사전에 chown 1000:1000 필요 (Section 05 참조)
- /mnt/data/02_automation/n8n/data:/home/node/.n8n
environment:
# 최초 생성 후 절대 변경 금지 (변경 시 모든 자격증명 무효화)
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=${HOST_IP}
- N8N_PORT=5678
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://${HOST_IP}:5678
- GENERIC_TIMEZONE=Asia/Seoul
- N8N_LOG_LEVEL=info
# 실행 이력 자동 정리 (7일 보존)
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
# ── DB: Postgres 사용 (서비스명 n8n-postgres로 접근, container_name 아님) ──
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=n8n-postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
# N8N_RUNNERS_ENABLED는 최신 버전에서 deprecated (기본 내장되어 더 이상 불필요)
# 네이티브 Ollama, NAS SSH 등 호스트 서비스 접근
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- ai-common-net
logging: *default-logging
labels:
- "com.centurylinklabs.watchtower.enable=true"
# ────────────────────────────────────────────────────────────
# Meilisearch — 고속 검색 엔진
# ────────────────────────────────────────────────────────────
meilisearch:
image: getmeili/meilisearch:latest
container_name: meilisearch
restart: unless-stopped
ports:
- "7700:7700"
volumes:
# Meilisearch는 uid 1000으로 실행
# → 사전에 chown 1000:1000 필요 (Section 05 참조)
- /mnt/data/02_automation/meilisearch/data:/meili_data
environment:
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
- MEILI_ENV=production
- TZ=Asia/Seoul
networks:
- ai-common-net
logging: *default-logging
labels:
- "com.centurylinklabs.watchtower.enable=true"
# ────────────────────────────────────────────────────────────
# Cloudflared — 공인 IP 없이 외부 안전 접속
# network_mode: host → 내부 서비스에 직접 접근 가능
# ────────────────────────────────────────────────────────────
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
network_mode: host
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TOKEN}
environment:
- TZ=Asia/Seoul
logging: *default-logging
labels:
- "com.centurylinklabs.watchtower.enable=false"
# ────────────────────────────────────────────────────────────────
# 네트워크 정의 — Dify·Firecrawl이 external로 참조
# ────────────────────────────────────────────────────────────────
networks:
ai-common-net:
name: ai-common-net
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16② 실행 & 확인
# 반드시 docker-compose.yml이 있는 폴더에서 실행 cd /mnt/data # 백그라운드 실행 docker compose up -d # 로그 확인 (전체) docker compose logs -f # 개별 서비스 로그 docker compose logs -f comfyui docker compose logs -f n8n # 전체 상태 확인 docker compose ps
③ 접속 확인
| 서비스 | 접속 주소 | 첫 접속 작업 |
|---|---|---|
| Portainer | http://192.168.1.100:9000 | 5분 내 관리자 계정 생성 (타임아웃 있음) |
| Open WebUI | http://192.168.1.100:3001 | 관리자 계정 생성 → Ollama 모델 선택 확인 |
| ComfyUI | http://192.168.1.100:8188 | 첫 실행 후 180초 대기 (Manager 설치 중) |
| n8n | http://192.168.1.100:5678 | 이메일+비밀번호로 첫 계정 생성 |
| Meilisearch | http://192.168.1.100:7700 | curl로 health 확인 후 Dify에서 연동 |
③-1 서비스별 실제 기능 테스트 — "켜졌다"가 아니라 "제대로 동작한다" 확인
위 표는 웹 화면이 열리는지만 확인합니다. 아래는 각 서비스가 실제로 기능을 수행하는지 터미널에서 검증하는 방법입니다.
🅰 Open WebUI — 실제 채팅 응답 테스트
# Ollama가 Open WebUI를 거치지 않고 직접 응답하는지 먼저 확인
curl http://localhost:11434/api/generate -d '{
"model": "gemma4:12b",
"prompt": "1+1은?",
"stream": false
}'
# 성공: {"response":"2입니다...", ...} 형태의 JSON 응답
# 실패: "model not found" → ollama pull gemma4:12b 먼저 실행
# 실패: connection refused → systemctl status ollama 확인
# Open WebUI 컨테이너가 Ollama에 도달하는지 확인
# (open-webui 이미지엔 curl·wget이 없음 → python3로 테스트)
docker exec open-webui python3 -c "import urllib.request; print(urllib.request.urlopen('http://host.docker.internal:11434/api/tags').read().decode())"
# 모델 목록 JSON이 나오면 정상 — 빈 응답/에러면 extra_hosts 설정 확인🅱 ComfyUI — 실제 이미지 생성 테스트 (API 직접 호출)
# GPU가 실제로 잡히는지 ComfyUI 내부에서 확인
docker exec comfyui python3 -c "import torch; print('CUDA 사용 가능:', torch.cuda.is_available()); print('GPU:', torch.cuda.get_device_name(0))"
# 성공: CUDA 사용 가능: True / GPU: NVIDIA GeForce RTX 3060
# 실패 시: docker compose logs comfyui 에서 "CUDA error" 검색
# 시스템 상태 API로 확인 (모델 로드 없이 가벼운 체크)
curl -s http://localhost:8188/system_stats | python3 -m json.tool
# vram_total이 12000MB 근처로 나오면 GPU 인식 정상
# 큐 상태 확인 (워크플로우 실행 가능 상태인지)
curl -s http://localhost:8188/queue | python3 -m json.tool
# {"queue_running":[],"queue_pending":[]} 이면 대기 중 = 정상🅲 n8n — 워크플로우 실행 + DB 연결 테스트
# n8n이 Postgres에 실제로 붙어있는지 확인
# (주의: docker logs에서 "database" 문자열 grep은 신뢰도 낮음 —
# 마이그레이션 로그에 이 단어가 안 들어가는 경우가 많아 비어있어도 정상일 수 있음)
docker exec n8n env | grep DB_
# DB_TYPE=postgresdb, DB_POSTGRESDB_HOST=n8n-postgres 등이 보여야 정상
# DB_TYPE이 안 보이면 .env 치환 누락 또는 docker compose up -d 재실행 필요
# 진짜 에러가 있는지 확인 (이게 더 의미 있는 체크)
docker logs n8n 2>&1 | grep -iE "error|ECONNREFUSED|fail"
# 아무것도 안 나오면 정상, postgres 관련 에러가 보이면 실제 문제
# n8n-pg-db에 실제 테이블이 생성됐는지 직접 확인 (가장 확실한 방법)
docker exec n8n-pg-db psql -U n8n -d n8n -c "\dt" | head -10
# workflow_entity, credentials_entity 등의 테이블이 보이면 정상
# 헬스 체크 엔드포인트 확인
curl -s http://localhost:5678/healthz
# {"status":"ok"} 응답 확인
# 간단한 워크플로우를 직접 실행해서 테스트 (Webhook 트리거 예시)
# n8n UI에서 Webhook 노드 하나만 있는 워크플로우를 만들고 Publish 한 뒤:
# curl -X POST http://localhost:5678/webhook/test-path
# 워크플로우 실행 탭에 "Success" 기록이 보이면 정상🅳 Meilisearch — 인덱싱 + 검색 실제 동작 테스트
MEILI_KEY=$(grep MEILI_MASTER_KEY /mnt/data/.env | cut -d= -f2)
# 1. 헬스 체크
curl -s http://localhost:7700/health
# {"status":"available"}
# 2. 테스트 문서 추가
curl -s -X POST 'http://localhost:7700/indexes/test/documents' \
-H "Authorization: Bearer $MEILI_KEY" \
-H 'Content-Type: application/json' \
-d '[{"id":1,"title":"RTX 3060 홈랩 구축기"}]'
# 3. 실제 검색 — 결과가 나와야 인덱싱까지 제대로 동작한 것
sleep 1 # 인덱싱 반영 대기
curl -s -X POST 'http://localhost:7700/indexes/test/search' \
-H "Authorization: Bearer $MEILI_KEY" \
-H 'Content-Type: application/json' \
-d '{"q":"홈랩"}' | python3 -m json.tool
# hits 배열에 방금 넣은 문서가 보이면 정상
# 4. 테스트 인덱스 정리
curl -s -X DELETE 'http://localhost:7700/indexes/test' -H "Authorization: Bearer $MEILI_KEY"🅴 Portainer — 로그인 + Docker 제어 권한 테스트
# Portainer가 docker.sock에 제대로 접근하는지 확인
curl -s http://localhost:9000/api/status
# {"Version":"2.x.x", ...} 정상 응답
# 브라우저에서: 로그인 후 Containers 메뉴 클릭
# → 지금까지 만든 모든 컨테이너(7개 + Dify/Firecrawl 등)가 목록에 보이면
# docker.sock 마운트가 정상 동작 중인 것🅵 Cloudflared — 터널 연결 상태 직접 확인
# 터널이 Cloudflare 엣지와 정상 연결됐는지 로그로 확인 docker compose logs --tail=20 cloudflared | grep -E "Registered tunnel|connection.*registered" # "Registered tunnel connection" 메시지가 4개(커넥션 4개) 보이면 정상 # 현재 적용된 라우팅 규칙 확인 docker compose logs cloudflared 2>&1 | grep "Updated to new configuration" | tail -1 # hostname별 service 매핑이 의도한 대로인지 눈으로 확인
🅶 GPU 자원 경쟁 실측 테스트 (Ollama ↔ ComfyUI)
# 터미널 1: VRAM 사용량 1초마다 출력 watch -n 1 nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu --format=csv # 터미널 2: Ollama로 모델 로드 ollama run gemma4:12b "긴 글 하나 써줘" & # 터미널 1 화면에서 VRAM이 약 6~7GB로 올라가는지 확인 # → 이 상태에서 ComfyUI로 SDXL 이미지 생성을 시도하면 OOM 발생 여부 확인 # (의도된 동작 확인용 테스트이니 실제 운영에서는 동시 사용 자제)
RTX 3060 12GB VRAM은 하나의 서비스가 독점해야 합니다. Ollama에 14B 모델이 로드된 상태에서 ComfyUI로 이미지를 생성하면 OOM이 발생합니다. Ollama 사용 후 모델을 언로드하거나 OLLAMA_KEEP_ALIVE=0으로 자동 언로드 설정을 권장합니다.
④ 텔레그램으로 작업 요청하는 방법 — 초보자 완전 가이드
이 가이드는 n8n 2.x(2026년 6월 기준 안정 버전, 예: 2.26.x) UI 용어로 작성됐습니다. 1.x에서 익숙했던 "Active" 토글은 2.0부터 완전히 사라지고 "Publish" 버튼으로 바뀌었습니다. 작업 내용은 2초마다 자동 저장되지만, 실제로 운영(웹훅 수신 등)에 반영하려면 반드시 Publish를 눌러야 합니다. 사이드바도 재구성되어 1.x 시절 스크린샷과 위치가 다를 수 있습니다.
스마트폰 텔레그램 앱에서 메시지 한 줄만 보내면 n8n이 받아서 자동으로 처리하게 만드는 방법입니다. "서버에 SSH로 접속해서 명령어를 입력"하지 않고도, 외출 중에 블로그 글 작성을 요청하거나 서버 상태를 확인할 수 있습니다.
① 텔레그램 앱에서 봇에게 메시지 전송
② n8n이 메시지를 실시간으로 수신 (Telegram Trigger 노드)
③ n8n이 메시지 내용을 분석해서 어떤 작업인지 판단
④ n8n이 실제 작업 수행 (Dify 호출, 서버 상태 조회 등)
⑤ n8n이 처리 결과를 다시 텔레그램으로 답장
Telegram Trigger 노드가 포함된 워크플로우를 Publish하면 n8n이 텔레그램 서버에 "이 주소로 메시지를 보내달라"고 웹훅을 등록합니다.
이때 사설 IP(192.168.1.100(R730))나 plain HTTP는 텔레그램 정책상 거부되고 Bad Request: bad webhook: An HTTPS URL must be provided for webhook 에러가 납니다.
아래 0단계로 Cloudflare Tunnel을 먼저 적용해야 이후 단계가 동작합니다.
0단계 — n8n에 HTTPS 적용 (Cloudflare Tunnel)
one.dash.cloudflare.com → Networks → Tunnels → 기존 터널 → Public Hostname 추가 Subdomain: n8n Domain: 본인 도메인 Service Type: HTTP ← HTTPS 아님! (Cloudflare가 외부 HTTPS를 대신 처리) URL: localhost:5678
# n8n 서비스의 environment 블록을 도메인 기준으로 변경
n8n:
environment:
- N8N_PROTOCOL=https
- N8N_HOST=n8n.본인도메인.com
- WEBHOOK_URL=https://n8n.본인도메인.com/
# N8N_SECURE_COOKIE=false 를 넣었었다면 이제 제거 (HTTPS라 정상 동작)
# 환경변수 변경 → 재시작이 아니라 재생성 필요
docker compose up -d --force-recreate n8n
# 확인
curl -I https://n8n.본인도메인.com1단계 — 텔레그램 봇 만들기 (@BotFather)
@BotFather 입력 → 공식 인증 마크(파란 체크) 있는 계정 선택 → 대화 시작/newbot 입력/newbot 메시지 전송 → 봇 이름 입력(예: "Agibop Server Bot") → 봇 아이디 입력 (반드시 bot으로 끝나야 함, 예: agibop_server_bot)123456789:AAHn4xN... 형태의 토큰을 줍니다. 이 토큰이 있으면 누구나 내 봇을 조종할 수 있으니 캡처해서 안전하게 보관하세요.@userinfobot 검색 → 대화 시작 → 자동으로 즉시 본인의 User ID가 답장으로 옴. 1:1 대화에서는 이 User ID가 chat_id와 동일합니다.https://api.telegram.org/bot내토큰/getUpdates 방식도 흔히 쓰이지만, n8n에서 Telegram Trigger 워크플로우를 한 번이라도 Publish했다면 그 순간부터 영원히 빈 배열("result":[])만 나옵니다. 텔레그램은 웹훅(Webhook)과 getUpdates(폴링)를 동시에 못 쓰기 때문에, 웹훅이 등록된 봇은 모든 메시지가 n8n으로만 가고 getUpdates에는 안 잡힙니다. 이미 워크플로우를 Publish하신 상태라면 위 ④번(@userinfobot) 방법을 쓰세요 — 자신의 봇과 무관하게 항상 동작합니다.
이 봇은 전 세계 누구나 검색해서 메시지를 보낼 수 있습니다. 내 chat_id로 보낸 메시지만 처리하도록 나중에 n8n에서 필터링해야, 모르는 사람이 내 서버를 조작하는 사고를 막을 수 있습니다.
2단계 — n8n에 텔레그램 자격증명(Credential) 등록
3단계 — 워크플로우 만들기 (가장 쉬운 예제부터)
가장 간단한 예제로 시작합니다. "서버 상태 확인해줘"라고 텔레그램에 보내면 GPU·메모리 상태를 답장받는 워크플로우입니다.
| 노드 | 유형 | 설정 |
|---|---|---|
| N01 | Telegram Trigger | Credential: 2단계에서 등록한 것 선택 · Trigger On: Message 체크 (필드명이 "Updates"가 아니라 "Trigger On") |
| N02 | IF | 조건: {{ $json.message.chat.id }} equals 내chat_id |
| N03 | SSH 노드 (Execute Command 노드 아님) | Credential: R730 자신의 SSH 계정 (NAS-SSH 아님 — GPU는 R730에 있고 NAS엔 없음) · Command: nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv |
| N04 | Telegram (Send Message) | Chat ID: {{ $('Telegram Trigger').item.json.message.chat.id }} (주의: $json.message.chat.id로 쓰면 안 됨 — SSH 노드가 $json을 stdout으로 덮어써서 끊김) · Text: {{ $json.stdout }} |
N03(SSH) 노드는 자기 출력(stdout·stderr·exitCode)으로 $json을 완전히 덮어씁니다. N01에서 받은 message.chat.id는 그 시점에 사라집니다. N04에서 그냥 {{ $json.message.chat.id }}를 쓰면 빈 값이 되어 텔레그램이 "chat_id가 없다"며 조용히 실패하고, 사용자에게는 그냥 "응답 없음"으로만 보입니다. 반드시 노드 이름을 직접 지정해서 $('Telegram Trigger').item.json... 형태로 원본 데이터를 다시 가져와야 합니다.
{{ $json.message.chat.id }} = 본인의chat_id숫자 → True 라인만 다음 단계로 연결{{ $('Telegram Trigger').item.json.message.chat.id }} (트리거 노드 이름을 직접 지정 — 그냥 $json...으로 쓰면 SSH 노드가 데이터를 덮어써서 빈 값이 됨) · Text: {{ $json.stdout }}메시지 보냈는데 응답이 없을 때 — 실전 진단 순서 (실제 트러블슈팅 기반)
n8n → Executions에서 방금 보낸 메시지로 실행 기록이 떴는지 확인하세요. 아무것도 없으면 트리거 자체가 안 잡힌 것이고(아래 ①②), 기록이 있는데 특정 노드가 빨간색이면 그 노드 설정 문제입니다(N03 credential, N04 chat_id 표현식 등 위 표 참고).
① 텔레그램 서버에 웹훅이 실제로 등록됐는지 직접 확인
https://api.telegram.org/bot내토큰/getWebhookInfo
| 결과 | 의미 |
|---|---|
"url":"" (빈 값) | Publish가 실제로 웹훅 등록까지 성공한 적이 없음 — ②번으로 |
"last_error_message":"Wrong response from the webhook: 403 Forbidden" | Cloudflare가 막고 있음 — ③번으로 |
"pending_update_count":0, 에러 없음 | 정상 전달 중 — Executions 탭에서 노드별 확인 |
n8n은 활성화(Publish) "시점"에 한 번만 텔레그램에 웹훅 등록을 시도합니다. HTTPS·도메인 설정을 나중에 고쳐도, 이미 활성화돼 있던 워크플로우는 자동으로 재등록하지 않습니다. 워크플로우 목록에서 Active를 OFF → 다시 ON으로 전환해서 강제로 재등록을 유도하세요. (목록에서 안 보이면 필터가 "Active"로 걸려있는 것 — 검색창에 이름으로 찾으면 항상 나옵니다)
③ Cloudflare가 403으로 차단하는 경우 (가장 흔한 막힘 지점)
getWebhookInfo에서 "Wrong response from the webhook: 403 Forbidden"이 보이면, n8n이 아니라 Cloudflare 엣지 단계에서 먼저 막힌 것입니다. Cloudflare Security → Events 로그에서 어떤 규칙이 막았는지 확인할 수 있습니다.
1. Security → Bots → "Bot Fight Mode" 끄기 (커스텀 규칙과 별개로 독립적으로 차단하는 기능이라 따로 꺼야 함) 2. 그래도 안 되면 Security → WAF → Custom rules → Create rule 조건: URI Path contains "/webhook/" 동작: Skip (Bot Fight Mode 포함 전부 체크) 3. ⭐ 가장 흔히 놓치는 부분 — 규칙 우선순위(맨 위로 올리기) 규칙을 추가만 하고 순서를 안 올리면, 다른 규칙(특히 Bot Fight Mode나 WAF 매니지드 룰)이 먼저 평가되어 여전히 막힙니다. 규칙 목록에서 방금 만든 규칙을 드래그해서 1순위로 올리세요.
WAF 규칙을 추가했는데도 안 됐던 이유가 우선순위가 1번이 아니었기 때문이었던 경우가 있습니다. 규칙을 맨 위로 올리자마자 즉시 정상 작동했습니다 — 규칙 자체의 조건/동작은 맞았는데 순서 때문에 적용이 안 된 케이스였습니다.
Dify — 공식 git clone 단독 설치
Dify는 api·worker·web·nginx·postgres·redis·qdrant·sandbox 8개 서비스가 묶인 독립 스택입니다. 공식 팀이 관리하는 docker-compose.yaml을 그대로 사용하면 업데이트 시 git pull 한 줄로 해결됩니다. 통합 compose에 합치면 Dify 업데이트마다 수동으로 머지해야 하는 부담이 생깁니다.
Dify는 ai-common-net을 external로 참조합니다. 통합 compose(cd /mnt/data && docker compose up -d)가 먼저 실행되어 네트워크가 생성된 상태여야 합니다.
외부 접속을 하기 위해 dify.도메인.com 등으로 접속이 가능해야 합니다. 꼭 외부 접속 작업을 먼저 끝내고 진행하세요.
1. one.dash.cloudflare.com 접속
2. Networks → Tunnels → 기존 터널 클릭
3. "Public Hostname" 탭 → "Add a public hostname"
4. 입력:
Subdomain: dify
Domain: 내 도메인 명
Service Type: HTTP ← 반드시 HTTP! HTTPS 선택 시 502
URL: localhost:3002 ← 스킴(http://) 없이 입력
5. Save
흔한 실수: Service Type을 HTTPS로 선택하거나 URL에 https://localhost:3002처럼 스킴을 같이 입력하면 cloudflared 로그에 tls: first record does not look like a TLS handshake 에러가 뜨며 502가 발생합니다. nginx는 평범한 HTTP로 응답하므로 반드시 HTTP 타입으로 설정해야 합니다.
① 공식 저장소 클론
# Dify를 /mnt/data/01_ai/dify/ 경로에 설치 git clone https://github.com/langgenius/dify.git /mnt/data/01_ai/dify cd /mnt/data/01_ai/dify/docker # 환경변수 파일 생성 cp .env.example .env
② .env 전체 설정 — 한 번에 적용
아래 스크립트 하나로 도메인·보안키·DB·벡터스토어·프로필·포트까지 전부 설정합니다. INIT_PASSWORD만 원하는 값으로 직접 바꿔주세요.
# ── 도메인 (단일 도메인 — 브라우저가 접근하는 주소 전부 통일) ──
DOMAIN="https://dify.도메인.com"
sed -i "s|^CONSOLE_API_URL=.*|CONSOLE_API_URL=${DOMAIN}|" .env
sed -i "s|^CONSOLE_WEB_URL=.*|CONSOLE_WEB_URL=${DOMAIN}|" .env
sed -i "s|^SERVICE_API_URL=.*|SERVICE_API_URL=${DOMAIN}|" .env
sed -i "s|^APP_API_URL=.*|APP_API_URL=${DOMAIN}|" .env
sed -i "s|^APP_WEB_URL=.*|APP_WEB_URL=${DOMAIN}|" .env
sed -i "s|^FILES_URL=.*|FILES_URL=${DOMAIN}|" .env
sed -i "s|^ENDPOINT_URL_TEMPLATE=.*|ENDPOINT_URL_TEMPLATE=${DOMAIN}/e/{hook_id}|" .env
sed -i "s|^NEXT_PUBLIC_SOCKET_URL=.*|NEXT_PUBLIC_SOCKET_URL=wss://dify.도메인.com|" .env
# ── 내부 전용 (Sandbox·Plugin이 컨테이너간 직접 통신) ──
sed -i "s|^INTERNAL_FILES_URL=.*|INTERNAL_FILES_URL=http://api:5001|" .env
# ── 보안 키 ──
SECRET=$(openssl rand -base64 42)
sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${SECRET}|" .env
sed -i "s|^INIT_PASSWORD=.*|INIT_PASSWORD=원하는비밀번호로직접교체|" .env
# ── DB — 변수명 주의: POSTGRES_PASSWORD가 아니라 DB_PASSWORD ──
DB_PASS=$(openssl rand -hex 16)
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=${DB_PASS}|" .env
# ── Redis ──
REDIS_PASS=$(openssl rand -hex 16)
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=${REDIS_PASS}|" .env
# ── 벡터스토어 (Qdrant) ──
sed -i "s|^VECTOR_STORE=.*|VECTOR_STORE=qdrant|" .env
sed -i "s|^QDRANT_URL=.*|QDRANT_URL=http://qdrant:6333|" .env
QDRANT_KEY=$(openssl rand -hex 16)
sed -i "s|^QDRANT_API_KEY=.*|QDRANT_API_KEY=${QDRANT_KEY}|" .env
# ── 프로필 (qdrant 컨테이너 + postgresql 컨테이너 + 협업 websocket 켜기) ──
sed -i "s|^COMPOSE_PROFILES=.*|COMPOSE_PROFILES=qdrant,postgresql,collaboration|" .env
# ── nginx 포트 (3003은 Firecrawl 전용이므로 회피) ──
sed -i "s|^EXPOSE_NGINX_PORT=.*|EXPOSE_NGINX_PORT=3002|" .env
sed -i "s|^EXPOSE_NGINX_SSL_PORT=.*|EXPOSE_NGINX_SSL_PORT=8443|" .env
# ── 적용 확인 (placeholder가 그대로 남아있으면 안 됨) ──
grep '\$(' .env
grep -E "^(CONSOLE_API_URL|DB_PASSWORD|REDIS_PASSWORD|VECTOR_STORE|QDRANT_URL|COMPOSE_PROFILES|EXPOSE_NGINX_PORT|EXPOSE_NGINX_SSL_PORT)=" .envOllama Base URL은 .env 파일에 넣는 게 아니라, Dify 접속 후 설정 → 모델 공급자 → Ollama에서 직접 등록합니다. (④ 단계 참고)
③ 전체 컨테이너 이름 정리 — api/worker 등 -1 접미사 제거 + DB 분리
R730에는 Dify·Firecrawl·통합본까지 여러 스택이 동시에 돌기 때문에, docker ps에서 postgres·redis 같은 이름만 보이면 어느 서비스 것인지 헷갈립니다. container_name을 안 붙이면 Compose가 자동으로 docker-api-1처럼 프로젝트명+번호를 붙이는데, 이것도 같이 깔끔하게 정리합니다. DB는 클라이언트(DBeaver 등)로 직접 붙어서 디버깅할 수 있도록 외부 포트도 서비스별로 나눕니다.
# ── DB 3종: container_name + 외부 포트(127.0.0.1 한정) ──────────
# db_postgres 서비스 블록을 찾아서 (grep -n "^ db_postgres:" docker-compose.yaml)
# image 바로 아래에 container_name과 ports 추가:
db_postgres:
image: postgres:15-alpine
container_name: dify-pg-db # [추가]
ports: # [추가]
- "127.0.0.1:15432:5432" # 외부 접속용, 로컬호스트로만 제한
# redis 서비스 블록 (grep -n "^ redis:" docker-compose.yaml)
redis:
image: redis:6-alpine
container_name: dify-redis-db # [추가]
ports: # [추가]
- "127.0.0.1:16379:6379" # 외부 접속용, 로컬호스트로만 제한
# qdrant 서비스 블록 (grep -n "^ qdrant:" docker-compose.yaml)
qdrant:
image: langgenius/qdrant:v1.8.3
container_name: dify-qdrant-db # [추가]
ports: # [추가]
- "127.0.0.1:16333:6333" # 외부 접속용, 로컬호스트로만 제한
# ── api/worker 4종: container_name만 추가 (anchor 자체는 손대지 않음) ──
# 각 서비스 블록에서 <<: *shared-api-worker-config 바로 아래에 한 줄씩만 추가
# (container_name은 networks와 달리 anchor 동작에 전혀 영향 안 줌 — 안전)
api:
<<: *shared-api-worker-config
container_name: dify-api # [추가]
api_websocket:
<<: *shared-api-worker-config
container_name: dify-api-websocket # [추가]
worker:
<<: *shared-api-worker-config
container_name: dify-worker # [추가]
worker_beat:
<<: *shared-api-worker-config
container_name: dify-worker-beat # [추가]
# ── 나머지 단일 서비스들도 동일하게 ──────────────────────────────
# web: container_name: dify-web
# sandbox: container_name: dify-sandbox
# plugin_daemon: container_name: dify-plugin-daemon
# ssrf_proxy: container_name: dify-ssrf-proxy
# certbot: container_name: dify-certbot
# (각 서비스 블록의 image 줄 바로 아래에 한 줄씩 추가하면 됩니다)위처럼 127.0.0.1:포트:내부포트 형식으로 적으면 R730 자기 자신에서만 접속 가능하고 외부(인터넷·내부망 다른 기기)에서는 막힙니다. PC에서 DBeaver 등으로 붙고 싶다면 SSH 터널(ssh -L 15432:localhost:15432 [email protected])을 사용하세요. DB 포트를 그냥 "15432:5432"로 열면 비밀번호가 뚫릴 경우 외부에서 DB에 직접 접근할 수 있어 위험합니다.
④ api/worker/plugin_daemon 추가 설정 — 권한 문제 + Ollama 연결 문제 해결 (실제 트러블슈팅에서 확인)
docker compose logs api에서 /home/dify 관련 permission denied 류 에러가 보이면 이 문제입니다. shared-api-worker-config anchor가 기본으로 지정한 비root 사용자가 해당 경로에 쓰기 권한이 없어서 발생합니다.
Linux Docker에는 host.docker.internal이 기본 지원되지 않습니다(Mac/Windows Docker Desktop 전용 기능). 명시적으로 매핑해주지 않으면 NameResolutionError 또는 연결 타임아웃이 발생합니다. 추가로, Section 06③에서 UFW를 열어두지 않았다면 매핑이 되어도 방화벽에서 다시 막힙니다 — 두 가지를 같이 확인하세요.
docker logs dify-api에서 에러를 보면 "Error in stream response for plugin" · "PluginInvokeError"라고 나옵니다. 최신 Dify는 Ollama 같은 모델 공급자 연동을 plugin_daemon이라는 별도 컨테이너가 처리합니다. api/worker에만 extra_hosts를 추가하고 plugin_daemon을 빠뜨리면, 모델 등록 화면에서 계속 타임아웃이 납니다. 실제로 172.17, 172.18, 172.16 등 여러 IP로 실패하는 로그가 반복된다면 — 매번 다른 컨테이너(또는 잘못 추측한 게이트웨이)로 시도하고 있다는 신호입니다.
# api 서비스 블록 — anchor 적용 바로 아래에 user: root + extra_hosts 추가
api:
<<: *shared-api-worker-config
container_name: dify-api
user: root # ⭕ [추가] anchor의 비root 설정을 이 서비스에서만 덮어씀
extra_hosts: # ⭕ [추가] host.docker.internal이 Linux에서 자동 해석 안 됨
- "host.docker.internal:host-gateway"
image: langgenius/dify-api:1.14.2
environment:
MODE: api
SENTRY_DSN: ${API_SENTRY_DSN:-}
# ... (이하 기존 환경변수 그대로) ...
volumes:
- ./volumes/app/storage:/app/api/storage
# - ./volumes/home_dify:/home/dify ← ❌ 이 줄은 마운트하지 않음 (주석 처리 유지)
# api_websocket / worker / worker_beat 도 같은 anchor를 쓰므로
# 동일한 permission·네트워크 문제를 겪을 가능성이 높습니다 — 똑같이 추가 권장
api_websocket:
<<: *shared-api-worker-config
container_name: dify-api-websocket
user: root # [추가]
extra_hosts: # [추가]
- "host.docker.internal:host-gateway"
worker:
<<: *shared-api-worker-config
container_name: dify-worker
user: root # [추가]
extra_hosts: # [추가] — Ollama 모델 호출은 worker에서도 발생
- "host.docker.internal:host-gateway"
worker_beat:
<<: *shared-api-worker-config
container_name: dify-worker-beat
user: root # [추가]
extra_hosts: # [추가]
- "host.docker.internal:host-gateway"
# ── plugin_daemon: anchor를 안 쓰는 별도 블록 — 가장 빠뜨리기 쉬운 곳 ──
# (Ollama 등 모델 공급자 연동·검증을 실제로 처리하는 컨테이너)
# grep -n "^ plugin_daemon:" docker-compose.yaml 으로 위치 찾기
plugin_daemon:
container_name: dify-plugin-daemon # ⭕ [추가]
extra_hosts: # ⭕ [추가] — 이게 핵심으로 빠지기 쉬운 부분
- "host.docker.internal:host-gateway"
# ... 기존 image/environment/volumes 등은 그대로 ...위 설정을 했는데도 여전히 타임아웃이면, Section 06③의 UFW 규칙(sudo ufw allow from 172.16.0.0/12 to any port 11434)을 아직 안 하신 경우입니다. extra_hosts는 "이름을 어떤 IP로 풀지"만 정해주고, 그 IP로 실제 패킷이 나가는 건 방화벽 통과 여부에 달려있습니다. 둘 다 해야 완전히 해결됩니다.
# ⓪ 진짜 에러가 어느 컨테이너에서 나는지 먼저 확인 docker logs dify-api 2>&1 | grep -iE "error|fail" | tail -20 # "Error in stream response for plugin" / "PluginInvokeError" 가 보이면 # → 아래는 dify-api가 아니라 plugin_daemon을 대상으로 진행 # ① plugin_daemon 컨테이너 안에서 호스트 Ollama가 실제로 보이는지 확인 docker compose down && docker compose up -d # extra_hosts는 recreate 필요 docker exec dify-plugin-daemon curl -s http://host.docker.internal:11434/api/tags # 모델 목록 JSON이 나오면 성공 → Dify 관리자 화면에서 같은 주소로 등록하면 됨 # ② 그래도 타임아웃이면 방화벽이 원인 — UFW 규칙 확인 sudo ufw status | grep 11434 # "11434 ALLOW 172.16.0.0/12" 가 없으면 Section 06③ 명령어 다시 실행 # ③ ①②를 다 했는데도 안 되면 — plugin_daemon의 게이트웨이 IP 직접 알아내서 우회 # (api/worker와 plugin_daemon은 같은 default 네트워크라 보통 게이트웨이가 같지만, # 다를 수도 있으니 plugin_daemon 기준으로 다시 확인하는 게 정확합니다) docker inspect dify-plugin-daemon | grep -E '"Gateway"|"IPAddress"' # "Gateway": "172.18.0.1" 같은 값이 나옴 (환경마다 다른 대역일 수 있음) docker exec dify-plugin-daemon curl -s http://172.18.0.1:11434/api/tags # 여기서 모델 목록이 나오면, Dify Base URL에 host.docker.internal 대신 # 이 게이트웨이 IP를 직접 입력 (예: http://172.18.0.1:11434)
extra_hosts: host-gateway가 이론상 정답이지만, Docker 버전·네트워크 구성에 따라 설정해도 끝까지 해석이 안 되는 경우가 실제로 있습니다. 이럴 땐 망설이지 말고 ③번처럼 게이트웨이 IP를 직접 찾아서 박아넣는 게 가장 빠른 해결책입니다. 다만 docker compose down처럼 네트워크가 재생성되면 이 IP가 바뀔 수 있으니, Ollama 연결이 갑자기 끊기면 ③번부터 다시 확인하세요.
YAML에서 <<: *anchor로 병합한 뒤 같은 키(user, extra_hosts)를 다시 쓰면 나중에 쓴 값이 anchor 값을 덮어씁니다. anchor 원본은 그대로 두고 각 서비스에서만 override하는 방식이라, 다른 곳에 영향을 주지 않고 안전하게 문제만 해결됩니다. plugin_daemon은 애초에 이 anchor를 안 쓰는 별도 블록이라 더 단순하게 추가하면 됩니다. container_name도 같은 원리로 안전하게 추가 가능합니다 (anchor에 없는 새 키라 충돌 자체가 없음).
⑤ nginx만 ai-common-net에 연결 (api/worker/db는 그대로)
이 서비스들은 default 네트워크로 서로 통신합니다. ai-common-net을 여기에 추가하면 내부 통신이 끊깁니다. nginx 하나만 다리 역할을 하면 충분합니다.
# nginx 서비스 블록 찾기 (grep -n "^ nginx:" docker-compose.yaml)
# image 바로 아래, ports 아래에 추가:
nginx:
image: nginx:latest
container_name: dify-nginx # [추가] 고정 이름 — n8n 등에서 내부 호출 시 사용
restart: always
ports:
- "${EXPOSE_NGINX_PORT:-80}:${NGINX_PORT:-80}"
- "${EXPOSE_NGINX_SSL_PORT:-443}:${NGINX_SSL_PORT:-443}"
networks: # [추가]
- default # Dify 내부 통신 유지
- ai-common-net # 통합본(n8n 등) 통신용 추가
# 파일 맨 아래 networks: 섹션에 ai-common-net 추가
# (ssrf_proxy_network, milvus, opensearch-net 옆에 같이 작성)
networks:
ssrf_proxy_network:
driver: bridge
internal: true
milvus:
driver: bridge
opensearch-net:
driver: bridge
internal: true
ai-common-net: # [추가]
external: true
name: ai-common-net⑥ 실행 & 확인
docker compose up -d
# 상태 확인 (전체 서비스 healthy 대기 ~60초)
docker compose ps
# 컨테이너 이름이 의도대로 붙었는지 확인
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep dify
# 접속: http://192.168.1.100:3002 (또는 https://dify.도메인.com)
# 최초 접속 → 관리자 이메일 + INIT_PASSWORD 로 로그인
# Ollama 연동 설정
# 관리자 → 설정 → 모델 공급자 → Ollama
# Base URL: http://host.docker.internal:11434 로 먼저 시도
# 타임아웃 시 → 실제로 연결을 시도하는 건 plugin_daemon 컨테이너입니다
# ④번에서 plugin_daemon에 extra_hosts 추가했는지 먼저 확인
# (api/worker에만 추가하고 빠뜨리기 쉬운 부분)
# 그래도 안 되면 → UFW 172.16.0.0/12 확인 → 안 되면 plugin_daemon의
# 게이트웨이 IP 직접 알아내서 http://172.18.0.1:11434 식으로 입력
# (④번 진단 명령어·마지막 경고 참고)
# (plugin_daemon 컨테이너에서 호스트 Ollama에 접근)
# n8n에서 호출 시 (ai-common-net으로 내부 직접 통신, 포트는 80!)
# URL: http://dify-nginx/v1/workflows/run
docker exec -it n8n wget -qO- --server-response http://dify-nginx/ 2>&1 | head -5⑥-1 모델별 상세 등록 설정 — Context Size·Vision·Function Call
Ollama API 자체에는 이 정보가 있지만(/api/tags 응답의 capabilities 필드), Dify는 Ollama 공급자 연결 시 모델을 자동으로 가져오지 않고 하나씩 "Add Model"로 직접 등록해야 합니다. 이때 Context Size·Max Tokens·Vision Support·Function Call Support를 잘못 입력하면 — 비전 모델인데 Vision을 No로 해놨다거나, Context Size를 실제보다 작게 잡아서 긴 대화가 잘리는 등의 문제가 생깁니다.
판단 기준 — capabilities 필드로 정확히 매핑하기
curl -s http://localhost:11434/api/tags | python3 -m json.tool | grep -A6 '"name"' # 각 모델의 "capabilities" 배열을 확인: # "tools" 포함 → Function Call: Yes # "vision" 포함 → Vision Support: Yes # "embedding"만 있음 → LLM이 아니라 Text Embedding 타입으로 등록
| 모델 | Context Size | Max Tokens | Vision | Function Call | 등록 타입 |
|---|---|---|---|---|---|
| gemma4:12b | 8192 | 4096 | Yes | Yes | LLM |
| gemma4:e2b | 8192 | 4096 | No | Yes | LLM |
| hf.co/.../gemma-4-12B-coder-... | 8192 | 4096 | No | No | LLM |
| qwen3:14b | 4096 | 2048 | No | Yes | LLM |
| qwen2.5:14b | 4096 | 2048 | No | Yes | LLM |
| bge-m3:latest | 8192 | 8192 | 해당 없음 | Text Embedding | |
| nomic-embed-text:latest | 2048 | 2048 | 해당 없음 | Text Embedding | |
실제 등록 절차
gemma4:12b, qwen3:14b — 태그(:12b 등)까지 정확히 일치해야 합니다.tools가 없는 모델인데 Function Call을 Yes로 잘못 켜두면, Dify의 Agent/Tool 기능을 사용하는 워크플로우에서 모델이 응답을 제대로 못 만들거나 에러가 날 수 있습니다.bge-m3 또는 nomic-embed-text 선택 (한국어 위주라면 bge-m3 권장)새 모델을 추가로 받을 때마다 curl http://localhost:11434/api/tags로 capabilities 필드를 확인하고, tools·vision 포함 여부에 따라 위와 같은 방식으로 Function Call·Vision 값을 정확히 매핑해서 등록하세요.
⑥-2 Dify 실제 기능 테스트 — DB·Qdrant·Ollama 연동까지 확인
처음 보면 이게 정상인지 비정상인지 헷갈릴 수 있어서, 각 테스트마다 실제 정상 출력과 해석을 같이 적어둡니다.
① n8n ↔ Dify 네트워크 연결
docker exec -it n8n wget -qO- --server-response http://dify-nginx/ 2>&1 | head -5
| 정상 출력 | 해석 |
|---|---|
HTTP/1.1 307 Temporary RedirectServer: nginx | n8n 컨테이너가 ai-common-net을 통해 dify-nginx에 완벽히 도달했다는 뜻입니다. 307 리다이렉트는 로그인·설치 페이지로 토스하는 nginx의 정상 반응입니다. |
② PostgreSQL 데이터베이스
# dify-pg-db에 실제 테이블이 생성됐는지 확인 docker exec dify-pg-db psql -U postgres -d dify -c "\dt" | head -15
| 정상 출력 | 해석 |
|---|---|
accounts, alembic_version 등 10여 개 테이블 | DB 마이그레이션이 성공해서 회원 계정(accounts)·시스템 테이블이 정상 생성된 것입니다. DB 엔진이 건강하게 쿼리에 응답하고 있다는 뜻입니다. |
③ Qdrant 벡터 데이터베이스
QDRANT_KEY=$(grep QDRANT_API_KEY /mnt/data/01_ai/dify/docker/.env | cut -d= -f2) curl -s http://localhost:16333/collections -H "api-key: $QDRANT_KEY"
| 정상 출력 | 해석 |
|---|---|
{"result":{"collections":[]},"status":"ok"} | RAG(지식창고) 엔진인 Qdrant가 API 키 인증을 거쳐 정상 응답 중입니다. collections: []로 비어있는 건 막 설치한 새 가구의 빈 서랍과 같은 100% 정상 상태입니다 — 나중에 Dify 웹 UI에서 문서를 업로드하고 지식창고를 만들면 여기에 컬렉션이 하나씩 채워집니다. |
④ api 컨테이너 로그 — 에러 없는지 확인
# container_name을 dify-api로 고정했으므로 이름이 항상 동일함 (-1 접미사 없음) docker logs dify-api 2>&1 | grep -iE "error|fail" | tail -20 # 별다른 에러 없이 비어있으면 정상
⑤ 실제 챗봇 API 호출 테스트
# 1. Dify 관리자 화면에서 챗봇 앱 하나 생성 후 API 키 발급
# (스튜디오 → "앱 만들기" 패널 → "빈 상태로 시작"
# → 앱 유형 선택 화면에서 "초보자용 기본 앱 유형" 펼치기 (챗봇은 기본으로 안 보임)
# → "챗봇" 선택 → 앱 이름 입력 → 만들기
# → 만들어진 앱 안에서 좌측 사이드바 "API 액세스" 클릭
# (※ "설정" 메뉴는 없음 — 사이드바는 오케스트레이트/API 액세스/로그 및
# 어노테이션/모니터링 4개뿐. "설정"이라고 안내한 건 잘못된 정보였음)
# → 우측 상단 "API 키" 버튼 클릭 → 키 생성)
API_KEY="app-여기에발급받은키"
# 2. 실제 대화 API 호출
# API 액세스 화면 상단에 표시된 Base URL을 그대로 써도 됩니다
# (예: https://dify.도메인.com/v1 — 외부에서 테스트할 때 더 직관적)
curl -X POST http://localhost:3002/v1/chat-messages \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": {},
"query": "안녕, 너는 어떤 모델을 쓰고 있어?",
"response_mode": "blocking",
"user": "test-user"
}'
# answer 필드에 응답 텍스트가 오면 Ollama 연동까지 전부 정상 동작 확인된 것
# 에러 시: "model_currently_not_support" → 모델 공급자 설정에서 Ollama 모델이
# 실제로 선택(저장)되어 있는지 재확인⑦ Dify 업데이트 방법
cd /mnt/data/01_ai/dify
git pull
cd docker
docker compose pull
docker compose up -d
# → 통합 compose와 독립적으로 업데이트 가능
# 주의: git pull로 docker-compose.yaml이 덮어써지면 ③④에서 추가한
# container_name·ports·networks 설정이 사라질 수 있으니
# 업데이트 후 grep으로 재확인하세요.
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep dify아래 예제는 통합 compose 섹션에서 만든 텔레그램 봇·자격증명·기본 워크플로우가 이미 있다는 전제로 진행합니다. 아직 안 하셨다면 Section 07④로 돌아가 BotFather 봇 생성부터 먼저 끝내주세요.
⑧ 텔레그램 연동 — "/post 제목|카테고리|키워드" 명령으로 블로그 글 요청
이번엔 프롬프트로 노드를 하나씩 설명하지 않고, 실제로 동작하는 n8n 워크플로우 파일을 그대로 드립니다. ⑥-1에서 만든 평범한 챗봇 앱(Workflow 타입 앱 따로 안 만들어도 됨)만으로 동작하고, import 후 자격증명·chat_id·도메인 4곳만 채우면 바로 씁니다.
Telegram Trigger → 본인 확인(IF) → 명령어 파싱(Code) → 형식 오류 체크(IF) → Dify 챗봇 호출(HTTP Request) → 응답 포맷 정리(Code) → 성공/오류 답장(Telegram) 까지 8개 노드가 전부 연결된 완성된 워크플로우입니다.
import 방법
telegram-post-to-dify-workflow.json 선택채워야 하는 5곳
| 노드 | 필드 | placeholder | 채울 값 |
|---|---|---|---|
| Telegram Trigger | Credential | (빨간 느낌표) | Section 07④ ②단계에서 등록한 Telegram 자격증명 다시 선택 |
| 본인 확인 (chat_id) | rightValue | REPLACE_WITH_YOUR_CHAT_ID | @userinfobot으로 확인한 본인 숫자 ID |
| 본인 확인 (chat_id) | Convert types where required | JSON엔 loose로 들어있지만 화면에서 꺼져 보일 수 있음 | 토글 ON 확인 — 꺼져있으면 "Wrong type: number/string" 에러 발생 |
| Dify 챗봇 호출 | url | https://dify.도메인.com/v1/chat-messages | 본인의 실제 Dify 도메인으로 교체 |
| Dify 챗봇 호출 | Credential | (빨간 느낌표) | 바로 아래 "Dify 자격증명 등록" 안내대로 새 HTTP Header Auth 자격증명 생성 |
| 성공 답장 / 오류 답장 | Credential | (빨간 느낌표) | Telegram Trigger와 동일한 자격증명 선택 |
텔레그램의 chat.id는 숫자인데 비교 값은 텍스트로 입력하다 보니 타입이 안 맞아서 나는 에러입니다. "본인 확인 (chat_id)" 노드를 열고 "Convert types where required" 토글이 켜져(초록색) 있는지 확인하세요. JSON 파일 자체엔 typeValidation: loose로 이미 반영되어 있지만, n8n 화면에서 노드를 열어 직접 한 번 더 확인하는 걸 권장합니다.
Dify 자격증명 등록 — "Dify 챗봇 호출" 노드의 Credential 채우기
1. "HTTP Header Auth" 검색해서 선택 2. Name: Authorization 3. Value: Bearer app-⑥-1에서발급받은키 4. 저장 후, "Dify 챗봇 호출" 노드의 Credential 칸에서 이 자격증명 선택
실제 동작 — 텔레그램 대화 예시
나: /post Ollama로 로컬 LLM 서버 구축하기|ai-lab|Ollama,Docker,RTX3060
봇: ✅ 블로그 초안 생성 완료!
📝 제목: Ollama로 로컬 LLM 서버 구축하기
## Ollama로 로컬 LLM 서버 구축하기
### 서론
최근 개인 서버에 LLM을 직접 돌리는 사례가 늘고 있습니다...
### RTX 3060에서 Ollama 설치하기
...
### Docker와의 연동
...
### 결론
...Dify가 만든 블로그 초안에는 마크다운 문법(##, **, _, ` `, []() 등)이 들어있는데, 텔레그램 메시지 API가 이걸 파싱하려다 닫히지 않은 문법을 만나면 전체 전송이 거부됩니다. 게다가 긴 블로그 글은 텔레그램 1건당 4096자 제한도 같이 걸리기 쉽습니다. 위 JSON 파일은 이미 두 가지를 다 처리하도록 만들어져 있습니다 — "응답 포맷 정리" 노드에서 마크다운 특수문자를 전부 제거하고 3500자로 자르며, "성공 답장"·"오류 답장" 노드는 parse_mode: none으로 고정해서 plain text로만 전송합니다. (직접 워크플로우를 새로 만드신다면 이 두 가지를 꼭 챙겨주세요.)
이 워크플로우는 텔레그램으로 초안을 "답장"해주는 것까지만 합니다. 받은 초안을 NAS Obsidian vault에 자동으로 .md 파일로 저장하려면, "Dify 챗봇 호출" 노드 바로 다음(텔레그램용 sanitize 전, 원본 마크다운이 살아있는 answer 값을 그대로)에 SSH 노드를 하나 추가해서(Section 07④에서 만든 R730-SSH 자격증명 재사용) echo로 NAS 경로에 파일을 쓰는 명령을 실행하면 됩니다 — 마크다운을 제거하기 전 원본을 저장해야 보기 좋은 글이 됩니다. Section 11②의 NAS 폴더 구조를 그대로 활용하세요.
① "본인 확인(chat_id)" 노드를 빼먹는 것 — 이 노드 없이 Publish하면 전 세계 누구나 내 n8n을 조종할 수 있습니다.
② Publish를 깜빡하는 것 — n8n 2.x부터 Active 토글이 사라지고 Publish 버튼으로 바뀌었습니다. 자동저장만으론 운영에 반영 안 됩니다.
③ 4곳의 placeholder를 다 안 채우는 것 — 빨간 느낌표가 남아있는 노드가 하나라도 있으면 실행 중 에러가 납니다.
Firecrawl — 공식 단독 설치
Firecrawl도 ai-common-net을 external로 참조합니다. 통합 compose → Dify → Firecrawl 순서로 실행하세요.
api · redis(레이트리밋·캐시) · nuq-postgres(작업 큐 DB) · rabbitmq(메시지 브로커) · foundationdb + foundationdb-init(실험적 큐 백엔드, 기본은 대기 상태)가 한 세트입니다. foundationdb는 NUQ_BACKEND=fdb로 명시적으로 켜지 않는 한 그냥 떠있기만 하고 실제로는 안 쓰입니다 — docker ps에 보여도 정상입니다. extra_hosts(host.docker.internal)는 Dify와 달리 공식 compose에 이미 기본 포함되어 있어 별도로 추가할 필요 없습니다.
① 공식 저장소 클론 & 설정
git clone https://github.com/firecrawl/firecrawl.git /mnt/data/02_automation/firecrawl cd /mnt/data/02_automation/firecrawl # .env.example은 루트가 아니라 apps/api/ 안에 있음 # 그런데 .env 파일은 "두 군데 다" 필요합니다 (②번에서 이유 설명) cp apps/api/.env.example apps/api/.env cp apps/api/.env.example .env
apps/api/.env는 컨테이너 내부 애플리케이션이 읽는 설정이고, 루트의 .env는 docker-compose 자체가 YAML 안의 ${'{'}VAR{'}'} 변수를 치환할 때 읽는 설정입니다. 완전히 다른 두 단계에서 쓰입니다. 루트 .env가 없으면 docker compose up 실행 시 WARN: variable is not set 경고가 수십 줄 쏟아지고, 일부 값(특히 포트)이 의도와 다르게 동작합니다.
② 루트 .env 설정 (compose 변수 치환용 — 가장 중요)
# ── 포트: PORT(호스트)와 INTERNAL_PORT(컨테이너)는 서로 다른 변수입니다 ──
# docker-compose.yaml 117번 줄: "${PORT:-3003}:${INTERNAL_PORT:-3002}"
# PORT를 3002로 잘못 적으면 Dify nginx(3002)와 충돌해서 up 자체가 실패합니다
PORT=3003
INTERNAL_PORT=3002
# ── DB — 반드시 직접 값 채우기 ──
POSTGRES_USER=firecrawl
POSTGRES_DB=firecrawl
POSTGRES_PASSWORD=직접생성한비밀번호로교체공식 docker-compose.yaml을 보면 POSTGRES_PASSWORD: "${'{'}POSTGRES_PASSWORD:-(약한 기본값){'}'}" 처럼 기본값이 소스코드에 하드코딩되어 있습니다. 깃허브 소스를 볼 수 있는 사람이면 누구나 알 수 있는 값이라, .env에 직접 설정 안 하면 그 약한 기본값이 그대로 쓰입니다. 반드시 직접 값을 채워주세요.
PG_PASS=$(openssl rand -hex 16) sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$PG_PASS/" .env BULL_KEY=$(openssl rand -hex 16) echo "BULL_AUTH_KEY=$BULL_KEY" >> .env # 확인 (placeholder가 남아있지 않은지) grep -E "^PORT=|^INTERNAL_PORT=|^POSTGRES_PASSWORD=|^BULL_AUTH_KEY=" .env
OPENAI_API_KEY·SUPABASE_*·SLACK_WEBHOOK_URL·PROXY_*·SEARXNG_* 등은 전부 선택적 기능(AI 추출, 인증, 알림, 프록시, 검색엔진 연동)용입니다. 안 쓰면 빈 값으로 둬도 정상 동작합니다. 위에서 채운 4개(PORT·INTERNAL_PORT·POSTGRES_PASSWORD·BULL_AUTH_KEY)만 신경 쓰면 됩니다.
③ apps/api/.env 설정 (컨테이너 내부 애플리케이션용)
# 아래 항목 확인 및 수정 (여기 PORT는 ②번 루트 .env의 PORT와는 별개 스코프) USE_DB_AUTHENTICATION=false # 개인 홈랩: 인증 불필요 PORT=3002 # 컨테이너 내부용, 그대로 둬도 됨 HOST=0.0.0.0 NUM_WORKERS_PER_QUEUE=4 # R730 CPU 여유 있음 # ②번 루트 .env와 동일한 값으로 맞춰주기 BULL_AUTH_KEY=②번에서생성한값그대로 POSTGRES_USER=firecrawl POSTGRES_PASSWORD=②번에서생성한값그대로 POSTGRES_DB=firecrawl
②③번처럼 두 파일에 같은 값을 따로따로 입력하는 대신, 여기(③) apps/api/.env부터 먼저 다 채우고 그 파일을 루트로 그대로 복사하면 한 번만 입력해도 됩니다.
vi apps/api/.env # 여기서 PORT=3003으로(루트용 값) 한 번에 다 채우기 cp apps/api/.env .env sed -i 's/^PORT=.*/PORT=3003/' .env # 루트는 호스트포트라 3003 echo "INTERNAL_PORT=3002" >> .env # 컨테이너포트 명시
④ 전체 컨테이너 이름 정리 — 실제 서비스 6개 기준
Dify에서 쓴 포트(15432, 16379)와 겹치지 않도록 Firecrawl 전용 포트로 나눕니다. "worker"라는 별도 서비스는 실제로 없습니다 — api 컨테이너 하나가 harness.js로 워커까지 전부 내부 처리합니다 (예전 가이드에 있던 worker 항목은 틀린 정보였습니다).
# redis 서비스 블록 (grep -n "^ redis:" docker-compose.yaml)
redis:
image: redis:alpine
container_name: firecrawl-redis-db # [추가]
ports: # [추가]
- "127.0.0.1:16380:6379" # Dify의 16379와 겹치지 않게
# nuq-postgres 서비스 블록
nuq-postgres:
build: apps/nuq-postgres
container_name: firecrawl-pg-db # [추가]
ports: # [추가]
- "127.0.0.1:15433:5432" # Dify의 15432와 겹치지 않게
# rabbitmq 서비스 블록 (예전 가이드에 빠져있던 부분)
rabbitmq:
image: rabbitmq:3-management
container_name: firecrawl-rabbitmq # [추가]
# foundationdb 서비스 블록 (실험적 큐 백엔드, 기본은 대기상태)
foundationdb:
image: foundationdb/foundationdb:7.3.63
container_name: firecrawl-foundationdb # [추가]
foundationdb-init:
image: foundationdb/foundationdb:7.3.63
container_name: firecrawl-foundationdb-init # [추가]
# api 서비스 블록 (container_name + ai-common-net 연결 — extra_hosts는 이미 기본 포함)
api:
<<: *common-service
container_name: firecrawl-api # [추가]
networks: # [추가] — backend는 원래 있던 것, default·ai-common-net 추가
- default
- ai-common-net
- backend<<: *common-service로 anchor를 병합할 때, 배열(리스트) 값은 합쳐지지 않고 새로 쓴 값으로 완전히 덮어써집니다. anchor엔 원래 networks: [backend]가 있는데, api 서비스에서 networks: [default, ai-common-net]만 쓰면 backend가 통째로 사라집니다. 그 결과 api 컨테이너가 같은 backend망에 있는 nuq-postgres·redis·rabbitmq를 이름으로 못 찾게 되어 getaddrinfo EAI_AGAIN nuq-postgres 에러가 나며 컨테이너가 계속 재시작 루프에 빠집니다. 반드시 위처럼 backend를 같이 적어줘야 합니다 — Dify의 user: root처럼 단일 값(스칼라)을 덮어쓰는 것과는 다른 동작이니 주의하세요.
네트워크·환경변수가 이미 꼬인 상태로 여러 번 시도했다면, 부분 수정보다 완전히 지우고 다시 올리는 게 빠릅니다.
docker compose down --volumes --remove-orphans docker compose build docker compose up -d
Dify와 동일하게 127.0.0.1:포트:내부포트 형식만 사용합니다. PC에서 직접 붙고 싶다면 SSH 터널을 사용하세요.
⑤ 포트 충돌 재확인 (sed 명령은 안 통합니다 — ②번에서 이미 처리됨)
docker-compose.yaml의 실제 포트 줄은 "${'{'}PORT:-3003{'}'}:${'{'}INTERNAL_PORT:-3002{'}'}" 형태라, sed -i 's/"3002:3002"/"3003:3002"/' 같은 문자열 치환은 애초에 매칭이 안 돼서 무효합니다. 이미 ②번에서 루트 .env에 PORT=3003·INTERNAL_PORT=3002로 설정했으니 여기서는 추가 작업이 필요 없습니다 — 확인만 하면 됩니다.
grep -n "3002\|3003" docker-compose.yaml
# 117번째 줄 근처: "${PORT:-3003}:${INTERNAL_PORT:-3002}" 확인
grep -E "^PORT=|^INTERNAL_PORT=" .env
# PORT=3003 / INTERNAL_PORT=3002 로 나와야 정상⑥ 빌드 & 실행 & 확인
공식 docker-compose.yaml은 기본적으로 5개 서비스를 전부 소스에서 빌드합니다. R730 CPU 성능에 따라 다르지만 첫 빌드는 수 분~10분 이상 걸릴 수 있습니다. (Github Container Registry의 사전 빌드 이미지로 바꾸는 방법도 있지만, 이 가이드는 공식 기본 방식인 소스 빌드를 따릅니다.)
docker compose build
docker compose up -d
# 컨테이너 이름 확인
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep firecrawl
# 동작 확인 — 두 엔드포인트 다 시도 (버전에 따라 둘 중 하나만 있을 수 있음)
curl -I http://localhost:3003/test
curl -I http://localhost:3003/is-alive
# "Hello, world!" 또는 even 404("Cannot GET ...")여도 정상입니다
# 핵심은 Connection refused/timeout이 아니라 HTTP 응답 자체가 왔다는 것
# (404는 그 경로가 없다는 뜻일 뿐, 서버는 정상적으로 요청을 받고 응답한 상태)
# 실제 크롤 테스트
curl -X POST http://localhost:3003/v1/scrape \
-H "Content-Type: application/json" \
-d '{"url":"https://agibop.com","formats":["markdown"]}'
# 성공: {"success":true,"data":{"markdown":"...agibop.com 페이지 내용..."}}
# 실패: success:false → playwright 서비스 로그 확인
# docker compose logs playwright-service --tail=30특히 playwright-service는 2~4GB 정도 메모리를 씁니다. R730은 128GB라 보통 문제없지만, 다른 서비스(ComfyUI·Ollama 모델 등)와 동시에 무겁게 돌고 있었다면 일시적으로 부족할 수 있습니다. docker-compose.yaml의 해당 서비스 mem_limit 값을 늘리거나, 그 시점에 다른 무거운 작업을 줄여보세요.
⑥-1 DB 연결 실제 확인 (redis·nuq-postgres)
볼륨이 완전히 빈 상태에서 처음 구동하면, 큐 관리용 SQL 스크립트가 자동으로 로드되지 않아 nuq 스키마 테이블이 하나도 없는 채로 시작되는 경우가 실제로 보고되어 있습니다(firecrawl/firecrawl#2551). docker logs firecrawl-api에서 relation "nuq.xxx" does not exist 에러와 함께 컨테이너가 Exited 상태로 반복 종료되면 이 문제입니다. (참고: USE_DB_AUTHENTICATION=false 때문에 그냥 \dt가 비어있는 정상 상태와는 다릅니다 — 이건 실제로 크래시가 나는 경우입니다.)
# 패키지에 포함된 nuq.sql을 DB에 직접 주입 cat apps/nuq-postgres/nuq.sql | docker exec -i firecrawl-pg-db psql -U postgres -d postgres # 멈췄던 서비스 재기동 docker compose up -d # 정상화 확인 — 이제 테이블이 보여야 함 docker exec firecrawl-pg-db psql -U firecrawl -d firecrawl -c "\dt"
# nuq-postgres에 큐 테이블이 생성됐는지 확인
docker exec firecrawl-pg-db psql -U firecrawl -d firecrawl -c "\dt" | head -10
# redis 연결 확인
docker exec firecrawl-redis-db redis-cli ping
# PONG 응답이면 정상
# 크롤 작업이 실제로 큐에 들어가고 처리되는지 비동기 방식으로 확인
JOB=$(curl -s -X POST http://localhost:3003/v1/crawl \
-H "Content-Type: application/json" \
-d '{"url":"https://agibop.com","limit":3}')
echo "$JOB"
JOB_ID=$(echo "$JOB" | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
# 잠시 후 진행 상태 조회 — completed로 바뀌면 큐 처리까지 정상 동작 확인
sleep 5
curl -s http://localhost:3003/v1/crawl/$JOB_ID | python3 -m json.toolOpenClaw — 메신저 기반 범용 AI 에이전트
OpenClaw는 메신저로 말을 걸면 파일 관리·이메일·웹 브라우징·터미널 명령 실행까지 자유롭게 수행하는 범용 AI 에이전트입니다.
n8n처럼 정해진 워크플로우만 도는 게 아니라, 자연어로 시키면 AI가 즉석에서 판단해 시스템에 직접 명령을 실행합니다.
Microsoft("완전히 격리된 환경에서만 실행하라"), Cisco("개인 AI 에이전트는 보안 악몽"), Aikido("안전하게 만들려는 시도 자체가 무의미")
등 업계 전반이 동일하게 경고하고 있고, Gateway 자체에 인증이 내장되어 있지 않습니다. 이 가이드의 다른 서비스들(Dify, n8n, Firecrawl)과 같은 ai-common-net에 절대 연결하지 마세요.
NAS 자격증명·API 키·블로그 DB가 있는 R730과 분리된 별도의 저전력 미니PC나 VPS에 설치하는 걸 강력히 권장합니다.
그래도 활용해보고 싶다면, 아래는 완전히 분리된 별도 서버/VM을 기준으로 한 설치 가이드입니다. R730 가이드의 다른 섹션과 IP·네트워크를 공유하지 않습니다.
① 설치 전 체크리스트
| 항목 | 권장 사항 |
|---|---|
| 서버 | R730과 다른 물리 장비 또는 별도 VM (네트워크 분리) |
| RAM | 최소 4GB, 브라우저 자동화 사용 시 8GB |
| 접근 방법 | SSH 터널 또는 Tailscale로만 — Gateway 포트를 0.0.0.0로 절대 열지 않음 |
| 민감 정보 | 이 서버에는 NAS·Dify·블로그 DB 자격증명을 저장/마운트하지 않음 |
② OpenClaw 설치 (별도 서버에서)
curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER newgrp docker mkdir -p ~/openclaw && cd ~/openclaw
cat > docker-compose.yml << 'EOF'
services:
openclaw:
image: openclaw/openclaw:latest
# 운영 환경에서는 latest 대신 버전 고정 권장 (예: openclaw/openclaw:2026.5.0)
container_name: openclaw
restart: unless-stopped
ports:
# 127.0.0.1로만 바인딩 — 외부·내부망 어디서도 직접 접근 불가
- "127.0.0.1:18789:18789"
volumes:
- ./state:/root/.openclaw
- /etc/localtime:/etc/localtime:ro
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENCLAW_TZ=Asia/Seoul
shm_size: "1gb"
cap_add:
- SYS_ADMIN # 브라우저 자동화(헤드리스 Chrome)에 필요
deploy:
resources:
limits:
memory: 4G
cpus: "2"
healthcheck:
test: ["CMD", "openclaw", "gateway", "status"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
EOF
echo "ANTHROPIC_API_KEY=sk-ant-여기에입력" > .env
chmod 600 .envdocker compose up -d # 온보딩 마법사 실행 (LLM 공급자·메신저 연동을 대화형으로 설정) docker compose exec openclaw openclaw onboard # 상태 확인 docker compose exec openclaw openclaw gateway status
③ OpenClaw CLI 설치 (내 PC/노트북에서 원격 제어용)
매번 docker compose exec로 들어가지 않고, 내 PC에서 바로 OpenClaw에 명령을 내리고 싶다면 CLI를 로컬에 설치합니다.
# npm으로 CLI 설치 (Node.js 18+ 필요) npm install -g openclaw # CLI에서 원격 Gateway 접속 설정 # (Gateway가 127.0.0.1로만 열려있으므로 SSH 터널 필수) ssh -L 18789:localhost:18789 user@별도서버주소 -N & openclaw config set gateway.url http://localhost:18789 # 연결 확인 openclaw status
④ 메신저 연동 (Telegram 예시)
# 이 가이드 Section 07 ④에서 만든 것과는 별개의 새 봇 토큰 사용 권장 # (n8n 워크플로우 봇과 OpenClaw 봇을 분리해야 책임 범위가 명확해짐) docker compose exec openclaw openclaw plugins enable telegram # BotFather 토큰 입력 프롬프트가 뜨면 입력 # 이후 텔레그램에서 해당 봇에게 자연어로 작업 요청 가능
⑤ 사용 예시
나: 내일 날씨 확인하고 우산 챙겨야 하는지 알려줘
봇: 웹에서 확인했습니다. 내일 서울 강수확률 70%,
오후에 비가 예상되니 우산을 챙기시는 게 좋겠습니다.
나: 이 PDF 파일에서 표만 뽑아서 정리해줘 [파일 첨부]
봇: PDF에서 표 3개를 추출해 정리했습니다. (파일 첨부)⑥ n8n과의 역할 분담 — 언제 무엇을 쓸까
| n8n + Telegram (Section 07 ④) | OpenClaw | |
|---|---|---|
| 명령 형식 | 정해진 형식만 처리 (/post 제목|카테고리|키워드) | 자연어로 뭐든 시도 |
| 실행 범위 | 미리 짜둔 워크플로우만 | AI가 즉석 판단해 시스템 명령 실행 |
| 보안 | chat_id 필터 + 고정 동작만 | 인증 미내장, 프롬프트 인젝션 위험 |
| 적합한 경우 | 반복되는 정형 작업 (블로그 발행 등) | 한 번뿐인 비정형 잡일 |
| 설치 위치 | R730 통합 compose (ai-common-net) | 반드시 별도 격리 서버 |
지금 이 가이드의 블로그 자동화·홈랩 AI 인프라 목적이라면 n8n + Dify 조합으로 충분히 커버됩니다. OpenClaw는 "이메일 정리해줘", "이 사이트 가서 정보 가져와줘" 같은 비정형 요청이 자주 필요할 때만 고려하고, 그 경우에도 반드시 R730과 분리된 환경에서 운영하세요.
NAS — 기존 유지 + Synology Drive 추가
WordPress · MariaDB · Redis · Nginx는 삭제하지 않습니다. R730 초기화와 NAS는 완전히 독립적입니다.
① NAS 기존 서비스 점검
# NAS SSH 접속 ssh [email protected] # 실행 중인 서비스 확인 docker ps --format "table {{.Names}} {{.Status}}" # MariaDB 연결 테스트 (MySQL이 아닌 MariaDB!) docker exec -it mariadb mariadb -u root -p -e "SHOW DATABASES;" # wordpress 데이터베이스 확인 # WordPress 접속 테스트 curl -I https://agibop.com # HTTP/2 200 응답 확인
② 블로그 자동화 폴더 생성
mkdir -p /volume1/obsidian-vault/1.\ blog-content/{posts,templates,attachments,ready,published,scripts,logs}
chmod -R 755 /volume1/obsidian-vault
# 확인 — 공백 이스케이프가 제대로 됐는지, logs 등 7개 폴더가 정확한 위치에 생겼는지
ls -la "/volume1/obsidian-vault/1. blog-content/"
touch /volume1/obsidian-vault/1.\ blog-content/.env
chmod 600 /volume1/obsidian-vault/1.\ blog-content/.env
# .env 내용 작성
vi /volume1/obsidian-vault/1.\ blog-content/.env
# .env 내용 붙여넣기
# ── [1] 시놀로지 NAS 및 네트워크 설정 ───────────────────
NAS_IP=192.168.1.250 # 시놀로지 NAS의 내부 IP 주소
TZ=Asia/Seoul # 타임존 설정
# ── [2] MariaDB (wp-db) 접속 정보 ─────────────────────
# 앞선 단계에서 확인한 워드프레스 DB 접속용 정보입니다.
DB_HOST=192.168.1.250 # 혹은 Docker 내부망인 wp-db
DB_PORT=3306
DB_NAME=wordpress
DB_USER=wordpress
DB_PASSWORD=여기에_wordpress_유저_비밀번호_입력
# 만약 스크립트에서 root 권한이 필요하다면 아래도 추가 (선택)
DB_ROOT_PASSWORD=여기에_마스터키_비밀번호_입력
# ── [3] 워드프레스 REST API 설정 ──────────────────────
# n8n이 워드프레스에 글을 직접 발행할 때 사용하는 인증 정보입니다.
# (워드프레스 관리자 페이지 > 사용자 > 프로필에서 생성한 '애플리케이션 비밀번호')
WP_URL=https://agibop.com
WP_USERNAME=agibop # 워드프레스 관리자 로그인 ID
WP_APPLICATION_PASSWORD=xxxx xxxx xxxx xxxx xxxx
# ── [4] 외부 AI 공급자 API 키 (n8n 워크플로우용) ─────────
# 로컬 LLM(Ollama) 외에 외부 AI를 믹스해서 쓸 경우 입력합니다.
OPENAI_API_KEY=sk-proj-여기에입력
GEMINI_API_KEY=AIzaSy여기에입력
ANTHROPIC_API_KEY=sk-ant-여기에입력
# ── [5] 기타 자동화 도구 연동 (선택) ───────────────────
# R730 서버에 구축한 AI 도구들과 n8n이 통신할 때 사용합니다.
AI_SERVER_IP=192.168.1.100
OLLAMA_BASE_URL=http://192.168.1.100:11434
FIRECRAWL_API_URL=http://192.168.1.100:3003
③ Synology Drive Server 설치 & 설정
④ 실제 발행 테스트 — WordPress REST API로 글 직접 올려보기
# .env에 적어둔 WP_USERNAME / WP_APPLICATION_PASSWORD 그대로 사용
curl -X POST https://agibop.com/wp-json/wp/v2/posts \
-u "agibop:xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "자동화 테스트 글 (삭제 예정)",
"content": "n8n 연동 테스트용 임시 글입니다.",
"status": "draft"
}'
# 성공: {"id":123, "link":"https://agibop.com/?p=123", ...} 응답
# 실패: 401 Unauthorized → 애플리케이션 비밀번호 재발급 필요
# 실패: 403 Forbidden → 워드프레스 보안 플러그인이 REST API 차단 중인지 확인
# 테스트 글 삭제 (위 응답의 id 사용)
curl -X DELETE "https://agibop.com/wp-json/wp/v2/posts/123?force=true" \
-u "agibop:xxxx xxxx xxxx xxxx xxxx"⑤ n8n → NAS SSH 실제 명령 실행 테스트
1. n8n에서 새 워크플로우 생성 → "SSH" 노드 하나만 추가 (Execute Command 노드 아님 — 그건 자격증명 없이 n8n 로컬에서 실행되는 별개 노드) 2. Credential: NAS-SSH 선택 3. Resource: Command → Operation: Execute 4. Command: echo "NAS 연결 성공 $(date)" >> /volume1/obsidian-vault/1.\ blog-content/logs/test.log 5. 노드에 마우스 올리면 나오는 "Execute step" 아이콘(▶) 클릭 → 즉시 1회 실행 6. 결과 패널에 에러 없이 초록색 체크가 뜨면 성공 # NAS에서 직접 확인 ssh [email protected] "cat '/volume1/obsidian-vault/1. blog-content/logs/test.log'" # 방금 넣은 타임스탬프가 보이면 n8n→NAS SSH 경로 전체가 정상 동작 확인된 것
Obsidian + Synology Drive 연동
① PC — Drive Client 설치 & 동기화
② 모바일 — 무료 동기화 (Obsidian Sync 불필요)
n8n 블로그 자동화로 글을 발행하면 NAS /volume1/obsidian-vault/1. blog-content/posts/에 .md 파일이 생성되고, Synology Drive가 수 초 안에 PC·모바일 Obsidian에 동기화합니다.
③ 동기화 왕복 테스트 — NAS → PC → 모바일까지 실제로 전달되는지 확인
ssh [email protected] "echo '동기화테스트 $(date)' > '/volume1/obsidian-vault/sync-test.md'"sync-test.md가 새로 나타나는지 확인. 안 보이면 Synology Drive Client 트레이 아이콘에서 동기화 상태(에러 여부) 확인sync-test.md 보이면 3개 기기 전부 동기화 정상sync-test.md 내용 수정·저장 후, ssh [email protected] "cat '/volume1/obsidian-vault/sync-test.md'"로 NAS에도 변경 내용이 반영됐는지 확인ssh [email protected] "rm '/volume1/obsidian-vault/sync-test.md'" — 삭제도 모든 기기에 동기화되는지 확인하면 완벽통합 모니터링 & 최종 체크리스트
전체 서비스 상태 점검 스크립트
#!/bin/bash
echo "=== R730 홈랩 서비스 상태 $(date '+%Y-%m-%d %H:%M') ==="
echo ""
echo "▶ GPU (RTX 3060 12GB)"
nvidia-smi --query-gpu=name,temperature.gpu,memory.used,memory.total,utilization.gpu --format=csv,noheader,nounits | awk -F',' '{printf " %s | %sC | VRAM:%s/%sMiB | %s%%\n",$1,$2,$3,$4,$5}'
echo ""
echo "▶ Ollama (네이티브 systemd)"
if systemctl is-active --quiet ollama; then
echo " ✅ 실행 중"
ollama list 2>/dev/null | tail -n +2 | awk '{print " 모델: "$1" ("$3$4")"}'
else
echo " ❌ 중지됨 → sudo systemctl start ollama"
fi
echo ""
echo "▶ 통합 Docker Compose (/mnt/data)"
cd /mnt/data
for svc in portainer watchtower open-webui comfyui n8n n8n-postgres meilisearch cloudflared; do
status=$(docker inspect -f '{{.State.Status}}' $svc 2>/dev/null || echo "없음")
health=$(docker inspect -f '{{.State.Health.Status}}' $svc 2>/dev/null)
if [ "$status" = "running" ]; then
[ -n "$health" ] && icon="✅($health)" || icon="✅"
else
icon="❌($status)"
fi
echo " $icon $svc"
done
echo ""
echo "▶ Dify (공식 단독)"
cd /mnt/data/01_ai/dify/docker
docker compose ps --format " {{.Name}}: {{.State}}" 2>/dev/null
docker ps --format " └ {{.Names}}: {{.Ports}}" | grep -E "dify-(pg|redis|qdrant)-db"
echo ""
echo "▶ Firecrawl (공식 단독)"
cd /mnt/data/02_automation/firecrawl
docker compose ps --format " {{.Name}}: {{.State}}" 2>/dev/null
docker ps --format " └ {{.Names}}: {{.Ports}}" | grep -E "firecrawl-(pg|redis)-db"
echo ""
echo "▶ 시스템 리소스"
echo " RAM: $(free -h | awk '/Mem:/{print $3"/"$2}')"
echo " /mnt/data: $(df -h /mnt/data | awk 'NR==2{print $3"/"$2" ("$5")"}')"정기 백업 전략 — R730 스크립트 + NAS 하이퍼 백업 2단계 구조
R730 스크립트는 Postgres를 pg_dump로 안전하게 떠서 NAS로 옮기는 역할만 합니다 (이건 하이퍼 백업이 대신할 수 없는 일입니다 — NAS는 R730 컨테이너 안에 들어가서 명령을 실행할 수 없습니다).
버전 관리(7일치·4주치 보관 등)는 하이퍼 백업에 맡깁니다 — bash로 직접 구현하면 코드도 복잡해지고 버그 위험도 있어서, DSM의 검증된 기능을 쓰는 게 더 안전합니다.
① 백업 대상 정리
| 대상 | 방법 | 중요도 |
|---|---|---|
| n8n-pg-db / dify-pg-db / firecrawl-pg-db | pg_dump (컨테이너 내부 실행) | 🔴 필수 |
| .env 파일 4종 (통합·Dify·Firecrawl·n8n 키) | tar 압축 | 🔴 필수 — 분실 시 모든 자격증명 무효화 |
| n8n/data (암호화 키·설정) | tar 압축 | 🔴 필수 |
| dify-qdrant-db (벡터 임베딩) | 볼륨 tar 압축 | 🟡 권장 — 재생성 가능하나 시간 소요 |
| open-webui/data (대화 기록) | tar 압축 | 🟡 권장 |
| meilisearch/data | tar 압축 | 🟢 선택 — 재인덱싱 가능 |
| comfyui/workflows | tar 압축 | 🟢 선택 — 직접 만든 워크플로우만 |
| Redis (dify-redis-db, firecrawl-redis-db) | 백업 제외 | ⚪ 불필요 — 캐시·큐 데이터, 유실돼도 자동 재생성 |
② R730 스크립트 — "최신 상태"만 NAS로 (버전관리 없음)
이전 방식과 다른 점: daily/·weekly/ 날짜별 폴더를 만들지 않고, staging 폴더 하나를 매번 덮어쓰기만 합니다. 과거 버전 보관은 ④번 하이퍼 백업이 전담합니다.
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이전 버전(set -e 사용)에서는 firecrawl DB 백업이 한 번 실패하자 그 뒤의 모든 단계(.env·Qdrant·NAS 동기화)가 통째로 스킵된 적이 있었습니다. 위 버전은 각 단계를 run_step 함수로 감싸서, 하나가 실패해도 로그만 남기고 나머지는 계속 진행합니다.
이전에 별도로 만들었던 daily→weekly 승격 스크립트는 삭제하세요. 하이퍼 백업의 Smart Versioning이 일·주·월 단위 보관을 전부 대신합니다.
③ NAS 사전 준비 (1회만)
ssh [email protected] "mkdir -p /volume1/backups/r730/latest" # 비밀번호 없이 rsync 되도록 SSH 키 등록 (최초 1회) ssh-keygen -t ed25519 -f ~/.ssh/r730_to_nas -N "" ssh-copy-id -i ~/.ssh/r730_to_nas.pub [email protected] # 테스트 ssh -i ~/.ssh/r730_to_nas [email protected] "echo 연결 성공"
④ NAS 하이퍼 백업 설정 — 버전 관리 + 오프사이트 보관
backups/r730 체크/volume1/backups/r730 공유 폴더를 백업 소스로 지정합니다.⑤ crontab 등록 — R730 스크립트만 (weekly.sh 삭제됨)
# crontab -e 실행 후 한 줄만 추가 (하이퍼 백업은 DSM이 자체 스케줄러로 실행) 0 3 * * * /mnt/data/backups/backup.sh >> /mnt/data/backups/backup.log 2>&1 crontab -l # 수동 테스트 /mnt/data/backups/backup.sh cat /mnt/data/backups/backup.log
⑥ 복구 방법 — 두 가지 시나리오
| 상황 | 복구 방법 |
|---|---|
| 오늘 백업이 잘못된 걸 바로 발견 | NAS /volume1/backups/r730/latest/에서 직접 파일 가져오기 |
| 며칠 전 정상이었던 버전이 필요 | DSM Hyper Backup → 복원 → 날짜 선택 → 해당 시점 버전 복원 |
# 1. NAS 또는 하이퍼백업 복원으로 가져온 n8n.sql.gz 위치에서 gunzip -c n8n.sql.gz | docker exec -i n8n-pg-db psql -U n8n -d n8n # 2. n8n 재시작 cd /mnt/data && docker compose restart n8n # .env 복원 시 tar -xzf env-files.tar.gz -C /
1주일에 한 번 ssh [email protected] "ls -la /volume1/backups/r730/latest/postgres/"로 파일 크기가 0이 아닌지 확인하고, DSM Hyper Backup 화면에서 최근 작업이 "성공"으로 표시되는지, 버전 목록에 여러 날짜가 쌓이고 있는지 확인하세요.
실행 순서 요약 (재부팅 시 자동, 수동 시작 시 순서 준수)
# 1. Ollama (재부팅 시 자동, 수동 시작 시) sudo systemctl start ollama # 2. 통합 compose (ai-common-net 생성) cd /mnt/data && docker compose up -d # 3. Dify (ai-common-net 참조) cd /mnt/data/01_ai/dify/docker && docker compose up -d # 4. Firecrawl (ai-common-net 참조) cd /mnt/data/02_automation/firecrawl && docker compose up -d
✅ 최종 완료 체크리스트
| 단계 | 서비스 | 확인 방법 | 완료 |
|---|---|---|---|
| Sec 03 | RTX 3060 드라이버 | nvidia-smi 정상 출력 | □ |
| Sec 04 | Docker + NVIDIA Toolkit | docker run --gpus all nvidia/cuda nvidia-smi | □ |
| Sec 05 | 폴더 & 권한 | ls -la /mnt/data/02_automation/n8n/ → uid 1000 소유 | □ |
| Sec 06 | Ollama (네이티브) | ollama list 모델 확인 | □ |
| Sec 07 | 통합 compose 7개 | cd /mnt/data && docker compose ps 전체 running | □ |
| Sec 07 | Portainer | http://192.168.1.100:9000 접속 | □ |
| Sec 07 | Open WebUI | http://192.168.1.100:3001 → Ollama 모델 선택 | □ |
| Sec 07 | ComfyUI | http://192.168.1.100:8188 접속 (180초 대기) | □ |
| Sec 07 | n8n | http://192.168.1.100:5678 접속 | □ |
| Sec 08 | Dify | http://192.168.1.100:3002 → Ollama 연동 | □ |
| Sec 08 | Dify DB 컨테이너명 | docker ps | grep dify- → pg-db·redis-db·qdrant-db 확인 | □ |
| Sec 09 | Firecrawl | curl localhost:3003/health → ok | □ |
| Sec 09 | Firecrawl DB 컨테이너명 | docker ps | grep firecrawl- → pg-db·redis-db 확인 | □ |
| Sec 10 | OpenClaw (선택) | 설치했다면 별도 서버에 있는지, ai-common-net 미연결 재확인 | □ |
| Sec 11 | NAS WordPress | agibop.com 정상 접속 | □ |
| Sec 12 | Synology Drive | PC·모바일 Obsidian 동기화 확인 | □ |
| Sec 13 | 백업 — 수동 1회 테스트 | /mnt/data/backups/backup.sh 에러 없이 완료 | □ |
| Sec 13 | 백업 — NAS 복사 확인 | NAS /volume1/backups/r730/latest/에 파일 생성 확인 | □ |
| Sec 13 | 백업 — 하이퍼 백업 작업 확인 | DSM Hyper Backup에서 작업 "성공" 표시 + 버전 1개 이상 생성 | □ |
| Sec 13 | 백업 — crontab 등록 | crontab -l로 두 줄 등록 확인 | □ |
🎉 끝판왕 홈랩 구축 완료! Ollama(네이티브) + 통합 docker-compose(7개) + Dify(공식) + Firecrawl(공식) 구조로 각 서비스를 최적의 방식으로 운영합니다. RTX 3060 12GB로 gemma4:12b·qwen3:14b를 로컬에서 돌리고, ComfyUI로 FLUX.1-schnell 이미지를 생성하며, n8n이 Telegram 한 줄로 블로그 전체 발행을 자동화합니다.
참고자료 — ai-common-net 네트워크 아키텍처
ai-common-net은 docker network create로 수동 생성하지 않습니다. Section 07(통합 docker-compose.yml) 안에서 정의되고, Dify·Firecrawl이 external: true로 참조하는 식으로 이미 본문 곳곳에서 자연스럽게 만들어집니다. 전체 구조가 한눈에 안 잡힐 때 이 페이지로 돌아와 확인하세요.
통합 docker-compose.yml → ai-common-net 정의 (소유) ├── Portainer → ai-common-net 사용 ├── Open WebUI → ai-common-net 사용 ├── ComfyUI → ai-common-net 사용 ├── n8n → ai-common-net 사용 ├── Meilisearch → ai-common-net 사용 └── Cloudflared → network_mode: host (별도) Dify docker-compose.yaml → ai-common-net: external: true 참조 (nginx만 연결) Firecrawl docker-compose.yaml → ai-common-net: external: true 참조 (api만 연결) 실행 순서 (본문에서 이미 이 순서로 안내됨): 1. cd /mnt/data && docker compose up -d ← 네트워크 생성됨 2. cd /mnt/data/01_ai/dify && docker compose up -d 3. cd /mnt/data/02_automation/firecrawl && docker compose up -d
docker network ls | grep ai-common-net
docker network inspect ai-common-net | grep -E "Name|Subnet"
# 어떤 컨테이너가 실제로 연결돼 있는지 한눈에 확인
docker network inspect ai-common-net --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}'
