The panel domain is editable on the System page again — safely this time. It is
stored in the DB (Setting `panel_domain`, overriding install-time APP_DOMAIN) and
APPLIED ON RESTART, never mid-session: a snapshot file (storage/framework/active-domain)
is frozen at container start by the entrypoint (`clusev:snapshot-domain`), so saving a
new domain only takes effect after `docker compose ... restart`.
How it works
- DeploymentService: pending (configuredDomain, DB) vs active (domain(), snapshot file);
setDomain() persists; restartPending() drives the UI notice. Snapshot reads the DB
DIRECTLY (cache-independent) and retries until the settings table is readable; if it
never is, it freezes the env fallback so the active domain is always a FIXED value
(never a live one that could shift after startup).
- AppServiceProvider: derives app.url from the active domain at boot; pins server->Reverb
publishing to the internal reverb:8080 (domain/cert-independent).
- Caddy: on-demand TLS gated by /_caddy/ask (issues a cert only for the configured
domain); HTTP always served for bare-IP recovery; /app,/apps forced to HTTPS for any
hostname (plaintext only on a bare IP).
- Reverb client endpoint is derived from the live request and rides the same front door
(/app tunnel — Caddy in prod, nginx in dev), so realtime follows a domain change with
no JS rebuild and no stale .env value.
- System page: domain form + R5 confirm + "restart required" notice with the exact
command; DE/EN strings (R16).
Anti-lockout / security
- session.secure + the HTTPS redirect follow the real request scheme; bare-IP HTTP is
always a recovery path. trustProxies('*') only in production (dev can't be tricked into
faking HTTPS via X-Forwarded-*). When a domain is active only that domain (HTTPS) and
the literal server IP (HTTP) serve the panel; any other/stale host is refused (404),
and IP-recovery redirects stay on the IP.
Bump 0.3.0 -> 0.4.0; CHANGELOG. Follow-up tracked: make the public `metrics` broadcast
channel private (wire broadcasting auth).
Verified: Pint clean; npm build; Caddyfile validates; R12 all routes 200 + 0 console
errors; Echo connects via the unified /app tunnel; domain set/clear + restart-gating +
/_caddy/ask (200 active / 403 other) + host-enforcement matrix all confirmed in dev;
Codex review iterated to no actionable in-scope findings; 14-agent adversarial
lockout/security review (real trustProxies finding fixed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker | ||
| docs | ||
| lang | ||
| public | ||
| resources | ||
| routes | ||
| storage | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| README.md | ||
| artisan | ||
| composer.json | ||
| composer.lock | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| handoff.md | ||
| install.sh | ||
| kickoff-prompt.md | ||
| package-lock.json | ||
| package.json | ||
| phpunit.xml | ||
| rules.md | ||
| vite.config.js | ||
README.md
Clusev
Self-hosted control panel to administer a fleet of Linux servers from one dashboard, agentless over SSH. Clusev is the control-plane (UI + orchestration); it talks to real servers with phpseclib (exec + SFTP) — it never reimplements daemons. Security-first: 2FA, encrypted SSH-credential vault, and a full audit log. Multi-server is free and never paywalled.
Status: v1 — dashboard/live metrics, systemd services, SFTP file manager, server details, auth + 2FA. UI copy is German; meta-docs are English.
Stack: Laravel 13 · Livewire 3 · Tailwind v4 · Reverb (realtime) · Redis · MariaDB · phpseclib3.
Everything runs in Docker. See CLAUDE.md for architecture and rules.md for the hard conventions.
Development
The host needs only Docker (no PHP/Composer/Node). The dev app container runs php-fpm + nginx +
Vite via supervisor; reverb, queue, mariadb, redis are their own services.
cp .env.example .env # then set DB_PASSWORD / DB_ROOT_PASSWORD
docker compose up -d --build # app on :80, Vite HMR on :5173
docker compose run --rm --no-deps -u "${HOST_UID:-1002}:${HOST_GID:-1002}" app php artisan key:generate
docker compose exec app php artisan migrate --seed
Run any tooling inside the container, e.g.
docker compose exec app php artisan make:livewire Servers/Show (class-based — never Volt).
Production install
One host, only Docker + a sudo user. Caddy is the single host-exposed service: auto-TLS when a
domain is set, plain HTTP on the bare IP otherwise. install.sh is idempotent — it generates
secrets once (never regenerates), brings up the prod stack, migrates, and creates the first admin
with a one-time random password printed only on the terminal.
git clone https://git.bave.dev/boban/clusev.git && cd clusev
./install.sh
Prompts (non-interactive: set CLUSEV_DOMAIN / CLUSEV_ADMIN_EMAIL in the environment):
Domain (empty = access by IP over HTTP): clusev.example.com
Admin e-mail (login + Let's Encrypt): admin@example.com
The closing banner shows the URL + the one-time admin password. On first login Clusev forces a password change and 2FA enrolment before the panel unlocks.
One knob: APP_DOMAIN
APP_DOMAIN |
proxy | URL | Reverb |
|---|---|---|---|
| (empty) | Caddy serves plain HTTP on APP_PORT |
http://<ip> |
ws://<ip>/app/* |
clusev.example.com |
Caddy gets a Let's Encrypt cert, forces HTTPS | https://… |
wss://…/app/* |
SITE_ADDRESS, APP_URL, REVERB_* are derived from it by the installer — nothing hardcoded.
Bare-IP mode serves 2FA/audit over cleartext HTTP; the installer warns loudly. Let's Encrypt needs
publicly reachable 80/443 — a private (RFC1918) target needs a DNS-01 Caddy build instead.
Manual deploy (for operators who don't curl | bash)
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec -u app app php artisan migrate --force
docker compose -f docker-compose.prod.yml exec -u app app php artisan clusev:install --email=admin@example.com
In-dashboard updates (signed intent file + host-side updater, digest-pinned, cosign-verified, 2FA-gated, with backup + rollback) are designed in
docs/install-update-design.mdand land in v1.x — they are intentionally not shipped yet.
License
AGPL core + commercial Pro modules (open-core). Multi-server fleet management is always free.