Skip to content

feat: Docker Compose dedicated build server support - #4150

Closed
farzanshibu wants to merge 23 commits into
Dokploy:canaryfrom
farzanshibu:copilot/implement-docker-compose-build-server
Closed

farzanshibu wants to merge 23 commits into
Dokploy:canaryfrom
farzanshibu:copilot/implement-docker-compose-build-server

Conversation

@farzanshibu

@farzanshibu farzanshibu commented Apr 4, 2026 •

Copy link
Copy Markdown

Summary

Closes #4148

  • Adds optional build server field to Docker Compose apps
  • Runs compose build on dedicated build server when configured
  • Falls back to deployment server when no build server is set
  • Includes migration and data model support for compose build server assignment

Test plan

  • Verify compose build runs on selected build server
  • Verify fallback to deployment server when no build server configured
  • Verify logs show which server handled build vs deploy
  • Verify migration adds build server field correctly

🤖 Generated with Claude Code

Greptile Summary

This PR adds a dedicated build server field to Docker Compose apps, letting all compose operations (clone, build, deploy, start, stop, remove) run on an optional buildServerId instead of the default serverId. It also introduces commit-hash-based deployments for both applications and compose, with image caching via commit-tagged registry pushes.

  • P1 — Duplicate rollback entries: When an application has rollback enabled (rollbackActive + rollbackRegistry), createRollback is called twice per successful non-cached deployment: once inside uploadImageRemoteCommand (invoked by getBuildCommand) and again in the new createRollbackForDeploymentIfNeeded in the finally block. The same deploymentId produces two DB rows on every build.
  • P2 — rebuildCompose finally block on error path: Unlike deployCompose (success-path only), rebuildCompose updates the deployment title/description/commitHash in a finally block, so a failed rebuild silently overwrites the deployment description with the last-known commit message.

Confidence Score: 3/5

Not safe to merge — duplicate rollback entries will be created on every successful deployment that has rollback enabled.

The duplicate createRollback call is a confirmed data-integrity bug: uploadImageRemoteCommand already calls createRollback when deploymentId is provided, and the new createRollbackForDeploymentIfNeeded in the finally block calls it again. This needs to be fixed before merging.

packages/server/src/services/application.ts (finally blocks in deployApplication and rebuildApplication) and packages/server/src/utils/cluster/upload.ts (uploadImageRemoteCommand + createRollbackForDeploymentIfNeeded).

Reviews (1): Last reviewed commit: "Merge branch 'copilot/implement-specific..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

(4/5) You can add custom instructions or style guidelines for the agent here!

Copilot AI and others added 23 commits March 17, 2026 04:09
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
…copilot/implement-specific-commit-deployment
- Added a new journal entry for commit hash deployment in _journal.json.
- Removed redundant buildServer column definitions in deployment service.
- Expanded the columns returned in findAllDeploymentsCentralized to include additional deployment details.
- Improved the getCheckoutCommitCommand function to handle cases where the .git directory is missing, providing a clearer error message.
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
… fetching

- Added support for reusing commit images during deployment to optimize build times.
- Implemented logic to fetch branch information based on commit hash for GitHub, GitLab, Bitbucket, and Gitea sources.
- Updated the deployment command to include commit hash and deployment ID for better tracking.
- Improved error handling and rollback creation for deployments using reused images.
- Refactored the build command to accept optional parameters for commit hash and deployment ID.
- Enhanced user interface to display branch information related to the provided commit hash during deployment.
… handling

- Introduced `buildServerId` in the compose schema to allow specifying a separate build server.
- Updated various functions to utilize `buildServerId` where applicable, ensuring that deployments and commands are executed on the correct server.
- Added permission checks to ensure that the build server belongs to the same organization as the compose environment.
- Refactored code to centralize the logic for determining the target server ID, improving code readability and maintainability.
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com>
Agent-Logs-Url: https://github.com/farzanshibu/dokploy/sessions/7d3c0c9c-5a6d-498b-ab73-0dae14c7a385
…-commit

Add git history picker for deploying specific commits in applications and compose
…ld-server

fix: clean up deployment logs from the correct build server for Docker Compose
…copilot/implement-specific-commit-deployment
…copilot/implement-docker-compose-build-server
Copilot AI review requested due to automatic review settings April 4, 2026 14:18
@farzanshibu
farzanshibu requested a review from Siumauricio as a code owner April 4, 2026 14:18
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Apr 4, 2026
@github-actions github-actions Bot closed this Apr 4, 2026
@dosubot dosubot Bot added the enhancement New feature or request label Apr 4, 2026

Copilot AI left a comment

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.

Pull request overview

Adds per-service commit-aware deployment capabilities and introduces a new “build server” assignment for Docker Compose services, routing certain operations to a selected server with fallback behavior.

Changes:

  • Add optional commitHash to deployments (DB + APIs), with checkout/reuse flows and git history endpoints for selecting commits in the UI.
  • Add buildServerId to Compose (DB + services + UI) and route compose operations based on build-server selection.
  • Update deployment queue/job payloads and webhook deploy endpoints to propagate commitHash.

Reviewed changes

Copilot reviewed 30 out of 33 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/server/src/utils/providers/git.ts Adds commit checkout command generation + git log history retrieval.
packages/server/src/utils/cluster/upload.ts Adds commit-tag pushes, commit image reuse command, and rollback helper.
packages/server/src/utils/builders/index.ts Threads commitHash/deploymentId into build command generation.
packages/server/src/services/application.ts Supports commit-based deploy/redeploy including image reuse path.
packages/server/src/services/compose.ts Adds build-server resolution and commit checkout support for compose deployments.
packages/server/src/services/deployment.ts Persists commitHash, adjusts log cleanup behavior, and alters centralized deployment query shape.
packages/server/src/db/schema/deployment.ts Adds commitHash column and exposes it via API schemas.
packages/server/src/db/schema/compose.ts Adds buildServerId column, relations, and commitHash inputs for deploy/redeploy.
apps/dokploy/server/api/routers/{compose,application}.ts Adds getGitHistory endpoints and propagates commitHash into jobs.
apps/dokploy/components/dashboard/** UI for commit selection/history + compose build server selection + “promote by commit” flow.
apps/dokploy/drizzle/* Adds migrations for commitHash and compose.buildServerId plus journal updates.
apps/dokploy/test/* Adds/updates tests for admin service changes and commit checkout behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +330 to 335
await createRollbackForDeploymentIfNeeded(
applicationEntity,
deployment.deploymentId,
);
}
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

createRollbackForDeploymentIfNeeded calls createRollback unconditionally when rollback is enabled, but getBuildCommand() already calls uploadImageRemoteCommand() which creates a rollback as part of the build/push flow. This will create duplicate rollback rows and overwrite deployment.rollbackId to the last one, leaving orphaned rollbacks. Consider only calling createRollbackForDeploymentIfNeeded when reusedCommitImage === true (or refactor rollback creation into a single path).

Suggested change
await createRollbackForDeploymentIfNeeded(
applicationEntity,
deployment.deploymentId,
);
}
}
}
}
if (reusedCommitImage) {
await createRollbackForDeploymentIfNeeded(
applicationEntity,
deployment.deploymentId,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +448 to +456
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
commitHash: commitInfo.hash,
});
await createRollbackForDeploymentIfNeeded(
application,
deployment.deploymentId,
);
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

createRollbackForDeploymentIfNeeded is also called in the rebuild flow even though the rebuild path already generates rollback(s) inside getBuildCommand() via uploadImageRemoteCommand(). This risks creating multiple rollback records per deployment and ending up with inconsistent rollback history. Gate this helper behind the reusedCommitImage path or centralize rollback creation.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +54
const getComposeTargetServerId = (
compose: Pick<Compose, "buildServerId" | "serverId">,
) => {
return compose.buildServerId || compose.serverId;
};

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

getComposeTargetServerId always prefers buildServerId over serverId, and the rest of this file uses that value for all compose operations (clone/build/deploy, start/stop, remove, logs). This makes runtime actions run on the build server too, which conflicts with the intended behavior described in the PR/issue (build on build server, deploy/run on deployment server). Consider splitting into separate buildServerId vs runtimeServerId resolution and only routing build-related steps to the build server.

Copilot uses AI. Check for mistakes.
return getGitHistory({
appName: composeData.appName,
type: "compose",
serverId: composeData.serverId || null,

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

In composeRouter.getGitHistory, the serverId passed to getGitHistory is composeData.serverId. If a compose has buildServerId configured, the repo/code likely lives on the build server (based on the rest of the PR routing), so history queries will hit the wrong machine and return empty/error. Use the same target resolution (composeData.buildServerId || composeData.serverId) here.

Suggested change
serverId: composeData.serverId || null,
serverId:
composeData.buildServerId || composeData.serverId || null,

Copilot uses AI. Check for mistakes.
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

publicProcedure is imported but not used in this router. If the project enables noUnusedLocals/eslint unused-import checks, this will fail the build. Remove the unused import or switch the relevant procedure to publicProcedure if that was intended.

Suggested change
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { createTRPCRouter, protectedProcedure } from "../trpc";

Copilot uses AI. Check for mistakes.
composeId: deployment.composeId,
title: deployment.title || "Deployment",
description: deployment.description || "",
commitHash: deployment.commitHash ?? null,

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

createDeploymentCompose now writes logs to targetServerId (buildServerId or serverId) but the inserted deployment row still does not set serverId (and only conditionally sets buildServerId). Downstream cleanup like removeDeployment() relies on deployment.buildServerId || deployment.serverId to remove the log file remotely; for the common case of no build server, both fields can be null and log deletion will run locally instead of on the server that holds the log. Consider persisting the log-hosting server ID (e.g., set serverId: targetServerId when creating the deployment, or add a dedicated logServerId).

Suggested change
commitHash: deployment.commitHash ?? null,
commitHash: deployment.commitHash ?? null,
serverId: targetServerId ?? null,

Copilot uses AI. Check for mistakes.
Comment on lines 805 to 841
const centralizedDeploymentsWith = {
application: {
columns: { applicationId: true, name: true, appName: true },
with: {
environment: {
columns: { environmentId: true, name: true },
with: {
project: {
columns: { projectId: true, name: true },
},
},
},
server: {
columns: { serverId: true, name: true, serverType: true },
},
buildServer: {
columns: { serverId: true, name: true, serverType: true },
},
},
},
compose: {
columns: { composeId: true, name: true, appName: true },
with: {
environment: {
columns: { environmentId: true, name: true },
with: {
project: {
columns: { projectId: true, name: true },
},
},
},
server: {
columns: { serverId: true, name: true, serverType: true },
},
},
},
server: {
columns: { serverId: true, name: true, serverType: true },
},
buildServer: {
columns: { serverId: true, name: true, serverType: true },
},
} as const;

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

findAllDeploymentsCentralized no longer includes buildServer in centralizedDeploymentsWith (neither on the deployment nor on the application). The UI (e.g. centralized deployments table) references d.buildServer?.name / d.application?.buildServer?.name, so build server info will silently disappear. Add buildServer back to the with graph (and include buildServerId in selected columns if you need it on the client).

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 36
.$defaultFn(() => nanoid()),
title: text("title").notNull(),
description: text("description"),
commitHash: text("commitHash"),
status: deploymentStatus("status").default("running"),
logPath: text("logPath").notNull(),

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This PR introduces broader functionality beyond the stated summary (e.g., persisting commitHash on deployments, commit-based checkout/reuse flows, git history endpoints, and UI promotion by commit). If intentional, the PR description/title should be updated to reflect this expanded scope so reviewers and release notes capture it accurately.

Copilot uses AI. Check for mistakes.
Comment on lines 317 to +332
@@ -276,7 +325,12 @@ export const deployApplication = async ({
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
commitHash: commitInfo.hash,
});
await createRollbackForDeploymentIfNeeded(
applicationEntity,
deployment.deploymentId,

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 Duplicate rollback creation on every successful deployment

When rollbackActive is true and rollbackRegistry is set, createRollback is called twice for the same deploymentId: once inside uploadImageRemoteCommand (triggered by getBuildCommand in the try block) and again in createRollbackForDeploymentIfNeeded inside this finally block. Each call inserts a new row into the rollbacks table with an auto-incremented version, so every successful deployment produces two rollback entries pointing to the same image but with consecutive version numbers.

The same double-call exists in rebuildApplication's finally block. The fix is to remove the createRollback call inside uploadImageRemoteCommand and keep only createRollbackForDeploymentIfNeeded in the finally block, or vice-versa — but not both.

Comment on lines 439 to +454
composeStatus: "error",
});
throw error;
} finally {
if (compose.sourceType !== "raw") {
const commitInfo = await getGitCommitInfo({
...compose,
serverId: targetServerId,
type: "compose",
});
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
commitHash: commitInfo.hash,
});

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.

P2 finally block in rebuildCompose overwrites deployment info on error path

Unlike deployCompose (where commit info is updated only in the success path), rebuildCompose now updates the deployment's title, description, and commitHash in a finally block — meaning it also runs when an error is thrown and the catch block has already set composeStatus: "error". If getGitCommitInfo succeeds after a failed rebuild, it silently overwrites the description with the last-known commit message, which can mislead users reading deployment history.

Consider moving this block inside the try block (after the build succeeds) to match the behaviour of deployCompose, or guard it with a succeeded flag.

@@ -438,17 +503,18 @@ export const removeCompose = async (

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.

P2 removeCompose/startCompose/stopCompose routed to buildServerId, not deployment server

getComposeTargetServerId returns buildServerId || serverId. For applications, buildServerId is used only during the build phase — the containers run on serverId. Here, the same helper is used for removeCompose, startCompose, and stopCompose, so lifecycle commands are sent to the build server. If a user adds a build server after an initial deployment, removeCompose will target the new build server while containers are still running on serverId, leaving orphaned containers.

If the intent is truly that everything (including running containers) lives on the build server, consider renaming the field to executionServerId to avoid confusion with the application-level buildServerId.

farzanshibu added a commit to farzanshibu/dokploy that referenced this pull request Apr 4, 2026
- Remove duplicate rollback creation in deployApplication and rebuildApplication
  finally blocks (getBuildCommand already handles rollback for non-reused paths)
- Split getComposeTargetServerId into getComposeBuildServerId (for build ops) and
  getComposeRuntimeServerId (for runtime ops like start/stop/remove)
- Fix getGitHistory to use buildServerId || serverId for compose
- Remove unused publicProcedure import from compose router
- Persist serverId in createDeploymentCompose for log-hosting server tracking
- Add buildServer relation to centralizedDeploymentsWith
- Move commit info update in rebuildCompose from finally to try block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docker Compose: Dedicated Build Server Support

3 participants