Skip to content

feat(projects): install services from Coolify templates - #855

Open
dviejokfs wants to merge 21 commits into
mainfrom
feat/service-template-catalog
Open

feat(projects): install services from Coolify templates#855
dviejokfs wants to merge 21 commits into
mainfrom
feat/service-template-catalog

Conversation

@dviejokfs

@dviejokfs dviejokfs commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a runtime-synced Coolify service-template catalog to New Project, explicitly marked Beta.
  • Install templates as first-class Temps-owned Docker Compose projects. Users can inspect and edit the saved YAML, change image versions, save immutable revisions, redeploy, and roll back without remaining coupled to Coolify.
  • Normalize upstream Compose into a typed install plan with public routes, application health paths, optional/required inputs, generated encrypted secrets, backing-service discovery, compatibility tiers, and explicit limited startup permissions.
  • Improve discovery with debounced focus-stable search, tags, category icons, and accurate compatibility presentation.
  • Add anonymous install-funnel telemetry for attempts, successes, failures, cancellations, fixed failure stages/codes, and the attested public template slug.
  • Fix Compose deployment-stage logs, runtime container logs, and immediate post-deploy monitor reconciliation.

Current tested catalog snapshot: 342 total / 23 standard / 247 elevated / 48 host access / 24 other blocked. That leaves 270 currently installable templates. Host-access entries remain blocked pending a separately reviewed administrator-controlled integration.

Product behavior

  • Installable services are shown as Ready. The catalog does not imply that a service is unavailable merely because it needs the fixed limited startup profile.
  • A single required checkbox confirms the exact stack containers that need startup permissions; the Install action remains disabled until it is checked.
  • View Docker Compose exposes the complete normalized YAML with syntax highlighting, line numbers/wrapping controls, and copy support. Resolved secret values are never shown.
  • Variables with defaults and declaration-only variables are optional unless Compose explicitly uses ${VAR?} or ${VAR:?}.
  • Internal endpoints such as redis-service:6379 remain endpoints and are not converted into random passwords. Generated usernames use service-aware defaults: postgres, Redis default, otherwise admin.
  • Narrow app-owned writable host paths such as /apprise-api/config:/config are converted to project-owned named volumes. Docker sockets, system paths, host namespaces, devices, privileged mode, external resources, and guarded interpolation remain blocked.
  • Per-service documentation links use Temps attribution rather than Coolify's utm_source.

Architecture and safety

  • Fetches Coolify's Apache-2.0 generated catalog through a bounded HTTPS client with redirects disabled, response/entry/Compose limits, successful-snapshot caching, stale-on-error serving, and cold-start backoff.
  • Expands YAML merges, removes global project/container names, rewrites fixed published ports to random loopback bindings, declares implicit named volumes, and discovers public routes plus Compose health paths.
  • Runs docker compose config --quiet with final values before project creation in an environment-cleared, concurrency-limited subprocess. A SHA-256 digest binds preflight to the complete install plan.
  • Stores editable Compose YAML in revisioned source bundles with optimistic concurrency; each deployment captures an immutable selected revision.
  • Requires project-create and deployment-create permissions for catalog access and installation.
  • Keeps compatibility classification separate from enforcement. The final Compose executor still independently rejects unsafe documents and applies privileged=false, cap_drop=ALL, no-new-privileges, and a PID limit.
  • The limited profile restores only CHOWN, DAC_OVERRIDE, FOWNER, SETUID, and SETGID for explicitly confirmed services.

See docs/adr/040-runtime-service-template-catalog.md for the architecture and docs/release-testing.md#12-service-template-catalog-beta for the Beta/stable QA gate.

Activepieces failure: root cause and fix

Activepieces logged missing pieces-framework and pieces-common directories, but its own log classified both errors as non-fatal. The Node API then listened on port 3000 and its worker connected.

The real failure was the bundled nginx process. Temps correctly applies cap_drop=ALL; Activepieces' nginx starts as root and needs to chown runtime paths and switch user before serving port 80. Reproduction under the Temps sandbox failed with:

nginx: [emerg] chown("/var/lib/nginx/body", 65534) failed (1: Operation not permitted)

The catalog now recognizes only the exact official activepieces/activepieces and ghcr.io/activepieces/activepieces repositories (after conservative tag/digest removal), requests the existing limited profile after explicit confirmation, and rewrites the upstream static / probe to Activepieces' application endpoint /api/v1/health.

Negative tests prove lookalike repositories, custom registries, docker.io prefixes, and whole-image interpolation do not inherit this exception.

Real slot-12 deployment evidence:

Project: Activepieces QA
Deployment: 25
Prepare Compose Source: succeeded
Deploy Compose Stack: succeeded
Finalize Deployment: succeeded

activepieces: running, Docker health=healthy
healthcheck=["CMD","curl","-f","http://127.0.0.1:80/api/v1/health"]
cap_add=[CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID]
cap_drop=[ALL]

GET http://activepieces-qa-production.localho.st:8200/api/v1/health
200 {"status":"Healthy"}

managed monitor path=/api/v1/health
status=operational
response_time_ms=6

Anonymous install telemetry

The deployment funnel carries safe template provenance:

  • deploy_attempted
  • deploy_succeeded
  • deploy_failed with fixed failure_stage, failure_code, and classifier version
  • deploy_cancelled with a fixed trigger

Attribution is server-controlled and source-bound. Project creation stores a temporary marker only after validating the provider, canonical catalog URL, and complete install-plan digest against the live catalog. The first immutable Compose save must exactly match the server-prepared bytes before provenance is promoted; a mismatch clears it permanently.

Telemetry never includes Compose YAML, project IDs/names, repositories, domains, environment variable names/values, raw errors, or private/operator-defined slugs. It remains globally opt-out and never affects deployment behavior.

Runtime issues resolved

  1. Deployment logs: preserve the browser-facing Host header through the frontend WebSocket proxy so backend Host/Origin validation succeeds; expose connecting, waiting, retrying, disconnected, and emitted-log states.
  2. Container logs: apply the same secure WebSocket proxy handling and request a bounded backlog.
  3. Monitor Down/Unknown: synchronize the discovered application health path, ensure one managed monitor, and schedule an immediate stale-safe probe after successful deployment.

Security review

Security auditor: APPROVED, no findings.

The review verified exact Activepieces image matching, explicit confirmation, the fixed capability allowlist, continued rejection of host access, and structured loopback-only healthcheck generation. No shell/YAML injection, SSRF, host-access broadening, or interpolation bypass was found.

Verification

cargo test --lib -p temps-projects
199 passed

cargo test --lib -p temps-presets
264 passed

cargo check --lib
0 errors

cargo clippy --all-targets --all-features -- -D warnings
0 errors

cargo fmt --all --check
passed

web: bun test src
476 passed, 0 failed

web: bunx tsc --noEmit
passed

targeted frontend ESLint
passed

source attribution check
3334 files passed

git diff --check
passed

Browser QA against http://localhost:3012 verified the Beta entry point, catalog counts, debounced search focus, category/tags, host-access blocking, one required permission checkbox, editable generated inputs, Compose inspection, Activepieces installation, deployment logs, container health, public routing, and monitor convergence.

Release position

This should ship as Beta, not Stable. Stable graduation requires continuous catalog qualification, multi-architecture cold-install coverage, a seven-day staging soak, lifecycle/security E2E coverage, and at least 95% of installable entries completing three clean installs with working routes, logs, health, monitor convergence, redeploy, and cleanup.

Attribution

The catalog is attributed to Coolify and links to its Apache-2.0 repository plus each service's documentation. Installed projects are independent, editable Temps Compose projects; origin metadata is informational and does not create runtime coupling.

Failed application container diagnostics

Failed single-container application deployments now receive the same diagnostics behavior as failed Compose stacks:

  • The failed container remains alive but unrouted and is registered under its deployment for authenticated inspection.
  • The deployment page shows each retained container with inline live logs and a full-log link.
  • The next successful deployment cleans retained failures in a bounded, fair retry rotation. Missing/offline worker nodes cannot starve newer cleanup candidates.
  • Remote candidates are retained only after Docker confirms their published port is bound to the worker's private address.
  • The CLI can fetch the same logs by deployment and optional container selector:
temps deployments container-logs \
  --project <slug> \
  --environment production \
  --deployment <id> \
  --container <id-or-name> \
  --follow

Runtime and regression evidence:

cargo test --lib -p temps-deployments -- --test-threads=1
712 passed, 3 ignored

cargo test --lib -p temps-deployer
319 passed

cargo check --lib -p temps-deployer -p temps-deployments -p temps-cli
passed

cargo clippy --lib -p temps-deployer -p temps-deployments -p temps-cli -- -D warnings
passed (workspace/profile warnings only)

apps/temps-cli:
bun test src/commands/runtime-logs.test.ts
3 passed, 0 failed
bun run typecheck
passed

web:
bun test src/lib/retained-failed-containers.test.ts
3 passed, 0 failed
bunx tsc --noEmit
passed
targeted ESLint
passed

slot 12:
http://localhost:8201/readyz -> 200
http://localhost:3012 -> 200

Security auditor: APPROVED. It specifically verified unrouted ownership scoping, private-only remote bindings, node-aware deletion, and fair bounded cleanup retries.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

📓 Changelog preview

This is what your commits will add to the generated CHANGELOG.md at release time (via git-cliff). Do not edit CHANGELOG.md by hand — it is generated from your Conventional Commit messages.

## [Unreleased]

### Added

- **projects:** Install services from Coolify templates
- **service-templates:** Mark catalog beta and classify host access
- **service-templates:** Track anonymous install outcomes
- **compose:** Show retained container logs inline
- **deployments:** Prioritize concise failure diagnostics
- **deployments:** Retain failed app containers for logs

### Fixed

- **projects:** Harden service template installs
- **deployments:** Harden compose service lifecycle
- **deployments:** Harden compose revisions and monitoring
- **deployments:** Serialize compose lifecycle transitions
- **service-templates:** Improve compose install compatibility
- **compose:** Retain failed containers for debugging
- **compose:** Label inline retained logs
- **compose:** Preserve image-owned init processes
- **compose:** Detect runtime compatibility generically
- **service-templates:** Use responsive catalog pagination

### Refactor

- **web:** Use generated compose source client

### Testing

- **migrations:** Verify compose source rollback
- **migrations:** Assert compose source ordering
- **migrations:** Preserve creator rollback coverage

@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown

Greptile Summary

The latest head completes the service-catalog pagination migration and strengthens install-plan snapshot binding across preflight, project creation, and the first Compose save.

  • Delegates catalog navigation to the shared responsive pagination component.
  • Binds initial and retry preflight to the browser-held install-plan digest.
  • Adds focused regression coverage for pagination and revision-preflight rejection.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
web/src/components/service-templates/ServiceTemplateCatalog.tsx Integrates shared pagination and digest-bound install retries; no blocking failure remains.
web/src/components/service-templates/ServiceCatalogPagination.tsx Wraps ResponsivePagination with the fixed catalog page size and required mobile and desktop controls.
crates/temps-projects/src/handlers/service_templates.rs Enforces the expected install-plan digest during service-template preflight.

Reviews (15): Last reviewed commit: "fix(service-templates): use responsive c..." | Re-trigger Greptile

Comment thread web/src/components/service-templates/ServiceTemplateCatalog.tsx Outdated
Comment thread web/src/components/service-templates/ServiceTemplateCatalog.tsx
Comment thread web/src/components/service-templates/ServiceTemplateCatalog.tsx Outdated
@dviejokfs
dviejokfs force-pushed the feat/service-template-catalog branch 2 times, most recently from 41d6060 to 38e26a1 Compare August 31, 2026 11:54
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
@dviejokfs
dviejokfs force-pushed the feat/service-template-catalog branch from ad9ecef to 2c4256e Compare August 31, 2026 13:23
Comment thread web/src/components/service-templates/ServiceTemplateCatalog.tsx
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
@dviejokfs

Copy link
Copy Markdown
Contributor Author

Follow-up evidence: failed Compose container retention

Commit ba536493c keeps failed Compose candidates available for authenticated log inspection without promoting them into the serving route.

Runtime behavior

  • Compose startup/readiness failures carry the ownership-verified candidate containers alongside the typed deployment error.
  • Failed candidates are registered with ready_at = NULL and status = retained:<docker-state>; finalization is skipped, so the failed deployment is never promoted.
  • The failed-deployment page links directly to deployment-scoped container logs. Lifecycle controls are hidden on this diagnostic view.
  • Retention is bounded to one deterministic stack per environment. The next deployment retires the old rows and tears the old stack down; project/environment deletion uses the existing Compose cleanup path.
  • Previous container rows and pg_notify('route_table_changes', '') are committed in one transaction, evicting the old route before a replacement candidate starts.
  • Candidates are retained only after exact Temps project/environment/service labels are verified and every Docker host binding is loopback. Unsafe candidates are force-removed by ID after re-verifying ownership, independently of Compose teardown.
  • Secret generations are pruned only after the previous stack is confirmed stopped. Failed retained containers keep only their current generation until retry/delete cleanup.
  • Readiness now requires two consecutive ready snapshots, closing the Docker race where an immediately exiting process briefly appeared running before health/exit state was populated.

Slot-12 browser + database QA

Controlled Compose deployment 28 deliberately failed on an unhealthy dependency:

deployment_url=http://localhost:3012/projects/compose-failure-retention-evidence/deployments/28
deployment state=failed
deployment ready_at=NULL

app  status=retained:running ready_at=NULL
gate status=retained:created ready_at=NULL

failed deployment UI:
  Failed Compose containers retained for debugging
  Retained 2 failed Compose container(s) for authenticated log inspection.

authenticated container log UI:
  status=Live
  service=app
  line=temps-retained-log-marker

The retained candidate has no public route. The QA screenshots were captured locally as:

  • /tmp/temps-compose-retained-failure.png
  • /tmp/temps-compose-retained-logs.png

Verification

cargo check --lib
0 errors

cargo clippy --lib -p temps-deployer -p temps-deployments -- -D warnings
0 errors

cargo test --lib -p temps-deployer
310 passed

cargo test --lib -p temps-deployments
706 passed, 3 ignored

web: bun test src
478 passed, 0 failed

web: bunx tsc --noEmit
passed

targeted frontend ESLint + Prettier
passed

cargo fmt --all -- --check
passed

source attribution check
10 changed source files passed

git diff --check
passed

Security re-review: APPROVED, no findings. The auditor verified exact ownership re-checks, independent cleanup of unsafe candidates, transactional route invalidation, and the deployment-scoped authenticated log path.

Signed-off-by: David Viejo <dviejo@kfs.es>
@dviejokfs

Copy link
Copy Markdown
Contributor Author

Evidence — inline retained-container logs

Browser/runtime: expanded temps-23-23-activepieces-1 directly on the failed deployment page. The existing authenticated WebSocket connected, reported Live, and filtering for Operation not permitted rendered the nginx chown("/var/lib/nginx/body", 65534) failure inline.

url=http://localhost:3012/projects/activepieces/deployments/29
inlineLive=true
operationNotPermitted=true
showButtons=2
hideButtons=1

Screenshot: /tmp/retained-compose-inline-logs.png

Regression test:

bun test src/lib/retained-compose-containers.test.ts
3 pass
0 fail

Full frontend suite:

bun test src
479 pass
0 fail

bunx tsc --noEmit, targeted ESLint, targeted Prettier, source attribution, and git diff --check also pass.

Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
@dviejokfs

Copy link
Copy Markdown
Contributor Author

Evidence — Budge/LinuxServer PID 1 compatibility

Catalog + deployer regressions: new LinuxServer installs persist init: false; the deployer preserves that explicit setting while retaining the remaining sandbox controls.

cargo test -p temps-deployer test_explicit_init_false_keeps_sandbox_but_does_not_wrap_image_init --lib -- --nocapture
cargo test -p temps-projects linuxserver_templates_keep_s6_as_pid_one --lib -- --nocapture
cargo test -p temps-projects recognizes_supported_linuxserver_image_repositories --lib -- --nocapture
cargo test: 1 passed, 310 filtered out (1 suite, 0.00s)
cargo test: 1 passed, 200 filtered out (1 suite, 0.00s)
cargo test: 1 passed, 200 filtered out (1 suite, 0.00s)

Activepieces regression: its one-click plan still requests the known startup capability profile and uses the application health endpoint.

cargo test -p temps-projects activepieces_uses_application_healthcheck_and_startup_permissions --lib -- --nocapture
cargo test: 1 passed, 199 filtered out (1 suite, 0.02s)

Runtime proof: launched the exact lscr.io/linuxserver/budge:latest image in a fresh disposable Compose project with the generated init: false setting and Temps' capability/no-new-privileges/PID-limit sandbox profile.

Container temps-budge-regression-budge-1 Healthy
status=running health=healthy docker_init=false pid=84637
    1 s6-svscan
pid1_error=false

The disposable container, network, and volume were removed afterward. The existing failed Budge project was not modified.

Affected crate suites and static gates

cargo test -p temps-deployer --lib: 311 passed
cargo test -p temps-projects --lib: 200 passed before the two additional coverage cases; all focused post-change tests pass
cargo check --lib: 0 errors
cargo clippy -p temps-deployer -p temps-projects --lib -- -D warnings: 0 code errors
cargo fmt --all -- --check: pass
git diff --check: pass

Security review: no findings. init: false suppresses only Docker's wrapper; privileged:false, cap_drop:ALL, the allowlisted startup capabilities, no-new-privileges, and pids_limit:512 remain enforced. Registry lookalikes and non-boolean YAML fail closed.

Evidence — concise failure diagnostics

The deployment failure banner now omits embedded container logs by default, offers “Show full error,” and keeps retained live container logs directly below the banner.

summaryAboveDiagnostics=true
embeddedLogsVisibleByDefault=false
fullErrorExpandable=true
bun test src: 482 passed, 0 failed
bunx tsc --noEmit: pass

Screenshot: /tmp/deployment-failure-top-diagnostics.png

Signed-off-by: David Viejo <dviejo@kfs.es>
Signed-off-by: David Viejo <dviejo@kfs.es>
Keep failed application containers ownership-scoped and unrouted so their runtime logs remain available from the deployment page and CLI. Clean them after the next successful deploy with bounded, fair retries, including remote-node failures.

Signed-off-by: David Viejo <dviejo@kfs.es>
@dviejokfs
dviejokfs force-pushed the feat/service-template-catalog branch 2 times, most recently from 4febbcd to 93cc494 Compare September 1, 2026 16:41
Reuse the shared pagination contract so mobile users retain labeled navigation and stable page context. Document that revision-preflight rejection stops slug-conflict retries before a stale install plan can create another project.

Signed-off-by: David Viejo <dviejo@kfs.es>
@dviejokfs

Copy link
Copy Markdown
Contributor Author

@greptile-apps please review the latest head. The remaining pagination finding is fixed in 22cbaa4f, and the snapshot-binding invariant now has focused retry regression coverage.

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.

1 participant