From 3105d7764c6cb1a6de883d99a55900293519e058 Mon Sep 17 00:00:00 2001 From: Marc Fernandez Date: Fri, 9 Jan 2026 21:59:05 +0100 Subject: [PATCH 1/4] feat: show compose volumes --- .../advanced/volumes/show-volumes.tsx | 77 +++++++++++++++++-- apps/dokploy/server/api/routers/compose.ts | 44 +++++++++++ 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index d3803c42ab..adff437046 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -37,6 +37,13 @@ export const ShowVolumes = ({ id, type }: Props) => { const { data, refetch } = queryMap[type] ? queryMap[type]() : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + // Get volumes in docker-compose.yml for compose services + const { data: composeVolumes } = api.compose.loadDefinedVolumes.useQuery( + { composeId: id }, + { enabled: type === "compose" && !!id } + ); + const { mutateAsync: deleteVolume, isLoading: isRemoving } = api.mounts.remove.useMutation(); return ( @@ -50,14 +57,15 @@ export const ShowVolumes = ({ id, type }: Props) => { - {data && data?.mounts.length > 0 && ( + {data && data?.mounts?.length > 0 && ( Add Volume )} - {data?.mounts.length === 0 ? ( + {data?.mounts?.length === 0 && + (type !== "compose" || !composeVolumes || Object.keys(composeVolumes).length === 0) ? (
@@ -69,12 +77,24 @@ export const ShowVolumes = ({ id, type }: Props) => {
) : (
- - Please remember to click Redeploy after adding, editing, or - deleting a mount to apply the changes. - + {data?.mounts?.length > 0 && ( + + Please remember to click Redeploy after adding, editing, or + deleting a mount to apply the changes. + + )} + {data?.mounts?.length > 0 && type === "compose" && composeVolumes && Object.keys(composeVolumes).length > 0 && ( +
+
+

File Mounts

+

+ File mounts configured through Dokploy interface +

+
+
+ )}
- {data?.mounts.map((mount) => ( + {data?.mounts?.map((mount) => (
{
)} + {/* Show defined volumes from docker-compose.yml for compose services */} + {type === "compose" && composeVolumes && Object.keys(composeVolumes).length > 0 && ( +
+
+

Defined Volumes

+

+ Volumes defined in your docker-compose.yml file +

+
+
+ {Object.entries(composeVolumes).map(([volumeName, volumeConfig]) => ( +
+
+
+ Volume Name + + {volumeName} + +
+
+ Driver + + {typeof volumeConfig === 'object' && volumeConfig !== null + ? (volumeConfig as any)?.driver || 'default' + : 'default' + } + +
+
+ External + + {typeof volumeConfig === 'object' && volumeConfig !== null + ? (volumeConfig as any)?.external ? 'Yes' : 'No' + : 'No' + } + +
+
+
+ ))} +
+
+ )} ); diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 9354988a8f..2ce5289c60 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -20,6 +20,8 @@ import { getComposeContainer, getWebServerSettings, IS_CLOUD, + loadDockerCompose, + loadDockerComposeRemote, loadServices, randomizeComposeFile, randomizeIsolatedDeploymentComposeFile, @@ -339,6 +341,48 @@ export const composeRouter = createTRPCRouter({ } }), + /** + * Load volumes defined in the docker-compose file + */ + loadDefinedVolumes: protectedProcedure + .input(apiFindCompose) + .query(async ({ input, ctx }) => { + 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 load this compose", + }); + } + + try { + // Clone and load the docker-compose file + const command = await cloneCompose(compose); + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } + + // Load and parse the docker-compose.yml file + let composeData; + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + return composeData?.volumes || {}; + } catch (err) { + console.error("Error loading defined volumes:", err); + return {}; + } + }), + randomizeCompose: protectedProcedure .input(apiRandomizeCompose) .mutation(async ({ input, ctx }) => { From 60c566830955186624b4ea089569b790d0031e10 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:06:03 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- .../advanced/volumes/compose-volumes.tsx | 130 +++++++++++++++++ .../advanced/volumes/show-volumes.tsx | 89 ++++-------- apps/dokploy/server/api/routers/compose.ts | 26 +--- packages/server/src/services/compose.ts | 137 ++++++++++++++++++ 4 files changed, 295 insertions(+), 87 deletions(-) create mode 100644 apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx new file mode 100644 index 0000000000..93ec36c2c8 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx @@ -0,0 +1,130 @@ +interface ComposeVolumesProps { + composeVolumes: Record< + string, + { + config: any; + usage: Array<{ service: string; mountPath: string }>; + hostPath?: string; + isBindMount?: boolean; + } + >; +} + +/** + * Generates a display string for the mount path of a volume. + */ +const getMountPathDisplay = ( + volumeName: string, + volumeData: any, +): string => { + const hasUsage = volumeData?.usage && volumeData.usage.length > 0; + + if (!hasUsage) { + return volumeData?.isBindMount ? volumeData.hostPath : volumeName; + } + + return volumeData.usage + .map((usage: { service: string; mountPath: string }) => { + const source = volumeData?.isBindMount + ? volumeData.hostPath + : volumeName; + return `${source}:${usage.mountPath}`; + }) + .join(", "); +}; + +/** + * Retrieves the driver value from the volume configuration. + */ +const getDriverValue = (volumeData: any): string => { + const hasValidConfig = + typeof volumeData?.config === "object" && volumeData?.config !== null; + return hasValidConfig ? volumeData.config.driver || "default" : "default"; +}; + +/** + * Retrieves the external value from the volume configuration. + */ +const getExternalValue = (volumeData: any): string => { + const hasValidConfig = + typeof volumeData?.config === "object" && volumeData?.config !== null; + return hasValidConfig && volumeData.config.external ? "Yes" : "No"; +}; + +/** + * Component to display individual volume fields. + */ +const VolumeField = ({ + label, + value, + breakText = false, +}: { label: string; value: string; breakText?: boolean }) => ( +
+ {label} + + {value} + +
+); + +/** + * Component to display compose volumes information. + */ +export const ComposeVolumes = ({ composeVolumes }: ComposeVolumesProps) => { + if (!composeVolumes || Object.keys(composeVolumes).length === 0) { + return null; + } + + return ( +
+
+

Compose Volumes

+

+ Volumes defined in the docker-compose.yml file of the service +

+
+
+ {Object.entries(composeVolumes).map( + ([volumeName, volumeData]: [string, any]) => { + const isBindMount = volumeData?.isBindMount; + const mountPath = getMountPathDisplay(volumeName, volumeData); + const type = isBindMount ? "Bind Mount" : "Volume"; + + return ( +
+
+ + + + {isBindMount ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ ); + }, + )} +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index adff437046..933b90b92d 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -13,6 +13,7 @@ import { import { api } from "@/utils/api"; import type { ServiceType } from "../show-resources"; import { AddVolumes } from "./add-volumes"; +import { ComposeVolumes } from "./compose-volumes"; import { UpdateVolume } from "./update-volume"; interface Props { @@ -41,7 +42,7 @@ export const ShowVolumes = ({ id, type }: Props) => { // Get volumes in docker-compose.yml for compose services const { data: composeVolumes } = api.compose.loadDefinedVolumes.useQuery( { composeId: id }, - { enabled: type === "compose" && !!id } + { enabled: type === "compose" && !!id }, ); const { mutateAsync: deleteVolume, isLoading: isRemoving } = @@ -57,42 +58,42 @@ export const ShowVolumes = ({ id, type }: Props) => {
- {data && data?.mounts?.length > 0 && ( - - Add Volume - - )} + + Add Volume + - {data?.mounts?.length === 0 && - (type !== "compose" || !composeVolumes || Object.keys(composeVolumes).length === 0) ? ( + {!data?.mounts?.length && + !Object.keys(composeVolumes || {}).length && (
No volumes/mounts configured - - Add Volume -
- ) : ( + )} + {((data?.mounts?.length ?? 0) > 0 || + Object.keys(composeVolumes || {}).length > 0) && (
- {data?.mounts?.length > 0 && ( + {(data?.mounts?.length ?? 0) > 0 && ( Please remember to click Redeploy after adding, editing, or deleting a mount to apply the changes. )} - {data?.mounts?.length > 0 && type === "compose" && composeVolumes && Object.keys(composeVolumes).length > 0 && ( -
-
-

File Mounts

-

- File mounts configured through Dokploy interface -

+ {(data?.mounts?.length ?? 0) > 0 && + type === "compose" && + composeVolumes && + Object.keys(composeVolumes).length > 0 && ( +
+
+

File Mounts

+

+ File mounts configured through Dokploy interface +

+
-
- )} + )}
{data?.mounts?.map((mount) => (
@@ -100,7 +101,6 @@ export const ShowVolumes = ({ id, type }: Props) => { key={mount.mountId} className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" > - {/* */}
Mount Type @@ -190,47 +190,8 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} {/* Show defined volumes from docker-compose.yml for compose services */} - {type === "compose" && composeVolumes && Object.keys(composeVolumes).length > 0 && ( -
-
-

Defined Volumes

-

- Volumes defined in your docker-compose.yml file -

-
-
- {Object.entries(composeVolumes).map(([volumeName, volumeConfig]) => ( -
-
-
- Volume Name - - {volumeName} - -
-
- Driver - - {typeof volumeConfig === 'object' && volumeConfig !== null - ? (volumeConfig as any)?.driver || 'default' - : 'default' - } - -
-
- External - - {typeof volumeConfig === 'object' && volumeConfig !== null - ? (volumeConfig as any)?.external ? 'Yes' : 'No' - : 'No' - } - -
-
-
- ))} -
-
+ {type === "compose" && composeVolumes && ( + )} diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 2ce5289c60..7f5feb847f 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -20,6 +20,7 @@ import { getComposeContainer, getWebServerSettings, IS_CLOUD, + loadDefinedVolumes, loadDockerCompose, loadDockerComposeRemote, loadServices, @@ -348,7 +349,7 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .query(async ({ input, ctx }) => { const compose = await findComposeById(input.composeId); - + if ( compose.environment.project.organizationId !== ctx.session.activeOrganizationId @@ -359,28 +360,7 @@ export const composeRouter = createTRPCRouter({ }); } - try { - // Clone and load the docker-compose file - const command = await cloneCompose(compose); - if (compose.serverId) { - await execAsyncRemote(compose.serverId, command); - } else { - await execAsync(command); - } - - // Load and parse the docker-compose.yml file - let composeData; - if (compose.serverId) { - composeData = await loadDockerComposeRemote(compose); - } else { - composeData = await loadDockerCompose(compose); - } - - return composeData?.volumes || {}; - } catch (err) { - console.error("Error loading defined volumes:", err); - return {}; - } + return await loadDefinedVolumes(input.composeId); }), randomizeCompose: protectedProcedure diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 89a12a1564..4ca434e8b1 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -185,6 +185,143 @@ export const loadServices = async ( return [...services]; }; +export const loadDefinedVolumes = async (composeId: string) => { + const compose = await findComposeById(composeId); + + try { + // Clone and load the docker-compose file + const command = await cloneCompose(compose); + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } + + // Load and parse the docker-compose.yml file + let composeData: ComposeSpecification | null; + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + const volumesDefinition = composeData?.volumes || {}; + const services = composeData?.services || {}; + + // Build a map of volume usage across services + const volumeUsage: Record< + string, + Array<{ service: string; mountPath: string }> + > = {}; + + // Track bind mounts (paths starting with / or .) that are not in volumes section + const bindMounts: Record< + string, + { + hostPath: string; + usage: Array<{ service: string; mountPath: string }>; + } + > = {}; + + // Iterate through all services to find volume usage + for (const [serviceName, serviceConfig] of Object.entries(services)) { + const serviceVolumes = (serviceConfig as any)?.volumes || []; + + for (const volumeEntry of serviceVolumes) { + let volumeName: string | undefined; + let mountPath: string | undefined; + let hostPath: string | undefined; + + if (typeof volumeEntry === "string") { + // Format: "volume_name:/path" or "/host/path:/container/path" + const parts = volumeEntry.split(":"); + if (parts.length >= 2 && parts[0]) { + // Check if it's a named volume (not a path starting with / or .) + if (!parts[0].startsWith("/") && !parts[0].startsWith(".")) { + volumeName = parts[0]; + mountPath = parts[1]; + } else { + // It's a bind mount (path starting with / or .) + hostPath = parts[0]; + mountPath = parts[1]; + } + } + } else if (typeof volumeEntry === "object" && volumeEntry !== null) { + // Long syntax: { type: 'volume', source: 'volume_name', target: '/path' } + if ( + (volumeEntry as any).type === "volume" || + !(volumeEntry as any).type + ) { + volumeName = (volumeEntry as any).source; + mountPath = (volumeEntry as any).target; + } else if ((volumeEntry as any).type === "bind") { + hostPath = (volumeEntry as any).source; + mountPath = (volumeEntry as any).target; + } + } + + if (volumeName && mountPath) { + if (!volumeUsage[volumeName]) { + volumeUsage[volumeName] = []; + } + volumeUsage[volumeName]!.push({ + service: serviceName, + mountPath: mountPath, + }); + } else if (hostPath && mountPath) { + // Track bind mount + const bindKey = `${hostPath}:${mountPath}`; + if (!bindMounts[bindKey]) { + bindMounts[bindKey] = { + hostPath: hostPath, + usage: [], + }; + } + bindMounts[bindKey]!.usage.push({ + service: serviceName, + mountPath: mountPath, + }); + } + } + } + + // Combine volume definitions with usage information + const result: Record< + string, + { + config: any; + usage: Array<{ service: string; mountPath: string }>; + hostPath?: string; + isBindMount?: boolean; + } + > = {}; + + for (const [volumeName, volumeConfig] of Object.entries( + volumesDefinition, + )) { + result[volumeName] = { + config: volumeConfig, + usage: volumeUsage[volumeName] || [], + }; + } + + // Add bind mounts to the result + for (const [bindKey, bindData] of Object.entries(bindMounts)) { + result[bindKey] = { + config: null, + usage: bindData.usage, + hostPath: bindData.hostPath, + isBindMount: true, + }; + } + + return result; + } catch (err) { + console.error("Error loading defined volumes:", err); + return {}; + } +}; + export const updateCompose = async ( composeId: string, composeData: Partial, From b155de2fc0a0bc4409ee855c7172049545f78f8b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 07:36:12 +0000 Subject: [PATCH 3/4] [autofix.ci] apply automated fixes (attempt 2/3) --- .../advanced/volumes/compose-volumes.tsx | 15 +- .../advanced/volumes/show-volumes.tsx | 60 +++- .../server/api/models/compose.models.ts | 113 +++++++ apps/dokploy/server/api/routers/compose.ts | 33 +-- packages/server/src/services/compose.ts | 276 +++++++++++------- 5 files changed, 345 insertions(+), 152 deletions(-) create mode 100644 apps/dokploy/server/api/models/compose.models.ts diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx index 93ec36c2c8..9f9a9df0f1 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/compose-volumes.tsx @@ -13,10 +13,7 @@ interface ComposeVolumesProps { /** * Generates a display string for the mount path of a volume. */ -const getMountPathDisplay = ( - volumeName: string, - volumeData: any, -): string => { +const getMountPathDisplay = (volumeName: string, volumeData: any): string => { const hasUsage = volumeData?.usage && volumeData.usage.length > 0; if (!hasUsage) { @@ -25,9 +22,7 @@ const getMountPathDisplay = ( return volumeData.usage .map((usage: { service: string; mountPath: string }) => { - const source = volumeData?.isBindMount - ? volumeData.hostPath - : volumeName; + const source = volumeData?.isBindMount ? volumeData.hostPath : volumeName; return `${source}:${usage.mountPath}`; }) .join(", "); @@ -58,7 +53,11 @@ const VolumeField = ({ label, value, breakText = false, -}: { label: string; value: string; breakText?: boolean }) => ( +}: { + label: string; + value: string; + breakText?: boolean; +}) => (
{label} { + return type === "compose" && data && "definedVolumesInComposeFile" in data; +}; + +/** + * Get the count of defined volumes in docker-compose.yml + */ +const getComposeVolumesCount = (data: any, type: string) => { + if (!isComposeWithVolumes(data, type)) return 0; + return Object.keys(data.definedVolumesInComposeFile || {}).length; +}; + +/** + * Check if the service has any volumes/mounts configured + */ +const hasAnyVolumes = (data: any, type: string) => { + const mountsCount = data?.mounts?.length ?? 0; + const composeVolumesCount = getComposeVolumesCount(data, type); + return mountsCount > 0 || composeVolumesCount > 0; +}; + +/** + * Get the defined volumes in docker-compose.yml + */ +const getComposeVolumes = (data: any, type: string) => { + if (!isComposeWithVolumes(data, type)) return null; + return data.definedVolumesInComposeFile; +}; + +/** + * Show Volumes component + */ export const ShowVolumes = ({ id, type }: Props) => { + console.log("Rendering ShowVolumes with id:", id, "and type:", type); const queryMap = { postgres: () => api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), @@ -39,12 +75,6 @@ export const ShowVolumes = ({ id, type }: Props) => { ? queryMap[type]() : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); - // Get volumes in docker-compose.yml for compose services - const { data: composeVolumes } = api.compose.loadDefinedVolumes.useQuery( - { composeId: id }, - { enabled: type === "compose" && !!id }, - ); - const { mutateAsync: deleteVolume, isLoading: isRemoving } = api.mounts.remove.useMutation(); return ( @@ -63,8 +93,7 @@ export const ShowVolumes = ({ id, type }: Props) => { - {!data?.mounts?.length && - !Object.keys(composeVolumes || {}).length && ( + {!hasAnyVolumes(data, type) && (
@@ -72,8 +101,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} - {((data?.mounts?.length ?? 0) > 0 || - Object.keys(composeVolumes || {}).length > 0) && ( + {hasAnyVolumes(data, type) && (
{(data?.mounts?.length ?? 0) > 0 && ( @@ -82,9 +110,8 @@ export const ShowVolumes = ({ id, type }: Props) => { )} {(data?.mounts?.length ?? 0) > 0 && - type === "compose" && - composeVolumes && - Object.keys(composeVolumes).length > 0 && ( + isComposeWithVolumes(data, type) && + getComposeVolumesCount(data, type) > 0 && (

File Mounts

@@ -190,9 +217,10 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} {/* Show defined volumes from docker-compose.yml for compose services */} - {type === "compose" && composeVolumes && ( - - )} + {(() => { + const composeVolumes = getComposeVolumes(data, type); + return composeVolumes && ; + })()} ); diff --git a/apps/dokploy/server/api/models/compose.models.ts b/apps/dokploy/server/api/models/compose.models.ts new file mode 100644 index 0000000000..3e2dd880d0 --- /dev/null +++ b/apps/dokploy/server/api/models/compose.models.ts @@ -0,0 +1,113 @@ +/** + * Compose model + */ +export interface Compose { + composeId: string; + name: string; + appName: string; + description: string | null; + env: string | null; + composeFile: string; + refreshToken: string | null; + sourceType: "git" | "github" | "gitlab" | "bitbucket" | "gitea" | "raw"; + composeType: "docker-compose" | "stack"; + repository: string | null; + owner: string | null; + branch: string | null; + autoDeploy: boolean | null; + gitlabProjectId: number | null; + gitlabRepository: string | null; + gitlabOwner: string | null; + gitlabBranch: string | null; + gitlabPathNamespace: string | null; + bitbucketRepository: string | null; + bitbucketOwner: string | null; + bitbucketBranch: string | null; + giteaRepository: string | null; + giteaOwner: string | null; + giteaBranch: string | null; + customGitUrl: string | null; + customGitBranch: string | null; + customGitSSHKeyId: string | null; + command: string; + enableSubmodules: boolean; + composePath: string; + suffix: string; + randomize: boolean; + isolatedDeployment: boolean; + isolatedDeploymentsVolume: boolean; + triggerType: string | null; + composeStatus: string; + environmentId: string; + createdAt: string; + watchPaths: string[] | null; + githubId: string | null; + gitlabId: string | null; + bitbucketId: string | null; + giteaId: string | null; + serverId: string | null; + environment: { + environmentId: string; + name: string; + projectId: string; + project: { + projectId: string; + name: string; + description: string | null; + organizationId: string; + createdAt: string; + }; + }; + deployments: Array<{ + deploymentId: string; + status: string | null; + composeId: string | null; + createdAt: string; + }>; + mounts: Array<{ + mountId: string; + type: "bind" | "volume" | "file"; + hostPath: string | null; + volumeName: string | null; + filePath: string | null; + content: string | null; + serviceType: "application" | "postgres" | "mysql" | "mariadb" | "mongo" | "redis" | "compose"; + mountPath: string; + applicationId: string | null; + postgresId: string | null; + mariadbId: string | null; + mongoId: string | null; + mysqlId: string | null; + redisId: string | null; + composeId: string | null; + }>; + domains: Array<{ + domainId: string; + host: string; + path: string | null; + port: number | null; + https: boolean; + certificateType: string; + composeId: string | null; + createdAt: string; + }>; + github: any; + gitlab: any; + bitbucket: any; + gitea: any; + server: any; + backups: Array<{ + backupId: string; + composeId: string | null; + destination: any; + deployments: any[]; + }>; + hasGitProviderAccess: boolean; + unauthorizedProvider: string | null; + definedVolumesInComposeFile?: Record; + hostPath?: string; + isBindMount?: boolean; + }>; +}; diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 7f5feb847f..bd0e86ee4b 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -20,7 +20,7 @@ import { getComposeContainer, getWebServerSettings, IS_CLOUD, - loadDefinedVolumes, + loadDefinedVolumesInComposeFile, loadDockerCompose, loadDockerComposeRemote, loadServices, @@ -61,6 +61,7 @@ import { apiUpdateCompose, compose as composeTable, } from "@/server/db/schema"; +import type { Compose } from "@/server/api/models/compose.models"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { cleanQueuesByCompose, @@ -119,9 +120,12 @@ export const composeRouter = createTRPCRouter({ } }), + /** + * Get a compose by ID + */ one: protectedProcedure .input(apiFindCompose) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { if (ctx.user.role === "member") { await checkServiceAccess( ctx.user.id, @@ -175,10 +179,14 @@ export const composeRouter = createTRPCRouter({ } } + // Load volumes defined in docker-compose.yml if exists + const definedVolumesInComposeFile = await loadDefinedVolumesInComposeFile(input.composeId); + return { ...compose, hasGitProviderAccess, unauthorizedProvider, + definedVolumesInComposeFile, }; }), @@ -342,27 +350,6 @@ export const composeRouter = createTRPCRouter({ } }), - /** - * Load volumes defined in the docker-compose file - */ - loadDefinedVolumes: protectedProcedure - .input(apiFindCompose) - .query(async ({ input, ctx }) => { - 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 load this compose", - }); - } - - return await loadDefinedVolumes(input.composeId); - }), - randomizeCompose: protectedProcedure .input(apiRandomizeCompose) .mutation(async ({ input, ctx }) => { diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 4ca434e8b1..58cb1d056e 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -108,6 +108,11 @@ export const createComposeByTemplate = async ( return newDestination; }; +/** + * Find compose by ID + * + * @param composeId ID of the compose service + */ export const findComposeById = async (composeId: string) => { const result = await db.query.compose.findFirst({ where: eq(compose.composeId, composeId), @@ -185,10 +190,38 @@ export const loadServices = async ( return [...services]; }; -export const loadDefinedVolumes = async (composeId: string) => { +/** + * Load defined volumes from a docker-compose.yml file + * + * @param composeId ID of the compose service + */ +export const loadDefinedVolumesInComposeFile = async (composeId: string) => { const compose = await findComposeById(composeId); try { + // For raw compose type, we don't need to clone anything + if (compose.sourceType === "raw") { + let composeData: ComposeSpecification | null; + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + if (!composeData) { + return {}; + } + + return extractVolumesFromComposeData(composeData); + } + + // Validate that we have the necessary provider configuration + const hasValidProvider = validateComposeProvider(compose); + if (!hasValidProvider) { + console.warn(`No valid provider configuration for compose ${composeId}, returning empty volumes`); + return {}; + } + // Clone and load the docker-compose file const command = await cloneCompose(compose); if (compose.serverId) { @@ -205,121 +238,154 @@ export const loadDefinedVolumes = async (composeId: string) => { composeData = await loadDockerCompose(compose); } - const volumesDefinition = composeData?.volumes || {}; - const services = composeData?.services || {}; - - // Build a map of volume usage across services - const volumeUsage: Record< - string, - Array<{ service: string; mountPath: string }> - > = {}; - - // Track bind mounts (paths starting with / or .) that are not in volumes section - const bindMounts: Record< - string, - { - hostPath: string; - usage: Array<{ service: string; mountPath: string }>; - } - > = {}; - - // Iterate through all services to find volume usage - for (const [serviceName, serviceConfig] of Object.entries(services)) { - const serviceVolumes = (serviceConfig as any)?.volumes || []; - - for (const volumeEntry of serviceVolumes) { - let volumeName: string | undefined; - let mountPath: string | undefined; - let hostPath: string | undefined; - - if (typeof volumeEntry === "string") { - // Format: "volume_name:/path" or "/host/path:/container/path" - const parts = volumeEntry.split(":"); - if (parts.length >= 2 && parts[0]) { - // Check if it's a named volume (not a path starting with / or .) - if (!parts[0].startsWith("/") && !parts[0].startsWith(".")) { - volumeName = parts[0]; - mountPath = parts[1]; - } else { - // It's a bind mount (path starting with / or .) - hostPath = parts[0]; - mountPath = parts[1]; - } - } - } else if (typeof volumeEntry === "object" && volumeEntry !== null) { - // Long syntax: { type: 'volume', source: 'volume_name', target: '/path' } - if ( - (volumeEntry as any).type === "volume" || - !(volumeEntry as any).type - ) { - volumeName = (volumeEntry as any).source; - mountPath = (volumeEntry as any).target; - } else if ((volumeEntry as any).type === "bind") { - hostPath = (volumeEntry as any).source; - mountPath = (volumeEntry as any).target; - } - } + if (!composeData) { + return {}; + } - if (volumeName && mountPath) { - if (!volumeUsage[volumeName]) { - volumeUsage[volumeName] = []; - } - volumeUsage[volumeName]!.push({ - service: serviceName, - mountPath: mountPath, - }); - } else if (hostPath && mountPath) { - // Track bind mount - const bindKey = `${hostPath}:${mountPath}`; - if (!bindMounts[bindKey]) { - bindMounts[bindKey] = { - hostPath: hostPath, - usage: [], - }; + return extractVolumesFromComposeData(composeData); + } catch (err) { + console.error("Error loading defined volumes:", err); + return {}; + } +}; + +/** + * Validate that the compose has a valid provider configuration + */ +const validateComposeProvider = (compose: any): boolean => { + switch (compose.sourceType) { + case "github": + return !!compose.repository && !!compose.owner && !!compose.githubId; + case "gitlab": + return !!compose.gitlabRepository && !!compose.gitlabOwner && !!compose.gitlabId; + case "bitbucket": + return !!compose.bitbucketRepository && !!compose.bitbucketOwner && !!compose.bitbucketId; + case "gitea": + return !!compose.giteaRepository && !!compose.giteaOwner && !!compose.giteaId; + case "git": + return !!compose.customGitUrl; + case "raw": + return true; // Raw always valid + default: + return false; + } +}; + +/** + * Extract volumes information from compose data + */ +const extractVolumesFromComposeData = (composeData: ComposeSpecification) => { + const volumesDefinition = composeData?.volumes || {}; + const services = composeData?.services || {}; + + // Build a map of volume usage across services + const volumeUsage: Record< + string, + Array<{ service: string; mountPath: string }> + > = {}; + + // Track bind mounts (paths starting with / or .) that are not in volumes section + const bindMounts: Record< + string, + { + hostPath: string; + usage: Array<{ service: string; mountPath: string }>; + } + > = {}; + + // Iterate through all services to find volume usage + for (const [serviceName, serviceConfig] of Object.entries(services)) { + const serviceVolumes = (serviceConfig as any)?.volumes || []; + + for (const volumeEntry of serviceVolumes) { + let volumeName: string | undefined; + let mountPath: string | undefined; + let hostPath: string | undefined; + + if (typeof volumeEntry === "string") { + // Format: "volume_name:/path" or "/host/path:/container/path" + const parts = volumeEntry.split(":"); + if (parts.length >= 2 && parts[0]) { + // Check if it's a named volume (not a path starting with / or .) + if (!parts[0].startsWith("/") && !parts[0].startsWith(".")) { + volumeName = parts[0]; + mountPath = parts[1]; + } else { + // It's a bind mount (path starting with / or .) + hostPath = parts[0]; + mountPath = parts[1]; } - bindMounts[bindKey]!.usage.push({ - service: serviceName, - mountPath: mountPath, - }); + } + } else if (typeof volumeEntry === "object" && volumeEntry !== null) { + // Long syntax: { type: 'volume', source: 'volume_name', target: '/path' } + if ( + (volumeEntry as any).type === "volume" || + !(volumeEntry as any).type + ) { + volumeName = (volumeEntry as any).source; + mountPath = (volumeEntry as any).target; + } else if ((volumeEntry as any).type === "bind") { + hostPath = (volumeEntry as any).source; + mountPath = (volumeEntry as any).target; } } - } - // Combine volume definitions with usage information - const result: Record< - string, - { - config: any; - usage: Array<{ service: string; mountPath: string }>; - hostPath?: string; - isBindMount?: boolean; + if (volumeName && mountPath) { + if (!volumeUsage[volumeName]) { + volumeUsage[volumeName] = []; + } + volumeUsage[volumeName]!.push({ + service: serviceName, + mountPath: mountPath, + }); + } else if (hostPath && mountPath) { + // Track bind mount + const bindKey = `${hostPath}:${mountPath}`; + if (!bindMounts[bindKey]) { + bindMounts[bindKey] = { + hostPath: hostPath, + usage: [], + }; + } + bindMounts[bindKey]!.usage.push({ + service: serviceName, + mountPath: mountPath, + }); } - > = {}; - - for (const [volumeName, volumeConfig] of Object.entries( - volumesDefinition, - )) { - result[volumeName] = { - config: volumeConfig, - usage: volumeUsage[volumeName] || [], - }; } + } - // Add bind mounts to the result - for (const [bindKey, bindData] of Object.entries(bindMounts)) { - result[bindKey] = { - config: null, - usage: bindData.usage, - hostPath: bindData.hostPath, - isBindMount: true, - }; + // Combine volume definitions with usage information + const result: Record< + string, + { + config: any; + usage: Array<{ service: string; mountPath: string }>; + hostPath?: string; + isBindMount?: boolean; } + > = {}; + + for (const [volumeName, volumeConfig] of Object.entries( + volumesDefinition, + )) { + result[volumeName] = { + config: volumeConfig, + usage: volumeUsage[volumeName] || [], + }; + } - return result; - } catch (err) { - console.error("Error loading defined volumes:", err); - return {}; + // Add bind mounts to the result + for (const [bindKey, bindData] of Object.entries(bindMounts)) { + result[bindKey] = { + config: null, + usage: bindData.usage, + hostPath: bindData.hostPath, + isBindMount: true, + }; } + + return result; }; export const updateCompose = async ( From 0d8af590c42637466e4a48f20559dab8eaefe2db Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 09:28:55 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes (attempt 3/3) --- .../advanced/volumes/show-volumes.tsx | 10 +++--- .../server/api/models/compose.models.ts | 28 ++++++++++------ apps/dokploy/server/api/routers/compose.ts | 4 ++- packages/server/src/services/compose.ts | 32 ++++++++++++------- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index d95c93b8a6..c9dcd56eea 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -217,10 +217,12 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} {/* Show defined volumes from docker-compose.yml for compose services */} - {(() => { - const composeVolumes = getComposeVolumes(data, type); - return composeVolumes && ; - })()} + {(() => { + const composeVolumes = getComposeVolumes(data, type); + return ( + composeVolumes && + ); + })()} ); diff --git a/apps/dokploy/server/api/models/compose.models.ts b/apps/dokploy/server/api/models/compose.models.ts index 3e2dd880d0..3f5ef1017e 100644 --- a/apps/dokploy/server/api/models/compose.models.ts +++ b/apps/dokploy/server/api/models/compose.models.ts @@ -71,7 +71,14 @@ export interface Compose { volumeName: string | null; filePath: string | null; content: string | null; - serviceType: "application" | "postgres" | "mysql" | "mariadb" | "mongo" | "redis" | "compose"; + serviceType: + | "application" + | "postgres" + | "mysql" + | "mariadb" + | "mongo" + | "redis" + | "compose"; mountPath: string; applicationId: string | null; postgresId: string | null; @@ -101,13 +108,16 @@ export interface Compose { composeId: string | null; destination: any; deployments: any[]; - }>; + }>; hasGitProviderAccess: boolean; unauthorizedProvider: string | null; - definedVolumesInComposeFile?: Record; - hostPath?: string; - isBindMount?: boolean; - }>; -}; + definedVolumesInComposeFile?: Record< + string, + { + config: any; + usage: Array<{ service: string; mountPath: string }>; + hostPath?: string; + isBindMount?: boolean; + } + >; +} diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index bd0e86ee4b..515502dc01 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -180,7 +180,9 @@ export const composeRouter = createTRPCRouter({ } // Load volumes defined in docker-compose.yml if exists - const definedVolumesInComposeFile = await loadDefinedVolumesInComposeFile(input.composeId); + const definedVolumesInComposeFile = await loadDefinedVolumesInComposeFile( + input.composeId, + ); return { ...compose, diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 58cb1d056e..33c4ac0b85 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -110,7 +110,7 @@ export const createComposeByTemplate = async ( /** * Find compose by ID - * + * * @param composeId ID of the compose service */ export const findComposeById = async (composeId: string) => { @@ -192,7 +192,7 @@ export const loadServices = async ( /** * Load defined volumes from a docker-compose.yml file - * + * * @param composeId ID of the compose service */ export const loadDefinedVolumesInComposeFile = async (composeId: string) => { @@ -207,18 +207,20 @@ export const loadDefinedVolumesInComposeFile = async (composeId: string) => { } else { composeData = await loadDockerCompose(compose); } - + if (!composeData) { return {}; } - + return extractVolumesFromComposeData(composeData); } // Validate that we have the necessary provider configuration const hasValidProvider = validateComposeProvider(compose); if (!hasValidProvider) { - console.warn(`No valid provider configuration for compose ${composeId}, returning empty volumes`); + console.warn( + `No valid provider configuration for compose ${composeId}, returning empty volumes`, + ); return {}; } @@ -257,11 +259,21 @@ const validateComposeProvider = (compose: any): boolean => { case "github": return !!compose.repository && !!compose.owner && !!compose.githubId; case "gitlab": - return !!compose.gitlabRepository && !!compose.gitlabOwner && !!compose.gitlabId; + return ( + !!compose.gitlabRepository && + !!compose.gitlabOwner && + !!compose.gitlabId + ); case "bitbucket": - return !!compose.bitbucketRepository && !!compose.bitbucketOwner && !!compose.bitbucketId; + return ( + !!compose.bitbucketRepository && + !!compose.bitbucketOwner && + !!compose.bitbucketId + ); case "gitea": - return !!compose.giteaRepository && !!compose.giteaOwner && !!compose.giteaId; + return ( + !!compose.giteaRepository && !!compose.giteaOwner && !!compose.giteaId + ); case "git": return !!compose.customGitUrl; case "raw": @@ -366,9 +378,7 @@ const extractVolumesFromComposeData = (composeData: ComposeSpecification) => { } > = {}; - for (const [volumeName, volumeConfig] of Object.entries( - volumesDefinition, - )) { + for (const [volumeName, volumeConfig] of Object.entries(volumesDefinition)) { result[volumeName] = { config: volumeConfig, usage: volumeUsage[volumeName] || [],