SFPanel

# SFPanel

한국어 · English

Release CI License: AGPL-3.0

**홈서버·VPS·NAS를 위한 올인원 서버 관리 웹 패널.** 단일 Go 바이너리 하나로 Docker·방화벽·디스크·네트워크·터미널·앱스토어까지 — 설치 즉시, 런타임 의존성 없이. - 🪶 **단일 바이너리** — Go 백엔드 + 임베디드 React SPA. SQLite 내장(CGO-free), 외부 의존성 0. `curl | sudo bash` 한 줄이면 끝. - 🐳 **Docker는 선택** — 소켓이 있으면 컨테이너·Compose 스택까지 관리하고, 없으면 해당 메뉴만 빠지고 나머지는 그대로 동작. - 🤖 **브라우저 안의 AI 코딩 세션** — Claude Code·Codex·Gemini CLI를 서버에서 띄우고, 탭을 닫아도 세션이 살아 있어 휴대폰에서 이어서 작업. - 🔒 **보안 기본 탑재** — 신규 설치는 HTTPS(자체 CA) 기본, JWT + TOTP 2FA + 일회용 복구 코드, 로그인 rate-limit, 파괴적 작업 type-to-confirm. - 📱 **데스크탑부터 모바일까지** — 한국어/English, 반응형 웹 + PWA, Windows/macOS/Linux 네이티브 앱(Tauri), Android 앱. - 🧩 **클러스터는 선택** — 기본은 단일 노드. 필요하면 Raft 멀티노드(실시간 오버뷰 + `?node=` 투명 프록시)로 확장. ## 스크린샷 | 대시보드 | 터미널 · AI 코딩 세션 | |:---:|:---:| | 대시보드 | 터미널과 AI 코딩 세션 | | **Docker** | **앱스토어** | | Docker 컨테이너 | 앱스토어 | | **클러스터** | **모바일** | | 클러스터 개요 | 모바일 대시보드 | ## 주요 기능 | 영역 | 내용 | |------|------| | **대시보드** | 리소스 상태·조치가 필요한 알림 중심 구성, CPU·메모리·디스크·네트워크 실시간 모니터링(WebSocket), 24시간 히스토리 차트(구간 선택·데이터 표), Docker 요약, 사용자 지정 바로가기 | | **Docker** | 컨테이너·이미지·볼륨·네트워크(검색·필터·정렬, 네트워크 연결/해제), Compose 스택(배포 전 변경 미리보기·서비스별 상세·업데이트 현재→대상 digest·롤백), 위험 설정 확인 절차(docker.sock·호스트 네임스페이스 등은 확인 후 허용, 패널 비밀 경로 마운트는 차단), Hub 검색, 리소스 정리, 노드 간 스택 콜드 마이그레이션(정의·.env·볼륨/바인드 데이터·이미지 이전, retain/delete/clone, 전송 속도 제한) | | **앱스토어** | 큐레이션된 **90+** 셀프호스팅 앱 원클릭 설치(\*arr·Nextcloud·Vaultwarden·Immich·AdGuard·Authentik·Forgejo 등). 추천 배지·정렬·검색, "업데이트 있음" 배지, 데이터 보존 삭제, 설치 후 헬스 체크 | | **파일 관리** | 브라우저 파일 탐색기 + Monaco 에디터, 재귀 검색, 복사·다중선택 삭제, 업로드/다운로드 | | **터미널 · AI 코딩** | 셸과 AI CLI(Claude Code·Codex·Gemini)를 tmux 세션으로 실행 — 브라우저를 닫거나 패널을 재시작해도 유지, 디렉토리별 세션 목록, 실행 옵션·계정 프로파일, 모바일 키 바. tmux가 없으면 PTY 터미널로 대체 | | **프로세스·서비스·크론** | 프로세스 트리·renice·시그널(TERM/KILL/STOP/CONT 등); systemd 서비스 제어·유닛 보기; crontab GUI + 즉시 실행·출력 캡처 + **시스템 크론 실행 로그** | | **로그** | 시스템/커스텀 로그 실시간 스트리밍(SSE), 구조화 파싱(auth·UFW·Fail2ban·sfpanel), 검색·레벨 색상·다운로드 | | **네트워크 / VPN** | 인터페이스(DHCP/Static)·DNS·라우팅·본딩, **WireGuard**(피어 관리·키 생성·클라이언트 QR·부팅 자동시작), **Tailscale** | | **디스크** | 파티션·파일시스템·LVM·RAID·스왑, 사용량 탐색기, 네트워크 드라이브(NFS/SMB) 마운트, **S.M.A.R.T.** 자체 검사 실행 + 로그 | | **방화벽** | UFW 규칙(잠금 방지 가드)·Fail2ban Jail·Docker 방화벽(DOCKER-USER 체인)·통합 포트맵(방화벽×컨테이너×프로세스) | | **패키지** | APT 검색/설치/업그레이드 + Docker·Node.js·Claude·Codex·Gemini 원클릭 설치(실시간 SSE 스트리밍) | | **보안·감사** | 패널 HTTPS(자체 CA, 신규 설치 기본), JWT + TOTP 2FA + 일회용 복구 코드, 브라우저 재시작 후 세션 복구, bcrypt, 로그인 rate-limit(5회→5분), 감사 로그(사용자·IP·경로·상태·노드) | | **백업·업데이트** | 설정 백업/복원, 예약 백업(보존 개수), 웹 셀프 업데이트(SSE + cosign/SHA-256 검증·자동 .bak 스냅샷·워치독 롤백) | | **알림** | 조건 기반 규칙 + 채널(Discord·Telegram·Webhook=Slack/Mattermost 호환) + 알림 이력 | | **클러스터** (선택) | Raft 멀티노드(자동 리더 선출·mTLS·조인 토큰), 실시간 오버뷰 WS, `?node=` 투명 프록시, 어느 노드에서나 시작하는 롤링 업데이트 | | **그 외** | 한국어/English(자동 감지) · 반응형 + PWA · 시스템 튜닝 프로파일 · 데스크탑 앱(Tauri, Win/macOS/Linux) · Android 앱 | ## 아키텍처 ``` Go Binary (Echo v4) ├── REST API (310+ endpoints) + WebSocket (8) + SSE (15+ streaming) ├── Embedded React SPA (go:embed) ├── SQLite (19 tables — 인증, 설정, 감사 로그, 메트릭 히스토리, 알림, 컨테이너 이벤트, 볼륨 사용량, 예약 백업 외) ├── Docker Go SDK (소켓 직접 통신, 미가용 시 Docker 라우트만 비활성) ├── Compose Manager (filesystem 기반, docker compose CLI) ├── System Metrics (gopsutil, 60초 주기 24시간 히스토리) └── Cluster (HashiCorp Raft + gRPC + mTLS) ├── 합의 기반 구성 동기화 (JWT 시크릿, 관리자 계정) ├── 비-리더 API 요청 프록시 (gRPC 30s / SSE HTTP relay 5m) ├── WebSocket 릴레이 (원격 노드 터미널/로그/메트릭) ├── 클러스터 오버뷰 WS 푸시 (/ws/cluster/overview, 5s 샘플러) └── Heartbeat 메트릭 수집 (CPU, 메모리, 디스크, 컨테이너) ``` ``` internal/ ├── api/ │ ├── router.go # 라우트 등록 │ ├── middleware/ # JWT, 감사 로그, 클러스터 프록시, 요청 로깅 │ └── response/ # 표준 응답, 에러 코드 (190+), 출력 새니타이징 ├── feature/ # 23개 독립 기능 모듈 │ ├── ai/ # AI 코딩·셸 세션 (tmux, 실행 옵션, 계정 프로파일) │ ├── auth/ # JWT, TOTP 2FA + 복구 코드, 비밀번호 │ ├── docker/ # 컨테이너, 이미지, 볼륨, 네트워크 │ ├── compose/ # Docker Compose 스택 (헬스체크 컴포저, 백업 retention) │ ├── portmap/ # 통합 포트 맵 (방화벽 × 컨테이너 × 프로세스) │ ├── firewall/ # UFW, Fail2ban, Docker 방화벽 │ ├── disk/ # 파티션, 파일시스템, LVM, RAID, 스왑 │ ├── network/ # 인터페이스, WireGuard (피어/QR), Tailscale │ ├── packages/ # APT, Docker, Node.js, Claude, Codex, Gemini │ ├── cluster/ # 클러스터 관리 API │ ├── alert/ # 알림 채널 (Discord/Telegram), 규칙, 이력 │ ├── system/ # 업데이트, 백업/복원, 예약 백업, 튜닝 │ ├── appstore/ # 앱스토어 (런타임 카탈로그 fetch + catalog.json 번들, 90+ 앱) │ ├── services/ # systemd 서비스 │ ├── files/ # 파일 매니저 │ ├── terminal/ # 웹 터미널 (PTY, 10,000줄 스크롤백, 재연결용 256KB 서버 버퍼) │ ├── websocket/ # WebSocket 실시간 데이터 │ ├── monitor/ # 대시보드 메트릭 │ ├── logs/ # 로그 뷰어 + 커스텀 소스 │ ├── cron/ # 크론 작업 │ ├── process/ # 프로세스 관리 │ ├── audit/ # 감사 로그 (50k 롤링) │ └── settings/ # 패널 설정 ├── cluster/ # Raft, gRPC, TLS, 합의 엔진 ├── composex/ # Compose 안전 검사 (차단/확인 2단계) ├── paneltls/ # 패널 HTTPS (자체 CA + 인증서 자동 갱신) ├── db/ # SQLite 마이그레이션, 스키마 ├── config/ # YAML 설정 로딩 ├── docker/ # Docker SDK 클라이언트 ├── monitor/ # 메트릭 히스토리 수집 └── common/ ├── exec/ # Commander 인터페이스 (테스트용 Mock 포함) └── logging/ # slog 구조화 로깅 ``` ## 기술 스택 | 영역 | 기술 | |------|------| | Backend | Go 1.26, Echo v4, SQLite (modernc.org/sqlite, CGO-free) | | Frontend | React 19, TypeScript 6, Vite 8 (rolldown), Tailwind CSS v4, shadcn/ui | | UI | uplot (차트), xterm.js v6 (터미널), Monaco Editor (코드 에디터) | | Auth | JWT (golang-jwt/jwt/v5) + TOTP (pquerna/otp) + bcrypt + refresh token rotation | | Docker | Docker Go SDK v28 | | Cluster | HashiCorp Raft v1.7, gRPC v1.83, mTLS (CA 자동 발급), peers.json 쿼럼 손실 복구 | | Monitoring | gopsutil v4, gorilla/websocket | | Desktop | Tauri 2 (Rust, Windows/Linux/macOS) | | i18n | 한국어 / English (i18next) | | E2E Test | Playwright | ## 요구사항 - Linux (x86_64 또는 arm64) - root 권한 (시스템 관리 명령 실행에 필요) - Docker (선택사항 — 없이도 패널 자체는 작동) > **검증 환경** — **Ubuntu 24.04 LTS / 26.04 LTS**에서 테스트했습니다. 다른 Debian/Ubuntu 계열에서도 대부분 동작하지만(systemd + apt 기반), 공식 검증은 위 두 버전 기준입니다. **Proxmox VE 8 / 9** 호스트도 지원합니다 — [Proxmox VE](#proxmox-ve) 참고. 그 외 배포판(RHEL/Fedora, Arch 등)은 미검증이며 패키지 관리·서비스 제어 일부 기능이 다를 수 있습니다. ## 설치 > **SFPanel은 root 권한으로 실행됩니다.** 시스템 관리(Docker, 방화벽, 디스크, 패키지 등)를 위해 root가 필요합니다. 설치 후 반드시 **2FA(TOTP)를 활성화**하여 계정 보안을 강화하세요. ```bash curl -fsSL https://raw.githubusercontent.com/svrforum/SFPanel/main/scripts/install.sh | sudo bash ``` 설치 스크립트는 다음을 자동으로 처리합니다: - SHA-256 체크섬 검증 후 바이너리를 `/usr/local/bin/sfpanel`에 설치 - `/etc/sfpanel/config.yaml` 생성 (32바이트 랜덤 JWT 시크릿, 모드 0600) - systemd 유닛 등록 (`Restart=always`, `MemoryHigh=1G`, `TasksMax=4096`) - logrotate 설정 (`/etc/logrotate.d/sfpanel`, 일별 7개 보관) - 업그레이드 시 자동 DB 스냅샷 (`sfpanel.db.bak-`, 최근 3개 보관) 설치 후: 1. 서비스가 떴는지 확인: `sudo curl --cacert /etc/sfpanel/tls/ca.crt https://localhost:3628/api/v1/health` → `{"success":true,"data":{"status":"ok","version":"…"}}` (`/etc/sfpanel`은 root만 읽을 수 있어 `sudo`가 필요합니다) 새로 설치한 패널은 자체 로컬 CA로 **HTTPS만** 제공합니다. 예전에 설치해 업그레이드한 패널은 설정을 건드리지 않으므로 계속 `http://`입니다. 2. **노출 결정부터.** 패널은 root 권한이므로, 공개 VPS라면 **먼저** 방화벽으로 3628을 LAN/VPN으로만 제한하세요. reverse proxy(Caddy/nginx/Cloudflare Tunnel)에서 TLS를 처리하려면 `server.tls.enabled: false`로 끄고 앞단을 감싸면 됩니다. 3. 셋업 위저드로 관리자 계정 생성 — **최초 설정은 LAN/loopback에서만 가능**합니다. LAN이면 `https://<서버IP>:3628`, 공개 호스트면 SSH 터널로: `ssh -L 3628:127.0.0.1:3628 <서버>` 후 `https://127.0.0.1:3628` 접속. 처음에는 브라우저 경고가 뜹니다. **설정 → 시스템**에서 CA(`sfpanel-ca.crt`)를 받아 기기에 한 번 설치하면 사라집니다. 클러스터에 붙일 서버라면 이 단계를 건너뛰어도 됩니다([아래](#새-서버를-클러스터에-추가하기) 참고). 4. **설정 → 계정 → 2단계 인증** 활성화 (강력 권장) 5. 설정 파일 수정이 필요하면: `/etc/sfpanel/config.yaml` (변경 후 `systemctl restart sfpanel`) ### Proxmox VE SFPanel은 Proxmox VE 8·9 호스트에 직접 설치할 수 있습니다. 호스트의 운영체제 — 서비스, 프로세스, 디스크, 패키지, 로그, 파일, 터미널 — 를 관리하고 하이퍼바이저는 Proxmox에 맡깁니다. VM·컨테이너, Proxmox 스토리지, Proxmox 방화벽 규칙은 관리하지 않으며, 이들을 망가뜨릴 변경은 거부합니다. - **`sudo` 없이 root로 실행하세요.** Proxmox VE에는 sudo가 없고 root가 관리자입니다. 이 문서의 명령에서 `sudo`를 빼고 실행하세요: ```bash curl -fsSL https://raw.githubusercontent.com/svrforum/SFPanel/main/scripts/install.sh | bash ``` 클러스터 토큰 화면과 `sfpanel cluster token`도 sudo 없는 참가 명령을 함께 보여 줍니다. - **방화벽.** 호스트의 패킷 필터는 Proxmox 방화벽의 것이어서, 설치 스크립트도 패널도 그곳에서 ufw를 켜지 않습니다. 그 옆에서 ufw는 8006의 웹 UI, 클러스터, 게스트 콘솔을 끊습니다. 데이터센터 방화벽이 켜져 있으면 허용 규칙을 넣기 전까지 3628이 막힙니다 — **데이터센터 → 방화벽 → 추가**, 또는 `/etc/pve/firewall/cluster.fw`의 `[RULES]` 아래에: ``` IN ACCEPT -source <신뢰-CIDR> -p tcp -dport 3628 IN ACCEPT -source -p tcp -dport 3628:3630 ``` 두 번째 규칙은 SFPanel 클러스터용입니다. 설치 스크립트는 방화벽이 켜져 있으면 두 규칙을 출력합니다. - **읽기 전용:** **네트워크**(모든 게스트가 붙어 있는 브리지는 Proxmox UI에서 편집합니다 — `:8006` → 노드 → 시스템 → 네트워크)와 **방화벽**(ufw 컨트롤 대신 Proxmox 방화벽 상태를 보여 줍니다). - **거부하거나 확인을 받는 것:** - **Docker** — 호스트에 설치하는 것은 거부합니다. 모든 게스트 아래의 포워딩과 브리지 필터링을 바꾸기 때문이며, Proxmox는 VM 안에서 쓰기를 권장합니다. 이미 설치된 Docker는 평소대로 관리합니다. - **서비스** — 노드가 잃으면 안 되는 유닛(corosync, pve-cluster, watchdog-mux, Proxmox 방화벽, networking)은 중지·비활성화할 수 없고, 다른 Proxmox 서비스는 이름을 직접 입력해야 진행됩니다. - **프로세스** — VM의 프로세스에는 시그널을 보낼 수 없습니다. VM은 `qm shutdown `로 끄세요. - **디스크** — 게스트 볼륨(zvol, 게스트 LV, VM이 열고 있는 디스크)과 부트 파티션은 거부합니다. - **Proxmox 방식으로:** **전체 업그레이드**는 Proxmox가 요구하는 대로 `dist-upgrade`를 실행하고, 구독 없는 엔터프라이즈 저장소는 실패가 아니라 경고로 알립니다. 시스템 튜닝은 브리지 필터링·IP 포워딩·conntrack을 Proxmox에 맡깁니다. ### 수동 설치 systemd가 없거나 설치 스크립트를 쓰고 싶지 않을 때: ```bash # 1. GitHub Releases에서 바이너리 다운로드 ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') VERSION=$(curl -fsSL https://api.github.com/repos/svrforum/SFPanel/releases/latest \ | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/') curl -fsSL "https://github.com/svrforum/SFPanel/releases/download/v${VERSION}/sfpanel_${VERSION}_linux_${ARCH}.tar.gz" \ | sudo tar -xzC /usr/local/bin/ # 2. 디렉토리 + 권한 sudo install -d -m 700 /etc/sfpanel /var/lib/sfpanel sudo install -d -m 755 /var/log/sfpanel # 3. 최소 config.yaml 작성 (JWT 시크릿은 반드시 고유한 값으로) sudo tee /etc/sfpanel/config.yaml > /dev/null < 수동 설치 환경에서는 클러스터 탈퇴/해산, 패널 업데이트 같은 핸들러가 의도적으로 종료되어 supervisor가 재기동해야 합니다. 가능하면 systemd 유닛을 함께 작성하거나 설치 스크립트로 옮기세요. ## 설정 `/etc/sfpanel/config.yaml`: ```yaml server: host: "0.0.0.0" port: 3628 # 기본: 3628 패널, 3629 클러스터 gRPC, 3630 Raft tls: enabled: true # 신규 설치 기본값 — 자체 CA로 인증서 발급·갱신 (/etc/sfpanel/tls) database: path: "/var/lib/sfpanel/sfpanel.db" auth: jwt_secret: "랜덤-시크릿-키" # 설치 스크립트가 자동 생성 token_expiry: "24h" docker: socket: "unix:///var/run/docker.sock" log: level: "info" # debug, info, warn, error file: "/var/log/sfpanel/sfpanel.log" cluster: enabled: false name: "" node_id: "" node_name: "" grpc_port: 3629 # Raft 전송은 자동으로 grpc_port + 1 (3630) data_dir: "/var/lib/sfpanel/cluster" cert_dir: "/etc/sfpanel/cluster" advertise_address: "" raft_tls: false ``` 환경 변수 오버라이드: | 변수 | 설명 | 기본값 | |------|------|--------| | `SFPANEL_PORT` | 서버 포트 | 3628 | | `SFPANEL_JWT_SECRET` | JWT 서명 시크릿 | config.yaml | | `SFPANEL_DB_PATH` | SQLite DB 경로 | config.yaml | | `SFPANEL_LOG_LEVEL` | 로그 레벨 (`debug`/`info`/`warn`/`error`) | `info` | | `GOMAXPROCS` | Go 동시 실행 코어 수 | 2 (홈서버 기본) | | `GOGC` | Go GC 트리거 비율(%) | 50 (메모리 절약) | > 8코어+ 클러스터 호스트에서 클러스터 fanout이 많다면 `GOMAXPROCS=4` 또는 `GOMAXPROCS=8` 정도로 풀어주는 것이 좋습니다. systemd 유닛의 `[Service]` 섹션에 `Environment="GOMAXPROCS=4"` 추가 후 `systemctl daemon-reload && systemctl restart sfpanel`. ## CLI 명령어 ```bash sfpanel # 기본 config.yaml로 실행 sfpanel /path/to/config.yaml # 지정 설정 파일로 실행 sfpanel version # 버전 정보 sfpanel update # GitHub에서 최신 버전으로 업데이트 sfpanel reset # 데이터베이스 초기화 (셋업 위저드 재실행) sfpanel help # 도움말 ``` ### 클러스터 CLI ```bash sfpanel cluster init [--name NAME] [--advertise IP] # 클러스터 초기화 sfpanel cluster token [--ttl DURATION] [--agent] # 조인 토큰 생성 (실행할 join 명령을 그대로 출력, --agent는 에이전트용) sfpanel cluster join ADDR:PORT TOKEN --ca FINGERPRINT [--advertise IP] # 클러스터 참여 (ADDR = 리더, PORT = 3629, FINGERPRINT = 토큰 화면의 CA 지문) sfpanel cluster status # 클러스터 상태 확인 sfpanel cluster list # 노드 목록 sfpanel cluster remove NODE_ID [--force] # 노드 제거 (--force: 마지막 두 투표자 중 하나도, 투표자 하나만 남아도) sfpanel cluster leave [--force] # 클러스터 탈퇴 (--force: 남는 매니저가 쿼럼을 못 이뤄도) sfpanel cluster leader-transfer NODE_ID # 리더 넘기기 sfpanel cluster reissue-cert # 이 노드의 인증서 재발급 ``` 모든 클러스터 명령은 `--config PATH` 옵션을 지원합니다 (기본값: `/etc/sfpanel/config.yaml`). ### 에이전트 CLI 에이전트는 클러스터가 관리하지만 Raft에는 들지 않는 서버입니다. 투표하지 않고, 로그인·계정·웹 UI가 없으며, 매니저의 패널에서 그 노드를 골라 관리합니다. 매니저에서 `sudo sfpanel cluster token --agent`를 실행하면 아래 명령을 리더 주소·토큰·CA 지문까지 채워 출력합니다. ```bash sfpanel agent join ADDR:PORT TOKEN --ca FINGERPRINT [--advertise IP] [--name NAME] # 에이전트로 참여 (ADDR = 리더, PORT = 3629) sfpanel agent status # 매니저·리더·인증서·패널 상태 확인 sfpanel agent reset --yes # 단독 패널로 되돌림 (--yes 없이는 할 일만 출력) # 새 서버 — 설치와 에이전트 가입을 한 줄로 curl -fsSL https://raw.githubusercontent.com/svrforum/SFPanel/main/scripts/install.sh | sudo bash -s -- --agent <리더-IP>:3629 --ca [--name <이름>] ``` 에이전트를 붙이기 전에 모든 매니저를 최신 버전으로 올리세요. 에이전트에서는 `sfpanel cluster` 명령이 거부됩니다. 포트, 제거 순서, 패널 HTTPS가 필요한 이유는 [docs/specs/cluster-ops.md](docs/specs/cluster-ops.md#agents)의 "Agents"를 보세요. ## 서비스 관리 ```bash sudo systemctl status sfpanel # 상태 확인 sudo systemctl restart sfpanel # 재시작 sudo systemctl stop sfpanel # 중지 sudo journalctl -u sfpanel -f # 실시간 로그 ``` ## 업그레이드 ```bash # 방법 1: 웹 UI # 설정 → 패널 업데이트 → 업데이트 확인 → 업데이트 설치 # 방법 2: CLI sudo sfpanel update # 방법 3: 설치 스크립트 재실행 (자동 업그레이드 감지) curl -fsSL https://raw.githubusercontent.com/svrforum/SFPanel/main/scripts/install.sh | sudo bash ``` ### 무결성 검증 릴리스마다 두 단계 검증을 수행합니다: 1. **Sigstore cosign 키리스 OIDC** — Sigstore 번들(`checksums.txt.sigstore.json`, v0.56+ 우선)이나 레거시 쌍(`checksums.txt.sig` + `checksums.txt.pem`)을 GitHub Actions OIDC 신원으로 검증 (v0.13.0+ 릴리스) 2. **SHA-256 체크섬** — 검증된 `checksums.txt`로 바이너리 아카이브 해시 대조 cosign 자료가 없는 구버전 릴리스로 다운그레이드 시도는 거부되며 (`release.IsForwardUpdate`), SHA-256만으로 fallback합니다. ### 자동 DB 스냅샷 업그레이드 직전(설치 스크립트는 서비스 정지 후, CLI는 바이너리 교체 직전)에 `sfpanel.db.bak-` 형태로 스냅샷을 자동 생성합니다. 최근 3개를 보관하고 그 이전은 자동 삭제됩니다. 마이그레이션 회귀로 패널이 부팅 안 될 때 롤백: ```bash sudo systemctl stop sfpanel sudo cp /var/lib/sfpanel/sfpanel.db.bak- /var/lib/sfpanel/sfpanel.db sudo systemctl start sfpanel ``` ### 클러스터 환경의 롤링 업그레이드 여러 노드를 동시에 재시작하면 mTLS 핸드셰이크 + Raft pre-vote 흐름 때문에 **15~20초간 leader 재선출 지연**이 발생할 수 있습니다. 무중단 업그레이드는 노드 한 대씩 처리하고 사이에 10초 이상 간격을 두세요. 또는 웹 UI의 **클러스터 → 업데이트 → 롤링 모드**(SSE 진행률 스트리밍)를 사용하면 자동으로 한 노드씩 처리합니다. ## 백업 / 복원 설정 → 설정 백업에서 웹 UI로 백업/복원 가능. 백업에 포함되는 항목: - `sfpanel.db` — 관리자 계정(+ TOTP), 설정, Compose 프로젝트 메타, 커스텀 로그 소스, 감사 로그, 메트릭 히스토리, 알림 규칙/채널/이력 - `config.yaml` — 서버 포트, JWT 시크릿, DB 경로, Docker 소켓, 클러스터 설정 - `compose/*` — Docker Compose 프로젝트 파일 (docker-compose.yml, .env) > Docker 데이터(볼륨, 이미지, 컨테이너)는 백업에 포함되지 않습니다. ## Android 앱 Android 8.0 이상에서 서버 연결과 전체 웹 관리 화면을 사용할 수 있습니다. 터미널·AI 코딩을 위한 Shift/Ctrl/Alt 키, 출력 기록 이동, 여러 줄 프롬프트 편집, 출력 읽기·검색을 제공합니다. 큰 터치 영역, 시스템 글자 크기, TalkBack 레이블과 한국어/영어를 지원합니다. 빌드·설치 방법과 검증 범위는 [Android 앱 안내](android/README.md)를 참고하세요. [최신 릴리즈](https://github.com/svrforum/SFPanel/releases/latest)에서 서명된 APK(`SFPanel-Android-*.apk`)를 받을 수 있습니다. `android-v*` 태그를 push하면 테스트와 서명 검증 후 자동 배포되고, 같은 APK가 최신 서버 릴리즈에도 첨부됩니다. ## 데스크탑 앱 (Tauri) 서버 바이너리와 별개로, **Windows / macOS / Linux 데스크탑 앱**도 같은 릴리스에 함께 제공됩니다. 이 앱은 단순한 WebView 래퍼 — 서버를 따로 띄우지 않고, 이미 운영 중인 SFPanel 인스턴스 주소(`http://<서버IP>:3628`)를 입력하면 그 패널을 데스크탑 창으로 띄워줍니다. 브라우저 탭과 기능 차이는 없고, 별도 창으로 보고 싶을 때 또는 OS-네이티브 알림/단축키와 함께 쓰고 싶을 때 유용합니다. [Releases 페이지](https://github.com/svrforum/SFPanel/releases/latest) 의 자산: | OS | 자산 | 비고 | |----|------|------| | Windows | `SFPanel__x64_en-US.msi` | 표준 MSI 설치 | | Windows | `SFPanel__x64-setup.exe` | NSIS 설치 마법사 | | Windows | `SFPanel_portable_windows_amd64.exe` | 포터블 (설치 불필요) | | macOS | `SFPanel__aarch64.dmg` | Apple Silicon (M1/M2/M3) | | Linux | `SFPanel__amd64.deb` | Debian/Ubuntu | | Linux | `SFPanel--1.x86_64.rpm` | RHEL/Fedora | | Linux | `SFPanel__amd64.AppImage` | 배포판 무관 (chmod +x 후 실행) | **macOS에서 처음 열 때:** 앱이 Apple 공증을 받지 않아서, 받은 dmg로 설치한 앱을 처음 열면 "손상되었기 때문에 열 수 없습니다"라는 메시지가 나옵니다. 파일이 손상된 것은 아닙니다. 앱을 응용 프로그램 폴더로 옮긴 뒤 터미널에서 아래 명령을 한 번 실행하고 다시 여세요. 새 dmg로 다시 설치하면 또 필요할 수 있습니다. ```bash xattr -cr /Applications/SFPanel.app ``` 데스크탑 앱은 **서버와 동일 버전 라인**으로 출시됩니다 (v0.13.5+, 그 이전 데스크탑은 `0.6.x` 별도 라인). 새 버전이 release 페이지에 올라오면 앱이 자동으로 감지하고 업데이트 확인 다이얼로그를 띄웁니다 (ed25519 서명 검증, `tauri-plugin-updater`). 수동 재설치 없이 그 자리에서 업데이트하면 됩니다. ## 클러스터 SFPanel은 HashiCorp Raft 합의 알고리즘 기반 멀티노드 클러스터를 지원합니다. ### 특징 - **자동 리더 선출** — 리더 장애 시 자동으로 새 리더 선출 - **mTLS 통신** — 노드 간 gRPC 통신에 자동 발급된 CA 인증서 사용 - **토큰 기반 조인** — 시간 제한 HMAC 서명 토큰으로 노드 인증 - **Zero-Restart 라이프사이클** — 클러스터 생성/참여 시 서비스 재시작 없이 즉시 활성화 - **구성 동기화** — JWT 시크릿, 관리자 계정이 모든 매니저 노드에 자동 동기화 (에이전트는 받지 않음) - **API 프록시** — 비-리더 노드의 API 요청을 리더에게 자동 릴레이 - **WebSocket 릴레이** — 원격 노드의 터미널, 로그를 릴레이로 접속 - **메트릭 공유** — 각 노드의 CPU, 메모리, 디스크, 컨테이너 메트릭을 클러스터 오버뷰에서 집계 - **클러스터 업데이트** — 롤링/동시 모드로 전체 클러스터 SFPanel 업데이트 (SSE 진행률 스트리밍). 어느 노드에서나 시작할 수 있고, 접속한 노드가 재시작돼도 리더가 끝까지 진행 ### 새 서버를 클러스터에 추가하기 **준비** — 모든 노드에서: - **같은 버전.** 새 서버에는 최신 버전이 설치됩니다. 기존 노드가 뒤처져 있으면 먼저 올리세요(설정 → 시스템 → 패널 업데이트, 또는 `sudo sfpanel update`). 가입 시 버전은 검사하지 않습니다. - **포트.** 노드끼리 TCP **3628**(패널), **3629**(gRPC), **3630**(Raft)을 **양방향**으로 열어 두세요. ufw를 쓴다면 각 노드에서 상대 노드마다: `sudo ufw allow from <상대-노드-IP> to any port 3628:3630 proto tcp`. Proxmox VE에서는 대신 Proxmox 방화벽에서 허용하세요([위](#proxmox-ve) 참고). - **서로 닿는 주소.** 같은 LAN이나 Tailscale 같은 VPN으로 묶으세요. 가입할 때 클러스터의 JWT 시크릿과 관리자 계정이 새 서버로 전달되므로 공개 인터넷을 거치지 않게 합니다. - **시간 동기화.** NTP(`systemd-timesyncd`)를 켜 두세요. 토큰 만료와 인증서 검증이 시계에 의존합니다. **CLI로:** ```bash # 기존 노드 — 클러스터가 아직 없다면 먼저 만듭니다 sudo sfpanel cluster init --name my-cluster --advertise <기존-노드-IP> sudo sfpanel cluster token # 새 서버에 붙여 넣을 한 줄을 리더 주소·토큰까지 채워 출력합니다 # 새 서버 — 설치부터 가입까지 한 줄로(셋업 위저드는 필요 없음) curl -fsSL https://raw.githubusercontent.com/svrforum/SFPanel/main/scripts/install.sh | sudo bash && sudo sfpanel cluster join <리더-IP>:3629 --ca # 확인 — 새 노드가 1분 안에 follower / online으로 보이면 끝 sudo sfpanel cluster list ``` sudo가 없는 서버(Proxmox VE)에서는 같은 명령을 root로, `sudo`를 빼고 실행하세요. `sfpanel cluster token`은 참가 명령을 두 가지로 모두 출력합니다. **웹 UI로:** 기존 노드의 **클러스터** 메뉴에서 (처음이면) **클러스터 초기화**를 하고 **토큰** 탭에서 토큰을 만듭니다(새 서버용 원스텝 한 줄도 함께 나옵니다). 새 서버는 셋업 위저드로 임시 관리자를 만든 뒤 **클러스터** 메뉴의 **기존 클러스터에 가입**에 리더 주소(`<리더-IP>:3629`)와 토큰을 넣습니다. 주소를 따로 고르지 않으면 리더에 닿는 주소를 자동으로 씁니다. **가입하면 달라지는 것:** - 새 서버의 로컬 관리자 계정은 **클러스터 관리자 계정으로 바뀝니다**(이름·비밀번호·2FA). 이후 모든 노드에서 같은 계정으로 로그인합니다. - 새 노드는 투표권 없는 멤버로 들어왔다가 리더와의 통신이 확인되면(보통 1분 이내) voter로 승격됩니다. - **노드 수:** Raft는 과반수가 살아 있어야 리더를 뽑습니다. **2대 클러스터는 한 대만 꺼져도 리더가 없어** 토큰 발급·노드 제거·계정 변경·클러스터 업데이트가 멈춥니다(각 노드의 로컬 관리는 계속 됩니다). 장애를 견디려면 **3대 이상**을 권장합니다. **문제 해결:** | 증상 | 조치 | |------|------| | `cannot reach leader …:3629` | 새 서버에서 리더의 3629로 나가는 길(방화벽·주소)을 확인 | | `not the cluster leader` | 리더 주소로 다시 실행 — `sfpanel cluster token`이 출력한 주소가 리더입니다 | | `token has already been used` / `expired` / `does not exist` | 리더에서 새 토큰을 만드세요. 토큰은 만든 노드에만 있어서 리더가 바뀌면 이전 토큰은 쓸 수 없습니다 | | 가입은 성공했는데 새 노드가 계속 offline | 리더에서 새 서버의 3629/3630으로 들어가는 길이 막혀 있습니다. 열면 승격됩니다 | | `found duplicate address in configuration` | 이전 시도가 남긴 노드가 리더에 있습니다. `sudo sfpanel cluster list`로 찾아 `sudo sfpanel cluster remove ` 후 새 토큰으로 다시 시도 | | 재부팅 후 클러스터 화면에 "클러스터 서비스가 시작되지 않았습니다" | 클러스터 기동이 실패해 단독으로 떠 있는 상태입니다. `sudo systemctl restart sfpanel` 후 `journalctl -u sfpanel`에서 원인을 확인 | 클러스터 생성/참여는 웹 UI 및 CLI 모두에서 **서비스 재시작 없이** 즉시 활성화됩니다 (zero-restart, `JoinEngine` PreFlight → Execute 파이프라인). 탈퇴/해산은 서비스 재시작이 필요하며, `scripts/install.sh`가 설치하는 `Restart=always` systemd 유닛(탈퇴/해산 핸들러는 의도적으로 `os.Exit`하여 supervisor 재기동을 유도) 하에서 안전하게 동작합니다. ### TLS 인증서 수명 / 회전 | 항목 | 수명 | 회전 방법 | |------|------|-----------| | 클러스터 CA | 10년 | 코디네이션된 재시작 (전 노드 동시 신뢰 갱신) | | 노드 cert | 5년 | `sudo sfpanel cluster reissue-cert` (재시작 불필요, ≤1분 내 반영) | ### 쿼럼 손실 복구 (peers.json) 하드웨어 장애나 노드 영구 분실로 잔존 voter 수가 쿼럼에 못 미쳐 정상 멤버십 변경이 불가능할 때: ```bash # 잔존 노드의 /var/lib/sfpanel/cluster/peers.json 작성: sudo tee /var/lib/sfpanel/cluster/peers.json > /dev/null <","address":":3630","non_voter":false}, {"id":"","address":":3630","non_voter":false} ] EOF sudo systemctl restart sfpanel ``` 부팅 시 Raft가 파일을 감지해 `RecoverCluster()`로 로컬 구성을 재작성하고, 다음 부팅에서 재적용되지 않도록 `peers.info`로 이름을 바꿉니다. 목록에 남은, 뺀 노드의 기록은 리더에서 `sudo sfpanel cluster remove `로 지웁니다(Raft 구성에 없는 기록이라 쿼럼 검사에 걸리지 않습니다). 정상적인 노드 제거에도 `sudo sfpanel cluster remove `를 사용하세요. peers.json 은 쿼럼 손실 외에 **운영 중 클러스터의 Raft 전송 포트 변경**에도 사용됩니다 (`grpc_port` 를 옮기면 Raft 포트가 `grpc_port + 1` 로 자동 따라가는데, 기존 멤버십에 박힌 peer 주소를 새 포트로 재작성해야 함). 두 가지 다 잘못 쓰면 멤버십이 깨질 수 있으니 [docs/specs/cluster-partition-runbook.md](docs/specs/cluster-partition-runbook.md) 의 "Port migration on a live cluster" 절차를 따라 진행하세요. ## 제거 ```bash curl -fsSL https://raw.githubusercontent.com/svrforum/SFPanel/main/scripts/install.sh | sudo bash -s -- uninstall ``` 바이너리와 서비스만 제거합니다. 설정(`/etc/sfpanel`)과 데이터(`/var/lib/sfpanel`)는 보존되니, 완전 제거하려면 다음을 추가로 실행: ```bash sudo rm -rf /etc/sfpanel /var/lib/sfpanel /var/log/sfpanel ``` ## 개발 ### 빌드 ```bash # 전체 빌드 (프론트엔드 + 백엔드 → 단일 바이너리) make build # 또는 수동: cd web && npm install && npm run build cd .. && go build -o sfpanel ./cmd/sfpanel ``` ### 개발 모드 ```bash # 터미널 1: 프론트엔드 (핫 리로드, API 프록시 → :3628) cd web && npm run dev # 터미널 2: 백엔드 (반드시 root) sudo go run ./cmd/sfpanel ``` > 수동 실행 환경 주의: systemd 없이 바이너리를 직접 띄운 경우, 웹 UI에서 **클러스터 탈퇴/해산** 또는 **패널 업데이트**를 누르면 백엔드가 의도적으로 종료된 뒤 재기동될 supervisor가 없어 패널이 꺼진 채로 남습니다. 이 버튼들은 `Restart=always` systemd 유닛(`scripts/install.sh`로 설치) 하에서만 안전합니다. 클러스터 생성/참여는 재시작 없이 즉시 활성화되므로 수동 환경에서도 안전합니다. ### 테스트 ```bash make test # Go 단위 테스트 make test-coverage # 커버리지 리포트 make lint # Go + 프론트엔드 린트 make ci # 전체 CI 파이프라인 (lint + test + build) # E2E 테스트 (Playwright) cd e2e && npm run test # 헤드리스 cd e2e && npm run test:headed # 브라우저 UI ``` ## API 모든 REST 응답은 통일된 JSON 형식: ```json {"success": true, "data": {...}} {"success": false, "error": {"code": "ERROR_CODE", "message": "..."}} ``` - 인증: `Authorization: Bearer ` 헤더 - WebSocket 인증: `POST /api/v1/auth/ws-ticket`으로 받은 일회용 티켓(`?ticket=`, 60초). `?token=`는 로컬 루프백에서만 허용 - 클러스터 원격 노드 호출: 모든 보호 라우트에 `?node=` 추가 시 `ClusterProxyMiddleware`가 대상 노드로 투명 포워딩 (gRPC 30s, SSE/WS는 HTTP/WS 직접 릴레이) - SSE 스트리밍 엔드포인트 15+개 (시스템 업데이트, Docker 이미지 풀, Compose up/update, Compose 스택 마이그레이션, 패키지·VPN 설치, appstore 설치, 클러스터 업데이트) ## 문서 | 문서 | 내용 | |------|------| | [docs/specs/tech-features.md](docs/specs/tech-features.md) | 전체 기능 상세 + 기술 스택 | | [docs/specs/api-spec.md](docs/specs/api-spec.md) | REST/SSE 엔드포인트 전수 + 요청·응답 스키마 | | [docs/specs/websocket-spec.md](docs/specs/websocket-spec.md) | WebSocket 8개 + SSE 15+개 메시지 스키마 + 클러스터 릴레이 | | [docs/specs/db-schema.md](docs/specs/db-schema.md) | SQLite 19개 테이블 + 보존 정책 + 마이그레이션 | | [docs/specs/frontend-spec.md](docs/specs/frontend-spec.md) | 페이지/컴포넌트/라우팅/상태/빌드 | | [docs/specs/cluster-partition-runbook.md](docs/specs/cluster-partition-runbook.md) | 클러스터 운영자 런북: 파티션 감지·복구, 강제 disband, 포트 마이그레이션 절차 | | [CHANGELOG.md](CHANGELOG.md) | 릴리스 노트 | | [CONTRIBUTING.md](CONTRIBUTING.md) | 기여자 가이드 (빌드/테스트, 코드 규약, PR 절차) | ## 보안 주의사항 SFPanel은 **root 권한으로 실행**되며, 서버 전체를 관리할 수 있는 강력한 도구입니다. 반드시 아래 보안 조치를 적용하세요. ### 운영자가 직접 적용해야 하는 항목 - **2FA 필수 권장** — 설정 → 2단계 인증에서 TOTP 앱(Google Authenticator 등)으로 활성화. 패널이 탈취되면 서버 전체가 위험합니다. - **강력한 비밀번호** — 초기 설정 시 최소 12자 이상 - **HTTPS 확인** — 신규 설치는 패널 자체 CA로 HTTPS가 켜집니다(`server.tls.enabled`). 기존 설치는 직접 켜거나 Nginx/Caddy/Cloudflare Tunnel 등 역방향 프록시로 HTTPS를 적용하세요. 브라우저 경고 없이 쓰려면 패널의 CA 인증서를 기기에 설치합니다. - **접근 제한** — 방화벽(UFW, Proxmox VE에서는 Proxmox 방화벽)으로 3628/3629/3630을 신뢰 IP/CIDR에만 허용 - **수동 설치 시 JWT 시크릿** — 반드시 `openssl rand -hex 32`로 고유한 값 설정 (설치 스크립트는 자동 생성) ### 패널/스크립트가 자동 적용하는 항목 - **로그인 보호** — 5회 실패 시 5분간 IP 차단 (내장 rate limiting) - **JWT 시크릿** — 32바이트 랜덤 자동 생성 + config.yaml 0600 권한 - **클러스터 mTLS** — 노드 간 gRPC/Raft 통신 자동 암호화 (CA 자동 발급) - **조인 토큰** — 시간 제한 HMAC 서명, 일회성 - **systemd 하드닝** — `MemoryHigh=1G`, `TasksMax=4096` (`scripts/install.sh`로 설치된 유닛 기준). `PrivateTmp`는 쓰지 않습니다. 패널이 마운트한 디스크와 네트워크 드라이브가 호스트와 Docker에 보이지 않게 되기 때문입니다. `RestrictSUIDSGID`도 쓰지 않습니다. 패널이 실행하는 apt와 dpkg가 설치하는 패키지의 setuid 비트를 설정하기 때문이며, 예전 유닛에서는 패널이 시작할 때 이 줄을 지웁니다. - **민감 디렉토리 권한** — `/etc/sfpanel/` 0700, `config.yaml` 0600, `/etc/sfpanel/cluster/*.key` 0600, `/var/lib/sfpanel/` 0700 ## 후원 / 링크 SFPanel이 유용하다면 커피 한 잔으로 개발을 응원해주세요. 패널 사이드바 하단에서도 동일한 후원 / 깃허브 링크를 확인할 수 있습니다. [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/svrforum) [![GitHub](https://img.shields.io/badge/GitHub-svrforum%2FSFPanel-181717?style=for-the-badge&logo=github)](https://github.com/svrforum/SFPanel) ## 라이선스 Copyright © 2026 svrforum [GNU AGPL-3.0](LICENSE) — 셀프호스팅·수정·재배포 모두 자유지만, 수정한 버전을 **네트워크 서비스로 제공**할 경우 그 소스 코드도 공개해야 합니다(§13). 정직한 셀프호스터에게는 추가 의무가 없습니다. > **상용/클로즈드 라이선스 문의** — AGPL 조건 없이 임베드·재판매하고 싶다면 별도 상용 라이선스가 가능합니다. 으로 문의하세요.