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

View File

@@ -33,6 +33,7 @@ const baseApp: ApplicationNested = {
buildServerId: "",
buildRegistryId: "",
buildRegistry: null,
args: [],
giteaBuildPath: "",
previewRequireCollaboratorPermissions: false,
giteaId: "",

View File

@@ -16,6 +16,7 @@ const baseApp: ApplicationNested = {
buildRegistry: null,
giteaBuildPath: "",
giteaId: "",
args: [],
cleanCache: false,
applicationStatus: "done",
endpointSpecSwarm: null,

View File

@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button";
@@ -28,6 +29,13 @@ interface Props {
const AddRedirectSchema = z.object({
command: z.string(),
args: z
.array(
z.object({
value: z.string().min(1, "Argument cannot be empty"),
}),
)
.optional(),
});
type AddCommand = z.infer<typeof AddRedirectSchema>;
@@ -47,22 +55,36 @@ export const AddCommand = ({ applicationId }: Props) => {
const form = useForm<AddCommand>({
defaultValues: {
command: "",
args: [],
},
resolver: zodResolver(AddRedirectSchema),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "args",
});
useEffect(() => {
if (data?.command) {
form.reset({
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) => {
await mutateAsync({
applicationId,
command: data?.command,
args: data?.args?.map((arg) => arg.value).filter(Boolean),
})
.then(async () => {
toast.success("Command Updated");
@@ -100,13 +122,65 @@ export const AddCommand = ({ applicationId }: Props) => {
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Input placeholder="Custom command" {...field} />
<Input placeholder="/bin/sh" {...field} />
</FormControl>
<FormMessage />
</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 className="flex justify-end">
<Button isLoading={isLoading} type="submit" className="w-fit">

View File

@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button";
@@ -20,6 +21,13 @@ import type { ServiceType } from "../../application/advanced/show-resources";
const addDockerImage = z.object({
dockerImage: z.string().min(1, "Docker image is required"),
command: z.string(),
args: z
.array(
z.object({
value: z.string().min(1, "Argument cannot be empty"),
}),
)
.optional(),
});
interface Props {
@@ -61,15 +69,22 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
defaultValues: {
dockerImage: "",
command: "",
args: [],
},
resolver: zodResolver(addDockerImage),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "args",
});
useEffect(() => {
if (data) {
form.reset({
dockerImage: data.dockerImage,
command: data.command || "",
args: data.args?.map((arg) => ({ value: arg })) || [],
});
}
}, [data, form, form.reset]);
@@ -83,6 +98,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
mariadbId: id || "",
dockerImage: formData?.dockerImage,
command: formData?.command,
args: formData?.args?.map((arg) => arg.value).filter(Boolean),
})
.then(async () => {
toast.success("Custom Command Updated");
@@ -128,13 +144,68 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Input placeholder="Custom command" {...field} />
<Input placeholder="/bin/sh" {...field} />
</FormControl>
<FormMessage />
</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">
<Button isLoading={form.formState.isSubmitting} type="submit">
Save

View File

@@ -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

View File

@@ -862,6 +862,13 @@
"when": 1764479387555,
"tag": "0122_absent_frightful_four",
"breakpoints": true
},
{
"idx": 123,
"version": "7",
"when": 1764525308939,
"tag": "0123_cloudy_piledriver",
"breakpoints": true
}
]
}

View File

@@ -111,6 +111,7 @@ export const applications = pgTable("application", {
enabled: boolean("enabled"),
subtitle: text("subtitle"),
command: text("command"),
args: text("args").array(),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceType("sourceType").notNull().default("github"),
cleanCache: boolean("cleanCache").default(false),
@@ -293,6 +294,7 @@ const createSchema = createInsertSchema(applications, {
username: z.string().optional(),
isPreviewDeploymentsActive: z.boolean().optional(),
password: z.string().optional(),
args: z.array(z.string()).optional(),
registryUrl: z.string().optional(),
customGitSSHKeyId: z.string().optional(),
repository: z.string().optional(),

View File

@@ -45,6 +45,7 @@ export const mariadb = pgTable("mariadb", {
databaseRootPassword: text("rootPassword").notNull(),
dockerImage: text("dockerImage").notNull(),
command: text("command"),
args: text("args").array(),
env: text("env"),
// RESOURCES
memoryReservation: text("memoryReservation"),
@@ -114,6 +115,7 @@ const createSchema = createInsertSchema(mariadb, {
.optional(),
dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),

View File

@@ -50,6 +50,7 @@ export const mongo = pgTable("mongo", {
databasePassword: text("databasePassword").notNull(),
dockerImage: text("dockerImage").notNull(),
command: text("command"),
args: text("args").array(),
env: text("env"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
@@ -110,6 +111,7 @@ const createSchema = createInsertSchema(mongo, {
databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),

View File

@@ -45,6 +45,7 @@ export const mysql = pgTable("mysql", {
databaseRootPassword: text("rootPassword").notNull(),
dockerImage: text("dockerImage").notNull(),
command: text("command"),
args: text("args").array(),
env: text("env"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
@@ -112,6 +113,7 @@ const createSchema = createInsertSchema(mysql, {
.optional(),
dockerImage: z.string().default("mysql:8"),
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),

View File

@@ -44,6 +44,7 @@ export const postgres = pgTable("postgres", {
description: text("description"),
dockerImage: text("dockerImage").notNull(),
command: text("command"),
args: text("args").array(),
env: text("env"),
memoryReservation: text("memoryReservation"),
externalPort: integer("externalPort"),
@@ -103,6 +104,7 @@ const createSchema = createInsertSchema(postgres, {
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"),
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),

View File

@@ -41,6 +41,7 @@ export const redis = pgTable("redis", {
databasePassword: text("password").notNull(),
dockerImage: text("dockerImage").notNull(),
command: text("command"),
args: text("args").array(),
env: text("env"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
@@ -93,6 +94,7 @@ const createSchema = createInsertSchema(redis, {
databasePassword: z.string(),
dockerImage: z.string().default("redis:8"),
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),

View File

@@ -80,6 +80,7 @@ export const mechanizeDockerContainer = async (
memoryReservation,
cpuReservation,
command,
args,
ports,
} = application;
@@ -128,12 +129,14 @@ export const mechanizeDockerContainer = async (
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,

View File

@@ -29,6 +29,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
cpuLimit,
cpuReservation,
command,
args,
mounts,
} = mariadb;
@@ -75,12 +76,14 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,

View File

@@ -28,6 +28,7 @@ export const buildMongo = async (mongo: MongoNested) => {
databaseUser,
databasePassword,
command,
args,
mounts,
replicaSets,
} = mongo;
@@ -128,12 +129,17 @@ ${command ?? "wait $MONGOD_PID"}`;
Command: ["/bin/bash"],
Args: ["-c", startupScript],
}
: {
...(command && {
Command: ["/bin/bash"],
Args: ["-c", command],
}),
}),
: {}),
...(command &&
!replicaSets && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 &&
!replicaSets && {
Args: args,
}),
Labels,
},
Networks,

View File

@@ -30,6 +30,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
cpuLimit,
cpuReservation,
command,
args,
mounts,
} = mysql;
@@ -81,12 +82,14 @@ export const buildMysql = async (mysql: MysqlNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,

View File

@@ -28,6 +28,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
databaseUser,
databasePassword,
command,
args,
mounts,
} = postgres;
@@ -74,12 +75,14 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,

View File

@@ -26,6 +26,7 @@ export const buildRedis = async (redis: RedisNested) => {
cpuLimit,
cpuReservation,
command,
args,
mounts,
} = redis;
@@ -72,11 +73,20 @@ export const buildRedis = async (redis: RedisNested) => {
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
Command: ["/bin/sh"],
Args: [
"-c",
command ? command : `redis-server --requirepass ${databasePassword}`,
],
...(command || args
? {
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
}
: {
Command: ["/bin/sh"],
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
}),
Labels,
},
Networks,