mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-17 21:25:23 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6413fa54e6 | ||
|
|
1c9dcc0c9e | ||
|
|
fee802a57b | ||
|
|
af2b053caa | ||
|
|
42a4cc7fff | ||
|
|
2a7807c2b3 | ||
|
|
425b8ec3c2 | ||
|
|
96dff0c1bb | ||
|
|
f53e1a6543 | ||
|
|
9e2788e764 | ||
|
|
4884ee3352 | ||
|
|
3a17c9b9e8 |
311
apps/dokploy/__test__/env/environment.test.ts
vendored
311
apps/dokploy/__test__/env/environment.test.ts
vendored
@@ -1,4 +1,7 @@
|
||||
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||
import {
|
||||
prepareEnvironmentVariables,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const projectEnv = `
|
||||
@@ -332,4 +335,310 @@ IS_DEV=\${{environment.DEVELOPMENT}}
|
||||
"IS_DEV=0",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles environment variables with single quotes in values", () => {
|
||||
const envWithSingleQuotes = `
|
||||
ENV_VARIABLE='ENVITONME'NT'
|
||||
ANOTHER_VAR='value with 'quotes' inside'
|
||||
SIMPLE_VAR=no-quotes
|
||||
`;
|
||||
|
||||
const serviceWithSingleQuotes = `
|
||||
TEST_VAR=\${{environment.ENV_VARIABLE}}
|
||||
ANOTHER_TEST=\${{environment.ANOTHER_VAR}}
|
||||
SIMPLE=\${{environment.SIMPLE_VAR}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceWithSingleQuotes,
|
||||
"",
|
||||
envWithSingleQuotes,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"TEST_VAR=ENVITONME'NT",
|
||||
"ANOTHER_TEST=value with 'quotes' inside",
|
||||
"SIMPLE=no-quotes",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareEnvironmentVariablesForShell (shell escaping)", () => {
|
||||
it("escapes single quotes in environment variable values", () => {
|
||||
const serviceEnv = `
|
||||
ENV_VARIABLE='ENVITONME'NT'
|
||||
ANOTHER_VAR='value with 'quotes' inside'
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// shell-quote should wrap these in double quotes
|
||||
expect(resolved).toEqual([
|
||||
`"ENV_VARIABLE=ENVITONME'NT"`,
|
||||
`"ANOTHER_VAR=value with 'quotes' inside"`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("escapes double quotes in environment variable values", () => {
|
||||
const serviceEnv = `
|
||||
MESSAGE="Hello "World""
|
||||
QUOTED_PATH="/path/to/"file""
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// shell-quote wraps in single quotes when there are double quotes inside
|
||||
expect(resolved).toEqual([
|
||||
`'MESSAGE=Hello "World"'`,
|
||||
`'QUOTED_PATH=/path/to/"file"'`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("escapes dollar signs in environment variable values", () => {
|
||||
const serviceEnv = `
|
||||
PRICE=$100
|
||||
VARIABLE=$HOME/path
|
||||
TEMPLATE=Hello $USER
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// Dollar signs should be escaped to prevent variable expansion
|
||||
for (const env of resolved) {
|
||||
expect(env).toContain("$");
|
||||
}
|
||||
});
|
||||
|
||||
it("escapes backticks in environment variable values", () => {
|
||||
const serviceEnv = `
|
||||
COMMAND=\`echo "test"\`
|
||||
NESTED=value with \`backticks\` inside
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// Backticks are escaped/removed by dotenv parsing, but values should be safely quoted
|
||||
expect(resolved.length).toBe(2);
|
||||
expect(resolved[0]).toContain("COMMAND");
|
||||
expect(resolved[1]).toContain("NESTED");
|
||||
});
|
||||
|
||||
it("handles environment variables with spaces", () => {
|
||||
const serviceEnv = `
|
||||
FULL_NAME="John Doe"
|
||||
MESSAGE='Hello World'
|
||||
SENTENCE=This is a test
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// shell-quote uses single quotes for strings with spaces
|
||||
expect(resolved).toEqual([
|
||||
`'FULL_NAME=John Doe'`,
|
||||
`'MESSAGE=Hello World'`,
|
||||
`'SENTENCE=This is a test'`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles environment variables with backslashes", () => {
|
||||
const serviceEnv = `
|
||||
WINDOWS_PATH=C:\\Users\\Documents
|
||||
ESCAPED=value\\with\\backslashes
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// Backslashes should be properly escaped
|
||||
expect(resolved.length).toBe(2);
|
||||
for (const env of resolved) {
|
||||
expect(env).toContain("\\");
|
||||
}
|
||||
});
|
||||
|
||||
it("handles simple environment variables without special characters", () => {
|
||||
const serviceEnv = `
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
DEBUG=true
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// shell-quote escapes the = sign in some cases
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV\\=production",
|
||||
"PORT\\=3000",
|
||||
"DEBUG\\=true",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles environment variables with mixed special characters", () => {
|
||||
const serviceEnv = `
|
||||
COMPLEX='value with "double" and 'single' quotes'
|
||||
BASH_COMMAND=echo "$HOME" && echo 'test'
|
||||
WEIRD=\`echo "$VAR"\` with 'quotes' and "more"
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// All should be escaped, none should throw errors
|
||||
expect(resolved.length).toBe(3);
|
||||
// Verify each can be safely used in shell
|
||||
for (const env of resolved) {
|
||||
expect(typeof env).toBe("string");
|
||||
expect(env.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles environment variables with newlines", () => {
|
||||
const serviceEnv = `
|
||||
MULTILINE="line1
|
||||
line2
|
||||
line3"
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(1);
|
||||
expect(resolved[0]).toContain("MULTILINE");
|
||||
});
|
||||
|
||||
it("handles empty environment variable values", () => {
|
||||
const serviceEnv = `
|
||||
EMPTY=
|
||||
EMPTY_QUOTED=""
|
||||
EMPTY_SINGLE=''
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
// shell-quote escapes the = sign for empty values
|
||||
expect(resolved).toEqual([
|
||||
"EMPTY\\=",
|
||||
"EMPTY_QUOTED\\=",
|
||||
"EMPTY_SINGLE\\=",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles environment variables with equals signs in values", () => {
|
||||
const serviceEnv = `
|
||||
EQUATION=a=b+c
|
||||
CONNECTION_STRING=user=admin;password=test
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(2);
|
||||
expect(resolved[0]).toContain("EQUATION");
|
||||
expect(resolved[1]).toContain("CONNECTION_STRING");
|
||||
});
|
||||
|
||||
it("resolves and escapes environment variables together", () => {
|
||||
const projectEnv = `
|
||||
BASE_URL=https://example.com
|
||||
API_KEY='secret-key-with-quotes'
|
||||
`;
|
||||
|
||||
const environmentEnv = `
|
||||
ENV_NAME=production
|
||||
DB_PASS='pa$$word'
|
||||
`;
|
||||
|
||||
const serviceEnv = `
|
||||
FULL_URL=\${{project.BASE_URL}}/api
|
||||
AUTH_KEY=\${{project.API_KEY}}
|
||||
ENVIRONMENT=\${{environment.ENV_NAME}}
|
||||
DB_PASSWORD=\${{environment.DB_PASS}}
|
||||
CUSTOM='value with 'quotes' inside'
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(resolved.length).toBe(5);
|
||||
// All resolved values should be properly escaped
|
||||
for (const env of resolved) {
|
||||
expect(typeof env).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("handles environment variables with semicolons and ampersands", () => {
|
||||
const serviceEnv = `
|
||||
COMMAND=echo "test" && echo "test2"
|
||||
MULTIPLE=cmd1; cmd2; cmd3
|
||||
URL_WITH_PARAMS=https://example.com?a=1&b=2&c=3
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(3);
|
||||
// These should be safely escaped to prevent command injection
|
||||
for (const env of resolved) {
|
||||
expect(typeof env).toBe("string");
|
||||
expect(env.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles environment variables with pipes and redirects", () => {
|
||||
const serviceEnv = `
|
||||
PIPE_COMMAND=cat file | grep test
|
||||
REDIRECT=echo "test" > output.txt
|
||||
BOTH=cat input.txt | grep pattern > output.txt
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(3);
|
||||
// Pipes and redirects should be safely quoted
|
||||
expect(resolved[0]).toContain("PIPE_COMMAND");
|
||||
expect(resolved[1]).toContain("REDIRECT");
|
||||
expect(resolved[2]).toContain("BOTH");
|
||||
// At least one should contain a pipe
|
||||
const hasPipe = resolved.some((env) => env.includes("|"));
|
||||
expect(hasPipe).toBe(true);
|
||||
});
|
||||
|
||||
it("handles environment variables with parentheses and brackets", () => {
|
||||
const serviceEnv = `
|
||||
MATH=(a+b)*c
|
||||
ARRAY=[1,2,3]
|
||||
JSON={"key":"value"}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(3);
|
||||
expect(resolved[0]).toContain("(");
|
||||
expect(resolved[1]).toContain("[");
|
||||
expect(resolved[2]).toContain("{");
|
||||
});
|
||||
|
||||
it("handles very long environment variable values", () => {
|
||||
const longValue = "a".repeat(10000);
|
||||
const serviceEnv = `LONG_VAR=${longValue}`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(1);
|
||||
expect(resolved[0]).toContain("LONG_VAR");
|
||||
expect(resolved[0]?.length).toBeGreaterThan(10000);
|
||||
});
|
||||
|
||||
it("handles special unicode characters in environment variables", () => {
|
||||
const serviceEnv = `
|
||||
EMOJI=Hello 🌍 World 🚀
|
||||
CHINESE=你好世界
|
||||
SPECIAL=café résumé naïve
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||
|
||||
expect(resolved.length).toBe(3);
|
||||
expect(resolved[0]).toContain("🌍");
|
||||
expect(resolved[1]).toContain("你好");
|
||||
expect(resolved[2]).toContain("café");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Scissors } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "application" | "compose";
|
||||
}
|
||||
|
||||
export const KillBuild = ({ id, type }: Props) => {
|
||||
const { mutateAsync, isLoading } =
|
||||
type === "application"
|
||||
? api.application.killBuild.useMutation()
|
||||
: api.compose.killBuild.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" className="w-fit" isLoading={isLoading}>
|
||||
Kill Build
|
||||
<Scissors className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure to kill the build?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will kill the build process
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId: id || "",
|
||||
composeId: id || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Build killed successfully");
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||
import { CancelQueues } from "./cancel-queues";
|
||||
import { KillBuild } from "./kill-build";
|
||||
import { RefreshToken } from "./refresh-token";
|
||||
import { ShowDeployment } from "./show-deployment";
|
||||
|
||||
@@ -143,6 +144,9 @@ export const ShowDeployments = ({
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{(type === "application" || type === "compose") && (
|
||||
<KillBuild id={id} type={type} />
|
||||
)}
|
||||
{(type === "application" || type === "compose") && (
|
||||
<CancelQueues id={id} type={type} />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.7",
|
||||
"version": "v0.25.9",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -98,6 +98,7 @@
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "5.4.2",
|
||||
"shell-quote": "^1.8.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^0.2.1",
|
||||
@@ -157,6 +158,7 @@
|
||||
"zod-form-data": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
|
||||
@@ -58,7 +58,11 @@ import {
|
||||
applications,
|
||||
} from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
cleanQueuesByApplication,
|
||||
killDockerBuild,
|
||||
myQueue,
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { uploadFileSchema } from "@/utils/schema";
|
||||
|
||||
@@ -725,7 +729,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
}),
|
||||
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to kill this build",
|
||||
});
|
||||
}
|
||||
await killDockerBuild("application", application.serverId);
|
||||
}),
|
||||
readTraefikConfig: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -59,7 +59,11 @@ import {
|
||||
compose as composeTable,
|
||||
} from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
cleanQueuesByCompose,
|
||||
killDockerBuild,
|
||||
myQueue,
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
@@ -248,6 +252,21 @@ export const composeRouter = createTRPCRouter({
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(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 kill this build",
|
||||
});
|
||||
}
|
||||
await killDockerBuild("compose", compose.serverId);
|
||||
}),
|
||||
|
||||
loadServices: protectedProcedure
|
||||
.input(apiFetchServices)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { Queue } from "bullmq";
|
||||
import { redisConfig } from "./redis-connection";
|
||||
|
||||
@@ -41,4 +45,31 @@ export const cleanQueuesByCompose = async (composeId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const killDockerBuild = async (
|
||||
type: "application" | "compose",
|
||||
serverId: string | null,
|
||||
) => {
|
||||
try {
|
||||
if (type === "application") {
|
||||
const command = `pkill -2 -f "docker build"`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} else if (type === "compose") {
|
||||
const command = `pkill -2 -f "docker compose"`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { myQueue };
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"ssh2": "1.15.0",
|
||||
"toml": "3.0.0",
|
||||
@@ -93,6 +94,7 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ssh2": "1.15.1",
|
||||
"@types/ws": "8.5.10",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
|
||||
@@ -375,7 +375,7 @@ export const removeCompose = async (
|
||||
} else {
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker compose -p ${compose.appName} down ${
|
||||
cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
|
||||
deleteVolumes ? "--volumes" : ""
|
||||
} && rm -rf ${projectPath}`;
|
||||
|
||||
@@ -402,7 +402,7 @@ export const startCompose = async (composeId: string) => {
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const path =
|
||||
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
|
||||
const baseCommand = `env -i PATH="$PATH" docker compose -p ${compose.appName} -f ${path} up -d`;
|
||||
if (compose.composeType === "docker-compose") {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
@@ -437,14 +437,17 @@ export const stopCompose = async (composeId: string) => {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && env -i PATH="$PATH" docker compose -p ${
|
||||
compose.appName
|
||||
} stop`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
});
|
||||
await execAsync(
|
||||
`env -i PATH="$PATH" docker compose -p ${compose.appName} stop`,
|
||||
{
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { dirname, join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import boxen from "boxen";
|
||||
import { writeDomainsToComposeRemote } from "../docker/domain";
|
||||
import { quote } from "shell-quote";
|
||||
import { writeDomainsToCompose } from "../docker/domain";
|
||||
import {
|
||||
encodeBase64,
|
||||
getEnviromentVariablesObject,
|
||||
@@ -22,7 +23,7 @@ export const getBuildComposeCommand = async (compose: ComposeNested) => {
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const exportEnvCommand = getExportEnvCommand(compose);
|
||||
|
||||
const newCompose = await writeDomainsToComposeRemote(compose, domains);
|
||||
const newCompose = await writeDomainsToCompose(compose, domains);
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
@@ -54,7 +55,7 @@ Compose Type: ${composeType} ✅`;
|
||||
|
||||
${exportEnvCommand}
|
||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
||||
docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
env -i PATH="$PATH" docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||
|
||||
echo "Docker Compose Deployed: ✅";
|
||||
@@ -137,7 +138,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
compose.environment.project.env,
|
||||
);
|
||||
const exports = Object.entries(envVars)
|
||||
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
|
||||
.map(([key, value]) => `export ${key}=${quote([value])}`)
|
||||
.join("\n");
|
||||
|
||||
return exports ? `\n# Export environment variables\n${exports}\n` : "";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
getEnviromentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "@dokploy/server/utils/docker/utils";
|
||||
import { quote } from "shell-quote";
|
||||
import {
|
||||
getBuildAppDirectory,
|
||||
getDockerContextPath,
|
||||
@@ -40,14 +41,14 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
commandArgs.push("--no-cache");
|
||||
}
|
||||
|
||||
const args = prepareEnvironmentVariables(
|
||||
const args = prepareEnvironmentVariablesForShell(
|
||||
buildArgs,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", `'${arg}'`);
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
|
||||
const secrets = getEnviromentVariablesObject(
|
||||
@@ -57,7 +58,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
);
|
||||
|
||||
const joinedSecrets = Object.entries(secrets)
|
||||
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
.join(" ");
|
||||
|
||||
for (const key in secrets) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
@@ -6,7 +6,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
const envVariables = prepareEnvironmentVariablesForShell(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -26,7 +26,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { getStaticCommand } from "@dokploy/server/utils/builders/static";
|
||||
import { nanoid } from "nanoid";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
const envVariables = prepareEnvironmentVariablesForShell(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -23,7 +23,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (publishDirectory) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
@@ -6,7 +6,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
const envVariables = prepareEnvironmentVariablesForShell(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -26,7 +26,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { nanoid } from "nanoid";
|
||||
import { quote } from "shell-quote";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
@@ -18,7 +20,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
|
||||
export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
const envVariables = prepareEnvironmentVariablesForShell(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -35,7 +37,7 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
prepareArgs.push("--env", `'${env}'`);
|
||||
prepareArgs.push("--env", env);
|
||||
}
|
||||
|
||||
// Calculate secrets hash for layer invalidation
|
||||
@@ -63,12 +65,18 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
];
|
||||
|
||||
// Add secrets properly formatted
|
||||
// Use prepareEnvironmentVariables (without ForShell) to get raw values for parsing
|
||||
const rawEnvVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
const exportEnvs = [];
|
||||
for (const pair of envVariables) {
|
||||
for (const pair of rawEnvVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
exportEnvs.push(`export ${key}='${value}'`);
|
||||
exportEnvs.push(`export ${key}=${quote([value])}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export const readComposeFile = async (compose: Compose) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const writeDomainsToComposeRemote = async (
|
||||
export const writeDomainsToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
@@ -120,19 +120,16 @@ echo "❌ Error: Compose file not found";
|
||||
exit 1;
|
||||
`;
|
||||
}
|
||||
if (compose.serverId) {
|
||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||
const encodedContent = encodeBase64(composeString);
|
||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||
}
|
||||
|
||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||
const encodedContent = encodeBase64(composeString);
|
||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
return `echo "❌ Has occured an error: ${error?.message || error}";
|
||||
return `echo "❌ Has occurred an error: ${error?.message || error}";
|
||||
exit 1;
|
||||
`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
export const addDomainToCompose = async (
|
||||
compose: Compose,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { docker, paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import { parse } from "dotenv";
|
||||
import { quote } from "shell-quote";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import type { MariadbNested } from "../databases/mariadb";
|
||||
import type { MongoNested } from "../databases/mongo";
|
||||
@@ -310,6 +311,21 @@ export const prepareEnvironmentVariables = (
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const prepareEnvironmentVariablesForShell = (
|
||||
serviceEnv: string | null,
|
||||
projectEnv?: string | null,
|
||||
environmentEnv?: string | null,
|
||||
): string[] => {
|
||||
const envVars = prepareEnvironmentVariables(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
// Using shell-quote library to properly escape shell arguments
|
||||
// This is the standard way to handle special characters in shell commands
|
||||
return envVars.map((env) => quote([env]));
|
||||
};
|
||||
|
||||
export const parseEnvironmentKeyValuePair = (
|
||||
pair: string,
|
||||
): [string, string] => {
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -406,6 +406,9 @@ importers:
|
||||
rotating-file-stream:
|
||||
specifier: 3.2.3
|
||||
version: 3.2.3
|
||||
shell-quote:
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.2
|
||||
slugify:
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
@@ -488,6 +491,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
'@types/shell-quote':
|
||||
specifier: ^1.7.5
|
||||
version: 1.7.5
|
||||
'@types/ssh2':
|
||||
specifier: 1.15.1
|
||||
version: 1.15.1
|
||||
@@ -726,6 +732,9 @@ importers:
|
||||
rotating-file-stream:
|
||||
specifier: 3.2.3
|
||||
version: 3.2.3
|
||||
shell-quote:
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.2
|
||||
slugify:
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
@@ -778,6 +787,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
'@types/shell-quote':
|
||||
specifier: ^1.7.5
|
||||
version: 1.7.5
|
||||
'@types/ssh2':
|
||||
specifier: 1.15.1
|
||||
version: 1.15.1
|
||||
@@ -4033,6 +4045,9 @@ packages:
|
||||
'@types/readable-stream@4.0.20':
|
||||
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
|
||||
|
||||
'@types/shell-quote@1.7.5':
|
||||
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
|
||||
|
||||
'@types/shimmer@1.2.0':
|
||||
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
||||
|
||||
@@ -11383,6 +11398,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.17.51
|
||||
|
||||
'@types/shell-quote@1.7.5': {}
|
||||
|
||||
'@types/shimmer@1.2.0': {}
|
||||
|
||||
'@types/ssh2@1.15.1':
|
||||
|
||||
Reference in New Issue
Block a user