# SFPanel
한국어 · English
**홈서버·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 코딩 세션 |
|:---:|:---:|
|
|
|
| **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이 유용하다면 커피 한 잔으로 개발을 응원해주세요. 패널 사이드바 하단에서도 동일한 후원 / 깃허브 링크를 확인할 수 있습니다.
[](https://buymeacoffee.com/svrforum)
[](https://github.com/svrforum/SFPanel)
## 라이선스
Copyright © 2026 svrforum
[GNU AGPL-3.0](LICENSE) — 셀프호스팅·수정·재배포 모두 자유지만, 수정한 버전을 **네트워크 서비스로 제공**할 경우 그 소스 코드도 공개해야 합니다(§13). 정직한 셀프호스터에게는 추가 의무가 없습니다.
> **상용/클로즈드 라이선스 문의** — AGPL 조건 없이 임베드·재판매하고 싶다면 별도 상용 라이선스가 가능합니다. 으로 문의하세요.