Skip to content

Platform enhancements, expanded catalog, and atomic updates#50

Merged
jhd3197 merged 19 commits into
mainfrom
dev
Jun 24, 2026
Merged

Platform enhancements, expanded catalog, and atomic updates#50
jhd3197 merged 19 commits into
mainfrom
dev

Conversation

@jhd3197

@jhd3197 jhd3197 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

This is the big one — eighteen commits that grow dev into a proper release. At its heart sit ten new platform capabilities spanning developer experience (container-status aggregation, build packs, deploy-time config snapshots), team-and-scale organization (projects/environments, shared resources, PR previews), fleet (a server-onboarding state machine and opt-in per-server proxy stacks), and security (fine-grained API-key scopes). Around that, the one-click service catalog roughly doubles — 42 new templates across AI/LLM, media, identity, monitoring, search, and business — backed by a documented schema and auto-resolved ${SERVICE_*} magic variables so a template never hardcodes a generated secret or host. The installer and updater got a ground-up rewrite into atomic blue/green slots with pre-flight checks, automatic rollback, checksum-verified release tarballs, and offline installs. Two long-standing gaps also close here: shared variable groups were resolved and displayed but never actually injected into containers, and there was nothing stopping a domain from being claimed by both host Nginx and a Dockerized proxy at once — both are fixed. Rounding it out is a per-site Fail2ban jail for WordPress brute-force protection and a pass of UX hardening across all the new surfaces.

Highlights

  • Projects & Environments — group applications into a Workspace → Project → Environment hierarchy, with resource counts and a "Move to project" bulk action.
  • Container status — one deterministic health badge per app (healthy / degraded / restarting / unhealthy), aggregated from its containers and pushed live.
  • Build Packs — point ServerKit at a repo with no Dockerfile; it detects the language/framework and generates a Dockerfile + compose (and defers to your own Dockerfile if you ship one).
  • Config snapshots — an immutable, secret-masked snapshot is captured before every deploy, with a plain-language diff and one-click restore + redeploy.
  • PR preview environments — open a pull request and get an ephemeral preview that redeploys on new commits and tears down when the PR closes.
  • 42 new one-click templates — AI/LLM (LibreChat, Ollama WebUI, LiteLLM, Flowise, Langflow, AnythingLLM, vector DBs), media (Radarr/Sonarr/Prowlarr/qBittorrent, Audiobookshelf, Jellyseerr), identity & monitoring (Authentik, SignOz, Beszel, PostHog), search, RSS, finance, and networking (Pi-hole, WG-Easy).
  • Server onboarding wizard — a guided validate → install prerequisites → install Docker → pair agent → ready flow with a live progress log and ETA.
  • Per-server proxy stack — optionally switch a server to a managed Traefik or Caddy stack with a compose preview before you commit; host Nginx stays the default.
  • API token scopes — fine-grained, additive scopes for API keys, enforced for programmatic X-API-Key callers while web/JWT users stay RBAC-governed.
  • Shared variable groups now actually apply at deploy — merged into the container environment for both single-container and Compose apps, with local values winning and optional per-service targeting.
  • WordPress brute-force protection — an on-by-default per-site Fail2ban jail with ban counts and a one-click unban in the Security tab.
  • serverkit update — atomic blue/green updates with pre-flight checks, DB backup + migration, automatic rollback, --dry-run/--branch/--release flags, and offline-tarball installs.
Technical changes

Developer experience

  • Container status: container_status_service.py provides a pure, Docker-free aggregation function over a fixed priority hierarchy plus short-TTL live-fetch helpers; api/container_status.py mounts GET /api/v1/status/app/<id> and /status/apps; sockets.py adds subscribe_container_status with change-only deltas on the container_status channel. Frontend: useContainerStatus.js, ContainerStatusPill.jsx, _container-status.scss.
  • Build packs: buildpack_service.py inspects a repo (cloning via a stored source connection when given), emits a build plan, and renders a Dockerfile + compose; api/buildpacks.py exposes /detect (cached by repo+commit) and pure /generate. Plan persisted on applications.buildpack_type / buildpack_plan / buildpack_overrides. Frontend BuildpackPreview.jsx + _buildpack.scss.
  • Config snapshots: deployment_snapshot.py model + configuration_service.py capture secret-masked env/domain snapshots before deploy; api/snapshots.py lists, fetches, diffs (?against=<id|previous>), and restores (@developer_required, re-uses the normal deploy path). Frontend ConfigDiffModal.jsx, DeploymentTimeline.jsx, _deployments.scss.

Declarative template catalog

  • template_service.py resolves ${SERVICE_PASSWORD/USER/FQDN/URL/BASE64_<NAME>} magic variables and the string|password|port|uuid|random variable types at install time; api/templates.py exposes GET /templates/catalog/schema. Schema documented in docs/TEMPLATE_CATALOG_SCHEMA.md.
  • 42 new backend/templates/*.yaml files plus a heavily expanded index.json; auto_domain: true publishes a template at <slug>.<base_domain> via SiteDomainService (HTTPS only if the wildcard cert already exists — never forces SSL; remote installs skipped).
  • Catalog improvements: getCategoryIcon covers the new tags; inline lucide-style base64 icons with a keyed fallback; update_app normalizes list-format variables and re-renders files: + bind mounts so file-based templates (litellm/signoz/posthog) update correctly; build_repo_index() / export_repo_index() back GET /repos/index and POST /repos/index/export. Frontend NewService.jsx, Services.jsx, Templates.jsx, _services.scss.

Team & scale

  • Projects/Environments: project.py + environment.py models, project_service.py, api/projects.py and api/environments.py (create auto-makes a default environment; deletes refuse 409 when apps remain / when it's the only environment). Applications gain nullable project_id / environment_id; writes gated by WorkspaceService.can_write_in_workspace, active workspace resolved from X-Workspace-Id/?workspace_id=. Frontend Projects.jsx, ProjectDetail.jsx, _projects.scss.
  • Shared resources: shared_resource.py (resource_tags, shared_variable_groups, shared_variables, shared_variable_group_attachments) + shared_resource_service.py + api/shared_resources.py for polymorphic tags, attachable variable groups, and a merged /shared/resolved view; secrets masked via group-level masking plus app/utils/sensitive_data_filter.py. Frontend SharedVariableGroups.jsx, EnvironmentVariablesPanel.jsx, TagsPanel.jsx, _shared-resources.scss.
  • PR previews: application_preview.py (application_previews + application_preview_settings) + preview_service.py registering preview.create|sync|destroy Queue Bus jobs; api/previews.py (developer-gated settings/sync/redeploy/teardown) and public api/webhooks.py POST /webhooks/pull-request/<token> reusing the push-webhook signature, always 200 to avoid provider retries. Frontend PreviewList.jsx, _previews.scss.

Fleet

  • Server onboarding: server_onboarding_service.py drives pending → validating → installing_prerequisites → installing_docker → pairing_agent → ready|failed via the server.onboarding.advance job kind; server_onboarding_log.py holds the authoritative step history, servers caches onboarding_state/onboarding_progress/onboarding_updated_at; api/servers.py exposes start/retry/status. Frontend OnboardingWizard.jsx (Step X of 5, progress bar, ETA, collapsible history) + _onboarding-wizard.scss.
  • Proxy stacks: proxy_stack.py model (one row per server, default nginx) + proxy_stack_service.py generating Traefik/Caddy compose joined to the external serverkit network; api/proxy.py for state, compose-preview, configure/regenerate/switch (developer-gated) and ingress-audit. Frontend FleetProxy.jsx, ProxyStackPanel.jsx, _fleet-proxy.scss, _proxy.scss.

Security & access

  • API token scopes: api/middleware/api_scope_middleware.py holds the canonical scope catalog and a require_scope decorator that is a pass-through for JWT/session callers and only enforces scopes on X-API-Key requests (* master scope, resource:* wildcards); GET /api/v1/api-keys/scopes surfaces the catalog. Frontend ApiKeyScopesModal.jsx, ApiKeyModal.jsx, ApiSettingsTab.jsx, _api-key-scopes.scss.
  • WordPress brute-force jail: fail2ban_jail_service.py (write-side) renders a filter + per-site jail, enables/disables, reloads fail2ban, and unbans IPs (Linux-only, best-effort, ownership-guarded serverkit-*); wired into wordpress_service.py lifecycle and wp_security_service.py; nginx_service.py gains canonical per-site log/error path helpers. API status/toggle/unban + WordPressDetail.jsx Security tab + _wordpress.scss.

Ingress plane

  • app/utils/ingress.py (pure helpers): only container/Compose apps are proxy-stack eligible; a requested proxy_stack is forced back to nginx for PHP/WordPress/static/Python apps. applications.ingress_plane (migration 035; NULL reads as host Nginx); to_dict() exposes ingress_proxy_eligible. ProxyStackService.ingress_audit() reports mismatched apps; fleet_overview carries per-server app_count/mismatch_count and a {level,text} recommendation pill. UI: ingress badge on app detail, mismatch warnings in the proxy panel and fleet dashboard.

Shared-variable env injection

  • env_service.py get_effective_env(app) merges shared groups (workspace < project < environment < direct) under the app's own local env vars, decrypting secrets for injection (best-effort, never blocks a deploy); _deploy_docker now passes the merged env to run_container (also fixing a latent bug that called .get('success') on a list result, so env was effectively never applied).
  • compose_env_service.py refresh_for_project writes a managed docker-compose.serverkit.yml overlay ($ escaped to $$); docker_service.py compose_up adds it as a second -f via _compose_cmd_with_overlay, non-destructively and regenerated each up. Per-service targeting: EnvironmentVariable and SharedVariable gain target_service (migration 036; NULL = all services), with get_effective_env_for_services and GET /apps/<id>/compose-services. Frontend EnvironmentVariables.jsx "Applies to" selector + _env-vars.scss.

Installer & updater

  • scripts/update.sh rewritten with a pre-flight validator (disk/memory/Python/deps/network/health) and automatic rollback; atomic blue/green layout (/opt/serverkit-a|b + symlink); explicit flask db upgrade before slot switch; --dry-run/--force/--branch/--release flags; SERVERKIT_OFFLINE_TARBALL / SERVERKIT_MIRROR_URL; SHA-256 checksum verification on download. serverkit delegates fully to update.sh (inline _update_source fallback removed); install.sh hardened for blue/green, checksums, offline, and a pre-built relocatable venv; uninstall removes both slots + backups. scripts/build-release.sh ships pre-built frontend/dist + venv; release.yml updated.

Wiring, migrations & docs

  • app/__init__.py registers the new blueprints and job handlers; config.py adds related settings; App.jsx/sidebarItems.js add routes and collapse Projects/Shared Variables/Workspaces under an "Organization" group.
  • Migrations 029–036: server onboarding, build packs + snapshots, projects/environments, shared resources, application previews, proxy stacks, app ingress plane, env target_service (single Alembic head; chain applies from empty).
  • New docs: docs/ENHANCEMENTS.md, docs/TEMPLATE_CATALOG_SCHEMA.md, docs/INSTALLATION.md; CHANGELOG.md/README.md updated.

jhd3197 and others added 19 commits June 23, 2026 11:02
…e support, pre-built releases

- Rewrite scripts/update.sh with pre-flight validator (disk, memory, Python,
  deps, network, health) and automatic rollback on failure.
- Implement atomic blue/green install layout (/opt/serverkit-a|b + symlink).
- Add explicit database migration step (flask db upgrade) before switching slots.
- Add --dry-run, --force, --branch, --release flags to update.sh.
- Add SERVERKIT_OFFLINE_TARBALL and SERVERKIT_MIRROR_URL support.
- Verify release tarball SHA-256 checksums on download.
- Ship pre-built frontend/dist and relocatable venv in release tarballs.
- Make serverkit update delegate fully to scripts/update.sh; remove inline
  _update_source fallback.
- Harden install.sh for blue/green, checksums, offline, and pre-built venv.
- Update uninstall to remove both blue/green slots and backups.
- Update docs/INSTALLATION.md and README.md with new options.
Adds the AI/LLM one-click templates and wires the panel's own AI assistant to a self-hosted Prompture Hub:

- Templates: prompture-hub, ollama-webui, qdrant, chroma, litellm, flowise, langflow, anythingllm, librechat (brand-neutral, auto-port, generated secrets, lucide icons).
- AI provider: new 'prompture-hub' provider routed through Prompture's OpenAI-compatible cachibot driver (hub key + endpoint), so ServerKit AI can call a local hub instead of raw provider keys.
- Featured Prompture Hub + icon fallbacks in the Services UI.

Roadmap: SERVICES_ROADMAP #1-#8.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds 16 one-click templates across the most-requested self-hosting categories:

- Media/automation: audiobookshelf, calibre-web, and a *arr stack (sonarr, radarr, prowlarr, qbittorrent, jellyseerr) sharing a serverkit-media docker network so they interconnect by container name.
- News/RSS: freshrss, miniflux. Documents: paperless-ngx (+redis +postgres), stirling-pdf, memos.
- Finance: actualbudget, firefly-iii (+postgres). Project mgmt: vikunja, plane (full multi-service stack).
- Brand-neutral, auto-port, generated secrets, lucide icons; icon fallbacks wired in Templates.jsx.

Roadmap: SERVICES_ROADMAP #9-#15.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…plates

Fills operational gaps so the catalog feels like a serious self-hosting panel:

- Identity/SSO: authentik (server+worker+postgres+redis, bootstrap admin).
- Monitoring: beszel (hub) and signoz (full OTel/ClickHouse observability stack; OTLP 4317/4318 exposed).
- Search: meilisearch, typesense, searxng (+valkey).
- Notifications: gotify, ntfy.
- Networking/security: wg-easy (WireGuard VPN, fixed UDP 51820) and pihole (DNS sinkhole, fixed 53). Tailscale is host-level and intentionally deferred.

Roadmap: SERVICES_ROADMAP #16-#20.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds the heavier business/community tools (larger support surface, gated behind Phases 0-2):

- Customer engagement & signing: chatwoot (rails+sidekiq+pgvector+redis), documenso (+postgres, self-signed cert flow documented).
- Product analytics: metabase (+postgres), posthog (full ClickHouse/Kafka/Zookeeper/MinIO hobby stack, ~4GB RAM).
- Community & bookmarks: nodebb (+postgres), linkding, karakeep (web+chrome+meilisearch).

Roadmap: SERVICES_ROADMAP #21-#23.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Makes the expanded catalog usable end-to-end:

- #24 Auto-domain: templates can opt in with 'auto_domain: true'; on install the app is published at <slug>.<base_domain> via the existing SiteDomainService (HTTPS only if the wildcard cert is already set up — never forces SSL; remote installs skipped).
- #25 Category consolidation: getCategoryIcon now covers the new tags (ai/llm/search/finance/documents/rss/vpn/dns/identity/observability/...).
- #26 Icon strategy: each template ships an inline lucide-style base64 icon with a keyed lucide fallback map; documented in Templates.jsx.
- #27 Update flow: update_app now normalizes list-format variables and re-renders the files: section + bind mounts, so converted/file-based templates (litellm, signoz, posthog) update correctly.
- #28 Community repo: TemplateService.build_repo_index()/export_repo_index() generate the index.json the sync mechanism already expects, exposed via GET /repos/index and POST /repos/index/export.

Roadmap: SERVICES_ROADMAP #24-#28.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Validates all 105 bundled templates parse + validate via the real TemplateService, ids match filenames, and every template has an icon; the 42 roadmap templates additionally use inline base64 icons, list-shaped vars, and are brand-neutral; and build_repo_index() covers the whole catalog. 149 cases, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Introduce a ServerKit-managed per-site Fail2ban layer to protect WordPress login endpoints. Adds Fail2banJailService (write-side) to generate filter and per-site jail configs, enable/disable jails, reload fail2ban, and unban IPs (Linux-only, best-effort, ownership-guarded). Wire it into WordPress service lifecycle (enable on site create, remove on delete) and WpSecurityService wrapper methods. Expose API endpoints for status, toggle and unban in backend API. Add unit tests for Fail2banJailService (config rendering, naming/safety, lifecycle, graceful degradation). Frontend: Security tab loads brute-force status, can toggle protection, shows ban counts and unban action; API client and styles updated. NginxService gains canonical per-site log/error path helpers. Docs updated to reflect shipped brute-force jail feature.
… template magic vars

Four foundational enhancements toward the deploy-code developer experience:

- Container status aggregator (C7): new ContainerStatusService applies a
  deterministic priority hierarchy (degraded > restarting > running:unhealthy >
  starting > running:healthy > exited > unknown) over an app's containers,
  cached with a short TTL. Exposed at /api/v1/status and pushed over a new
  container_status Socket.IO channel; Applications page now renders an
  aggregated ContainerStatusPill with sub-status tooltip.

- API token scopes + sensitive data masking (C10): require_scope() decorator
  enforces fine-grained scopes for API-key requests (pass-through for JWT),
  a canonical SCOPES catalog at /api/v1/api-keys/scopes, and a reusable
  sensitive_data_filter (mask_sensitive) honoring secrets:read. Frontend gains
  a grouped ApiKeyScopesModal picker and per-key scope pills.

- Server onboarding state machine (C8): new onboarding lifecycle
  (pending -> validating -> installing_prerequisites -> installing_docker ->
  pairing_agent -> ready/failed) with ServerOnboardingService, a
  server_onboarding_logs table, servers.onboarding_* columns, REST endpoints,
  and an OnboardingWizard surfaced on the server detail overview.

- Declarative template catalog (C5): documented catalog schema and
  magic-variable resolution (SERVICE_PASSWORD_/USER_/FQDN_/URL_/BASE64_*)
  layered onto the existing YAML template loader without changing behavior for
  templates that don't use them; GET /api/v1/templates/catalog/schema.

Migration 029 adds the onboarding table + columns. 60 new tests pass; frontend
builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…atalog certification

- Build packs / zero-Dockerfile deploys (C1): new BuildpackService detects the
  stack from a cloned repo (Node/Python/Go/PHP/Ruby/Rust/static), produces a
  transparent build plan and a generated Dockerfile, and persists the plan +
  overrides on the Application. POST /api/v1/buildpacks/{detect,generate}; a
  BuildpackPreview shows the detected plan + editable overrides + Dockerfile in
  the create wizard and the app Build tab. Builds on the existing build_method
  plumbing instead of duplicating it.

- Deployment config snapshots + diff engine (C4): ConfigurationService captures
  an immutable, hashed snapshot of resolved config (env keys + masked values,
  domains, image/tag, build method, volumes) at every deploy and rollback; new
  DeploymentSnapshot model + /api/v1/apps/<id>/snapshots[/diff|/restore]. A
  DeploymentTimeline + ConfigDiffModal surface "what changed" and one-click
  restore in the app Deploy tab. Secret values are never serialized.

- Template catalog certification (C5 cont.): a whole-catalog validation test
  proves all 105 declarative templates conform to the documented schema; fixed
  the stale index.json (was 63/105 entries) to list every bundled template.

Migration 030 adds the buildpack columns + deployment_snapshots table. Full
backend suite: 820 passed, 14 skipped; frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… PR previews, proxy stacks

- Project / Environment hierarchy (C2): Workspace -> Project -> Environment
  org model. New Project/Environment models + CRUD APIs, nullable
  applications.project_id/environment_id (opt-in, no destructive backfill),
  ProjectService.ensure_default, a Projects list + ProjectDetail (env tabs),
  a project/environment selector in the create wizard, and sidebar nav.

- Polymorphic shared resources facade (C9): ResourceTag + SharedVariableGroup
  /SharedVariable/attachment models keyed by (resource_type, resource_id),
  reusing the env-var Fernet encryption; SharedResourceService resolves merged
  variables (most-recent attachment wins) with secrets masked. /api/v1/shared
  endpoints + reusable TagsPanel/EnvironmentVariablesPanel + a Shared Variables
  management page. Existing per-resource tables untouched (facade first).

- PR preview environments (C3): ApplicationPreview + settings models, a
  PreviewService with pure domain-templating + reconcile (open PRs vs active
  previews) cores and best-effort provisioning (incl. guarded WordPress
  file+DB clone), a pull_request webhook endpoint, preview.* jobs, and a
  Previews tab on the app detail page.

- Per-server managed proxy stack (C6): opt-in Dockerized Traefik/Caddy as a
  Compose stack (host Nginx stays default and superior for PHP/WordPress).
  ProxyStack model + ProxyStackService with pure compose/config generators and
  versioned config backups, /api/v1/servers/<id>/proxy endpoints, and a Proxy
  tab on the server detail page.

Migrations 031-034 add the new tables/columns; the full chain applies cleanly
from an empty DB. Full backend suite: 871 passed, 14 skipped; frontend builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ared-resource depth, docs

- Server onboarding completion (C8): a pure pre-flight compatibility check
  (arch/distro/kernel/cpu/memory) gating validate; real-but-guarded
  distro-aware install_prerequisites/install_docker/pair_agent steps that
  no-op safely on dev/Windows and never raise; audit + best-effort
  notifications on each transition; and a live auto-scrolling log trail +
  elapsed timer in the onboarding wizard.

- Fleet-wide proxy dashboard (C6): ProxyStackService.fleet_overview aggregates
  every server's proxy posture (configured stack vs host Nginx default) behind
  GET /api/v1/servers/proxy/overview, surfaced as a new Fleet Proxy page in the
  Servers tab group with summary tiles + click-through to each server's proxy.

- Polymorphic shared resources depth (C9): hierarchical variable resolution
  (workspace < project < environment < direct attachment < resource) with
  source provenance and {{group.KEY}} interpolation, an audit trail on every
  tag/group/variable/attachment mutation, GET /api/v1/shared/resolved/
  hierarchical, and the reusable Tags/Environment-Variables panels embedded on
  the application and server detail pages.

- Docs & coverage: docs/ENHANCEMENTS.md (brand-neutral guide to all ten
  capabilities + verified endpoints), README/CHANGELOG updates, and a
  cross-cutting integration test asserting the new endpoints are wired.

Full backend suite: 924 passed, 14 skipped; frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…tack)

Both reverse proxies bind 80/443, so pointing the same domain at host Nginx and
a Dockerized proxy stack is an operational footgun. This tags each app with the
plane it expects and surfaces disagreements instead of leaving them implicit.

- Each app carries an `ingress_plane` ('nginx' | 'proxy_stack'); NULL reads as
  the host-Nginx default. New pure helpers in app/utils/ingress.py: only
  container-based services are proxy-stack eligible, and a requested proxy_stack
  is forced back to nginx for PHP/WordPress/static/Python apps. to_dict() also
  exposes `ingress_proxy_eligible`.
- ProxyStackService.ingress_audit(server) reports which of a server's apps
  disagree with its active proxy mode; fleet_overview now includes per-server
  app_count + mismatch_count. New GET /servers/<id>/proxy/ingress-audit.
- Create wizard offers the proxy stack only for Docker/Compose services and
  keeps the default on Nginx; app detail shows an Ingress badge; the fleet proxy
  dashboard flags mismatch counts; the server proxy panel warns and lists the
  specific mismatched apps with reasons.

Migration 035 adds applications.ingress_plane. Backend suite green (incl. 8 new
ingress tests); frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Tightens the seams the recent features introduced:

- Secret precedence/conflict: the embedded shared-variables panel now resolves
  via the hierarchical endpoint, shows a per-variable source-scope badge +
  precedence legend, and flags keys that are also set locally with a
  "Set locally — local value applies" badge (shared groups are a facade and are
  not injected into the container; the app's own env var is authoritative).
- Split inventory: apps now expose project_name/environment_name; the Services
  list shows a project/environment badge (or "Unassigned") and a "Move to
  project" bulk action (POST /apps/move-to-project, workspace-validated,
  null-unassigns).
- Fleet proxy actionability: fleet_overview rows carry a {level,text}
  recommendation (mismatch warning / idle stack / empty host / aligned),
  rendered as a per-row pill.
- Onboarding wizard: "Step X of 5" + progress bar + rough ETA, and a
  collapsible step-history trail so returning users get a clean summary.
- Config diff: a plain-language summary ("3 environment variables and the image
  tag (app:1.2.1 -> app:1.1.9) changed") above the technical diff and echoed in
  the restore confirmation.
- Container status: an always-visible "N issues" / "healthy/total" badge so a
  degraded sub-container isn't hidden behind hover.
- Sidebar: Projects, Shared Variables and Workspaces collapse under a single
  "Organization" group.

No schema change (project/environment names are derived). Backend suite: 935
passed, 14 skipped; frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ence)

Shared variable groups were resolved/displayed but never reached the container.
EnvService.get_effective_env(app) now merges them in: shared groups
(workspace < project < environment < direct) form the base layer and the app's
own local env vars override them, so a key set in both yields the local value —
matching the "Set locally — local value applies" UI hint. Secrets are decrypted
for injection; shared resolution is best-effort and never blocks a deploy.

The docker deploy path (_deploy_docker) now injects this merged env via
run_container, which also fixes a latent bug there (it called .get('success')
on get_env_vars()'s list result, so env was effectively never applied). Updated
the shared-variables panel note to say shared vars now apply at deploy with
local taking precedence.

Proving test test_effective_env.py (6 cases) covers the merge precedence, the
best-effort fallback, and that _deploy_docker hands run_container the merged env.
Full backend suite: 941 passed, 14 skipped; frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Single-container apps already get the effective env via docker run -e. Compose
apps get container env from the compose file's environment:, so this wires them
the same way through a managed override compose file.

ComposeEnvService.refresh_for_project writes docker-compose.serverkit.yml next to
a managed app's base compose, adding an environment: overlay to every service
built from EnvService.get_effective_env (shared variable groups under the app's
own local env vars). Compose merges the override on top of the base, so the
effective env wins on collisions — consistent with the docker-run path. Dollar
signs are escaped ($ -> $$) so values survive compose interpolation intact.

DockerService.compose_up now includes the override as a second -f (via
_compose_cmd_with_overlay); `up -d` recreates changed containers so updated env
takes effect on the next deploy. It's non-destructive (ServerKit only owns the
override; the base compose is never edited), best-effort/guarded (a build failure
falls back to the plain base command and never blocks a deploy), regenerated each
up, and removed when the app has no effective env. Non-app dirs (e.g. proxy
stacks) and env-less apps are left untouched.

Proving test test_compose_env_overlay.py (6 cases): overlay injects shared+local
into every service with local winning collisions, $ escaping, stale-override
cleanup, non-app dirs untouched, and compose_up running with -f base -f override.
Full backend suite: 947 passed, 14 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A variable can now land on one compose service instead of all of them. Both
EnvironmentVariable and SharedVariable gain an optional target_service
(NULL = all services), threaded through the shared resolver as an inclusion
filter (a var applies to a service when it targets all OR that service).

EnvService.get_effective_env_for_services(app, services) computes each service's
merged env (all-services vars + that-service vars, local overriding shared), and
ComposeEnvService builds the override's per-service environment from it — so a
var targeted at "db" only appears under the db service, untargeted vars appear
everywhere. The single-container docker-run path is unchanged (one container
gets the full effective env regardless of target).

API: env-var create/update and shared-variable create/update accept
target_service; GET /apps/<id>/compose-services lists a compose app's services.
UI: the Environment tab shows an "Applies to" selector (gated to compose apps via
the service list) with a per-row target chip; the shared variable editor gains a
target-service field.

Migration 036 adds the two columns; single Alembic head; chain applies from
empty. Proving tests cover per-service distribution + the resolver helper. Full
backend suite: 949 passed, 14 skipped; frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Copilot AI review requested due to automatic review settings June 24, 2026 04:08
@jhd3197 jhd3197 merged commit 167245b into main Jun 24, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants