Add pharmacy auto-registration and infrastructure improvements
- Auto-generate pharmacy_code (P001~P999) when creating new pharmacy - Add new pharmacy fields: owner info, institution code/type, API port - Change Headplane port mapping: 3000 → 3001 to avoid conflicts - Add code-server setup script for development environment - Add LXC Caddy setup documentation - Update .gitignore to exclude farmq-admin submodule 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f739916737
commit
8d27461f76
3
.gitignore
vendored
3
.gitignore
vendored
@ -159,3 +159,6 @@ dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Submodules managed separately
|
||||
farmq-admin/
|
||||
@ -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:
|
||||
|
||||
128
docs/code-server.sh
Normal file
128
docs/code-server.sh
Normal file
@ -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:<PORT> + 지정 비밀번호로 갱신
|
||||
# - 기존에 떠있는 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}" <<EOF
|
||||
bind-addr: 0.0.0.0:${PORT}
|
||||
auth: password
|
||||
password: ${PASS}
|
||||
cert: false
|
||||
EOF
|
||||
|
||||
say "✅ config.yaml 갱신됨 (bind-addr=0.0.0.0:${PORT})"
|
||||
|
||||
# 5) systemd로 떠있다면 중지(원하시는 게 '비-systemd' 운영이므로)
|
||||
if command -v systemctl >/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로 바꾸세요."
|
||||
@ -415,12 +415,41 @@ 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'
|
||||
@ -428,10 +457,11 @@ def create_app(config_name=None):
|
||||
|
||||
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)}'
|
||||
|
||||
@ -45,6 +45,9 @@ class PharmacyInfo(FarmqBase):
|
||||
|
||||
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 참조 (외래키 제약조건 없음)
|
||||
@ -52,15 +55,32 @@ class PharmacyInfo(FarmqBase):
|
||||
# 약국 기본 정보
|
||||
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
|
||||
@ -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"<PharmacyServer(id={self.id}, pharmacy_code='{self.pharmacy_code}', vpn_ip='{self.vpn_ip}')>"
|
||||
|
||||
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'
|
||||
|
||||
388
setup_doc/LXC_Caddysetup.md
Normal file
388
setup_doc/LXC_Caddysetup.md
Normal file
@ -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일
|
||||
Loading…
Reference in New Issue
Block a user