Skip to content

Comments

feat: Add patches functionality#3574

Merged
Siumauricio merged 31 commits intoDokploy:canaryfrom
ivanpadavan:patches-impl
Feb 19, 2026
Merged

feat: Add patches functionality#3574
Siumauricio merged 31 commits intoDokploy:canaryfrom
ivanpadavan:patches-impl

Conversation

@ivanpadavan
Copy link
Contributor

@ivanpadavan ivanpadavan commented Jan 31, 2026

What is this PR about?

This PR introduces a new Patches system for Applications and Docker Compose services — a powerful way to customize deployments directly from the dashboard without maintaining complex forks.

While there are many "ready-to-work" repositories available, most require slight modifications to run correctly on a self-hosted instance. This feature dramatically improves the user experience by making it easy to apply small, persistent fixes to any service.

How It Works:
Users can apply persistent modifications (Git patches) to source code or configuration files (like docker-compose.yml) directly from the Dokploy UI. These patches are automatically applied before every build or deployment.

Key Features:

  • Persistent Customization: Changes survive redeployments and updates from the upstream repository.
  • In-Browser Editor: A new "Patches" tab allows users to browse files, edit content, and generate patches automatically without leaving the browser.
  • Safety: Uses standard git apply mechanics; if the source file changes drastically, the patch fails safely, preventing silent breakages.

Implementation Details:

  • Backend: introduced a Patch entity and a PatchService to handle git operations (clone, diff, apply).
  • Deployment Pipeline: Integrated a patch application step into the deployment workflow for both Applications and Compose services.
  • Frontend: Added a file browser and code editor for managing patches.

I understand that the UI/UX and test coverage are open points for discussion. If you are open to including this feature in the product, I am ready to polish it further based on your feedback.

You can preview the implementation here: https://github.com/ivanpadavan/dokploy-patches

Checklist

Before submitting this PR, please make sure that:

Issues related (if applicable)

(If there is a feature request issue for this, link it here, e.g. closes #123)

Screenshots

Patches Tab (Empty State)
Patches Tab

Creating/Editing a Patch
Edit Patch

Patches List
Patches List

Greptile Summary

This PR adds a Patches system that lets users apply persistent git patches to Applications and Docker Compose services before each build. It introduces a new patch DB table, backend CRUD + git operations services, TRPC router, and a frontend file browser/editor UI.

Critical issues that must be resolved before merging:

  • Path traversal vulnerability: The repoPath and filePath parameters in readRepoFile, readRepoDirectories, and saveFileAsPatch endpoints accept arbitrary strings with no validation. Since repoPath is returned to the client by ensureRepo and sent back in subsequent requests, a malicious user can substitute any server path. Combined with unvalidated filePath, this enables reading and writing arbitrary files on the host system (e.g., /etc/passwd, SSH keys, cron jobs).
  • Missing authorization on mutation endpoints: The update, delete, toggleEnabled, and one endpoints perform no ownership verification — any authenticated user can modify or read any patch by ID, regardless of organization. Every other router in the codebase verifies organizationId before mutations.
  • Command injection risks: filePath from user input is interpolated unsanitized into shell echo strings in generateApplyPatchesCommand. The gitBranch variable is unquoted in the git clone command.
  • Silent patch failures contradict safety claims: The || echo "Warning..." pattern in generateApplyPatchesCommand silently swallows git apply failures, allowing builds to proceed with unpatched code. This contradicts the PR description's claim that patches "fail safely, preventing silent breakages."
  • SSH key race condition: Private keys are written to a hardcoded /tmp/patch_repo_id_rsa path with no cleanup, creating race conditions for concurrent operations and leaving sensitive keys on disk.
  • Missing checkServiceAccess for compose: The create endpoint checks member-level service access for applications but not for compose services.

Confidence Score: 1/5

  • This PR has critical security vulnerabilities (path traversal, missing authorization, command injection vectors) that must be addressed before merging.
  • Score of 1 reflects multiple critical security issues: arbitrary file read/write via path traversal, missing authorization on CRUD endpoints allowing cross-organization access, unsanitized user input in shell commands, and a hardcoded SSH key path with race conditions. The core feature concept is sound, but the implementation needs significant security hardening.
  • Pay close attention to apps/dokploy/server/api/routers/patch.ts (missing auth, path traversal), packages/server/src/services/patch-repo.ts (shell injection, SSH key handling), and packages/server/src/services/patch.ts (command injection, silent failures).

Last reviewed commit: 752f90c

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@ivanpadavan ivanpadavan changed the title Patches impl feat: Add patches functionality Jan 31, 2026
@autofix-ci
Copy link
Contributor

autofix-ci bot commented Feb 2, 2026

Hi! I'm autofix logoautofix.ci, a bot that automatically fixes trivial issues such as code formatting in pull requests.

I would like to apply some automated changes to this pull request, but it looks like I don't have the necessary permissions to do so. To get this pull request into a mergeable state, please do one of the following two things:

  1. Allow edits by maintainers for your pull request, and then re-trigger CI (for example by pushing a new commit).
  2. Manually fix the issues identified for your pull request (see the GitHub Actions output for details on what I would like to change).

@Siumauricio
Copy link
Contributor

Hey, can you allow to maintainers can edit the PR? @ivanpadavan

@ivanpadavan
Copy link
Contributor Author

Hey, can you allow to maintainers can edit the PR? @ivanpadavan

Sure!

@AlexDev404
Copy link
Contributor

Oh wow, this is a really useful feature, actually!

@Siumauricio
Copy link
Contributor

@greptile review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

24 files reviewed, 9 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 333 to 340
}),

readRepoFile: protectedProcedure
.input(
z.object({
applicationId: z.string().optional(),
composeId: z.string().optional(),
repoPath: z.string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Path traversal vulnerability in user-supplied paths

The repoPath and filePath parameters accept arbitrary strings with no validation. Since repoPath is returned by ensureRepo to the client and then sent back in subsequent requests, a malicious client can substitute any path. Combined with filePath, this enables reading arbitrary files on the server:

filePath: "../../../../../../etc/passwd"

This flows into readPatchRepoFilecat "${fullPath}" with no traversal check. The same issue exists in readRepoDirectories (line 290) and saveFileAsPatch (line 405), where filePath reaches generatePatchecho ... | base64 -d > "${fullPath}", enabling arbitrary file writes.

Both repoPath and filePath need strict validation:

  • repoPath should be validated server-side against the expected path for the given application/compose (not trusted from the client)
  • filePath should reject .. segments and absolute paths (e.g., .refine(p => !p.includes('..') && !path.isAbsolute(p)))

Comment on lines +196 to +207
}),

update: protectedProcedure
.input(apiUpdatePatch)
.mutation(async ({ input }) => {
const { patchId, ...data } = input;
return await updatePatch(patchId, data);
}),

delete: protectedProcedure
.input(apiDeletePatch)
.mutation(async ({ input }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing authorization on update, delete, and toggleEnabled

The update, delete, and toggleEnabled (line 209) endpoints perform no ownership/authorization checks — any authenticated user can modify or delete any patch by ID. Every other router in the codebase (e.g., mount.ts, port.ts, domain.ts) verifies organizationId before allowing mutations.

These endpoints should fetch the patch, resolve the parent application/compose, and verify organizationId matches ctx.session.activeOrganizationId before proceeding. For example:

update: protectedProcedure
  .input(apiUpdatePatch)
  .mutation(async ({ input, ctx }) => {
    const existing = await findPatchById(input.patchId);
    if (existing.applicationId) {
      const app = await findApplicationById(existing.applicationId);
      if (app.environment.project.organizationId !== ctx.session.activeOrganizationId) {
        throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized" });
      }
    } else if (existing.composeId) {
      const compose = await findComposeById(existing.composeId);
      if (compose.environment.project.organizationId !== ctx.session.activeOrganizationId) {
        throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized" });
      }
    }
    const { patchId, ...data } = input;
    return await updatePatch(patchId, data);
  }),

Comment on lines +156 to +160

return await createPatch(input);
}),

one: protectedProcedure.input(apiFindPatch).query(async ({ input }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing authorization on one endpoint

The one query returns any patch by ID without verifying the caller has access to the parent application/compose. An authenticated user from one organization can read patches belonging to another organization's services. This should include the same ownership verification pattern used in byApplicationId and byComposeId.

Comment on lines 47 to 53
const sshKey = await findSSHKeyById(sshKeyId);
const temporalKeyPath = "/tmp/patch_repo_id_rsa";
sshSetup = `
echo "${sshKey.privateKey}" > ${temporalKeyPath};
chmod 600 ${temporalKeyPath};
export GIT_SSH_COMMAND="ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath} -o StrictHostKeyChecking=accept-new";
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hardcoded SSH key path creates race condition and leaves key on disk

The SSH private key is written to a hardcoded path /tmp/patch_repo_id_rsa. This causes two problems:

  1. Race condition: If two patch repo operations run concurrently with different SSH keys, they overwrite each other's key file, causing authentication failures or using the wrong key.
  2. Key not cleaned up: The private key remains on disk in /tmp after the operation completes. Unlike ephemeral in-memory usage, this persists until the system clears /tmp.

Consider using a unique temp file (e.g., /tmp/patch_repo_id_rsa_${Date.now()}_${Math.random()}) and adding cleanup with a trap in the shell command:

trap "rm -f ${temporalKeyPath}" EXIT;

Comment on lines 163 to 170
rm -f ${patchFileName};
`;
}

return command;
};

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Patch filePath interpolated unsanitized into shell echo strings

The p.filePath value (from user input stored in the database) is interpolated directly into the shell command string inside echo statements. If a filePath contains characters like backticks, $(...), or double-quote escapes, it could lead to command injection when the shell evaluates these echo strings.

For example, a filePath like `whoami` or $(cat /etc/passwd) would be executed by the shell.

The filePath should be sanitized when stored (enforce a strict allowlist pattern like [a-zA-Z0-9./_-]+) or these echo statements should use single quotes for the interpolated path.

Comment on lines +38 to +43
unique("patch_filepath_compose_unique").on(table.filePath, table.composeId),
],
);

export const patchRelations = relations(patch, ({ one }) => ({
application: one(applications, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unique constraints allow duplicate patches when applicationId/composeId is NULL

The unique constraints (filePath, applicationId) and (filePath, composeId) do not prevent duplicates when the nullable column is NULL. In PostgreSQL, NULL != NULL, so multiple rows with the same filePath and applicationId = NULL are allowed. Since only one of applicationId/composeId should be set, the other is NULL — meaning the constraint on the NULL side provides no protection. This likely matches the intended behavior (one patch per file per service), but it means there's no DB-level enforcement preventing orphaned patches (both IDs null) from being created.

Comment on lines +140 to +151
ctx.session.activeOrganizationId,
"access",
);
}
} else if (input.composeId) {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
Copy link
Contributor

Choose a reason for hiding this comment

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

Compose create is missing checkServiceAccess for members

For applications, the create endpoint checks checkServiceAccess when ctx.user.role === "member" (line 132). For compose services, this check is absent — any member in the organization can create patches for any compose service regardless of their service-level access permissions.

- Updated the PatchEditor and ShowPatches components to accept a unified `id` and `type` prop, replacing the previous separate `applicationId` and `composeId` props.
- Refactored API calls in the patch router to handle both application and compose types, improving code clarity and maintainability.
- Enhanced the UI to provide consistent behavior for creating and managing patches across different types.
- Removed the local state for saving status and integrated loading state directly from the mutation hook.
- Updated the saveAsPatch function to handle success and error notifications more cleanly.
- Adjusted the file content loading to ensure it retrieves the latest data, improving user experience and consistency in the PatchEditor component.
- Refactored API calls in the patch router to unify patch retrieval logic for applications and composes, enhancing maintainability.
@Siumauricio
Copy link
Contributor

Hey, I think this feature is really good. There's a lot to do. I've done quite a bit of refactoring, and I think the next idea is great, but it could be simplified a lot more. I think integrating patches using git commands seems like the right way to go, but it can turn into hell.

I was thinking of a much simpler idea of

saving the file content and path, as well as supporting file creation and deletion.

That way, when the repo is cloned, the patches are applied. If the patch being applied does not exist, then the file or corresponding operation is created. I think this would be simpler and we would prevent a lot of weird synchronisation errors...

… configuration handling

- Removed redundant helper functions for retrieving git configurations for applications and composes, streamlining the codebase.
- Updated the `ensurePatchRepo` function to directly handle repository cloning based on the application or compose type, improving clarity and maintainability.
- Refactored patch creation logic to eliminate unnecessary checks and streamline the process of creating patches.
- Enhanced the handling of output paths in repository cloning functions across different git providers, ensuring consistent behavior.
- Consolidated patch retrieval for applications and composes into a single query method, improving code clarity and reducing redundancy.
- Updated the ShowPatches component to utilize the new unified query, simplifying data fetching logic.
- Refactored patch application commands to streamline the process for both application and compose types, enhancing maintainability and consistency across the codebase.
@Siumauricio
Copy link
Contributor

Almost all the changes are done, just a few more details and it will work great! I hope to be able to merge this into v0.28.0.

Siumauricio and others added 7 commits February 17, 2026 01:47
- Introduced the EditPatchDialog component to facilitate patch editing within the dashboard.
- Integrated the dialog into the ShowPatches component, allowing users to edit patches directly from the list view.
- Enhanced user experience with loading indicators and success/error notifications during patch updates.
- Updated the UI to ensure consistent styling and behavior across patch management features.
…ch handling

- Eliminated the repoPath parameter from the PatchEditor component and related API calls, simplifying the patch management logic.
- Updated the patch retrieval and saving processes to focus on filePath and content, enhancing clarity and maintainability.
- Adjusted the handling of file content in the CodeEditor to ensure it retrieves the correct data, improving user experience.
- Streamlined the command generation for applying patches by removing unnecessary checks for enabled status within the loop.
- Consolidated patch retrieval and filtering logic to enhance clarity and maintainability.
- Improved directory handling for file paths to ensure proper creation before applying patches, enhancing robustness.
- Introduced a new ENUM type "patchType" with values 'create', 'update', and 'delete' to categorize patch operations.
- Updated the "patch" table schema to include a new "type" column, defaulting to 'update', ensuring better management of patch types.
- Added a new snapshot file for version 7 to reflect the updated database schema.
…n PatchEditor

- Added CreateFileDialog component for creating new files within the dashboard.
- Integrated file creation functionality into the PatchEditor, allowing users to create files directly from the directory structure.
- Enhanced user experience with form validation and success/error notifications during file creation.
- Updated ShowPatches to display file types with badges for better clarity on patch operations.
…ction

- Removed redundant command initialization and ensured proper command structure for deployment.
- Enhanced clarity by consolidating command building logic, improving maintainability of the deployCompose function.
…epo directory access

- Updated ID input validation to require a minimum length of 1 character.
- Simplified the logic for accessing repository directories by consolidating return statements and removing unnecessary error handling for missing application or compose types.
- Introduced a new SQL file to create a "patch" table with a custom ENUM type "patchType" for tracking changes.
- Added foreign key constraints linking "applicationId" and "composeId" to their respective tables.
- Removed the previous SQL file that contained redundant definitions for the "patchType" and its column in the "patch" table.
- Added a mock for the patch table in application.command.test.ts to simulate findMany behavior.
- Updated application.real.test.ts to include new paths for COMPOSE_PATH, SSH_PATH, and BASE_PATH for improved test coverage.
- Introduced a mock for the patch table to simulate findMany behavior in application.real.test.ts, enhancing test coverage.
- Replaced the mock for the patch service with a mock for db.query.patch.findMany to better simulate database behavior.
- Added type and updatedAt fields to the mock patch object for improved test accuracy.
…l.test.ts

- Eliminated the mock for the patch service and the associated deployment test case to streamline the test suite.
- This change focuses on improving test clarity and reducing complexity by removing redundant code.
- Eliminated the mock constants for paths to simplify the test setup.
- This change enhances test clarity by focusing on relevant mocks and reducing unnecessary complexity.
- Removed the mock for the patch service to streamline the test setup.
- This change enhances test clarity by eliminating unnecessary mock definitions.
…t.ts

- Removed the mock for the patch service to streamline the test setup.
- This change enhances test clarity by eliminating unnecessary mock definitions.
…pplication.real.test.ts

- Introduced a mock for the patch service's findMany method in both test files to simulate database behavior.
- This change enhances test coverage and ensures consistency across the test suite.
@Siumauricio Siumauricio merged commit c98548f into Dokploy:canary Feb 19, 2026
4 checks passed
@ivanpadavan
Copy link
Contributor Author

ivanpadavan commented Feb 19, 2026

Wow, that was cool. Thank you, @AlexDev404, for your support, and @Siumauricio for everything :D

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.

3 participants