diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts
index c8b4295fe..f123fde85 100644
--- a/apps/dokploy/server/api/root.ts
+++ b/apps/dokploy/server/api/root.ts
@@ -22,6 +22,7 @@ import { mountRouter } from "./routers/mount";
import { mysqlRouter } from "./routers/mysql";
import { notificationRouter } from "./routers/notification";
import { organizationRouter } from "./routers/organization";
+import { patchRouter } from "./routers/patch";
import { licenseKeyRouter } from "./routers/proprietary/license-key";
import { ssoRouter } from "./routers/proprietary/sso";
import { portRouter } from "./routers/port";
@@ -90,6 +91,7 @@ export const appRouter = createTRPCRouter({
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
environment: environmentRouter,
+ patch: patchRouter,
});
// export type definition of API
diff --git a/apps/dokploy/server/api/routers/patch.ts b/apps/dokploy/server/api/routers/patch.ts
new file mode 100644
index 000000000..28dc2c8e8
--- /dev/null
+++ b/apps/dokploy/server/api/routers/patch.ts
@@ -0,0 +1,348 @@
+import {
+ checkServiceAccess,
+ cleanPatchRepos,
+ createPatch,
+ deletePatch,
+ ensurePatchRepo,
+ findApplicationById,
+ findComposeById,
+ findPatchByFilePath,
+ findPatchById,
+ findPatchesByEntityId,
+ markPatchForDeletion,
+ readPatchRepoDirectory,
+ readPatchRepoFile,
+ updatePatch,
+} from "@dokploy/server";
+import { TRPCError } from "@trpc/server";
+import { z } from "zod";
+import {
+ adminProcedure,
+ createTRPCRouter,
+ protectedProcedure,
+} from "@/server/api/trpc";
+import {
+ apiCreatePatch,
+ apiDeletePatch,
+ apiFindPatch,
+ apiTogglePatchEnabled,
+ apiUpdatePatch,
+} from "@/server/db/schema";
+
+export const patchRouter = createTRPCRouter({
+ create: protectedProcedure
+ .input(apiCreatePatch)
+ .mutation(async ({ input, ctx }) => {
+ if (input.applicationId) {
+ const app = await findApplicationById(input.applicationId);
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this application",
+ });
+ }
+ if (ctx.user.role === "member") {
+ await checkServiceAccess(
+ ctx.user.id,
+ input.applicationId,
+ 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",
+ message: "You are not authorized to access this compose",
+ });
+ }
+ }
+
+ return await createPatch(input);
+ }),
+
+ one: protectedProcedure.input(apiFindPatch).query(async ({ input }) => {
+ return await findPatchById(input.patchId);
+ }),
+
+ byEntityId: protectedProcedure
+ .input(
+ z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
+ )
+ .query(async ({ input, ctx }) => {
+ if (input.type === "application") {
+ const app = await findApplicationById(input.id);
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this application",
+ });
+ }
+ } else if (input.type === "compose") {
+ const compose = await findComposeById(input.id);
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this compose",
+ });
+ }
+ }
+ const result = await findPatchesByEntityId(input.id, input.type);
+
+ return result;
+ }),
+
+ update: protectedProcedure
+ .input(apiUpdatePatch)
+ .mutation(async ({ input }) => {
+ const { patchId, ...data } = input;
+ return await updatePatch(patchId, data);
+ }),
+
+ delete: protectedProcedure
+ .input(apiDeletePatch)
+ .mutation(async ({ input }) => {
+ return await deletePatch(input.patchId);
+ }),
+
+ toggleEnabled: protectedProcedure
+ .input(apiTogglePatchEnabled)
+ .mutation(async ({ input }) => {
+ return await updatePatch(input.patchId, { enabled: input.enabled });
+ }),
+
+ // Repository Operations
+ ensureRepo: protectedProcedure
+ .input(
+ z.object({
+ id: z.string(),
+ type: z.enum(["application", "compose"]),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ return await ensurePatchRepo({
+ type: input.type,
+ id: input.id,
+ });
+ }),
+
+ readRepoDirectories: protectedProcedure
+ .input(
+ z.object({
+ id: z.string().min(1),
+ type: z.enum(["application", "compose"]),
+ repoPath: z.string(),
+ }),
+ )
+ .query(async ({ input, ctx }) => {
+ let serverId: string | null = null;
+
+ if (input.type === "application") {
+ const app = await findApplicationById(input.id);
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this application",
+ });
+ }
+ serverId = app.serverId;
+ }
+
+ if (input.type === "compose") {
+ const compose = await findComposeById(input.id);
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this compose",
+ });
+ }
+ serverId = compose.serverId;
+ }
+
+ return await readPatchRepoDirectory(input.repoPath, serverId);
+ }),
+
+ readRepoFile: protectedProcedure
+ .input(
+ z.object({
+ id: z.string().min(1),
+ type: z.enum(["application", "compose"]),
+ filePath: z.string(),
+ }),
+ )
+ .query(async ({ input, ctx }) => {
+ let serverId: string | null = null;
+
+ if (input.type === "application") {
+ const app = await findApplicationById(input.id);
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this application",
+ });
+ }
+ serverId = app.serverId;
+ } else if (input.type === "compose") {
+ const compose = await findComposeById(input.id);
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this compose",
+ });
+ }
+ serverId = compose.serverId;
+ } else {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Either applicationId or composeId must be provided",
+ });
+ }
+ const existingPatch = await findPatchByFilePath(
+ input.filePath,
+ input.id,
+ input.type,
+ );
+
+ // For delete patches, show current file content from repo (what will be deleted)
+ if (existingPatch?.type === "delete") {
+ try {
+ return await readPatchRepoFile(input.id, input.type, input.filePath);
+ } catch {
+ return "(File not found in repo - will be removed if it exists)";
+ }
+ }
+ if (existingPatch?.content) {
+ return existingPatch.content;
+ }
+ return await readPatchRepoFile(input.id, input.type, input.filePath);
+ }),
+
+ saveFileAsPatch: protectedProcedure
+ .input(
+ z.object({
+ id: z.string().min(1),
+ type: z.enum(["application", "compose"]),
+ filePath: z.string(),
+ content: z.string(),
+ patchType: z.enum(["create", "update"]).default("update"),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ if (input.type === "application") {
+ const app = await findApplicationById(input.id);
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this application",
+ });
+ }
+ } else if (input.type === "compose") {
+ const compose = await findComposeById(input.id);
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this compose",
+ });
+ }
+ } else {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Either application or compose must be provided",
+ });
+ }
+
+ const existingPatch = await findPatchByFilePath(
+ input.filePath,
+ input.id,
+ input.type,
+ );
+
+ if (!existingPatch) {
+ return await createPatch({
+ filePath: input.filePath,
+ content: input.content,
+ type: input.patchType,
+ applicationId: input.type === "application" ? input.id : undefined,
+ composeId: input.type === "compose" ? input.id : undefined,
+ });
+ }
+
+ return await updatePatch(existingPatch.patchId, {
+ content: input.content,
+ type: input.patchType,
+ });
+ }),
+
+ markFileForDeletion: protectedProcedure
+ .input(
+ z.object({
+ id: z.string().min(1),
+ type: z.enum(["application", "compose"]),
+ filePath: z.string(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ if (input.type === "application") {
+ const app = await findApplicationById(input.id);
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this application",
+ });
+ }
+ } else if (input.type === "compose") {
+ const compose = await findComposeById(input.id);
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this compose",
+ });
+ }
+ }
+
+ return await markPatchForDeletion(input.filePath, input.id, input.type);
+ }),
+ cleanPatchRepos: adminProcedure
+ .input(z.object({ serverId: z.string().optional() }))
+ .mutation(async ({ input }) => {
+ await cleanPatchRepos(input.serverId);
+ return true;
+ }),
+});
diff --git a/openapi.json b/openapi.json
index 76366bdb2..d0403e5ae 100644
--- a/openapi.json
+++ b/openapi.json
@@ -1064,9 +1064,12 @@
"buildSecrets": {
"type": "string",
"nullable": true
+ },
+ "createEnvFile": {
+ "type": "boolean"
}
},
- "required": ["applicationId"],
+ "required": ["applicationId", "createEnvFile"],
"additionalProperties": false
}
}
@@ -1370,6 +1373,10 @@
"type": "string",
"nullable": true
},
+ "bitbucketRepositorySlug": {
+ "type": "string",
+ "nullable": true
+ },
"bitbucketId": {
"type": "string",
"nullable": true
@@ -1393,6 +1400,7 @@
"bitbucketBuildPath",
"bitbucketOwner",
"bitbucketRepository",
+ "bitbucketRepositorySlug",
"bitbucketId",
"applicationId",
"enableSubmodules"
@@ -1836,6 +1844,13 @@
"type": "string",
"nullable": true
},
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ },
"refreshToken": {
"type": "string",
"nullable": true
@@ -1925,6 +1940,10 @@
"type": "string",
"nullable": true
},
+ "bitbucketRepositorySlug": {
+ "type": "string",
+ "nullable": true
+ },
"bitbucketOwner": {
"type": "string",
"nullable": true
@@ -2274,6 +2293,9 @@
"type": "boolean",
"nullable": true
},
+ "createEnvFile": {
+ "type": "boolean"
+ },
"createdAt": {
"type": "string"
},
@@ -2281,6 +2303,10 @@
"type": "string",
"nullable": true
},
+ "rollbackRegistryId": {
+ "type": "string",
+ "nullable": true
+ },
"environmentId": {
"type": "string"
},
@@ -3194,6 +3220,13 @@
"type": "string",
"nullable": true
},
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ },
"env": {
"type": "string",
"nullable": true
@@ -3624,7 +3657,7 @@
},
"dockerImage": {
"type": "string",
- "default": "postgres:15"
+ "default": "postgres:18"
},
"environmentId": {
"type": "string"
@@ -4077,12 +4110,19 @@
},
"dockerImage": {
"type": "string",
- "default": "postgres:15"
+ "default": "postgres:18"
},
"command": {
"type": "string",
"nullable": true
},
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ },
"env": {
"type": "string",
"nullable": true
@@ -4955,6 +4995,13 @@
"type": "string",
"nullable": true
},
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ },
"env": {
"type": "string",
"nullable": true
@@ -5843,6 +5890,13 @@
"type": "string",
"nullable": true
},
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ },
"env": {
"type": "string",
"nullable": true
@@ -6749,6 +6803,13 @@
"type": "string",
"nullable": true
},
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ },
"env": {
"type": "string",
"nullable": true
@@ -7333,6 +7394,10 @@
"type": "string",
"nullable": true
},
+ "bitbucketRepositorySlug": {
+ "type": "string",
+ "nullable": true
+ },
"bitbucketOwner": {
"type": "string",
"nullable": true
@@ -8569,7 +8634,10 @@
"type": "string",
"minLength": 1
},
- "name": {
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
"type": "string"
},
"isRegistered": {
@@ -8619,138 +8687,12 @@
"type": "string",
"format": "date-time"
},
- "serverIp": {
- "type": "string",
- "nullable": true
- },
- "certificateType": {
- "type": "string",
- "enum": ["letsencrypt", "none", "custom"]
- },
- "https": {
- "type": "boolean"
- },
- "host": {
- "type": "string",
- "nullable": true
- },
- "letsEncryptEmail": {
- "type": "string",
- "nullable": true
- },
- "sshPrivateKey": {
- "type": "string",
- "nullable": true
- },
- "enableDockerCleanup": {
- "type": "boolean"
- },
- "logCleanupCron": {
- "type": "string",
- "nullable": true
- },
"enablePaidFeatures": {
"type": "boolean"
},
"allowImpersonation": {
"type": "boolean"
},
- "metricsConfig": {
- "type": "object",
- "properties": {
- "server": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["Dokploy", "Remote"]
- },
- "refreshRate": {
- "type": "number"
- },
- "port": {
- "type": "number"
- },
- "token": {
- "type": "string"
- },
- "urlCallback": {
- "type": "string"
- },
- "retentionDays": {
- "type": "number"
- },
- "cronJob": {
- "type": "string"
- },
- "thresholds": {
- "type": "object",
- "properties": {
- "cpu": {
- "type": "number"
- },
- "memory": {
- "type": "number"
- }
- },
- "required": ["cpu", "memory"],
- "additionalProperties": false
- }
- },
- "required": [
- "type",
- "refreshRate",
- "port",
- "token",
- "urlCallback",
- "retentionDays",
- "cronJob",
- "thresholds"
- ],
- "additionalProperties": false
- },
- "containers": {
- "type": "object",
- "properties": {
- "refreshRate": {
- "type": "number"
- },
- "services": {
- "type": "object",
- "properties": {
- "include": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "exclude": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["include", "exclude"],
- "additionalProperties": false
- }
- },
- "required": ["refreshRate", "services"],
- "additionalProperties": false
- }
- },
- "required": ["server", "containers"],
- "additionalProperties": false
- },
- "cleanupCacheApplications": {
- "type": "boolean"
- },
- "cleanupCacheOnPreviews": {
- "type": "boolean"
- },
- "cleanupCacheOnCompose": {
- "type": "boolean"
- },
"stripeCustomerId": {
"type": "string",
"nullable": true
@@ -10878,6 +10820,52 @@
}
}
},
+ "/previewDeployment.redeploy": {
+ "post": {
+ "operationId": "previewDeployment-redeploy",
+ "tags": ["previewDeployment"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "previewDeploymentId": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["previewDeploymentId"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/mounts.create": {
"post": {
"operationId": "mounts-create",
@@ -11335,6 +11323,29 @@
}
}
},
+ "/settings.getWebServerSettings": {
+ "get": {
+ "operationId": "settings-getWebServerSettings",
+ "tags": ["settings"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/settings.reloadServer": {
"post": {
"operationId": "settings-reloadServer",
@@ -11759,8 +11770,7 @@
"type": "object",
"properties": {
"sshPrivateKey": {
- "type": "string",
- "nullable": true
+ "type": "string"
}
},
"required": ["sshPrivateKey"],
@@ -11800,15 +11810,23 @@
"type": "object",
"properties": {
"host": {
- "type": "string",
- "nullable": true
+ "type": "string"
},
"certificateType": {
"type": "string",
"enum": ["letsencrypt", "none", "custom"]
},
"letsEncryptEmail": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "email"
+ },
+ {
+ "type": "string",
+ "enum": [""]
+ }
+ ],
"nullable": true
},
"https": {
@@ -12329,6 +12347,46 @@
}
}
},
+ "/settings.updateServerIp": {
+ "post": {
+ "operationId": "settings-updateServerIp",
+ "tags": ["settings"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "serverIp": {
+ "type": "string"
+ }
+ },
+ "required": ["serverIp"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/settings.getOpenApiDocument": {
"get": {
"operationId": "settings-getOpenApiDocument",
@@ -13690,6 +13748,49 @@
}
}
},
+ "/registry.testRegistryById": {
+ "post": {
+ "operationId": "registry-testRegistryById",
+ "tags": ["registry"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "registryId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "serverId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/cluster.getNodes": {
"get": {
"operationId": "cluster-getNodes",
@@ -13851,6 +13952,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -13877,6 +13981,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -13926,6 +14031,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14045,6 +14153,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14075,6 +14186,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -14125,6 +14237,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14253,6 +14368,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14279,6 +14397,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -14328,6 +14447,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14448,6 +14570,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14494,6 +14619,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -14547,6 +14673,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14877,6 +15006,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -14908,6 +15040,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -14958,6 +15091,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -15091,6 +15227,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -15112,8 +15251,7 @@
"minLength": 1
},
"accessToken": {
- "type": "string",
- "minLength": 1
+ "type": "string"
},
"priority": {
"type": "number",
@@ -15123,6 +15261,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -15173,6 +15312,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -15194,8 +15336,7 @@
"minLength": 1
},
"accessToken": {
- "type": "string",
- "minLength": 1
+ "type": "string"
},
"priority": {
"type": "number",
@@ -15258,8 +15399,7 @@
"minLength": 1
},
"accessToken": {
- "type": "string",
- "minLength": 1
+ "type": "string"
},
"priority": {
"type": "number",
@@ -15286,6 +15426,206 @@
}
}
},
+ "/notification.createCustom": {
+ "post": {
+ "operationId": "notification-createCustom",
+ "tags": ["notification"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "appBuildError": {
+ "type": "boolean"
+ },
+ "databaseBackup": {
+ "type": "boolean"
+ },
+ "volumeBackup": {
+ "type": "boolean"
+ },
+ "dokployRestart": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "appDeploy": {
+ "type": "boolean"
+ },
+ "dockerCleanup": {
+ "type": "boolean"
+ },
+ "serverThreshold": {
+ "type": "boolean"
+ },
+ "endpoint": {
+ "type": "string",
+ "minLength": 1
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["name", "endpoint"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/notification.updateCustom": {
+ "post": {
+ "operationId": "notification-updateCustom",
+ "tags": ["notification"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "appBuildError": {
+ "type": "boolean"
+ },
+ "databaseBackup": {
+ "type": "boolean"
+ },
+ "volumeBackup": {
+ "type": "boolean"
+ },
+ "dokployRestart": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "appDeploy": {
+ "type": "boolean"
+ },
+ "dockerCleanup": {
+ "type": "boolean"
+ },
+ "serverThreshold": {
+ "type": "boolean"
+ },
+ "endpoint": {
+ "type": "string",
+ "minLength": 1
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "notificationId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "customId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "organizationId": {
+ "type": "string"
+ }
+ },
+ "required": ["notificationId", "customId"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/notification.testCustomConnection": {
+ "post": {
+ "operationId": "notification-testCustomConnection",
+ "tags": ["notification"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "endpoint": {
+ "type": "string",
+ "minLength": 1
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["endpoint"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/notification.createLark": {
"post": {
"operationId": "notification-createLark",
@@ -15308,6 +15648,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -15331,6 +15674,7 @@
"required": [
"appBuildError",
"databaseBackup",
+ "volumeBackup",
"dokployRestart",
"name",
"appDeploy",
@@ -15379,6 +15723,9 @@
"databaseBackup": {
"type": "boolean"
},
+ "volumeBackup": {
+ "type": "boolean"
+ },
"dokployRestart": {
"type": "boolean"
},
@@ -15471,6 +15818,249 @@
}
}
},
+ "/notification.createPushover": {
+ "post": {
+ "operationId": "notification-createPushover",
+ "tags": ["notification"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "appBuildError": {
+ "type": "boolean"
+ },
+ "databaseBackup": {
+ "type": "boolean"
+ },
+ "volumeBackup": {
+ "type": "boolean"
+ },
+ "dokployRestart": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "appDeploy": {
+ "type": "boolean"
+ },
+ "dockerCleanup": {
+ "type": "boolean"
+ },
+ "serverThreshold": {
+ "type": "boolean"
+ },
+ "userKey": {
+ "type": "string",
+ "minLength": 1
+ },
+ "apiToken": {
+ "type": "string",
+ "minLength": 1
+ },
+ "priority": {
+ "type": "number",
+ "minimum": -2,
+ "maximum": 2,
+ "default": 0
+ },
+ "retry": {
+ "type": "number",
+ "minimum": 30,
+ "nullable": true
+ },
+ "expire": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 10800,
+ "nullable": true
+ }
+ },
+ "required": ["name", "userKey", "apiToken"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/notification.updatePushover": {
+ "post": {
+ "operationId": "notification-updatePushover",
+ "tags": ["notification"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "notificationId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "pushoverId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "organizationId": {
+ "type": "string"
+ },
+ "userKey": {
+ "type": "string",
+ "minLength": 1
+ },
+ "apiToken": {
+ "type": "string",
+ "minLength": 1
+ },
+ "priority": {
+ "type": "number",
+ "minimum": -2,
+ "maximum": 2
+ },
+ "retry": {
+ "type": "number",
+ "minimum": 30,
+ "nullable": true
+ },
+ "expire": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 10800,
+ "nullable": true
+ },
+ "appBuildError": {
+ "type": "boolean"
+ },
+ "databaseBackup": {
+ "type": "boolean"
+ },
+ "volumeBackup": {
+ "type": "boolean"
+ },
+ "dokployRestart": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "appDeploy": {
+ "type": "boolean"
+ },
+ "dockerCleanup": {
+ "type": "boolean"
+ },
+ "serverThreshold": {
+ "type": "boolean"
+ }
+ },
+ "required": ["notificationId", "pushoverId"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/notification.testPushoverConnection": {
+ "post": {
+ "operationId": "notification-testPushoverConnection",
+ "tags": ["notification"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "userKey": {
+ "type": "string",
+ "minLength": 1
+ },
+ "apiToken": {
+ "type": "string",
+ "minLength": 1
+ },
+ "priority": {
+ "type": "number",
+ "minimum": -2,
+ "maximum": 2
+ },
+ "retry": {
+ "type": "number",
+ "minimum": 30,
+ "nullable": true
+ },
+ "expire": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 10800,
+ "nullable": true
+ }
+ },
+ "required": ["userKey", "apiToken", "priority"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/notification.getEmailProviders": {
"get": {
"operationId": "notification-getEmailProviders",
@@ -17905,6 +18495,29 @@
}
}
},
+ "/stripe.getInvoices": {
+ "get": {
+ "operationId": "stripe-getInvoices",
+ "tags": ["stripe"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/swarm.getNodes": {
"get": {
"operationId": "swarm-getNodes",
@@ -18696,6 +19309,50 @@
}
}
},
+ "/organization.updateMemberRole": {
+ "post": {
+ "operationId": "organization-updateMemberRole",
+ "tags": ["organization"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "memberId": {
+ "type": "string"
+ },
+ "role": {
+ "type": "string",
+ "enum": ["admin", "member"]
+ }
+ },
+ "required": ["memberId", "role"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
"/organization.setDefault": {
"post": {
"operationId": "organization-setDefault",
@@ -18808,6 +19465,10 @@
"enabled": {
"type": "boolean"
},
+ "timezone": {
+ "type": "string",
+ "nullable": true
+ },
"createdAt": {
"type": "string"
}
@@ -18904,6 +19565,10 @@
"enabled": {
"type": "boolean"
},
+ "timezone": {
+ "type": "string",
+ "nullable": true
+ },
"createdAt": {
"type": "string"
}
@@ -19832,6 +20497,540 @@
}
}
}
+ },
+ "/patch.create": {
+ "post": {
+ "operationId": "patch-create",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "filePath": {
+ "type": "string",
+ "minLength": 1
+ },
+ "content": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "applicationId": {
+ "type": "string",
+ "nullable": true
+ },
+ "composeId": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": ["filePath", "content"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.one": {
+ "get": {
+ "operationId": "patch-one",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "patchId",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.byApplicationId": {
+ "get": {
+ "operationId": "patch-byApplicationId",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "applicationId",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.byComposeId": {
+ "get": {
+ "operationId": "patch-byComposeId",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "composeId",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.update": {
+ "post": {
+ "operationId": "patch-update",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "patchId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "filePath": {
+ "type": "string",
+ "minLength": 1
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "content": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": ["patchId"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.delete": {
+ "post": {
+ "operationId": "patch-delete",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "patchId": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": ["patchId"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.toggleEnabled": {
+ "post": {
+ "operationId": "patch-toggleEnabled",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "patchId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": ["patchId", "enabled"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.ensureRepo": {
+ "post": {
+ "operationId": "patch-ensureRepo",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "applicationId": {
+ "type": "string"
+ },
+ "composeId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.readRepoDirectories": {
+ "get": {
+ "operationId": "patch-readRepoDirectories",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "applicationId",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "composeId",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "repoPath",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.readRepoFile": {
+ "get": {
+ "operationId": "patch-readRepoFile",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "applicationId",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "composeId",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "repoPath",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "filePath",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.saveFileAsPatch": {
+ "post": {
+ "operationId": "patch-saveFileAsPatch",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "applicationId": {
+ "type": "string"
+ },
+ "composeId": {
+ "type": "string"
+ },
+ "repoPath": {
+ "type": "string"
+ },
+ "filePath": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string"
+ }
+ },
+ "required": ["repoPath", "filePath", "content"],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
+ },
+ "/patch.cleanPatchRepos": {
+ "post": {
+ "operationId": "patch-cleanPatchRepos",
+ "tags": ["patch"],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "serverId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {}
+ }
+ },
+ "default": {
+ "$ref": "#/components/responses/error"
+ }
+ }
+ }
}
},
"components": {
diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts
index de0e48a2e..745264f24 100644
--- a/packages/server/src/constants/index.ts
+++ b/packages/server/src/constants/index.ts
@@ -32,5 +32,6 @@ export const paths = (isServer = false) => {
SCHEDULES_PATH: `${BASE_PATH}/schedules`,
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
VOLUME_BACKUP_LOCK_PATH: `${BASE_PATH}/volume-backup-lock`,
+ PATCH_REPOS_PATH: `${BASE_PATH}/patch-repos`,
};
};
diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts
index 3d912654c..2697e8de8 100644
--- a/packages/server/src/db/schema/application.ts
+++ b/packages/server/src/db/schema/application.ts
@@ -19,6 +19,7 @@ import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { mounts } from "./mount";
+import { patch } from "./patch";
import { ports } from "./port";
import { previewDeployments } from "./preview-deployments";
import { redirects } from "./redirects";
@@ -286,6 +287,7 @@ export const applicationsRelations = relations(
references: [registry.registryId],
relationName: "applicationRollbackRegistry",
}),
+ patches: many(patch),
}),
);
diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts
index 02bd60f0b..0c9b1ba28 100644
--- a/packages/server/src/db/schema/compose.ts
+++ b/packages/server/src/db/schema/compose.ts
@@ -12,6 +12,7 @@ import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { mounts } from "./mount";
+import { patch } from "./patch";
import { schedules } from "./schedule";
import { server } from "./server";
import { applicationStatus, triggerType } from "./shared";
@@ -143,6 +144,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
}),
backups: many(backups),
schedules: many(schedules),
+ patches: many(patch),
}));
const createSchema = createInsertSchema(compose, {
diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts
index 5bbf58a5c..fece17f02 100644
--- a/packages/server/src/db/schema/index.ts
+++ b/packages/server/src/db/schema/index.ts
@@ -18,6 +18,7 @@ export * from "./mongo";
export * from "./mount";
export * from "./mysql";
export * from "./notification";
+export * from "./patch";
export * from "./port";
export * from "./postgres";
export * from "./preview-deployments";
diff --git a/packages/server/src/db/schema/patch.ts b/packages/server/src/db/schema/patch.ts
new file mode 100644
index 000000000..c4fc1abd2
--- /dev/null
+++ b/packages/server/src/db/schema/patch.ts
@@ -0,0 +1,100 @@
+import { relations } from "drizzle-orm";
+import { boolean, pgEnum, pgTable, text, unique } from "drizzle-orm/pg-core";
+import { createInsertSchema } from "drizzle-zod";
+import { nanoid } from "nanoid";
+import { z } from "zod";
+import { applications } from "./application";
+import { compose } from "./compose";
+
+export const patchType = pgEnum("patchType", ["create", "update", "delete"]);
+
+export const patch = pgTable(
+ "patch",
+ {
+ patchId: text("patchId")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => nanoid()),
+ type: patchType("type").notNull().default("update"),
+ filePath: text("filePath").notNull(),
+ enabled: boolean("enabled").notNull().default(true),
+ content: text("content").notNull(),
+ createdAt: text("createdAt")
+ .notNull()
+ .$defaultFn(() => new Date().toISOString()),
+ updatedAt: text("updatedAt").$defaultFn(() => new Date().toISOString()),
+ // Relations - one of these must be set
+ applicationId: text("applicationId").references(
+ () => applications.applicationId,
+ { onDelete: "cascade" },
+ ),
+ composeId: text("composeId").references(() => compose.composeId, {
+ onDelete: "cascade",
+ }),
+ },
+ (table) => [
+ // Unique constraint: one patch per file per application/compose
+ unique("patch_filepath_application_unique").on(
+ table.filePath,
+ table.applicationId,
+ ),
+ unique("patch_filepath_compose_unique").on(table.filePath, table.composeId),
+ ],
+);
+
+export const patchRelations = relations(patch, ({ one }) => ({
+ application: one(applications, {
+ fields: [patch.applicationId],
+ references: [applications.applicationId],
+ }),
+ compose: one(compose, {
+ fields: [patch.composeId],
+ references: [compose.composeId],
+ }),
+}));
+
+const createSchema = createInsertSchema(patch, {
+ filePath: z.string().min(1),
+ content: z.string(),
+ type: z.enum(["create", "update", "delete"]).optional(),
+ enabled: z.boolean().optional(),
+ applicationId: z.string().optional(),
+ composeId: z.string().optional(),
+});
+
+export const apiCreatePatch = createSchema.pick({
+ filePath: true,
+ content: true,
+ type: true,
+ enabled: true,
+ applicationId: true,
+ composeId: true,
+});
+
+export const apiFindPatch = z.object({
+ patchId: z.string().min(1),
+});
+
+export const apiFindPatchesByApplicationId = z.object({
+ applicationId: z.string().min(1),
+});
+
+export const apiFindPatchesByComposeId = z.object({
+ composeId: z.string().min(1),
+});
+
+export const apiUpdatePatch = createSchema
+ .partial()
+ .extend({
+ patchId: z.string().min(1),
+ })
+ .omit({ applicationId: true, composeId: true });
+
+export const apiDeletePatch = z.object({
+ patchId: z.string().min(1),
+});
+
+export const apiTogglePatchEnabled = z.object({
+ patchId: z.string().min(1),
+ enabled: z.boolean(),
+});
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index c49a3005a..9adc9081e 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -27,6 +27,8 @@ export * from "./services/mongo";
export * from "./services/mount";
export * from "./services/mysql";
export * from "./services/notification";
+export * from "./services/patch";
+export * from "./services/patch-repo";
export * from "./services/port";
export * from "./services/postgres";
export * from "./services/preview-deployment";
diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts
index 11cff15a4..458ab34f5 100644
--- a/packages/server/src/services/application.ts
+++ b/packages/server/src/services/application.ts
@@ -44,6 +44,7 @@ import {
issueCommentExists,
updateIssueComment,
} from "./github";
+import { generateApplyPatchesCommand } from "./patch";
import {
findPreviewDeploymentById,
updatePreviewDeployment,
@@ -202,6 +203,14 @@ export const deployApplication = async ({
command += await buildRemoteDocker(application);
}
+ if (application.sourceType !== "docker") {
+ command += await generateApplyPatchesCommand({
+ id: application.applicationId,
+ type: "application",
+ serverId,
+ });
+ }
+
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts
index 7021ba29a..0e1fa805f 100644
--- a/packages/server/src/services/compose.ts
+++ b/packages/server/src/services/compose.ts
@@ -40,6 +40,7 @@ import {
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
+import { generateApplyPatchesCommand } from "./patch";
import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect;
@@ -247,8 +248,15 @@ export const deployCompose = async ({
} else {
await execAsync(commandWithLog);
}
-
command = "set -e;";
+ if (compose.sourceType !== "raw") {
+ command += await generateApplyPatchesCommand({
+ id: compose.composeId,
+ type: "compose",
+ serverId: compose.serverId,
+ });
+ }
+
command += await getBuildComposeCommand(entity);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
diff --git a/packages/server/src/services/patch-repo.ts b/packages/server/src/services/patch-repo.ts
new file mode 100644
index 000000000..35e734533
--- /dev/null
+++ b/packages/server/src/services/patch-repo.ts
@@ -0,0 +1,197 @@
+import { join } from "node:path";
+import { paths } from "@dokploy/server/constants";
+import { TRPCError } from "@trpc/server";
+import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
+import { cloneBitbucketRepository } from "../utils/providers/bitbucket";
+import { cloneGitRepository } from "../utils/providers/git";
+import { cloneGiteaRepository } from "../utils/providers/gitea";
+import { cloneGithubRepository } from "../utils/providers/github";
+import { cloneGitlabRepository } from "../utils/providers/gitlab";
+import { findApplicationById } from "./application";
+import { findComposeById } from "./compose";
+
+interface PatchRepoConfig {
+ type: "application" | "compose";
+ id: string;
+}
+
+/**
+ * Ensure patch repo exists and is up-to-date
+ * Returns path to the repo
+ */
+export const ensurePatchRepo = async ({
+ type,
+ id,
+}: PatchRepoConfig): Promise
=> {
+ let serverId: string | null = null;
+
+ if (type === "application") {
+ const application = await findApplicationById(id);
+ serverId = application.buildServerId || application.serverId;
+ } else {
+ const compose = await findComposeById(id);
+ serverId = compose.serverId;
+ }
+
+ const application =
+ type === "application"
+ ? await findApplicationById(id)
+ : await findComposeById(id);
+
+ const { PATCH_REPOS_PATH } = paths(!!serverId);
+ const repoPath = join(PATCH_REPOS_PATH, type, application.appName);
+
+ const applicationEntity = {
+ ...application,
+ type,
+ serverId: serverId,
+ outputPathOverride: repoPath,
+ };
+
+ let command = "set -e;";
+ if (application.sourceType === "github") {
+ command += await cloneGithubRepository(applicationEntity);
+ } else if (application.sourceType === "gitlab") {
+ command += await cloneGitlabRepository(applicationEntity);
+ } else if (application.sourceType === "gitea") {
+ command += await cloneGiteaRepository(applicationEntity);
+ } else if (application.sourceType === "bitbucket") {
+ command += await cloneBitbucketRepository(applicationEntity);
+ } else if (application.sourceType === "git") {
+ command += await cloneGitRepository(applicationEntity);
+ }
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command);
+ }
+
+ return repoPath;
+};
+
+interface DirectoryEntry {
+ name: string;
+ path: string;
+ type: "file" | "directory";
+ children?: DirectoryEntry[];
+}
+
+/**
+ * Read directory tree of the patch repo
+ */
+export const readPatchRepoDirectory = async (
+ repoPath: string,
+ serverId?: string | null,
+): Promise => {
+ // Use git ls-tree to get tracked files only
+ const command = `cd "${repoPath}" && git ls-tree -r --name-only HEAD`;
+
+ let stdout: string;
+ try {
+ if (serverId) {
+ const result = await execAsyncRemote(serverId, command);
+ stdout = result.stdout;
+ } else {
+ const result = await execAsync(command);
+ stdout = result.stdout;
+ }
+ } catch (error) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Failed to read repository: ${error}`,
+ });
+ }
+
+ const files = stdout.trim().split("\n").filter(Boolean);
+
+ // Build tree structure
+ const root: DirectoryEntry[] = [];
+ const dirMap = new Map();
+
+ for (const filePath of files) {
+ const parts = filePath.split("/");
+ let currentPath = "";
+
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i];
+ if (!part) continue;
+
+ const isFile = i === parts.length - 1;
+ const parentPath = currentPath;
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
+
+ if (!dirMap.has(currentPath)) {
+ const entry: DirectoryEntry = {
+ name: part,
+ path: currentPath,
+ type: isFile ? "file" : "directory",
+ children: isFile ? undefined : [],
+ };
+
+ dirMap.set(currentPath, entry);
+
+ if (parentPath) {
+ const parent = dirMap.get(parentPath);
+ parent?.children?.push(entry);
+ } else {
+ root.push(entry);
+ }
+ }
+ }
+ }
+
+ return root;
+};
+
+export const readPatchRepoFile = async (
+ id: string,
+ type: "application" | "compose",
+ filePath: string,
+) => {
+ let serverId: string | null = null;
+
+ if (type === "application") {
+ const application = await findApplicationById(id);
+ serverId = application.buildServerId || application.serverId;
+ } else {
+ const compose = await findComposeById(id);
+ serverId = compose.serverId;
+ }
+ const { PATCH_REPOS_PATH } = paths(!!serverId);
+
+ const application =
+ type === "application"
+ ? await findApplicationById(id)
+ : await findComposeById(id);
+
+ const repoPath = join(PATCH_REPOS_PATH, type, application.appName);
+ const fullPath = join(repoPath, filePath);
+
+ const command = `cat "${fullPath}"`;
+
+ if (serverId) {
+ const result = await execAsyncRemote(serverId, command);
+ return result.stdout;
+ }
+
+ const result = await execAsync(command);
+ return result.stdout;
+};
+
+/**
+ * Clean all patch repos
+ */
+export const cleanPatchRepos = async (
+ serverId?: string | null,
+): Promise => {
+ const { PATCH_REPOS_PATH } = paths(!!serverId);
+
+ const command = `rm -rf "${PATCH_REPOS_PATH}"/* 2>/dev/null || true`;
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command);
+ }
+};
diff --git a/packages/server/src/services/patch.ts b/packages/server/src/services/patch.ts
new file mode 100644
index 000000000..5d25d2baa
--- /dev/null
+++ b/packages/server/src/services/patch.ts
@@ -0,0 +1,171 @@
+import { join } from "node:path";
+import { paths } from "@dokploy/server/constants";
+import { db } from "@dokploy/server/db";
+import { type apiCreatePatch, patch } from "@dokploy/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { and, eq } from "drizzle-orm";
+import { encodeBase64 } from "../utils/docker/utils";
+import { findApplicationById } from "./application";
+import { findComposeById } from "./compose";
+
+export type Patch = typeof patch.$inferSelect;
+
+export const createPatch = async (input: typeof apiCreatePatch._type) => {
+ if (!input.applicationId && !input.composeId) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Either applicationId or composeId must be provided",
+ });
+ }
+
+ const newPatch = await db
+ .insert(patch)
+ .values({
+ ...input,
+ content: input.content,
+ enabled: true,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newPatch) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error creating the patch",
+ });
+ }
+
+ return newPatch;
+};
+
+export const findPatchById = async (patchId: string) => {
+ const result = await db.query.patch.findFirst({
+ where: eq(patch.patchId, patchId),
+ });
+
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Patch not found",
+ });
+ }
+
+ return result;
+};
+
+export const findPatchesByEntityId = async (
+ id: string,
+ type: "application" | "compose",
+) => {
+ return await db.query.patch.findMany({
+ where: eq(
+ type === "application" ? patch.applicationId : patch.composeId,
+ id,
+ ),
+ orderBy: (patch, { asc }) => [asc(patch.filePath)],
+ });
+};
+
+export const findPatchByFilePath = async (
+ filePath: string,
+ id: string,
+ type: "application" | "compose",
+) => {
+ return await db.query.patch.findFirst({
+ where: and(
+ eq(patch.filePath, filePath),
+ eq(type === "application" ? patch.applicationId : patch.composeId, id),
+ ),
+ });
+};
+
+export const updatePatch = async (patchId: string, data: Partial) => {
+ const result = await db
+ .update(patch)
+ .set({
+ ...data,
+ ...(data.content && {
+ content: data.content.endsWith("\n")
+ ? data.content
+ : `${data.content}\n`,
+ }),
+ updatedAt: new Date().toISOString(),
+ })
+ .where(eq(patch.patchId, patchId))
+ .returning();
+
+ return result[0];
+};
+
+export const deletePatch = async (patchId: string) => {
+ const result = await db
+ .delete(patch)
+ .where(eq(patch.patchId, patchId))
+ .returning();
+
+ return result[0];
+};
+
+export const markPatchForDeletion = async (
+ filePath: string,
+ entityId: string,
+ entityType: "application" | "compose",
+) => {
+ const existing = await findPatchByFilePath(filePath, entityId, entityType);
+
+ if (existing) {
+ return await updatePatch(existing.patchId, { type: "delete", content: "" });
+ }
+
+ return await createPatch({
+ filePath,
+ content: "",
+ type: "delete",
+ applicationId: entityType === "application" ? entityId : undefined,
+ composeId: entityType === "compose" ? entityId : undefined,
+ });
+};
+
+interface ApplyPatchesOptions {
+ id: string;
+ type: "application" | "compose";
+ serverId: string | null;
+}
+
+export const generateApplyPatchesCommand = async ({
+ id,
+ type,
+ serverId,
+}: ApplyPatchesOptions) => {
+ const entity =
+ type === "application"
+ ? await findApplicationById(id)
+ : await findComposeById(id);
+ const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
+ const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
+ const codePath = join(basePath, entity.appName, "code");
+
+ const resultPatches = await findPatchesByEntityId(id, type);
+ const patches = resultPatches.filter((p) => p.enabled);
+
+ let command = `echo "Applying ${patches.length} patch(es)...";`;
+
+ for (const p of patches) {
+ const filePath = join(codePath, p.filePath);
+
+ if (p.type === "delete") {
+ command += `
+ rm -f "${filePath}";
+ `;
+ } else {
+ command += `
+file="${filePath}"
+dir="$(dirname "$file")"
+mkdir -p "$dir"
+echo "${encodeBase64(p.content)}" | base64 -d > "$file"
+`;
+ }
+ }
+
+ return command;
+};
diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts
index 57d6de3bc..5a7072198 100644
--- a/packages/server/src/utils/providers/bitbucket.ts
+++ b/packages/server/src/utils/providers/bitbucket.ts
@@ -86,6 +86,7 @@ interface CloneBitbucketRepository {
enableSubmodules: boolean;
serverId: string | null;
type?: "application" | "compose";
+ outputPathOverride?: string;
}
export const cloneBitbucketRepository = async ({
@@ -101,6 +102,7 @@ export const cloneBitbucketRepository = async ({
bitbucketId,
enableSubmodules,
serverId,
+ outputPathOverride,
} = entity;
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
@@ -115,7 +117,7 @@ export const cloneBitbucketRepository = async ({
return command;
}
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
- const outputPath = join(basePath, appName, "code");
+ const outputPath = outputPathOverride ?? join(basePath, appName, "code");
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository;
diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts
index 8e640892d..4c0610921 100644
--- a/packages/server/src/utils/providers/git.ts
+++ b/packages/server/src/utils/providers/git.ts
@@ -14,6 +14,7 @@ interface CloneGitRepository {
enableSubmodules?: boolean;
serverId: string | null;
type?: "application" | "compose";
+ outputPathOverride?: string;
}
export const cloneGitRepository = async ({
@@ -28,6 +29,7 @@ export const cloneGitRepository = async ({
customGitSSHKeyId,
enableSubmodules,
serverId,
+ outputPathOverride,
} = entity;
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
@@ -47,7 +49,7 @@ export const cloneGitRepository = async ({
`;
}
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
- const outputPath = join(basePath, appName, "code");
+ const outputPath = outputPathOverride ?? join(basePath, appName, "code");
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
if (!isHttpOrHttps(customGitUrl)) {
diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts
index 1555e7713..4d26a9212 100644
--- a/packages/server/src/utils/providers/gitea.ts
+++ b/packages/server/src/utils/providers/gitea.ts
@@ -130,6 +130,7 @@ interface CloneGiteaRepository {
enableSubmodules: boolean;
serverId: string | null;
type?: "application" | "compose";
+ outputPathOverride?: string;
}
export const cloneGiteaRepository = async ({
@@ -145,6 +146,7 @@ export const cloneGiteaRepository = async ({
giteaRepository,
enableSubmodules,
serverId,
+ outputPathOverride,
} = entity;
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
@@ -162,7 +164,7 @@ export const cloneGiteaRepository = async ({
}
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
- const outputPath = join(basePath, appName, "code");
+ const outputPath = outputPathOverride ?? join(basePath, appName, "code");
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts
index 5b7763df7..e7907cb47 100644
--- a/packages/server/src/utils/providers/github.ts
+++ b/packages/server/src/utils/providers/github.ts
@@ -121,6 +121,7 @@ interface CloneGithubRepository {
type?: "application" | "compose";
enableSubmodules: boolean;
serverId: string | null;
+ outputPathOverride?: string;
}
export const cloneGithubRepository = async ({
type = "application",
@@ -136,6 +137,7 @@ export const cloneGithubRepository = async ({
githubId,
enableSubmodules,
serverId,
+ outputPathOverride,
} = entity;
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
@@ -155,7 +157,7 @@ export const cloneGithubRepository = async ({
const githubProvider = await findGithubById(githubId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
- const outputPath = join(basePath, appName, "code");
+ const outputPath = outputPathOverride ?? join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts
index 22e5df3ae..1ab1ddabd 100644
--- a/packages/server/src/utils/providers/gitlab.ts
+++ b/packages/server/src/utils/providers/gitlab.ts
@@ -107,6 +107,7 @@ interface CloneGitlabRepository {
enableSubmodules: boolean;
serverId: string | null;
type?: "application" | "compose";
+ outputPathOverride?: string;
}
export const cloneGitlabRepository = async ({
@@ -121,6 +122,7 @@ export const cloneGitlabRepository = async ({
gitlabPathNamespace,
enableSubmodules,
serverId,
+ outputPathOverride,
} = entity;
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
@@ -141,7 +143,7 @@ export const cloneGitlabRepository = async ({
}
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
- const outputPath = join(basePath, appName, "code");
+ const outputPath = outputPathOverride ?? join(basePath, appName, "code");
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);