From 49b8214e5dc67672722d694f0c3371b638db6f11 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 9 Sep 2026 17:13:24 -0600 Subject: [PATCH] feat(compose): add pull latest images on deploy toggle --- .../compose/compose-pull-images.test.ts | 44 + .../compose/advanced/add-command.tsx | 39 +- .../drizzle/0196_worthless_ravenous.sql | 1 + apps/dokploy/drizzle/meta/0196_snapshot.json | 9170 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + packages/server/src/db/schema/compose.ts | 1 + packages/server/src/utils/builders/compose.ts | 3 +- 7 files changed, 9259 insertions(+), 6 deletions(-) create mode 100644 apps/dokploy/__test__/compose/compose-pull-images.test.ts create mode 100644 apps/dokploy/drizzle/0196_worthless_ravenous.sql create mode 100644 apps/dokploy/drizzle/meta/0196_snapshot.json diff --git a/apps/dokploy/__test__/compose/compose-pull-images.test.ts b/apps/dokploy/__test__/compose/compose-pull-images.test.ts new file mode 100644 index 000000000..4a4e75a65 --- /dev/null +++ b/apps/dokploy/__test__/compose/compose-pull-images.test.ts @@ -0,0 +1,44 @@ +import { createCommand } from "@dokploy/server/utils/builders/compose"; +import { describe, expect, it } from "vitest"; + +const base = { + composeType: "docker-compose" as const, + appName: "compose-app", + sourceType: "github" as const, + composePath: "./docker-compose.yml", + command: "", +}; + +describe("compose createCommand --pull always", () => { + it("adds --pull always when pullImages is enabled", () => { + const cmd = createCommand({ ...base, pullImages: true } as any); + + expect(cmd).toContain("up -d --build --remove-orphans --pull always"); + }); + + it("omits --pull when pullImages is disabled", () => { + const cmd = createCommand({ ...base, pullImages: false } as any); + + expect(cmd).not.toContain("--pull"); + }); + + it("does not add --pull to stack deploy", () => { + const cmd = createCommand({ + ...base, + composeType: "stack", + pullImages: true, + } as any); + + expect(cmd).not.toContain("--pull"); + }); + + it("leaves a custom command untouched", () => { + const cmd = createCommand({ + ...base, + command: "compose -p compose-app up -d", + pullImages: true, + } as any); + + expect(cmd).toBe("compose -p compose-app up -d"); + }); +}); diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx index c5f9334ec..2e5871560 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx @@ -22,6 +22,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; interface Props { @@ -30,6 +31,7 @@ interface Props { const AddRedirectSchema = z.object({ command: z.string(), + pullImages: z.boolean(), }); type AddCommand = z.infer; @@ -57,22 +59,25 @@ export const AddCommandCompose = ({ composeId }: Props) => { const form = useForm({ defaultValues: { command: "", + pullImages: false, }, resolver: zodResolver(AddRedirectSchema), }); useEffect(() => { - if (data?.command) { + if (data) { form.reset({ - command: data?.command || "", + command: data.command || "", + pullImages: data.pullImages ?? false, }); } - }, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]); + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); - const onSubmit = async (data: AddCommand) => { + const onSubmit = async (formData: AddCommand) => { await mutateAsync({ composeId, - command: data?.command, + command: formData.command, + pullImages: formData.pullImages, }) .then(async () => { toast.success("Command Updated"); @@ -109,6 +114,30 @@ export const AddCommandCompose = ({ composeId }: Props) => { docker.
+ {data?.composeType === "docker-compose" && ( + ( + +
+ Pull latest images on deploy + + Adds --pull always to the default + command so every deploy fetches the newest image for + each tag. Has no effect when a custom command is set. + +
+ + + +
+ )} + /> + )} { const envFileFlag = compose.createEnvFile ? `--env-file ${quote([join(dirname(compose.composePath || "docker-compose.yml"), ".env")])} ` : ""; - command = `compose -p ${quote([appName])} ${projectDirectoryFlag}${envFileFlag}-f ${quote([path])} up -d --build --remove-orphans`; + const pullFlag = compose.pullImages ? " --pull always" : ""; + command = `compose -p ${quote([appName])} ${projectDirectoryFlag}${envFileFlag}-f ${quote([path])} up -d --build --remove-orphans${pullFlag}`; } else if (composeType === "stack") { command = `stack deploy -c ${quote([path])} ${quote([appName])} --prune --with-registry-auth`; }