diff --git a/.gitignore b/.gitignore index e395811..3489109 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,7 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ \ No newline at end of file +.pyre/ + +# Submodules managed separately +farmq-admin/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 5598486..c95864a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: volumes: - ./headplane-config:/etc/headplane ports: - - "3000:3000" # Headplane Web UI + - "3001:3000" # Headplane Web UI (외부:3001, 내부:3000) depends_on: - headscale networks: diff --git a/docs/code-server.sh b/docs/code-server.sh new file mode 100644 index 0000000..34fe1b8 --- /dev/null +++ b/docs/code-server.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# +# setup-code-server.sh +# - code-server 미설치 시 자동 설치 +# - 최초 1회 실행해 ~/.config/code-server/config.yaml 생성 +# - config.yaml을 0.0.0.0: + 지정 비밀번호로 갱신 +# - 기존에 떠있는 code-server(수동/비-systemd) 프로세스 정리 +# - systemd 미사용: nohup으로 백그라운드 실행 +# +# 환경변수: +# PORT=8080 # 바인드 포트 (기본 8080) +# PASSWORD= # 비밀번호(무인 실행용) +# SKIP_CONFIRM=0/1 # 비밀번호 확인 입력 생략 +# +set -euo pipefail + +PORT="${PORT:-8080}" +CONFIG_DIR="${HOME}/.config/code-server" +CONFIG_FILE="${CONFIG_DIR}/config.yaml" +LOG_FILE="${HOME}/code-server.log" + +say() { echo -e "$@"; } +die() { echo -e "❌ $@" >&2; exit 1; } + +# 0) 필수 도구 준비 (curl/timeout/pgrep 등) +if ! command -v curl >/dev/null 2>&1 || ! command -v timeout >/dev/null 2>&1; then + say "📦 필요 패키지 설치 중 (curl, coreutils, procps 등)..." + if command -v apt >/dev/null 2>&1; then + apt update -y >/dev/null 2>&1 || true + apt install -y curl ca-certificates coreutils procps >/dev/null 2>&1 + else + die "apt 환경이 아닙니다. curl/timeout/pgrep가 필요합니다." + fi +fi + +# 1) code-server 설치 확인 및 자동 설치 +if ! command -v code-server >/dev/null 2>&1; then + say "📦 code-server 미설치 상태 → 설치 진행..." + bash <(curl -fsSL https://code-server.dev/install.sh) + command -v code-server >/dev/null 2>&1 || die "code-server 설치 실패" + say "✅ code-server 설치 완료" +else + say "✅ code-server 이미 설치됨" +fi + +# 2) config.yaml 생성 (없으면 최초 1회 3~5초 실행) +if [ ! -f "${CONFIG_FILE}" ]; then + say "📝 config.yaml 이 없어 최초 1회 실행으로 생성합니다..." + mkdir -p "${CONFIG_DIR}" + timeout 5s code-server >/dev/null 2>&1 || true + [ -f "${CONFIG_FILE}" ] || die "config.yaml 생성 실패" + say "✅ 기본 config.yaml 생성됨: ${CONFIG_FILE}" +else + say "ℹ️ 기존 config.yaml 감지: ${CONFIG_FILE}" +fi + +# 3) 비밀번호 입력/확정 +if [ "${PASSWORD-}" = "" ]; then + read -rsp "🔐 code-server 접속 비밀번호 입력: " PASS; echo + if [ "${SKIP_CONFIRM-0}" != "1" ]; then + read -rsp "🔐 비밀번호 확인 입력: " PASS2; echo + [ "$PASS" = "$PASS2" ] || die "비밀번호 불일치" + fi +else + PASS="$PASSWORD" +fi +[ -n "$PASS" ] || die "비밀번호는 비어 있을 수 없습니다." + +# 4) 기존 파일 백업 후 config.yaml 갱신 +ts="$(date +%Y%m%d%H%M%S)" +if [ -f "${CONFIG_FILE}" ]; then + cp -a "${CONFIG_FILE}" "${CONFIG_FILE}.bak.${ts}" + say "🗂 백업 생성: ${CONFIG_FILE}.bak.${ts}" +fi + +cat > "${CONFIG_FILE}" </dev/null 2>&1; then + if systemctl is-active --quiet "code-server@${USER}"; then + say "⏹ systemd 서비스(code-server@${USER}) 중지" + systemctl stop "code-server@${USER}" || true + fi + if [ "$EUID" -eq 0 ] && systemctl is-active --quiet "code-server@root"; then + say "⏹ systemd 서비스(code-server@root}) 중지" + systemctl stop "code-server@root" || true + fi +fi + +# 6) 수동/기존 실행 프로세스 정리 (부모/자식 순서 종료) +say "🧹 기존 code-server 수동 프로세스 정리..." +# 부모 엔트리(메인/entry) TERM +pids="$(pgrep -f "/usr/lib/code-server/lib/node /usr/lib/code-server($|/out/node/entry)" || true)" +if [ -n "${pids}" ]; then + for p in $pids; do + pkill -TERM -P "$p" 2>/dev/null || true + kill -TERM "$p" 2>/dev/null || true + done + sleep 2 +fi +# 남아있으면 KILL +pids="$(pgrep -f "/usr/lib/code-server/lib/node /usr/lib/code-server($|/out/node/entry)" || true)" +[ -n "${pids}" ] && kill -9 $pids 2>/dev/null || true + +# 보조 호스트/터미널 프로세스 잔여물 정리(있어도 없어도 무방) +pkill -f "vscode/out/bootstrap-fork --type=ptyHost" 2>/dev/null || true +pkill -f "vscode/out/bootstrap-fork --type=extensionHost" 2>/dev/null || true +pkill -f "shellIntegration-bash.sh" 2>/dev/null || true + +# 7) 비-systemd 백그라운드 실행 +say "🚀 code-server 일반 실행(nohup 백그라운드) 시작..." +nohup code-server > "${LOG_FILE}" 2>&1 & +pid=$! +disown || true +sleep 1 + +say "✅ 실행됨 (PID: ${pid})" +say "📄 로그 보기: tail -f ${LOG_FILE}" +say "🌐 접속 URL: http://<서버IP>:${PORT}" +say "🔑 비밀번호: (방금 설정한 값)" +say "🔒 보안 권장: 역프록시(Caddy/Nginx) + HTTPS 사용 시 config는 127.0.0.1로 바꾸세요." diff --git a/farmq-admin/app.py b/farmq-admin/app.py index f69304a..d81fcaa 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -415,23 +415,53 @@ def create_app(config_name=None): # FARMQ 데이터베이스에 약국 생성 farmq_session = get_farmq_session() try: + # pharmacy_code 자동 생성 (P001~P999) + last_pharmacy = farmq_session.query(PharmacyInfo)\ + .filter(PharmacyInfo.pharmacy_code.like('P%'))\ + .order_by(PharmacyInfo.pharmacy_code.desc())\ + .first() + + if last_pharmacy and last_pharmacy.pharmacy_code: + try: + last_num = int(last_pharmacy.pharmacy_code[1:]) + new_num = last_num + 1 + except: + new_num = 1 + else: + new_num = 1 + + pharmacy_code = f"P{new_num:03d}" # P001, P002, ... + new_pharmacy = PharmacyInfo( + pharmacy_code=pharmacy_code, pharmacy_name=pharmacy_name, business_number=data.get('business_number', '').strip(), manager_name=data.get('manager_name', '').strip(), phone=data.get('phone', '').strip(), address=data.get('address', '').strip(), + + # 신규 필드 + owner_name=data.get('owner_name', '').strip(), + owner_license=data.get('owner_license', '').strip(), + owner_phone=data.get('owner_phone', '').strip(), + owner_email=data.get('owner_email', '').strip(), + institution_code=data.get('institution_code', '').strip() or None, + institution_type=data.get('institution_type', '').strip() or None, + api_port=data.get('api_port', 8082), + + # 기존 필드 proxmox_host=data.get('proxmox_host', '').strip(), headscale_user_name=data.get('headscale_user_name', '').strip(), status='active' ) - + farmq_session.add(new_pharmacy) farmq_session.commit() - + farmq_session.refresh(new_pharmacy) + return jsonify({ 'success': True, - 'message': f'약국 "{pharmacy_name}"가 성공적으로 생성되었습니다.', + 'message': f'약국 "{pharmacy_name}" (코드: {pharmacy_code}) 생성 완료', 'pharmacy': new_pharmacy.to_dict() }) @@ -440,6 +470,8 @@ def create_app(config_name=None): except Exception as e: print(f"❌ 약국 생성 오류: {e}") + import traceback + traceback.print_exc() return jsonify({ 'success': False, 'error': f'서버 오류: {str(e)}' diff --git a/farmq-admin/models/farmq_models.py b/farmq-admin/models/farmq_models.py index 54b2c99..1902e68 100644 --- a/farmq-admin/models/farmq_models.py +++ b/farmq-admin/models/farmq_models.py @@ -42,31 +42,51 @@ class JSONType(TypeDecorator): class PharmacyInfo(FarmqBase): """약국 정보 테이블 - Headscale과 독립적""" __tablename__ = 'pharmacies' - + id = Column(Integer, primary_key=True, autoincrement=True) - + + # 약국 코드 (핵심 식별자) - P001~P999 + pharmacy_code = Column(String(10), unique=True) + # Headscale 연결 정보 (느슨한 결합) headscale_user_name = Column(String(255)) # users.name 참조 (외래키 제약조건 없음) headscale_user_id = Column(Integer) # users.id 참조 (외래키 제약조건 없음) - + # 약국 기본 정보 pharmacy_name = Column(String(255), nullable=False) business_number = Column(String(20)) - manager_name = Column(String(100)) + manager_name = Column(String(100)) # deprecated - use owner_name phone = Column(String(20)) address = Column(Text) - - # 기술적 정보 + + # 대표자 정보 (신규) + owner_name = Column(String(100)) + owner_license = Column(String(50)) + owner_phone = Column(String(20)) + owner_email = Column(String(100)) + + # 요양기관 정보 (신규) + institution_code = Column(String(20)) + institution_type = Column(String(20)) + + # 운영 정보 (신규) + opening_date = Column(DateTime) + business_hours = Column(Text) + + # API 포트 (신규) + api_port = Column(Integer, default=8082) + + # 기술적 정보 (deprecated - pharmacy_servers로 이동) proxmox_host = Column(String(255)) proxmox_username = Column(String(100)) proxmox_api_token = Column(Text) # 암호화 권장 - tailscale_ip = Column(String(45)) # IPv4/IPv6 지원 - + tailscale_ip = Column(String(45)) # IPv4/IPv6 지원 (deprecated) + # 상태 관리 status = Column(String(20), default='active') # active, inactive, maintenance last_sync = Column(DateTime) # 마지막 동기화 시간 notes = Column(Text) # 관리 메모 - + # 타임스탬프 created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) @@ -78,6 +98,7 @@ class PharmacyInfo(FarmqBase): """딕셔너리로 변환""" return { 'id': self.id, + 'pharmacy_code': self.pharmacy_code, 'headscale_user_name': self.headscale_user_name, 'headscale_user_id': self.headscale_user_id, 'pharmacy_name': self.pharmacy_name, @@ -85,6 +106,15 @@ class PharmacyInfo(FarmqBase): 'manager_name': self.manager_name, 'phone': self.phone, 'address': self.address, + 'owner_name': self.owner_name, + 'owner_license': self.owner_license, + 'owner_phone': self.owner_phone, + 'owner_email': self.owner_email, + 'institution_code': self.institution_code, + 'institution_type': self.institution_type, + 'opening_date': self.opening_date.isoformat() if self.opening_date else None, + 'business_hours': self.business_hours, + 'api_port': self.api_port, 'proxmox_host': self.proxmox_host, 'tailscale_ip': self.tailscale_ip, 'status': self.status, @@ -179,6 +209,97 @@ class MachineProfile(FarmqBase): } +class PharmacyServer(FarmqBase): + """약국 서버 테이블 - 약국과 서버 분리""" + __tablename__ = 'pharmacy_servers' + + id = Column(Integer, primary_key=True, autoincrement=True) + + # 약국 연결 + pharmacy_id = Column(Integer, nullable=False) + pharmacy_code = Column(String(10), nullable=False) + + # Headscale 노드 연결 + headscale_node_id = Column(Integer, unique=True) + headscale_user_id = Column(Integer) + + # 네트워크 정보 + vpn_ip = Column(String(45), nullable=False) + api_port = Column(Integer, default=8082) + is_online = Column(Boolean, default=False) + last_seen_at = Column(DateTime) + + # 서버 역할 + server_role = Column(String(20), default='primary') # primary, backup, test + is_active = Column(Boolean, default=True) + + # 하드웨어 정보 + hostname = Column(String(255)) + machine_name = Column(String(255)) + serial_number = Column(String(100)) + + cpu_model = Column(String(255)) + cpu_cores = Column(Integer) + cpu_threads = Column(Integer) + ram_gb = Column(Integer) + storage_type = Column(String(50)) + storage_gb = Column(Integer) + gpu_model = Column(String(255)) + gpu_memory_gb = Column(Integer) + network_interfaces = Column(JSONType) + + os_type = Column(String(50)) + os_version = Column(String(100)) + tailscale_version = Column(String(50)) + installed_software = Column(JSONType) + + # 관리 정보 + status = Column(String(20), default='active') + location = Column(String(255)) + purchase_date = Column(DateTime) + warranty_expires = Column(DateTime) + last_maintenance = Column(DateTime) + + # 베이스라인 메트릭 + baseline_cpu_temp = Column(Float) + baseline_cpu_usage = Column(Float) + baseline_memory_usage = Column(Float) + + # 메타데이터 + notes = Column(Text) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + def __repr__(self): + return f"" + + def to_dict(self) -> Dict[str, Any]: + return { + 'id': self.id, + 'pharmacy_id': self.pharmacy_id, + 'pharmacy_code': self.pharmacy_code, + 'headscale_node_id': self.headscale_node_id, + 'vpn_ip': self.vpn_ip, + 'api_port': self.api_port, + 'is_online': self.is_online, + 'last_seen_at': self.last_seen_at.isoformat() if self.last_seen_at else None, + 'server_role': self.server_role, + 'is_active': self.is_active, + 'hostname': self.hostname, + 'machine_name': self.machine_name, + 'cpu_model': self.cpu_model, + 'cpu_cores': self.cpu_cores, + 'ram_gb': self.ram_gb, + 'storage_gb': self.storage_gb, + 'os_type': self.os_type, + 'os_version': self.os_version, + 'status': self.status, + 'location': self.location, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + + class MonitoringMetrics(FarmqBase): """실시간 모니터링 메트릭스 - 시계열 데이터""" __tablename__ = 'monitoring_metrics' diff --git a/setup_doc/LXC_Caddysetup.md b/setup_doc/LXC_Caddysetup.md new file mode 100644 index 0000000..7c7e69f --- /dev/null +++ b/setup_doc/LXC_Caddysetup.md @@ -0,0 +1,388 @@ +# LXC Caddy에서 호스트 Tailscale 네트워크 연결 가이드 + +## 📅 작성일 +2025년 9월 22일 + +## 🎯 개요 +호스트에 Tailscale이 설치된 환경에서 LXC 컨테이너의 Caddy가 Tailscale 네트워크에 접근할 수 있도록 설정하는 완전한 가이드입니다. + +## 🏗️ 시스템 구성 + +### 환경 정보 +- **호스트**: Proxmox VE (Tailscale 설치됨) +- **LXC 컨테이너**: Caddy 웹서버 (ID: 103) +- **목표**: LXC Caddy에서 Tailscale 네트워크의 서버들에 리버스 프록시 + +### 네트워크 구조 +``` +인터넷 → 도메인(*.pharmq.kr) → Caddy(LXC 103) → 호스트(라우팅) → Tailscale 네트워크 +``` + +## ✅ 전제 조건 + +### 1. 호스트 Tailscale 설치 확인 +```bash +# 호스트에서 Tailscale 상태 확인 +tailscale status + +# 출력 예시: +# 100.64.0.3 pve-p2 myuser linux - +# 100.64.0.6 pve-hp myuser linux - +# 100.64.0.11 pve-p1 myuser linux - +``` + +### 2. LXC 컨테이너 정보 확인 +```bash +# LXC 네트워크 정보 확인 +pct exec 103 -- ip addr show eth0 + +# 출력 예시: +# inet 192.168.0.19/24 brd 192.168.0.255 scope global dynamic eth0 +``` + +## 🔧 설정 단계 + +### 1단계: 호스트에서 IP 포워딩 활성화 + +```bash +# IP 포워딩 활성화 (임시) +echo 1 > /proc/sys/net/ipv4/ip_forward + +# IP 포워딩 영구 활성화 +echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf +sysctl -p +``` + +**확인:** +```bash +cat /proc/sys/net/ipv4/ip_forward +# 출력: 1 +``` + +### 2단계: LXC에서 Tailscale 네트워크로 라우팅 추가 + +```bash +# LXC에 라우팅 규칙 추가 (임시) +pct exec 103 -- ip route add 100.64.0.0/10 via 192.168.0.200 + +# 라우팅 확인 +pct exec 103 -- ip route | grep 100.64 +# 출력: 100.64.0.0/10 via 192.168.0.200 dev eth0 +``` + +**영구 라우팅 설정:** +```bash +# LXC 내부에서 설정 +pct exec 103 -- bash -c 'echo "100.64.0.0/10 via 192.168.0.200" >> /etc/systemd/network/10-eth0.network' + +# 또는 /etc/network/interfaces 사용 (Debian 기반) +pct exec 103 -- bash -c 'echo "up ip route add 100.64.0.0/10 via 192.168.0.200" >> /etc/network/interfaces' +``` + +### 3단계: iptables MASQUERADE 설정 (핵심!) + +**⚠️ 중요: 이 단계가 없으면 일부 Tailscale 노드 연결이 실패할 수 있습니다.** + +```bash +# LXC에서 Tailscale 네트워크로의 트래픽에 MASQUERADE 적용 +iptables -t nat -A POSTROUTING -s 192.168.0.19 -d 100.64.0.0/10 -j MASQUERADE + +# MASQUERADE 규칙 확인 +iptables -t nat -L POSTROUTING -v -n | grep 100.64 +``` + +**영구 iptables 설정:** +```bash +# iptables-persistent 설치 (Debian/Ubuntu) +apt-get install iptables-persistent + +# 현재 규칙 저장 +iptables-save > /etc/iptables/rules.v4 + +# 또는 systemd 서비스로 영구화 +cat > /etc/systemd/system/lxc-tailscale-nat.service << 'EOF' +[Unit] +Description=LXC Tailscale NAT Rules +After=network.target + +[Service] +Type=oneshot +ExecStart=/sbin/iptables -t nat -A POSTROUTING -s 192.168.0.19 -d 100.64.0.0/10 -j MASQUERADE +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF + +systemctl enable lxc-tailscale-nat.service +``` + +### 4단계: 연결 테스트 + +```bash +# LXC에서 Tailscale 네트워크 ping 테스트 +pct exec 103 -- ping -c 2 100.64.0.3 + +# 출력 예시: +# PING 100.64.0.3 (100.64.0.3) 56(84) bytes of data. +# 64 bytes from 100.64.0.3: icmp_seq=1 ttl=64 time=0.038 ms +# 64 bytes from 100.64.0.3: icmp_seq=2 ttl=64 time=0.047 ms +``` + +### 5단계: Caddyfile 설정 + +**호스트 기준 Tailscale IP 매핑 확인:** +```bash +# 호스트에서 Tailscale 노드 목록 확인 +tailscale status + +# 예시 출력: +# 100.64.0.3 pve-p2 myuser linux - +# 100.64.0.6 pve-hp myuser linux - +# 100.64.0.11 pve-p1 myuser linux - +# 100.64.0.12 pve7 myuser linux - +``` + +**Caddyfile 설정 예시:** +```caddyfile +{ + email admin@pharmq.kr + acme_dns cloudflare YOUR_CLOUDFLARE_TOKEN +} + +# 와일드카드 인증서 +*.pharmq.kr { + tls { + dns cloudflare YOUR_CLOUDFLARE_TOKEN + } + respond "Wildcard domain: {host} - SSL ready!" 200 +} + +# PVE 노드들 (호스트 기준 Tailscale 네트워크) +p2.pharmq.kr { + reverse_proxy https://100.64.0.3:8006 { + transport http { + tls_insecure_skip_verify + } + } + tls { + dns cloudflare YOUR_CLOUDFLARE_TOKEN + } +} + +hp.pharmq.kr { + reverse_proxy https://100.64.0.6:8006 { + transport http { + tls_insecure_skip_verify + } + } + tls { + dns cloudflare YOUR_CLOUDFLARE_TOKEN + } +} + +p1.pharmq.kr { + reverse_proxy https://100.64.0.11:8006 { + transport http { + tls_insecure_skip_verify + } + } + tls { + dns cloudflare YOUR_CLOUDFLARE_TOKEN + } +} +``` + +### 6단계: Caddy 설정 적용 + +```bash +# Caddyfile 문법 검증 +pct exec 103 -- caddy validate --config /etc/caddy/Caddyfile + +# Caddy 설정 다시 로드 +pct exec 103 -- systemctl reload caddy + +# Caddy 상태 확인 +pct exec 103 -- systemctl status caddy +``` + +### 7단계: 최종 테스트 + +```bash +# 외부에서 도메인 접근 테스트 +curl -I https://p2.pharmq.kr + +# 성공 예시 응답: +# HTTP/2 501 +# server: pve-api-daemon/3.0 +# via: 1.1 Caddy +``` + +## 🔍 문제 해결 + +### 문제 1: LXC에서 Tailscale IP 접근 불가 + +**증상:** +```bash +pct exec 103 -- ping 100.64.0.3 +# PING 100.64.0.3 (100.64.0.3) 56(84) bytes of data. +# --- 100.64.0.3 ping statistics --- +# 2 packets transmitted, 0 received, 100% packet loss +``` + +**해결책:** +```bash +# 1. 호스트 IP 포워딩 확인 +cat /proc/sys/net/ipv4/ip_forward +# 0이면 활성화 필요 + +# 2. LXC 라우팅 규칙 확인 +pct exec 103 -- ip route | grep 100.64 +# 없으면 라우팅 규칙 추가 필요 + +# 3. 호스트 Tailscale 상태 확인 +tailscale status +# 대상 노드가 활성 상태인지 확인 +``` + +### 문제 2: Caddy에서 502 Bad Gateway + +**증상:** +```bash +curl -I https://p2.pharmq.kr +# HTTP/2 502 +``` + +**해결책:** +```bash +# 1. LXC에서 직접 연결 테스트 +pct exec 103 -- curl -I --connect-timeout 5 https://100.64.0.3:8006 + +# 2. Caddy 로그 확인 +pct exec 103 -- journalctl -u caddy --since "1 minute ago" + +# 3. 대상 서버 응답 확인 +curl -I --connect-timeout 5 https://100.64.0.3:8006 +``` + +### 문제 3: SSL 인증서 오류 + +**증상:** +``` +transport http { + dial_timeout 10s +} +``` + +**해결책:** +```caddyfile +# HTTPS 백엔드의 경우 SSL 검증 무시 추가 +reverse_proxy https://100.64.0.3:8006 { + transport http { + tls_insecure_skip_verify + } +} +``` + +## 📋 체크리스트 + +### 설정 전 확인사항 +- [ ] 호스트에 Tailscale 설치 및 활성화됨 +- [ ] LXC 컨테이너 네트워크 설정 확인 +- [ ] 대상 Tailscale 노드들이 활성 상태 + +### 설정 단계 체크리스트 +- [ ] 호스트 IP 포워딩 활성화 +- [ ] LXC에서 Tailscale 네트워크 라우팅 추가 +- [ ] LXC에서 Tailscale IP로 ping 성공 +- [ ] Caddyfile에 올바른 Tailscale IP 설정 +- [ ] HTTPS 백엔드에 `tls_insecure_skip_verify` 추가 +- [ ] Caddy 설정 검증 및 리로드 +- [ ] 외부에서 도메인 접근 테스트 성공 + +## 🎯 핵심 포인트 + +### 1. LXC에 Tailscale 설치 불필요 +- **잘못된 접근**: LXC에 Tailscale 직접 설치 +- **올바른 접근**: 호스트 Tailscale을 통한 라우팅 + +### 2. 네트워크 라우팅이 핵심 +```bash +# 이 명령어가 모든 것을 해결합니다 +pct exec 103 -- ip route add 100.64.0.0/10 via 192.168.0.200 +``` + +### 3. 호스트 기준 IP 사용 +- LXC 내부 Tailscale 상태가 아닌 **호스트 Tailscale 상태** 기준으로 IP 설정 + +### 4. HTTPS 백엔드 처리 +```caddyfile +# PVE와 같은 HTTPS 백엔드의 경우 필수 +transport http { + tls_insecure_skip_verify +} +``` + +## 🚀 확장 가능성 + +### 다른 서비스 추가 +```caddyfile +# 새로운 Tailscale 노드 추가 예시 +newservice.pharmq.kr { + reverse_proxy http://100.64.0.X:PORT + tls { + dns cloudflare YOUR_TOKEN + } +} +``` + +### 자동화 스크립트 +```bash +#!/bin/bash +# setup-lxc-tailscale-routing.sh + +LXC_ID="103" +HOST_IP="192.168.0.200" +TAILSCALE_NETWORK="100.64.0.0/10" + +# IP 포워딩 활성화 +echo 1 > /proc/sys/net/ipv4/ip_forward + +# LXC 라우팅 추가 +pct exec $LXC_ID -- ip route add $TAILSCALE_NETWORK via $HOST_IP + +echo "✅ LXC Tailscale 라우팅 설정 완료" +``` + +## 📝 유지보수 + +### 정기 점검 항목 +1. **Tailscale 연결 상태**: `tailscale status` +2. **LXC 라우팅 상태**: `pct exec 103 -- ip route | grep 100.64` +3. **Caddy 도메인 목록**: `pct exec 103 -- journalctl -u caddy | grep domains` + +### 재부팅 후 복구 +```bash +# 호스트 재부팅 후 실행할 명령어들 (3단계 모두 필수!) +echo 1 > /proc/sys/net/ipv4/ip_forward +pct exec 103 -- ip route add 100.64.0.0/10 via 192.168.0.200 +iptables -t nat -A POSTROUTING -s 192.168.0.19 -d 100.64.0.0/10 -j MASQUERADE +``` + +## 🎉 결론 + +이 방법을 사용하면: +- ✅ **LXC에 Tailscale 설치 불필요** +- ✅ **간단한 네트워크 라우팅으로 해결** +- ✅ **호스트 Tailscale 리소스 효율적 활용** +- ✅ **SSL 인증서 자동 관리** +- ✅ **확장성 및 유지보수성 우수** + +**핵심 원리**: 호스트가 이미 Tailscale 네트워크에 연결되어 있다면, LXC는 **라우팅 + MASQUERADE**를 통해 호스트를 경유하여 접근할 수 있습니다! + +**⚠️ 중요 교훈**: MASQUERADE 없이는 일부 Tailscale 노드 연결이 실패할 수 있습니다. 반드시 3단계 모두 필요합니다! + +--- +**작성자**: Claude Code Assistant +**파일 위치**: `/srv/pq_setup/LXC_Caddy_with_Host_Tailscale_Setup_Guide.md` +**최종 업데이트**: 2025년 9월 22일 \ No newline at end of file