The Dockerfile allowed 5s before failed health probes count, while
compose allows 40s; boot runs migrations and settings seeding, so a
plain docker run could mark a healthy first boot unhealthy.
The panel runs async_mode='threading' under a plain threaded gunicorn
worker; nothing imports gevent. It was still installed everywhere, and
on Python 3.14 (no wheel yet) its source build is what broke installs
in issue #148. backend/Dockerfile was unreferenced and still launched
the gevent-websocket worker that double-answers the WS handshake; the
run.py hint recommended the same worker.
The comment explaining why scripts/ is copied into the image read
"/api/v1/servers/install.sh and /install.ps1", which reads as though the Windows
installer is served at the domain root. It is not — both live under
/api/v1/servers/. A comment whose whole job is to stop the next person hunting
for a wrong path should not contain one.
Raised by a reviewer on the PR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panel serves the fleet-agent installers at /api/v1/servers/install.sh and
/install.ps1 by reading them off its own filesystem, resolving <tree root>/scripts
four dirnames up from app/api/servers.py. In the image that is /app/scripts --
and the Dockerfile only ever copied backend/, VERSION and frontend/dist, so the
directory did not exist. Every containerised panel answered the enrollment
one-liner printed in its own Add Server dialog with a 404, and no server could
join the fleet. Verified against the published image: `ls /app` has no scripts,
and a booted container returns 404 while /api/v1/system/health returns 200.
Bare metal was unaffected -- install.sh git-clones the whole repo, and the
release tarball excludes only scripts/test/output.
Ship just the two installers: both are standalone (they source nothing from
scripts/lib), so scripts/lib, keys, the test harnesses and stage-remote.sh stay
out of the image. The .dockerignore comment claiming the build "only needs
frontend/, backend/, VERSION" was wrong and authoritative, which is a large part
of why this went unnoticed; it now states the real list.
Closes#101 (packaging half).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two independent bugs, both surfaced by issue #96.
The image died on every run with "sqlite3.OperationalError: unable to open
database file": config.py defaults DATABASE_URL to
sqlite:////app/instance/serverkit.db, the Dockerfile never created
/app/instance, and SQLAlchemy will not create a parent directory. Creating it
before the USER switch also fixes named-volume ownership -- Docker seeds an
empty volume from the image directory, permissions included, but creates it
root-owned when the path is absent from the image.
Separately, every docker-compose.yml service sat behind the legacy-frontend
profile, so `docker compose up -d` started nothing at all while the README and
install guide both gave that command as the Docker install. Following the
profile hint yielded the legacy nginx container alone -- a static file server
plus a proxy to a backend on the host -- serving an empty bind mount, which is
the 403 the reporter saw. Default `up -d` now runs the all-in-one image on
:5000 with a named volume; the legacy service stays profiled and says in a
comment that it needs a host install.
DATABASE_URL is pinned rather than interpolated. .env.example shipped a
*relative* sqlite path, which resolves against the container WORKDIR instead of
the volume and is destroyed by the next `compose down`; anyone who copied that
.env would inherit the trap. SERVERKIT_DATABASE_URL is the documented override.
Verified against a real build: boots healthy, serves the SPA, survives a
container recreate on the volume with 143 tables intact, and comes up with no
.env present at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single-container image shipped its own build toolchain and an optional
antivirus in every running panel. Measured on linux/amd64:
before 1.07 GB
multi-stage venv 553 MB
+ venv prune 501 MB (-53%)
Four changes:
1. Split a python-builder stage. gcc and the *-dev headers build wheels into
/opt/venv; the runtime stage copies the venv and leaves the toolchain behind.
The apt layer goes from 284 MB to 5 MB. No runtime -dev packages are needed:
the xmlsec, lxml and cryptography wheels are self-contained, verified by
importing all of them in the built image.
2. ClamAV behind ARG INSTALL_CLAMAV=false (153 MB). Safe because
malware_scan_service already treats clamscan as best-effort -- it reports
clamav_available and falls back to the builtin YARA matcher -- which is what
the old Dockerfile comment claimed but did not do. Build with
--build-arg INSTALL_CLAMAV=true to bundle it; that path is verified too.
3. COPY --chown instead of a trailing `chown -R /app`, which rewrote every
file's metadata and duplicated the whole tree into a 22 MB layer.
4. Prune the venv (~50 MB): third-party bundled test suites, and botocore's API
models for 400+ AWS services the panel never talks to. ServerKit constructs
exactly three clients -- s3, route53, ses -- plus sts for signing.
pip stays in the runtime image on purpose: PluginService installs an extension's
Python requirements with `sys.executable -m pip install -r`, so PATH points at
the venv and the venv is owned by the serverkit user.
The botocore prune fails open in dev and closed in the image -- a new
boto3.client('sns') would work locally and raise UnknownServiceError only in the
built image, when a user hits the feature. test_botocore_prune_contract.py
parses ARG BOTOCORE_KEEP and every call site and fails the suite on drift.
Verified in the built image: 14 optional deps import, all four boto3 clients
construct, sns is gone, no gcc, no __pycache__ or venv leaked into /app,
/api/v1/system/health and the SPA index both return 200. Suite: 2546 passed,
47 skipped. Idle RSS is unchanged at ~208 MB -- image size and resident memory
are separate problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vite 8 upgrade (dcf05d1) makes the frontend build require Node ^20.19.0 ||
>=22.12.0 — on anything older npm silently skips rolldown's native binary and
`npm run build` dies with a cryptic MODULE_NOT_FOUND. Servers that build the SPA
on-box (source install + every update) would break their next update. So:
- install.sh node_ready() now gates on the real vite 8 floor (was Node >=18) and
provisions NodeSource 22 LTS when the host Node is too old.
- update.sh gains _node_build_ok(): build_frontend_bundle refuses (returns 1 →
caller rolls back) and the Docker-host rebuild halts, both with an actionable
"upgrade Node" message instead of a rolldown stack trace.
- Dockerfile + CI (release, release-smoke, extensions, scripts-nightly) bumped
node 20 -> 22 so the shipped/pre-built bundle is built on a compatible Node.
Also fixes a pre-existing parity bug the operator caught: update.sh rendered the
systemd unit WITHOUT substituting @BIND_HOST@ (install.sh does), so an update
left the literal placeholder and gunicorn would fail to bind. update.sh now
substitutes it (loopback default, SERVERKIT_BIND_HOST override) like install.sh.
Tests: test_update.sh +3 (node-floor gate, build refusal on old Node,
@BIND_HOST@ substitution) — 48 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The release workflow builds linux/amd64,linux/arm64 via buildx + QEMU on
an amd64 runner, so the frontend-builder stage ran `npm ci`/`npm run
build` under emulation for arm64 -- 20-40 minutes when it did not hang
outright. Its output (frontend/dist) is static, arch-independent assets,
so pin the stage to $BUILDPLATFORM: buildx runs it natively, once, shared
by both platform images. Final images are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app runs flask-socketio with async_mode='threading' (the backend
is real-thread based: jobs scheduler, queue bus, agent registry), where
engineio serves WebSocket via simple-websocket on the raw socket. The
systemd unit template and Dockerfile still launched gunicorn with the
GeventWebSocketWorker, which ALSO answers the upgrade — the client got
HTTP/1.1 101 twice and read the second one as a frame ('Invalid frame
header'), so every panel silently degraded to polling. Proven by
hexdump (double 101, first 'frame' byte 0x48 'H') and fixed live:
single 101 + 0x81 text frame, socket.io connects through Cloudflare.
- unit template + Dockerfile: gunicorn -w 1 --threads 100 (single
PROCESS stays mandatory — in-memory agent gateway)
- requirements: pin simple-websocket explicitly (was transitive-only)
- CLAUDE.md / ARCHITECTURE.md / SECURITY.md single-worker notes updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The About page's /system/version (and check-update's current-version
read) hardcoded /opt/serverkit first, duplicated in three places:
- app/utils/version.py is now the single resolver: explicit
SERVERKIT_INSTALL_DIR override -> the running code's own tree ->
/opt/serverkit last (a box moved to a custom dir may still have a
stale tree there); system.py's two inline copies now call it
- the systemd unit template renders SERVERKIT_INSTALL_DIR from the
installer's SERVERKIT_DIR (deliberately not the bare SERVERKIT_DIR
env name, which app/paths.py already uses for the /var/serverkit
data root); flows to existing installs via the updater's re-render
- crypto's .env discovery gains the same install-dir hint
- Docker image now ships VERSION (/app/VERSION) — containers showed
the 1.0.0 fallback because no candidate path existed in the image
- 4 proving tests in backend/tests/test_panel_version.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce fallback audit logging and enrich audit tooling; add builtin extension discovery/install; and update default launcher ports and related docs/config.
Key changes:
- New middleware app.middleware.audit registers an after-request fallback that records authenticated mutating API requests when routes don't emit explicit audit logs.
- Audit model/action constants extended and AuditService enhanced (request context handling, redaction for sensitive settings, optional commit flag, sets g.audit_logged, emits events).
- Routes/services updated to create explicit audit entries for marketplace and plugin operations (create/update/publish/install/uninstall/enable/disable) and SSO/settings flows switched to use AuditService.
- Added tests for audit logging behavior and redaction (backend/tests/test_audit_log.py).
- Plugin service: added BUILTIN_EXTENSIONS_DIR, list_builtin_extensions and install_builtin_extension; plugin API endpoints to list/install builtin extensions.
- Contribution system extended to support layouts and route layout metadata; frontend wiring updated to surface standalone/custom layouts and extension routes.
- Launcher port defaults changed (backend -> 47927, frontend -> 41921). Updated backend .env.example, config CORS defaults, Dockerfiles and docker-compose to use SERVERKIT_BACKEND_PORT, and many docs/README files and frontend envs to reflect new ports.
- Minor frontend cleanups and new builtin extension example (serverkit-git) plus associated manifest and frontend re-export.
These changes improve observability/security for mutating APIs, enable opt-in builtin plugins, and align local dev/run ports with the launcher defaults.
Introduce a full Services UI and detailed service pages: add Services list page and ServiceDetail page, plus many service-detail components (CommandsTab, EventsTab, GitConnectModal, GunicornTab, LogsTab, MetricsTab, PackagesTab, SettingsTab, ShellTab). Add hooks (useService, useDeployments), serviceTypes utility, and page-specific styles. Update App routing and Sidebar to use /services and wire routes for service list/detail. Minor UI style update in main.less. Update Dockerfiles to create /var/serverkit/apps directory for app storage. Fix backend analytics endpoint decorator by calling @auth_required() to correctly apply authentication.
Add backup dirs in Dockerfiles and adjust ownership. Change server routes to use a dynamic tab param and update components to support tabbed ServerDetail pages. Improve metrics handling by adding serverId support, more resilient field parsing, and server-specific metrics API calls. Revamp ServerDetail: introduce token modal and AgentRegistrationSection, reorganize Metrics, Settings and Security areas (IP allowlist, API key rotation, security alerts), move delete/generate token flows and wire up navigation. Update Servers and Docker pages to handle different API response shapes (Array checks) and tweak install modal copy/UX. Rename backend API call for registration token to /regenerate-token. Add many UI/Icon/CSS adjustments to support the new layouts and interactions, and fix various small bugs and data-shape assumptions.
Introduces a production-ready Dockerfile, a comprehensive CONTRIBUTING.md, and new documentation files (API, installation, roadmap). Updates .env.example with improved comments and structure, enhances .gitignore, and refines the README with expanded features, requirements, and setup instructions. Roadmap and planning files are reorganized, and obsolete HTML is removed.