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:
PharmQ Admin 2025-11-02 07:54:47 +00:00
parent f739916737
commit 8d27461f76
6 changed files with 686 additions and 14 deletions

3
.gitignore vendored
View File

@ -159,3 +159,6 @@ dmypy.json
# Pyre type checker
.pyre/
# Submodules managed separately
farmq-admin/

View File

@ -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
View 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로 바꾸세요."

View File

@ -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)}'

View File

@ -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
View 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일