From 8d27461f767f2f002c79de368b7d3fb9e741475c Mon Sep 17 00:00:00 2001 From: PharmQ Admin Date: Sun, 2 Nov 2025 07:54:47 +0000 Subject: [PATCH] Add pharmacy auto-registration and infrastructure improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 5 +- docker-compose.yml | 2 +- docs/code-server.sh | 128 ++++++++++ farmq-admin/app.py | 38 ++- farmq-admin/models/farmq_models.py | 139 ++++++++++- setup_doc/LXC_Caddysetup.md | 388 +++++++++++++++++++++++++++++ 6 files changed, 686 insertions(+), 14 deletions(-) create mode 100644 docs/code-server.sh create mode 100644 setup_doc/LXC_Caddysetup.md 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