Repository navigation
feat: Docker Compose dedicated build server support - #4150
farzanshibu wants to merge 23 commits into
Conversation
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>
…//github.com/farzanshibu/dokploy into copilot/implement-specific-commit-deployment
… 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/095541ad-059f-42e4-859e-6903d69ef11d
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com> Agent-Logs-Url: https://github.com/farzanshibu/dokploy/sessions/7d3c0c9c-5a6d-498b-ab73-0dae14c7a385
Co-authored-by: farzanshibu <69754165+farzanshibu@users.noreply.github.com> Agent-Logs-Url: https://github.com/farzanshibu/dokploy/sessions/8738cd23-b614-4057-a898-b1a3498b0ea8
…-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
…//github.com/farzanshibu/dokploy into copilot/implement-docker-compose-build-server
There was a problem hiding this comment.
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
commitHashto deployments (DB + APIs), with checkout/reuse flows and git history endpoints for selecting commits in the UI. - Add
buildServerIdto 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.
| await createRollbackForDeploymentIfNeeded( | ||
| applicationEntity, | ||
| deployment.deploymentId, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
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).
| await createRollbackForDeploymentIfNeeded( | |
| applicationEntity, | |
| deployment.deploymentId, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| if (reusedCommitImage) { | |
| await createRollbackForDeploymentIfNeeded( | |
| applicationEntity, | |
| deployment.deploymentId, | |
| ); | |
| } |
| title: commitInfo.message, | ||
| description: `Commit: ${commitInfo.hash}`, | ||
| commitHash: commitInfo.hash, | ||
| }); | ||
| await createRollbackForDeploymentIfNeeded( | ||
| application, | ||
| deployment.deploymentId, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| const getComposeTargetServerId = ( | ||
| compose: Pick<Compose, "buildServerId" | "serverId">, | ||
| ) => { | ||
| return compose.buildServerId || compose.serverId; | ||
| }; |
There was a problem hiding this comment.
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.
| return getGitHistory({ | ||
| appName: composeData.appName, | ||
| type: "compose", | ||
| serverId: composeData.serverId || null, |
There was a problem hiding this comment.
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.
| serverId: composeData.serverId || null, | |
| serverId: | |
| composeData.buildServerId || composeData.serverId || null, |
| import { cancelDeployment, deploy } from "@/server/utils/deploy"; | ||
| import { generatePassword } from "@/templates/utils"; | ||
| import { createTRPCRouter, protectedProcedure } from "../trpc"; | ||
| import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; |
There was a problem hiding this comment.
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.
| import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; | |
| import { createTRPCRouter, protectedProcedure } from "../trpc"; |
| composeId: deployment.composeId, | ||
| title: deployment.title || "Deployment", | ||
| description: deployment.description || "", | ||
| commitHash: deployment.commitHash ?? null, |
There was a problem hiding this comment.
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).
| commitHash: deployment.commitHash ?? null, | |
| commitHash: deployment.commitHash ?? null, | |
| serverId: targetServerId ?? null, |
| 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; |
There was a problem hiding this comment.
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).
| .$defaultFn(() => nanoid()), | ||
| title: text("title").notNull(), | ||
| description: text("description"), | ||
| commitHash: text("commitHash"), | ||
| status: deploymentStatus("status").default("running"), | ||
| logPath: text("logPath").notNull(), |
There was a problem hiding this comment.
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.
| @@ -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, | |||
There was a problem hiding this comment.
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.
| 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, | ||
| }); |
There was a problem hiding this comment.
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 ( | |||
|
|
|||
There was a problem hiding this comment.
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.
- 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>
Summary
Closes #4148
Test plan
🤖 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
buildServerIdinstead of the defaultserverId. It also introduces commit-hash-based deployments for both applications and compose, with image caching via commit-tagged registry pushes.rollbackActive + rollbackRegistry),createRollbackis called twice per successful non-cached deployment: once insideuploadImageRemoteCommand(invoked bygetBuildCommand) and again in the newcreateRollbackForDeploymentIfNeededin thefinallyblock. The samedeploymentIdproduces two DB rows on every build.rebuildComposefinally block on error path: UnlikedeployCompose(success-path only),rebuildComposeupdates the deployment title/description/commitHash in afinallyblock, 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
createRollbackcall is a confirmed data-integrity bug:uploadImageRemoteCommandalready callscreateRollbackwhendeploymentIdis provided, and the newcreateRollbackForDeploymentIfNeededin thefinallyblock calls it again. This needs to be fixed before merging.packages/server/src/services/application.ts(finally blocks indeployApplicationandrebuildApplication) andpackages/server/src/utils/cluster/upload.ts(uploadImageRemoteCommand+createRollbackForDeploymentIfNeeded).Reviews (1): Last reviewed commit: "Merge branch 'copilot/implement-specific..." | Re-trigger Greptile
(4/5) You can add custom instructions or style guidelines for the agent here!