Skip to content

feat(compose): support registry images and remote prebuilt deploys - #5159

Open
designly1 wants to merge 5 commits into
Dokploy:canaryfrom
designly1:feature/compose-registry
Open

feat(compose): support registry images and remote prebuilt deploys#5159
designly1 wants to merge 5 commits into
Dokploy:canaryfrom
designly1:feature/compose-registry

Conversation

@designly1

@designly1 designly1 commented Aug 22, 2026

Copy link
Copy Markdown

Summary

  • Adds optional build server and build registry settings to Compose stacks, mirroring the application remote-build flow: images are built and pushed on the build server, then the deploy server pulls prebuilt images instead of building locally.
  • Introduces a prebuilt deploy path (pull + up --no-build) when a build server is configured, with registry login on both build and deploy hosts.
  • Fixes split build/deploy logging so deploy-phase logs are written on the deploy server (including log directory creation and error-path log aggregation) and corrects chained prebuilt compose commands to prefix follow-up segments with docker compose.

Hardening

This feature introduced a second FK from compose to server (buildServerId, alongside existing serverId). Several server-management paths were updated to handle that safely:

Drizzle relation disambiguation

composeRelations already named its two server links (composeServer, composeBuildServer), but serverRelations still had an unnamed compose: many(compose). Any query that loaded a server with with: { compose: ... } — including remote server delete — failed with:

There are multiple relations between "compose" and "server". Please specify relation name

Fix: mirror the application pattern on the server side:

  • composerelationName: "composeServer" (deploy target)
  • buildComposerelationName: "composeBuildServer" (build host)

Server delete guardrails

haveActiveServices() now counts both deploy-target and build-server references before allowing deletion:

  • applications + buildApplications
  • compose + buildCompose

Previously only the deploy-target side was checked. A server used only as a build host could appear deletable (totalSum === 0 in the UI) even while compose stacks still referenced it via buildServerId.

Registry authorization (compose update)

compose.update now requires the target compose to belong to the active organization (matching one/delete) before any fields are persisted. Registry and build-server IDs are then validated against the same active org. This closes the cross-org credential association Greptile flagged when accessedServices spans organizations.

Split build/deploy log cleanup and read path

  • Cleanup: when buildServerId is set, deployment log directories are removed from the build server on cleanup, preventing orphaned log files after split build/deploy runs.
  • Read path: deployment.readLogs now resolves the log host as deployment.buildServerId || deployment.serverId || …, matching where createDeploymentCompose writes the main log file on split build/deploy runs.

Database migration

  • 0186_sharp_arclight.sql adds compose.buildServerId and compose.buildRegistryId (nullable FKs to server and registry).

Test plan

  • Run migration on a fresh/canary instance and confirm Compose settings show Build Server + Build Registry fields
  • Deploy a compose stack with build directives on the same server (no build server) — behavior unchanged
  • Deploy a compose stack with build server + registry on a different deploy server — build/push succeeds on build host, deploy host pulls and starts without --build
  • Confirm deployment logs include both build and deploy phases when servers differ
  • Verify failed deploys still surface deploy-phase logs in the main deployment log
  • Delete a remote server with no services — succeeds (no Drizzle relation error)
  • Delete a remote server referenced as a compose build server — blocked with "Server has active services"
  • Attempt compose update on a foreign-org service — rejected before registry is persisted
  • Read deployment logs via API for a split build/deploy compose run — returns build-server log content
  • Run pnpm test apps/dokploy/__test__/compose/create-command-prebuilt.test.ts
  • Run pnpm test apps/dokploy/__test__/compose/build-compose-command.test.ts
  • Run pnpm test apps/dokploy/__test__/compose/build-registry-images.test.ts

Greptile Summary

This PR adds remote build-server and registry support for Compose deployments, allowing images to be built and pushed on one host before deployment on another.

  • Adds Compose build-server and build-registry schema, relations, migration, API validation, and settings UI
  • Introduces remote build/push and prebuilt pull/deploy command paths
  • Coordinates split-host deployment logging and server-reference guardrails

Confidence Score: 5/5

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

No blocking failure remains.

Reviews (4): Last reviewed commit: "fix(compose): tighten update ACL and dep..." | Re-trigger Greptile

Context used (3)

designly1 and others added 2 commits August 21, 2026 20:37
Adds compose registry configuration, build pipeline updates, and migration for stored image metadata.

Co-authored-by: Cursor <cursoragent@cursor.com>
Ensure deploy log directories are created on the deploy server when build
and deploy run on different hosts, append deploy logs on failure, and
prefix chained prebuilt compose commands with `docker compose`.
Move onlyBuiltDependencies to pnpm-workspace.yaml per pnpm v10 convention.

Co-authored-by: Cursor <cursoragent@cursor.com>
@designly1
designly1 requested a review from Siumauricio as a code owner August 22, 2026 03:41
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Aug 22, 2026
Comment on lines +209 to +210
const { composeId, ...rest } = input;
const updated = await updateCompose(composeId, rest);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Unscoped build registry credentials

When a user submits another organization's registry ID while updating an accessible Compose service, this mutation persists it without validating registry access. A later remote deployment resolves the registry by ID and uses its credentials in Docker login commands, causing cross-tenant credential use.

How this was verified: The accepted registry ID flows from the update mutation through an organization-unscoped credential lookup into the generated Docker login commands.

Knowledge Base Used: Compose Deployment Flow

Comment on lines +299 to +305
const logServerId = compose.buildServerId || compose.serverId;
const { LOGS_PATH } = paths(!!logServerId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${compose.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, compose.appName, fileName);

if (compose.serverId) {
const server = await findServerById(compose.serverId);
if (logServerId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Build-server logs escape cleanup

When a Compose service has distinct build and deploy servers, this code creates its main log on the build server, while individual and service-level deletion paths still target deployment.serverId or compose.serverId. Deleting deployment records or the service therefore leaves the corresponding logs on the build server, causing orphaned disk usage.

Knowledge Base Used: Application Deployment Flow

Reject buildRegistryId from other organizations on compose update, and
delete deployment logs from buildServerId when present so split
build/deploy stacks do not leave orphaned files.

Co-authored-by: Cursor <cursoragent@cursor.com>
@designly1

Copy link
Copy Markdown
Author

Thanks @greptile-apps — both P1s were valid and are fixed in 38bcee3:

  1. Unscoped build registry credentialscompose.update now verifies buildRegistryId belongs to the active organization before persisting (same pattern as the registry router).
  2. Build-server logs escape cleanupremoveDeployment and removeDeploymentsByComposeId now delete logs via buildServerId || serverId, matching where create writes them.

Note: applications already have a similar buildRegistryId update ACL gap and related log-host cleanup quirks; leaving that as a separate follow-up since it predates this PR.

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Both fixes look correct.

Registry ACL (compose.ts) — the update handler now fetches the registry and gates on reg.organizationId !== ctx.session.activeOrganizationId before persisting, exactly matching the pattern used elsewhere. No gaps visible.

Log cleanup (deployment.ts) — both removeDeploymentsByComposeId and removeDeployment now resolve logServerId = deployment.buildServerId || deployment.serverId (or the compose equivalent) and route the rm to that host. createDeploymentCompose was already writing to buildServerId || serverId, so read and delete now use the same host.

The pre-existing application gap note is fair — it predates this PR and is a separate surface. Both P1s here are properly addressed.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Comment on lines +210 to +212
if (input.buildRegistryId) {
const reg = await findRegistryById(input.buildRegistryId);
if (reg.organizationId !== ctx.session.activeOrganizationId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Cross-tenant registry association persists

If a session's accessed-services list contains a Compose service from another organization, this mutation validates the submitted registry against only the active organization and then persists it on that foreign Compose service. A subsequent remote deployment uses the associated registry credentials for Docker login, causing cross-tenant credential use.

How this was verified: The permission check does not establish the Compose service's active-organization ownership before the organization-scoped registry ID reaches updateCompose.

Knowledge Base Used: Compose Deployment Flow

Name server-side compose relations to match compose.buildServerId and
count both deploy-target and build-server references in haveActiveServices
so remote server deletion no longer fails with ambiguous Drizzle relations.

Co-authored-by: Cursor <cursoragent@cursor.com>
@designly1

Copy link
Copy Markdown
Author

Re-submitted with an additional hardening commit (a662f163a).

What changed since last review:

  • Fixed remote server delete failing with Drizzle's ambiguous composeserver relation error after buildServerId was added
  • Named server-side relations (compose / buildCompose) to match the compose schema
  • Extended haveActiveServices() to block deletion when a server is referenced as a build host (compose or application)

PR description updated with a Hardening section covering relation disambiguation, delete guardrails, registry org validation, and split build/deploy log cleanup.

Validated on gpc3 (ghcr.io/designly1/dokploy:local).

Comment on lines +331 to +333
...(compose.buildServerId && {
buildServerId: compose.buildServerId,
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Split logs read from wrong server

When a Compose deployment uses distinct build and deploy servers, this code records the build server as the main log host, but the log reader ignores deployment.buildServerId and tails deployment.compose.serverId, causing the deployment viewer to display empty logs.

Knowledge Base Used: Application Deployment Flow

Require compose updates to target a service in the active organization
before persisting build registry credentials, and read deployment logs from
buildServerId when split compose builds write logs on the build host.

Co-authored-by: Cursor <cursoragent@cursor.com>
@designly1

Copy link
Copy Markdown
Author

Re-submitted with Greptile follow-up fixes in e74ffde28.

Addressed since last review:

  1. Cross-tenant registry associationcompose.update now loads the compose and rejects updates when the service is not in the active organization, before persisting buildRegistryId or other fields (same guard as one/delete).
  2. Split logs read from wrong serverdeployment.readLogs now prefers deployment.buildServerId when resolving the remote host, matching where split compose deployments write the main log file.

Prior hardening commits remain: server-relation disambiguation (a662f163a), registry org validation + log cleanup (38bcee359), split deploy logging (12a35e133).

Please re-review.

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

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant