From 2d8dfee9f34dc731f53552a1ab82af86fc8a273f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 27 Aug 2026 00:41:17 -0600 Subject: [PATCH] fix(server): keep ${VAR} self-references interpolatable in .env files Only escape $ signs that are not part of a well-formed ${VAR} reference when writing .env files, so Docker Compose's own variable interpolation still works for legitimate ASSET_URL=${APP_URL}-style references, while literal dollar signs (e.g. in passwords) stay escaped. Fixes #5151 --- .../compose/env-file-literals.test.ts | 33 ++++++++++++ apps/dokploy/__test__/env/environment.test.ts | 51 +++++++++++++++++++ packages/server/src/utils/docker/utils.ts | 5 +- 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/__test__/compose/env-file-literals.test.ts b/apps/dokploy/__test__/compose/env-file-literals.test.ts index 2dbb7fa4e..6718c2de7 100644 --- a/apps/dokploy/__test__/compose/env-file-literals.test.ts +++ b/apps/dokploy/__test__/compose/env-file-literals.test.ts @@ -93,4 +93,37 @@ describe("getCreateEnvFileCommand", () => { expect(actual[key], key).toBe(value); } }, 60000); + + it("still interpolates a ${VAR} self-reference in the .env file", () => { + mkdirSync(codePath, { recursive: true }); + + const serviceEnv = [ + "APP_URL=https://example.com", + 'ASSET_URL="${APP_URL}"', + ].join("\n"); + + const command = getCreateEnvFileCommand({ + appName, + composePath: "docker-compose.yml", + env: serviceEnv, + randomize: false, + suffix: "", + serverId: null, + environment: { project: { env: "" }, env: "" }, + } as Parameters[0]); + + execFileSync("bash", ["-c", command]); + + const composeFile = + "services:\n test:\n image: busybox\n environment:\n - ASSET_URL=${ASSET_URL}\n"; + writeFileSync(join(codePath, "docker-compose.yml"), composeFile); + + const out = execFileSync( + "docker", + ["compose", "run", "--rm", "-T", "test", "sh", "-c", "printf '%s' \"$ASSET_URL\""], + { cwd: codePath, encoding: "utf8" }, + ); + + expect(out).toBe("https://example.com"); + }, 60000); }); diff --git a/apps/dokploy/__test__/env/environment.test.ts b/apps/dokploy/__test__/env/environment.test.ts index 24ef18b00..fe370a008 100644 --- a/apps/dokploy/__test__/env/environment.test.ts +++ b/apps/dokploy/__test__/env/environment.test.ts @@ -1,5 +1,6 @@ import { prepareEnvironmentVariables, + prepareEnvironmentVariablesForFile, prepareEnvironmentVariablesForShell, } from "@dokploy/server/index"; import { describe, expect, it } from "vitest"; @@ -642,3 +643,53 @@ SPECIAL=café résumé naïve expect(resolved[2]).toContain("café"); }); }); + +describe("prepareEnvironmentVariablesForFile (.env file escaping)", () => { + it("keeps a well-formed ${VAR} self-reference interpolatable", () => { + const serviceEnv = ` +APP_URL=https://example.com +ASSET_URL=\${APP_URL} +`; + + const resolved = prepareEnvironmentVariablesForFile(serviceEnv, "", ""); + + expect(resolved).toEqual([ + `APP_URL="https://example.com"`, + `ASSET_URL="\${APP_URL}"`, + ]); + }); + + it("keeps a ${VAR:-default} self-reference with a modifier interpolatable", () => { + const serviceEnv = `ASSET_URL=\${APP_URL:-https://default.com}`; + + const resolved = prepareEnvironmentVariablesForFile(serviceEnv, "", ""); + + expect(resolved).toEqual([`ASSET_URL="\${APP_URL:-https://default.com}"`]); + }); + + it("escapes a literal dollar sign followed by a word so it isn't interpolated", () => { + const serviceEnv = "PASSWORD=pa$word"; + + const resolved = prepareEnvironmentVariablesForFile(serviceEnv, "", ""); + + expect(resolved).toEqual([`PASSWORD="pa\\$word"`]); + }); + + it("escapes a trailing dollar sign", () => { + const serviceEnv = "PRICE=100$"; + + const resolved = prepareEnvironmentVariablesForFile(serviceEnv, "", ""); + + expect(resolved).toEqual([`PRICE="100\\$"`]); + }); + + it("still escapes double quotes and backslashes", () => { + const serviceEnv = String.raw`MESSAGE=say "hi" and \backslash`; + + const resolved = prepareEnvironmentVariablesForFile(serviceEnv, "", ""); + + expect(resolved).toEqual([ + String.raw`MESSAGE="say \"hi\" and \\backslash"`, + ]); + }); +}); diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index fccd979fd..136d31c2f 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -548,7 +548,10 @@ export const prepareEnvironmentVariablesForFile = ( const escapedValue = value .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') - .replace(/\$/g, "\\$"); + .replace( + /\$(?!\{[A-Za-z_][A-Za-z0-9_]*(?::?[-+?][^}]*)?\})/g, + "\\$", + ); return `${key}="${escapedValue}"`; }); };