Skip to content

Commit 4ec282b

Browse files
authored
Merge pull request #3648 from Dokploy/ulimits-at-0a401843
Ulimits at 0a40184
2 parents d420311 + c039e63 commit 4ec282b

22 files changed

Lines changed: 7591 additions & 7 deletions

File tree

apps/dokploy/__test__/drop/drop.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ const baseApp: ApplicationNested = {
147147
dockerContextPath: null,
148148
rollbackActive: false,
149149
stopGracePeriodSwarm: null,
150+
ulimitsSwarm: null,
150151
};
151152

152153
describe("unzipDrop using real zip files", () => {

apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type MockCreateServiceOptions = {
66
TaskTemplate?: {
77
ContainerSpec?: {
88
StopGracePeriod?: number;
9+
Ulimits?: Array<{ Name: string; Soft: number; Hard: number }>;
910
};
1011
};
1112
[key: string]: unknown;
@@ -57,6 +58,7 @@ const createApplication = (
5758
},
5859
replicas: 1,
5960
stopGracePeriodSwarm: 0n,
61+
ulimitsSwarm: null,
6062
serverId: "server-id",
6163
...overrides,
6264
}) as unknown as ApplicationNested;
@@ -110,4 +112,50 @@ describe("mechanizeDockerContainer", () => {
110112
"StopGracePeriod",
111113
);
112114
});
115+
116+
it("passes ulimits to ContainerSpec when ulimitsSwarm is defined", async () => {
117+
const ulimits = [
118+
{ Name: "nofile", Soft: 10000, Hard: 20000 },
119+
{ Name: "nproc", Soft: 4096, Hard: 8192 },
120+
];
121+
const application = createApplication({ ulimitsSwarm: ulimits });
122+
123+
await mechanizeDockerContainer(application);
124+
125+
expect(createServiceMock).toHaveBeenCalledTimes(1);
126+
const call = createServiceMock.mock.calls[0];
127+
if (!call) {
128+
throw new Error("createServiceMock should have been called once");
129+
}
130+
const [settings] = call;
131+
expect(settings.TaskTemplate?.ContainerSpec?.Ulimits).toEqual(ulimits);
132+
});
133+
134+
it("omits Ulimits when ulimitsSwarm is null", async () => {
135+
const application = createApplication({ ulimitsSwarm: null });
136+
137+
await mechanizeDockerContainer(application);
138+
139+
expect(createServiceMock).toHaveBeenCalledTimes(1);
140+
const call = createServiceMock.mock.calls[0];
141+
if (!call) {
142+
throw new Error("createServiceMock should have been called once");
143+
}
144+
const [settings] = call;
145+
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
146+
});
147+
148+
it("omits Ulimits when ulimitsSwarm is an empty array", async () => {
149+
const application = createApplication({ ulimitsSwarm: [] });
150+
151+
await mechanizeDockerContainer(application);
152+
153+
expect(createServiceMock).toHaveBeenCalledTimes(1);
154+
const call = createServiceMock.mock.calls[0];
155+
if (!call) {
156+
throw new Error("createServiceMock should have been called once");
157+
}
158+
const [settings] = call;
159+
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
160+
});
113161
});

apps/dokploy/__test__/traefik/traefik.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const baseApp: ApplicationNested = {
125125
username: null,
126126
dockerContextPath: null,
127127
stopGracePeriodSwarm: null,
128+
ulimitsSwarm: null,
128129
};
129130

130131
const baseDomain: Domain = {

apps/dokploy/components/dashboard/application/advanced/show-resources.tsx

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { zodResolver } from "@hookform/resolvers/zod";
2-
import { InfoIcon } from "lucide-react";
2+
import { InfoIcon, Plus, Trash2 } from "lucide-react";
33
import { useEffect } from "react";
4-
import { useForm } from "react-hook-form";
4+
import { useFieldArray, useForm } from "react-hook-form";
55
import { toast } from "sonner";
66
import { z } from "zod";
77
import { AlertBlock } from "@/components/shared/alert-block";
@@ -21,10 +21,18 @@ import {
2121
FormLabel,
2222
FormMessage,
2323
} from "@/components/ui/form";
24+
import { Input } from "@/components/ui/input";
2425
import {
2526
createConverter,
2627
NumberInputWithSteps,
2728
} from "@/components/ui/number-input";
29+
import {
30+
Select,
31+
SelectContent,
32+
SelectItem,
33+
SelectTrigger,
34+
SelectValue,
35+
} from "@/components/ui/select";
2836
import {
2937
Tooltip,
3038
TooltipContent,
@@ -50,13 +58,36 @@ const memoryConverter = createConverter(1024 * 1024, (mb) => {
5058
: `${formatNumber(mb)} MB`;
5159
});
5260

61+
const ulimitSchema = z.object({
62+
Name: z.string().min(1, "Name is required"),
63+
Soft: z.coerce.number().int().min(-1, "Must be >= -1"),
64+
Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
65+
});
66+
5367
const addResourcesSchema = z.object({
5468
memoryReservation: z.string().optional(),
5569
cpuLimit: z.string().optional(),
5670
memoryLimit: z.string().optional(),
5771
cpuReservation: z.string().optional(),
72+
ulimitsSwarm: z.array(ulimitSchema).optional(),
5873
});
5974

75+
const ULIMIT_PRESETS = [
76+
{ value: "nofile", label: "nofile (Open Files)" },
77+
{ value: "nproc", label: "nproc (Processes)" },
78+
{ value: "memlock", label: "memlock (Locked Memory)" },
79+
{ value: "stack", label: "stack (Stack Size)" },
80+
{ value: "core", label: "core (Core File Size)" },
81+
{ value: "cpu", label: "cpu (CPU Time)" },
82+
{ value: "data", label: "data (Data Segment)" },
83+
{ value: "fsize", label: "fsize (File Size)" },
84+
{ value: "locks", label: "locks (File Locks)" },
85+
{ value: "msgqueue", label: "msgqueue (Message Queues)" },
86+
{ value: "nice", label: "nice (Nice Priority)" },
87+
{ value: "rtprio", label: "rtprio (Real-time Priority)" },
88+
{ value: "sigpending", label: "sigpending (Pending Signals)" },
89+
];
90+
6091
export type ServiceType =
6192
| "postgres"
6293
| "mongo"
@@ -107,17 +138,24 @@ export const ShowResources = ({ id, type }: Props) => {
107138
cpuReservation: "",
108139
memoryLimit: "",
109140
memoryReservation: "",
141+
ulimitsSwarm: [],
110142
},
111143
resolver: zodResolver(addResourcesSchema),
112144
});
113145

146+
const { fields, append, remove } = useFieldArray({
147+
control: form.control,
148+
name: "ulimitsSwarm",
149+
});
150+
114151
useEffect(() => {
115152
if (data) {
116153
form.reset({
117154
cpuLimit: data?.cpuLimit || undefined,
118155
cpuReservation: data?.cpuReservation || undefined,
119156
memoryLimit: data?.memoryLimit || undefined,
120157
memoryReservation: data?.memoryReservation || undefined,
158+
ulimitsSwarm: data?.ulimitsSwarm || [],
121159
});
122160
}
123161
}, [data, form, form.reset]);
@@ -134,6 +172,10 @@ export const ShowResources = ({ id, type }: Props) => {
134172
cpuReservation: formData.cpuReservation || null,
135173
memoryLimit: formData.memoryLimit || null,
136174
memoryReservation: formData.memoryReservation || null,
175+
ulimitsSwarm:
176+
formData.ulimitsSwarm && formData.ulimitsSwarm.length > 0
177+
? formData.ulimitsSwarm
178+
: null,
137179
})
138180
.then(async () => {
139181
toast.success("Resources Updated");
@@ -325,6 +367,145 @@ export const ShowResources = ({ id, type }: Props) => {
325367
}}
326368
/>
327369
</div>
370+
371+
{/* Ulimits Section */}
372+
<div className="space-y-4">
373+
<div className="flex items-center justify-between">
374+
<div className="flex items-center gap-2">
375+
<FormLabel className="text-base">Ulimits</FormLabel>
376+
<TooltipProvider>
377+
<Tooltip delayDuration={0}>
378+
<TooltipTrigger>
379+
<InfoIcon className="h-4 w-4 text-muted-foreground" />
380+
</TooltipTrigger>
381+
<TooltipContent className="max-w-xs">
382+
<p>
383+
Set resource limits for the container. Each ulimit has
384+
a soft limit (warning threshold) and hard limit
385+
(maximum allowed). Use -1 for unlimited.
386+
</p>
387+
</TooltipContent>
388+
</Tooltip>
389+
</TooltipProvider>
390+
</div>
391+
<Button
392+
type="button"
393+
variant="outline"
394+
size="sm"
395+
onClick={() =>
396+
append({ Name: "nofile", Soft: 65535, Hard: 65535 })
397+
}
398+
>
399+
<Plus className="h-4 w-4 mr-1" />
400+
Add Ulimit
401+
</Button>
402+
</div>
403+
404+
{fields.length > 0 && (
405+
<div className="space-y-3">
406+
{fields.map((field, index) => (
407+
<div
408+
key={field.id}
409+
className="flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
410+
>
411+
<FormField
412+
control={form.control}
413+
name={`ulimitsSwarm.${index}.Name`}
414+
render={({ field }) => (
415+
<FormItem className="flex-1">
416+
<FormLabel className="text-xs">Type</FormLabel>
417+
<Select
418+
onValueChange={field.onChange}
419+
value={field.value}
420+
>
421+
<FormControl>
422+
<SelectTrigger>
423+
<SelectValue placeholder="Select ulimit" />
424+
</SelectTrigger>
425+
</FormControl>
426+
<SelectContent>
427+
{ULIMIT_PRESETS.map((preset) => (
428+
<SelectItem
429+
key={preset.value}
430+
value={preset.value}
431+
>
432+
{preset.label}
433+
</SelectItem>
434+
))}
435+
</SelectContent>
436+
</Select>
437+
<FormMessage />
438+
</FormItem>
439+
)}
440+
/>
441+
<FormField
442+
control={form.control}
443+
name={`ulimitsSwarm.${index}.Soft`}
444+
render={({ field }) => (
445+
<FormItem className="w-32">
446+
<FormLabel className="text-xs">
447+
Soft Limit
448+
</FormLabel>
449+
<FormControl>
450+
<Input
451+
type="number"
452+
min={-1}
453+
placeholder="65535"
454+
{...field}
455+
onChange={(e) =>
456+
field.onChange(Number(e.target.value))
457+
}
458+
/>
459+
</FormControl>
460+
<FormMessage />
461+
</FormItem>
462+
)}
463+
/>
464+
<FormField
465+
control={form.control}
466+
name={`ulimitsSwarm.${index}.Hard`}
467+
render={({ field }) => (
468+
<FormItem className="w-32">
469+
<FormLabel className="text-xs">
470+
Hard Limit
471+
</FormLabel>
472+
<FormControl>
473+
<Input
474+
type="number"
475+
min={-1}
476+
placeholder="65535"
477+
{...field}
478+
onChange={(e) =>
479+
field.onChange(Number(e.target.value))
480+
}
481+
/>
482+
</FormControl>
483+
<FormMessage />
484+
</FormItem>
485+
)}
486+
/>
487+
<Button
488+
type="button"
489+
variant="ghost"
490+
size="icon"
491+
className="mt-6 text-destructive hover:text-destructive"
492+
onClick={() => remove(index)}
493+
>
494+
<Trash2 className="h-4 w-4" />
495+
</Button>
496+
</div>
497+
))}
498+
</div>
499+
)}
500+
501+
{fields.length === 0 && (
502+
<p className="text-sm text-muted-foreground">
503+
No ulimits configured. Click &quot;Add Ulimit&quot; to set
504+
resource limits.
505+
</p>
506+
)}
507+
</div>
508+
328509
<div className="flex w-full justify-end">
329510
<Button isLoading={isLoading} type="submit">
330511
Save
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE "application" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
2+
ALTER TABLE "mariadb" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
3+
ALTER TABLE "mongo" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
4+
ALTER TABLE "mysql" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
5+
ALTER TABLE "postgres" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
6+
ALTER TABLE "redis" ADD COLUMN "ulimitsSwarm" json;

0 commit comments

Comments
 (0)