Issue #96: an operator fronting the panel with Caddy and a smallstep private CA hit a redirect loop, and there was no documented path to follow. The architecture already tolerated this deployment -- the fix was a config file we already ship -- but nothing said so anywhere. Adds an INSTALLATION.md section covering both topologies (external proxy -> ServerKit nginx, full features, HOPS=2; external proxy -> the app directly, panel-only, HOPS=1), why the loop happens, how to count TRUSTED_PROXY_HOPS, Caddy (with acme_ca for a private CA), Traefik, Nginx Proxy Manager and plain nginx snippets, what a private CA does and does not require (the panel never sees it; a remote agent's host must trust the root), and a symptom-to-cause table for the loop, stale HSTS, the rejected websocket and wrong client IPs. Also corrects the Docker documentation, which had drifted badly: - README (en/es/pt/zh-CN), INSTALLATION and DEPLOYMENT all told people to run `docker compose up -d` and reach the panel at http://localhost. It starts the all-in-one container on :5000, and a containerised panel cannot manage its own host -- now stated up front rather than discovered. - DEPLOYMENT is a host-install guide that told operators to run `docker compose build && up` to fix a panel, which would build a second panel beside theirs, and carried a certbot cron copying certs and restarting a container that does not exist on a host install. - CLAUDE.md, AGENTS.md, CONTRIBUTING.md and LOCAL_DEVELOPMENT.md (x3) pointed at a docker-compose.dev.yml that has never existed; the snippet offering to create it referenced a frontend/Dockerfile.dev that does not exist either. Every anchor introduced here was checked to resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.2 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
ServerKit is a server control panel for managing web applications, databases, Docker containers, and security on VPS/dedicated servers. Flask backend (Python 3.11+), React frontend (Vite + SCSS), SQLite/PostgreSQL database, real-time updates via Socket.IO.
Development Commands
# Backend (launcher default port 47927, hot-reload)
cd backend && python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python run.py
# Frontend (launcher default port 41921, Vite HMR)
cd frontend && npm install && npm run dev
# Both at once (Linux/WSL)
./dev.sh
# Frontend lint
cd frontend && npm run lint
# Frontend production build
cd frontend && npm run build
# Backend tests
cd backend && pytest
cd backend && pytest --cov=app
# Docker (all-in-one panel container; no host management)
docker compose up -d --build
Default dev credentials: admin / admin
Architecture
Backend (backend/)
Flask app factory in app/__init__.py using create_app(). Three-layer architecture:
app/api/— Flask Blueprints, one file per feature (90+ files). All routes prefixed/api/v1/. JWT-protected via@jwt_required().app/services/— Business logic (165+ files). Services are stateless modules called by API routes. Heavy lifting (shell commands, Docker API, file operations) happens here.app/models/— SQLAlchemy ORM models (80+ files). Schema is managed by Alembic migrations inbackend/migrations/versions/— add a migration for every schema change; the app does not calldb.create_all().
Other backend components:
app/sockets.py— Socket.IO event handlers for real-time metrics, logs, terminalapp/agent_gateway.py— Defines the Socket.IO/agentnamespace for the multi-server agent fleet, backed by the in-memory singletonapp/services/agent_registry.py(HMAC auth, heartbeat/metrics, command routing);app/api/agent_poll.pyis the HTTP long-poll fallback. The native agent itself is a Go program maintained in the separateserverkit-agentrepo (extracted from this repo's formeragent/dir) — agent-side capability changes ship there, so panel↔agent protocol changes are no longer atomic in one commit.app/middleware/security.py— Security headers middlewareconfig.py— Environment-based config (development/production/testing)run.py— Entry point
Agent gateway is single-worker. All connected-agent state lives in-memory in one process, so the panel must run a single gunicorn worker process (
-w 1 --threads N, plain threaded worker — NOT the gevent-websocket worker class, which double-answers the WS handshake againstasync_mode='threading'); multi-worker deployments silently misroute agent commands. See docs/ARCHITECTURE.md and SECURITY.md.
Frontend (frontend/src/)
React 18 SPA with client-side routing:
pages/— Route-level components (60+ files). Each maps to a route inApp.jsx.components/— Reusable UI components shared across pages.contexts/— React Context providers:AuthContext(JWT auth + token refresh),ThemeContext,ToastContext,ResourceTierContext(feature gating).services/api.js— CentralizedApiServiceclass handling all HTTP requests, token management, and auto-refresh.hooks/— Custom React hooks for reusable logic.styles/— SCSS stylesheets with design system variables. Main entry ismain.scss. Page-specific styles instyles/pages/.layouts/—DashboardLayoutwraps authenticated pages (sidebar + header).
Route guards: PrivateRoute (auth check), PublicRoute (redirect if logged in), SetupRoute (redirect to /setup if not configured).
Request Flow
Browser → Nginx (:80/:443) → proxy_pass to Docker containers (:8001-8999) for managed apps, or to Flask (:5000) for the panel API. The 404 handler in Flask serves index.html for SPA client-side routing; API routes return JSON errors.
Production Build
The Dockerfile is multi-stage: Node 20 builds frontend, Python 3.11 serves everything via Gunicorn with a single plain threaded worker (--workers 1 --threads 100, no --worker-class — see the single-worker note above). Built frontend is served from Flask's static folder.
Platform & Distro Awareness
ServerKit deploys on Linux (bare metal, VPS, or Docker). Development may happen on Windows/macOS.
- Service layer is Linux-only — nginx, systemctl, apt/dnf, PHP-FPM, etc. are inherently Linux. No need to abstract these for Windows.
- Platform-agnostic code (config management, storage, API layer) should guard Unix-only calls like
os.chmodwithif os.name != 'nt'so the dev server can run locally on any OS. - Distro differences matter — use
backend/app/utils/system.pyhelpers (get_package_manager,is_package_installed,install_package) instead of callingapt/dpkg/dnfdirectly. Not all targets are Debian-based.
Code Style
Python
- PEP 8, type hints where helpful
- Service functions are standalone (no classes unless stateful)
- Consistent JSON error responses:
{'error': 'message'}, status_code
React/JavaScript
- Functional components with hooks only
- PascalCase for components (
Sidebar.jsx), camelCase for everything else - SCSS for styling — use existing design system variables (
$bg-card,$primary-color,$spacing-md, etc.) and BEM-like naming (.block__element--modifier) - Context API for global state; props drilling is fine for 2-3 levels
- No inline styles; no Tailwind/CSS-in-JS
Diffs & Commits
- One logical change per commit
- Minimal, focused diffs — don't silently refactor surrounding code
- Branch naming:
feature/,fix/,docs/,refactor/prefixes
Adding a New Feature (Full Stack)
- Model: Add SQLAlchemy model in
backend/app/models/ - Service: Add business logic in
backend/app/services/ - API: Create Blueprint in
backend/app/api/, register it inapp/__init__.pywithurl_prefix='/api/v1/<feature>' - Frontend API: Add methods to
ApiServiceinfrontend/src/services/api.js - Page: Create page component in
frontend/src/pages/, add route inApp.jsx - Styles: Add SCSS file in
frontend/src/styles/pages/, import inmain.scss
Key Environment Variables
| Variable | Purpose |
|---|---|
SECRET_KEY |
Flask session signing |
JWT_SECRET_KEY |
JWT token signing |
DATABASE_URL |
DB connection string (sqlite:///... or PostgreSQL) |
CORS_ORIGINS |
Comma-separated allowed origins |
FLASK_ENV |
development or production |
TRUST_PROXY_HEADERS |
Trust the reverse proxy's forwarding headers to derive the real client IP (Werkzeug ProxyFix). true behind the shipped nginx; false/unset for a directly-exposed dev server. See SECURITY.md. |
TRUSTED_PROXY_HOPS |
Number of trusted proxy hops in front of Flask (bundled nginx = 1). |
AUTH_IP_MAX_ATTEMPTS / AUTH_IP_WINDOW_MINUTES / AUTH_IP_BLOCK_MINUTES |
Per-IP login brute-force throttle (default 10/15/15), complementing the per-user lockout. |