From 69ae29db9c0c8571ef281b1d8c7172e28e5e45aa Mon Sep 17 00:00:00 2001 From: tanaymishra Date: Tue, 14 Jul 2026 00:12:54 +0530 Subject: [PATCH] fix: preserve named volume access mode in compose randomize transform The isolated-deployment and randomize transform split each volume mount on ":" and kept only the name and path, dropping any access mode. A named read-only volume like `data:/var/lib/mysql:ro` became `data-:/var/lib/mysql`, silently turning a read-only mount read-write (and losing the SELinux :z and :Z relabel flags). Preserve any segments after the path and re-append them so the mode survives the suffixing. Add a regression test covering :ro on a named volume and :z on a subdirectory volume. Fixes #4818 --- .../compose/volume/volume-services.test.ts | 34 +++++++++++++++++++ .../server/src/utils/docker/compose/volume.ts | 9 +++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/compose/volume/volume-services.test.ts b/apps/dokploy/__test__/compose/volume/volume-services.test.ts index a42ab5fa94..6d9674ae96 100644 --- a/apps/dokploy/__test__/compose/volume/volume-services.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-services.test.ts @@ -42,6 +42,40 @@ test("Add suffix to volumes declared directly in services", () => { ); }); +const composeFileReadOnly = ` +version: "3.8" + +services: + db: + image: postgres:latest + volumes: + - db_data:/var/lib/postgresql/data:ro + - config/nginx:/etc/nginx/conf.d:z +`; + +test("Preserve access mode when adding suffix to named volumes", () => { + const composeData = parse(composeFileReadOnly) as ComposeSpecification; + + const suffix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addSuffixToVolumesInServices( + composeData.services, + suffix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData.services?.db?.volumes).toContain( + `db_data-${suffix}:/var/lib/postgresql/data:ro`, + ); + expect(actualComposeData.services?.db?.volumes).toContain( + `config-${suffix}/nginx:/etc/nginx/conf.d:z`, + ); +}); + const composeFileTypeVolume = ` version: "3.8" diff --git a/packages/server/src/utils/docker/compose/volume.ts b/packages/server/src/utils/docker/compose/volume.ts index be4c7b2069..042e887519 100644 --- a/packages/server/src/utils/docker/compose/volume.ts +++ b/packages/server/src/utils/docker/compose/volume.ts @@ -26,7 +26,7 @@ export const addSuffixToVolumesInServices = ( if (_.has(newServiceConfig, "volumes")) { newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => { if (_.isString(volume)) { - const [volumeName, path] = volume.split(":"); + const [volumeName, path, ...modeParts] = volume.split(":"); // skip bind mounts and variables (e.g. $PWD) if ( @@ -38,15 +38,18 @@ export const addSuffixToVolumesInServices = ( return volume; } + // Preserve the access mode (e.g. :ro, :z, :Z) if present + const mode = modeParts.length > 0 ? `:${modeParts.join(":")}` : ""; + // Handle volume paths with subdirectories const parts = volumeName.split("/"); if (parts.length > 1) { const baseName = parts[0]; const rest = parts.slice(1).join("/"); - return `${baseName}-${suffix}/${rest}:${path}`; + return `${baseName}-${suffix}/${rest}:${path}${mode}`; } - return `${volumeName}-${suffix}:${path}`; + return `${volumeName}-${suffix}:${path}${mode}`; } if (_.isObject(volume) && volume.type === "volume" && volume.source) { return {