Reference for the current UI, Docker, WAF, and TLS surfaces on dev. All endpoints
are under /api/v1. "admin" requires an admin user; "edit" uses _can_edit_app
(owner, admin, or an editor grant); "auth" accepts any valid JWT. Migration
093_container_scale_policies versions the scaling-policy table.
| Surface | Where | Notes |
|---|---|---|
| Layout switcher | User menu (sidebar footer) → "Layout": Sidebar / Compact / Top bar | No route; persisted to localStorage['layout'] (sidebar|rail|topbar), applied as data-layout on <html>. Desktop only. |
| GPU Monitor | Route /gpu · sidebar nav "GPU Monitor" (System) |
Per-GPU cards + compute-process table; empty state when no GPU. |
| Dynamic DNS | Route /dynamic-dns · sidebar nav "Dynamic DNS" (Infrastructure) |
Host CRUD; shows the one-time token + ready update URL on create. |
| Container Ops tab | App detail page (Docker apps) | Image-update check/apply, auto-sleep, auto-scale. |
| WAF tab | App detail page (nginx-served apps: Docker + Python) | Install banner, mode/paranoia/anomaly, disabled-rule editor, apply, events. |
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /ddns/hosts |
auth | List hosts (token masked). |
| POST | /ddns/hosts |
admin | Body {zone_id, record_name, label?, enabled?} → returns host incl. one-time token. |
| DELETE | /ddns/hosts/<id> |
admin | |
| POST | /ddns/hosts/<id>/regenerate-token |
admin | Returns host with a fresh token. |
| GET/POST | /ddns/update?token=&ip= |
public (token) | Updates the host's A/AAAA record. ip from ?ip= or request source. Wrong token → 401, bad IP → 400. Reuses DNSZoneService, so a configured provider (e.g. Cloudflare) syncs automatically. |
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /image-updates/applications/<id>/check |
admin | Compares local RepoDigest vs registry digest (docker buildx imagetools inspect). |
| GET | /image-updates/applications/<id> |
auth | Latest check (or null). |
| POST | /apps/<id>/image-update/apply |
edit | Pull + recreate (compose apps; compose_pull+compose_up, local/remote). Guarded to compose. |
App badge: app.image_update = {status, update_available, checked_at}.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /apps/<id>/sleep-policy |
auth | |
| PUT | /apps/<id>/sleep-policy |
edit | Body {enabled?, idle_timeout_minutes?}. |
| POST | /apps/<id>/sleep |
edit | Stop the app, mark asleep. |
| POST | /apps/<id>/wake |
edit | Start the app, record activity. |
| POST | /apps/sweep-idle |
admin | Sleep all enabled apps idle past their timeout. Cron-drivable. |
App badge: app.sleep = {enabled, asleep, idle_timeout_minutes}.
Idle is measured from last_activity_at (bumped on wake / record_activity); a
no-activity-baseline policy is never slept blind.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /apps/<id>/scale-policy |
auth | |
| PUT | /apps/<id>/scale-policy |
edit | Body {enabled?, service_name?, min_replicas?, max_replicas?, cpu_high_percent?, cpu_low_percent?, cooldown_seconds?}. Enabling requires a service name; low CPU must remain below high CPU. |
| POST | /apps/<id>/scale |
edit | Body {replicas}; manual scale through docker compose --scale, with a minimum of one replica. |
| POST | /apps/<id>/scale/evaluate |
edit | One auto decision (returns action: scaled_up/down/hold/cooldown/disabled/unknown). |
| POST | /apps/scale-sweep |
admin | Evaluate every enabled policy. Cron-drivable. |
Requires a scale-capable Compose service with no fixed host port or container_name.
Local apps only. The configured minimum acts as a floor on the next evaluation,
even when CPU metrics are unavailable. The feature does not configure a load
balancer, shared storage, health checks, or failover, so it must not be presented
as end-to-end high availability. current_replicas records the last successful
ServerKit scale command; it does not reconcile changes made through Docker outside
ServerKit.
Verification lives in frontend/src/components/apps/__tests__/autoScalePolicy.test.mjs,
frontend/src/services/api/__tests__/containerOps.test.mjs, and
backend/tests/test_container_scale.py. The backend workflow test covers policy
read/write, evaluation, manual scale, and persistence. Docker execution and traffic
continuity still require the real-host checklist in docs/HORIZONTAL_SCALING_SPEC.md.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /gpu/ |
auth | {available, gpus:[{index,name,utilization_gpu,memory_used,memory_total,memory_percent,temperature,power_draw,power_limit,fan_speed,driver_version}], processes:[{gpu_uuid,pid,process_name,used_memory,container}]}. Shells nvidia-smi; container resolved from /proc/<pid>/cgroup. |
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /waf/applications/<id>/policy |
auth | |
| PUT | /waf/applications/<id>/policy |
admin | Body {mode, paranoia_level, anomaly_threshold, disabled_rule_ids}; saves and best-effort applies. mode ∈ off|detect|block. |
| POST | /waf/applications/<id>/apply |
admin | Writes per-app rules + injects nginx include + reloads. May return manual_include when no vhost is found. |
| GET | /waf/applications/<id>/events?limit= |
auth | Parsed ModSecurity audit-log events. |
| GET | /waf/status |
auth | {installed}. |
| POST | /waf/install |
admin | Install libmodsecurity + connector + OWASP CRS (distro-aware, best-effort). |
| Table | Model | Key columns |
|---|---|---|
ddns_hosts |
DdnsHost |
zone_id, record_name, token (unique), last_ip, enabled |
image_update_checks |
ImageUpdateCheck |
application_id, current_digest, latest_digest, update_available, status |
container_sleep_policies |
ContainerSleepPolicy |
application_id (unique), enabled, idle_timeout_minutes, last_activity_at, asleep |
container_scale_policies |
ContainerScalePolicy |
application_id (unique), enabled, service_name, min/max_replicas, cpu_high/low_percent, cooldown_seconds, current_replicas |
waf_policies |
WafPolicy |
application_id (unique), mode, paranoia_level, anomaly_threshold, disabled_rule_ids |
Application.to_dict() gained lightweight image_update and sleep badges.
| Setting | Default | Purpose |
|---|---|---|
encrypt_backups (BackupService config) |
false |
Opt-in client-side backup encryption (Fernet, reuses SERVERKIT_ENCRYPTION_KEY). Encrypts each artifact (.enc) before _auto_upload; restore decrypts transparently. Key loss = unrecoverable backups. |
SERVERKIT_WAF_DIR (env) |
/etc/nginx/serverkit-conf.d/waf |
Where per-app WAF rules/includes are written. |
SERVERKIT_MODSEC_AUDIT_LOG (env) |
/var/log/modsec_audit.log |
ModSecurity audit log parsed for the WAF events view. |
localStorage['layout'] (browser) |
sidebar |
Shell geometry: sidebar | rail | topbar. |
- Cron the sweeps:
POST /apps/sweep-idle(auto-sleep) andPOST /apps/scale-sweep(auto-scale) are admin endpoints meant to be hit periodically. Wiring them to a built-in scheduler is a follow-up; for now drive them from cron. - Public DDNS endpoint:
/ddns/updateis the only unauthenticated route added (the per-host token is the credential). Serve it over HTTPS. - WAF integration is additive: per-app rules go to
serverkit-conf.d/wafand an include is injected into the app vhost behind a# serverkit-wafmarker; existing nginx/site generation is untouched. Enforcement needs nginx built with the ModSecurity connector + libmodsecurity + OWASP CRS on the host.
HTTPS is optional and best-effort — the installer never blocks on SSL, and the panel never forces it. Hardening is applied so that when TLS is served, it's strong; it stays inert in HTTP-only mode.
Install/update (install.sh, scripts/update.sh)
- Best-effort cert: tries Let's Encrypt (webroot); on failure falls back to plain
HTTP instead of failing the install.
SERVERKIT_SKIP_SSL=1forces HTTP. - Two panel vhosts:
serverkit.conf(HTTPS + HTTP→HTTPS redirect) andserverkit-insecure.conf(HTTP-only). The choice persists to/etc/serverkit/ssl-modeand is preserved across updates. - Server-wide TLS floor:
harden_global_tls()rewrites/injectsssl_protocols TLSv1.2 TLSv1.3+ AEAD-onlyssl_ciphersin the systemnginx.confhttp{}, so the default server and any non-ServerKit vhost are covered too. Edited in place to avoid nginx's duplicate-directive error on Debian/Ubuntu.update.shre-applies it to existing installs. - Cloudflare-aware configs (Origin CA option + dashboard guidance) in
nginx/sites-available/*.
Per-app vhosts (services/nginx_service.py SSL_BLOCK) — HSTS
(includeSubDomains; preload), CSP, X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, and ssl_ecdh_curve, in addition to the TLS 1.2/1.3 + AEAD set.
Auto-CAA on certificate issuance — ssl_service.obtain_certificate calls
DNSProviderService.ensure_caa_record(domain) (best-effort, never fails the cert).
It creates CAA 0 issue "letsencrypt.org" at the zone apex via whichever
connected provider manages the domain (Cloudflare / DigitalOcean / Route53), and
degrades to manual instructions otherwise. Cloudflare/DigitalOcean need CAA's
structured data object (not a flat string) — handled. The cert response gains a
caa: {created, provider, zone, record, …} field. The default DNS zone
web-hosting preset also seeds a CAA record.
HSTS gating (panel) — the Flask security middleware emits HSTS only when the
deployment terminates real HTTPS, resolved as SSL_MODE / HSTS_ENABLED in
config.py. Behind nginx/Cloudflare, Flask can't tell real TLS from a Cloudflare
Flexible edge via X-Forwarded-Proto, so it trusts the operator's recorded choice
rather than the request scheme. The nginx edge emits HSTS independently in its
secure server block.
| Setting | Where | Purpose |
|---|---|---|
SERVERKIT_SKIP_SSL=1 |
env (installer) | Skip HTTPS entirely; run on plain HTTP. |
SERVERKIT_SSL_MODE |
env / .env |
secure|insecure — gates the panel's HSTS header. |
/etc/serverkit/ssl-mode |
file | Persisted SSL mode (read by installer, updater, and config._resolve_ssl_mode). |
/etc/serverkit/panel-domain |
file | Persisted panel domain so update.sh re-applies the cert path without the old (broken) .env scrape. |
Proving tests: backend/tests/test_dns_caa.py, backend/tests/test_security_headers.py.
- Migration import (cPanel/CyberPanel) — not started; needs real archive samples.
- Backups: additional remote targets (WebDAV/Azure/Dropbox/SFTP), streaming encryption for very large archives, and restore drills.
- Auto-sleep: traffic-based idle detection (feed
record_activityfrom the nginx log) and request-triggered wake-on-demand; remote-server sleep. - Periodic scheduler wiring for the sleep/scale sweeps.
- A human visual pass over all of this session's new UI in a running environment.