Merge pull request #3143 from Dokploy/feat/add-args-to-advanced-command

feat: add support for command arguments in application and database s…
This commit is contained in:
Mauricio Siu
2025-11-30 12:24:19 -06:00
committed by GitHub
19 changed files with 7071 additions and 40 deletions
+1
View File
@@ -33,6 +33,7 @@ const baseApp: ApplicationNested = {
buildServerId: "", buildServerId: "",
buildRegistryId: "", buildRegistryId: "",
buildRegistry: null, buildRegistry: null,
args: [],
giteaBuildPath: "", giteaBuildPath: "",
previewRequireCollaboratorPermissions: false, previewRequireCollaboratorPermissions: false,
giteaId: "", giteaId: "",
@@ -16,6 +16,7 @@ const baseApp: ApplicationNested = {
buildRegistry: null, buildRegistry: null,
giteaBuildPath: "", giteaBuildPath: "",
giteaId: "", giteaId: "",
args: [],
cleanCache: false, cleanCache: false,
applicationStatus: "done", applicationStatus: "done",
endpointSpecSwarm: null, endpointSpecSwarm: null,
@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -28,6 +29,13 @@ interface Props {
const AddRedirectSchema = z.object({ const AddRedirectSchema = z.object({
command: z.string(), command: z.string(),
args: z
.array(
z.object({
value: z.string().min(1, "Argument cannot be empty"),
}),
)
.optional(),
}); });
type AddCommand = z.infer<typeof AddRedirectSchema>; type AddCommand = z.infer<typeof AddRedirectSchema>;
@@ -47,22 +55,36 @@ export const AddCommand = ({ applicationId }: Props) => {
const form = useForm<AddCommand>({ const form = useForm<AddCommand>({
defaultValues: { defaultValues: {
command: "", command: "",
args: [],
}, },
resolver: zodResolver(AddRedirectSchema), resolver: zodResolver(AddRedirectSchema),
}); });
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "args",
});
useEffect(() => { useEffect(() => {
if (data?.command) { if (data?.command) {
form.reset({ form.reset({
command: data?.command || "", command: data?.command || "",
args: data?.args?.map((arg) => ({ value: arg })) || [],
}); });
} }
}, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]); }, [
form,
form.reset,
form.formState.isSubmitSuccessful,
data?.command,
data?.args,
]);
const onSubmit = async (data: AddCommand) => { const onSubmit = async (data: AddCommand) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId,
command: data?.command, command: data?.command,
args: data?.args?.map((arg) => arg.value).filter(Boolean),
}) })
.then(async () => { .then(async () => {
toast.success("Command Updated"); toast.success("Command Updated");
@@ -100,13 +122,65 @@ export const AddCommand = ({ applicationId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Command</FormLabel> <FormLabel>Command</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Custom command" {...field} /> <Input placeholder="/bin/sh" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel>Arguments (Args)</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ value: "" })}
>
<Plus className="h-4 w-4 mr-1" />
Add Argument
</Button>
</div>
{fields.length === 0 && (
<p className="text-sm text-muted-foreground">
No arguments added yet. Click "Add Argument" to add one.
</p>
)}
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`args.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex gap-2">
<FormControl>
<Input
placeholder={
index === 0 ? "-c" : "echo Hello World"
}
{...field}
/>
</FormControl>
<Button
type="button"
variant="destructive"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button isLoading={isLoading} type="submit" className="w-fit"> <Button isLoading={isLoading} type="submit" className="w-fit">
@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -20,6 +21,13 @@ import type { ServiceType } from "../../application/advanced/show-resources";
const addDockerImage = z.object({ const addDockerImage = z.object({
dockerImage: z.string().min(1, "Docker image is required"), dockerImage: z.string().min(1, "Docker image is required"),
command: z.string(), command: z.string(),
args: z
.array(
z.object({
value: z.string().min(1, "Argument cannot be empty"),
}),
)
.optional(),
}); });
interface Props { interface Props {
@@ -61,15 +69,22 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
defaultValues: { defaultValues: {
dockerImage: "", dockerImage: "",
command: "", command: "",
args: [],
}, },
resolver: zodResolver(addDockerImage), resolver: zodResolver(addDockerImage),
}); });
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "args",
});
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
dockerImage: data.dockerImage, dockerImage: data.dockerImage,
command: data.command || "", command: data.command || "",
args: data.args?.map((arg) => ({ value: arg })) || [],
}); });
} }
}, [data, form, form.reset]); }, [data, form, form.reset]);
@@ -83,6 +98,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
mariadbId: id || "", mariadbId: id || "",
dockerImage: formData?.dockerImage, dockerImage: formData?.dockerImage,
command: formData?.command, command: formData?.command,
args: formData?.args?.map((arg) => arg.value).filter(Boolean),
}) })
.then(async () => { .then(async () => {
toast.success("Custom Command Updated"); toast.success("Custom Command Updated");
@@ -128,13 +144,68 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
<FormItem> <FormItem>
<FormLabel>Command</FormLabel> <FormLabel>Command</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Custom command" {...field} /> <Input placeholder="/bin/sh" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel>Arguments (Args)</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ value: "" })}
>
<Plus className="h-4 w-4 mr-1" />
Add Argument
</Button>
</div>
{fields.length === 0 && (
<p className="text-sm text-muted-foreground">
No arguments added yet. Click "Add Argument" to add one.
</p>
)}
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`args.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex gap-2">
<FormControl>
<Input
placeholder={
index === 0
? "-c"
: "redis-server --port 6379"
}
{...field}
/>
</FormControl>
<Button
type="button"
variant="destructive"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={form.formState.isSubmitting} type="submit"> <Button isLoading={form.formState.isSubmitting} type="submit">
Save Save
@@ -0,0 +1,6 @@
ALTER TABLE "application" ADD COLUMN "args" text[];--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "args" text[];--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "args" text[];--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "args" text[];--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "args" text[];--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "args" text[];
File diff suppressed because it is too large Load Diff
+7
View File
@@ -862,6 +862,13 @@
"when": 1764479387555, "when": 1764479387555,
"tag": "0122_absent_frightful_four", "tag": "0122_absent_frightful_four",
"breakpoints": true "breakpoints": true
},
{
"idx": 123,
"version": "7",
"when": 1764525308939,
"tag": "0123_cloudy_piledriver",
"breakpoints": true
} }
] ]
} }
@@ -111,6 +111,7 @@ export const applications = pgTable("application", {
enabled: boolean("enabled"), enabled: boolean("enabled"),
subtitle: text("subtitle"), subtitle: text("subtitle"),
command: text("command"), command: text("command"),
args: text("args").array(),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()), refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceType("sourceType").notNull().default("github"), sourceType: sourceType("sourceType").notNull().default("github"),
cleanCache: boolean("cleanCache").default(false), cleanCache: boolean("cleanCache").default(false),
@@ -293,6 +294,7 @@ const createSchema = createInsertSchema(applications, {
username: z.string().optional(), username: z.string().optional(),
isPreviewDeploymentsActive: z.boolean().optional(), isPreviewDeploymentsActive: z.boolean().optional(),
password: z.string().optional(), password: z.string().optional(),
args: z.array(z.string()).optional(),
registryUrl: z.string().optional(), registryUrl: z.string().optional(),
customGitSSHKeyId: z.string().optional(), customGitSSHKeyId: z.string().optional(),
repository: z.string().optional(), repository: z.string().optional(),
+2
View File
@@ -45,6 +45,7 @@ export const mariadb = pgTable("mariadb", {
databaseRootPassword: text("rootPassword").notNull(), databaseRootPassword: text("rootPassword").notNull(),
dockerImage: text("dockerImage").notNull(), dockerImage: text("dockerImage").notNull(),
command: text("command"), command: text("command"),
args: text("args").array(),
env: text("env"), env: text("env"),
// RESOURCES // RESOURCES
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
@@ -114,6 +115,7 @@ const createSchema = createInsertSchema(mariadb, {
.optional(), .optional(),
dockerImage: z.string().default("mariadb:6"), dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(), command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(), env: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(), memoryLimit: z.string().optional(),
+2
View File
@@ -50,6 +50,7 @@ export const mongo = pgTable("mongo", {
databasePassword: text("databasePassword").notNull(), databasePassword: text("databasePassword").notNull(),
dockerImage: text("dockerImage").notNull(), dockerImage: text("dockerImage").notNull(),
command: text("command"), command: text("command"),
args: text("args").array(),
env: text("env"), env: text("env"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"), memoryLimit: text("memoryLimit"),
@@ -110,6 +111,7 @@ const createSchema = createInsertSchema(mongo, {
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"), dockerImage: z.string().default("mongo:15"),
command: z.string().optional(), command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(), env: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(), memoryLimit: z.string().optional(),
+2
View File
@@ -45,6 +45,7 @@ export const mysql = pgTable("mysql", {
databaseRootPassword: text("rootPassword").notNull(), databaseRootPassword: text("rootPassword").notNull(),
dockerImage: text("dockerImage").notNull(), dockerImage: text("dockerImage").notNull(),
command: text("command"), command: text("command"),
args: text("args").array(),
env: text("env"), env: text("env"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"), memoryLimit: text("memoryLimit"),
@@ -112,6 +113,7 @@ const createSchema = createInsertSchema(mysql, {
.optional(), .optional(),
dockerImage: z.string().default("mysql:8"), dockerImage: z.string().default("mysql:8"),
command: z.string().optional(), command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(), env: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(), memoryLimit: z.string().optional(),
@@ -44,6 +44,7 @@ export const postgres = pgTable("postgres", {
description: text("description"), description: text("description"),
dockerImage: text("dockerImage").notNull(), dockerImage: text("dockerImage").notNull(),
command: text("command"), command: text("command"),
args: text("args").array(),
env: text("env"), env: text("env"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
externalPort: integer("externalPort"), externalPort: integer("externalPort"),
@@ -103,6 +104,7 @@ const createSchema = createInsertSchema(postgres, {
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"), dockerImage: z.string().default("postgres:15"),
command: z.string().optional(), command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(), env: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(), memoryLimit: z.string().optional(),
+2
View File
@@ -41,6 +41,7 @@ export const redis = pgTable("redis", {
databasePassword: text("password").notNull(), databasePassword: text("password").notNull(),
dockerImage: text("dockerImage").notNull(), dockerImage: text("dockerImage").notNull(),
command: text("command"), command: text("command"),
args: text("args").array(),
env: text("env"), env: text("env"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"), memoryLimit: text("memoryLimit"),
@@ -93,6 +94,7 @@ const createSchema = createInsertSchema(redis, {
databasePassword: z.string(), databasePassword: z.string(),
dockerImage: z.string().default("redis:8"), dockerImage: z.string().default("redis:8"),
command: z.string().optional(), command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(), env: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(), memoryLimit: z.string().optional(),
+9 -6
View File
@@ -80,6 +80,7 @@ export const mechanizeDockerContainer = async (
memoryReservation, memoryReservation,
cpuReservation, cpuReservation,
command, command,
args,
ports, ports,
} = application; } = application;
@@ -128,12 +129,14 @@ export const mechanizeDockerContainer = async (
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null && ...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }), StopGracePeriod !== undefined && { StopGracePeriod }),
...(command ...(command && {
? { Command: command.split(" "),
Command: ["/bin/sh"], }),
Args: ["-c", command], ...(args &&
} args.length > 0 && {
: {}), Args: args,
}),
Labels, Labels,
}, },
Networks, Networks,
@@ -29,6 +29,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
cpuLimit, cpuLimit,
cpuReservation, cpuReservation,
command, command,
args,
mounts, mounts,
} = mariadb; } = mariadb;
@@ -75,12 +76,14 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null && ...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }), StopGracePeriod !== undefined && { StopGracePeriod }),
...(command ...(command && {
? { Command: command.split(" "),
Command: ["/bin/sh"], }),
Args: ["-c", command], ...(args &&
} args.length > 0 && {
: {}), Args: args,
}),
Labels, Labels,
}, },
Networks, Networks,
+12 -6
View File
@@ -28,6 +28,7 @@ export const buildMongo = async (mongo: MongoNested) => {
databaseUser, databaseUser,
databasePassword, databasePassword,
command, command,
args,
mounts, mounts,
replicaSets, replicaSets,
} = mongo; } = mongo;
@@ -128,12 +129,17 @@ ${command ?? "wait $MONGOD_PID"}`;
Command: ["/bin/bash"], Command: ["/bin/bash"],
Args: ["-c", startupScript], Args: ["-c", startupScript],
} }
: { : {}),
...(command && { ...(command &&
Command: ["/bin/bash"], !replicaSets && {
Args: ["-c", command], Command: command.split(" "),
}), }),
}), ...(args &&
args.length > 0 &&
!replicaSets && {
Args: args,
}),
Labels, Labels,
}, },
Networks, Networks,
+9 -6
View File
@@ -30,6 +30,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
cpuLimit, cpuLimit,
cpuReservation, cpuReservation,
command, command,
args,
mounts, mounts,
} = mysql; } = mysql;
@@ -81,12 +82,14 @@ export const buildMysql = async (mysql: MysqlNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null && ...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }), StopGracePeriod !== undefined && { StopGracePeriod }),
...(command ...(command && {
? { Command: command.split(" "),
Command: ["/bin/sh"], }),
Args: ["-c", command], ...(args &&
} args.length > 0 && {
: {}), Args: args,
}),
Labels, Labels,
}, },
Networks, Networks,
@@ -28,6 +28,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
databaseUser, databaseUser,
databasePassword, databasePassword,
command, command,
args,
mounts, mounts,
} = postgres; } = postgres;
@@ -74,12 +75,14 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null && ...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }), StopGracePeriod !== undefined && { StopGracePeriod }),
...(command ...(command && {
? { Command: command.split(" "),
Command: ["/bin/sh"], }),
Args: ["-c", command], ...(args &&
} args.length > 0 && {
: {}), Args: args,
}),
Labels, Labels,
}, },
Networks, Networks,
+15 -5
View File
@@ -26,6 +26,7 @@ export const buildRedis = async (redis: RedisNested) => {
cpuLimit, cpuLimit,
cpuReservation, cpuReservation,
command, command,
args,
mounts, mounts,
} = redis; } = redis;
@@ -72,11 +73,20 @@ export const buildRedis = async (redis: RedisNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null && ...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }), StopGracePeriod !== undefined && { StopGracePeriod }),
Command: ["/bin/sh"], ...(command || args
Args: [ ? {
"-c", ...(command && {
command ? command : `redis-server --requirepass ${databasePassword}`, Command: command.split(" "),
], }),
...(args &&
args.length > 0 && {
Args: args,
}),
}
: {
Command: ["/bin/sh"],
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
}),
Labels, Labels,
}, },
Networks, Networks,