Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7b3d8b00ec Initial plan 2026-02-09 16:17:16 +00:00
Mauricio Siu
f5fa39b97e refactor(dokploy): restrict license key access to owners only and enhance validation
- Updated the license key settings to ensure only users with the "owner" role can access certain functionalities.
- Modified the license key activation input validation to require a non-empty string.
- Improved error handling for network issues when validating license keys, providing clearer feedback to users.
- Adjusted the dashboard settings to redirect non-owner users appropriately.
2026-02-09 01:15:35 -06:00
Mauricio Siu
0a3a90c4e9 Merge pull request #3541 from gentslava/feat/docker-compose-pull
Update docker-compose command to always pull images (reopened)
2026-02-09 00:18:14 -06:00
Mauricio Siu
f440df343a Merge pull request #3593 from fernandogiacomino/canary
Replace logo.svg with updated SVG design
2026-02-09 00:14:26 -06:00
Mauricio Siu
4ec282b2f3 Merge pull request #3648 from Dokploy/ulimits-at-0a401843
Ulimits at 0a401843
2026-02-08 23:40:40 -06:00
Mauricio Siu
c039e638a6 refactor(dokploy): reorganize imports and simplify ulimitsSwarm assignment
- Moved the Tooltip imports to a more appropriate location for better readability.
- Simplified the assignment of ulimitsSwarm to ensure it directly accesses the data property.
2026-02-08 23:36:20 -06:00
Mauricio Siu
65ffc63da4 feat(dokploy): add ulimitsSwarm column to multiple database tables and update journal
- Introduced a new column "ulimitsSwarm" of type json to the "application", "mariadb", "mongo", "mysql", "postgres", and "redis" tables.
- Added a corresponding entry in the journal for version 7 to track this migration.
2026-02-08 23:31:22 -06:00
Mauricio Siu
5ba120567f Merge branch 'canary' into ulimits-at-0a401843 2026-02-08 23:30:14 -06:00
Mauricio Siu
8a335789b3 chore: remove deprecated SQL migration and associated journal entry for ulimits configuration 2026-02-08 23:30:05 -06:00
Fernando Giacomino
de2579401c Replace logo.svg with updated SVG design
Updated logo.svg with polished stroke and centered space for better fit on apps.
2026-02-04 11:10:13 -03:00
Vyacheslav Scherbinin
6c90075a64 feat(compose): update docker-compose command to always pull images 2026-01-28 13:35:29 +07:00
nurikk
0a401843f8 core: add ulimits configuration for Docker Swarm deployments
Users deploying to Docker Swarm can now configure resource ulimits
(nofile, nproc, etc.) to prevent applications from hitting system
limits that cause crashes or degraded performance.
2026-01-08 21:57:58 +00:00
29 changed files with 7657 additions and 15 deletions

View File

@@ -147,6 +147,7 @@ const baseApp: ApplicationNested = {
dockerContextPath: null,
rollbackActive: false,
stopGracePeriodSwarm: null,
ulimitsSwarm: null,
};
describe("unzipDrop using real zip files", () => {

View File

@@ -6,6 +6,7 @@ type MockCreateServiceOptions = {
TaskTemplate?: {
ContainerSpec?: {
StopGracePeriod?: number;
Ulimits?: Array<{ Name: string; Soft: number; Hard: number }>;
};
};
[key: string]: unknown;
@@ -57,6 +58,7 @@ const createApplication = (
},
replicas: 1,
stopGracePeriodSwarm: 0n,
ulimitsSwarm: null,
serverId: "server-id",
...overrides,
}) as unknown as ApplicationNested;
@@ -110,4 +112,50 @@ describe("mechanizeDockerContainer", () => {
"StopGracePeriod",
);
});
it("passes ulimits to ContainerSpec when ulimitsSwarm is defined", async () => {
const ulimits = [
{ Name: "nofile", Soft: 10000, Hard: 20000 },
{ Name: "nproc", Soft: 4096, Hard: 8192 },
];
const application = createApplication({ ulimitsSwarm: ulimits });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings.TaskTemplate?.ContainerSpec?.Ulimits).toEqual(ulimits);
});
it("omits Ulimits when ulimitsSwarm is null", async () => {
const application = createApplication({ ulimitsSwarm: null });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
});
it("omits Ulimits when ulimitsSwarm is an empty array", async () => {
const application = createApplication({ ulimitsSwarm: [] });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
});
});

View File

@@ -125,6 +125,7 @@ const baseApp: ApplicationNested = {
username: null,
dockerContextPath: null,
stopGracePeriodSwarm: null,
ulimitsSwarm: null,
};
const baseDomain: Domain = {

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { InfoIcon, 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 { AlertBlock } from "@/components/shared/alert-block";
@@ -21,10 +21,18 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
createConverter,
NumberInputWithSteps,
} from "@/components/ui/number-input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
@@ -50,13 +58,36 @@ const memoryConverter = createConverter(1024 * 1024, (mb) => {
: `${formatNumber(mb)} MB`;
});
const ulimitSchema = z.object({
Name: z.string().min(1, "Name is required"),
Soft: z.coerce.number().int().min(-1, "Must be >= -1"),
Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
});
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
ulimitsSwarm: z.array(ulimitSchema).optional(),
});
const ULIMIT_PRESETS = [
{ value: "nofile", label: "nofile (Open Files)" },
{ value: "nproc", label: "nproc (Processes)" },
{ value: "memlock", label: "memlock (Locked Memory)" },
{ value: "stack", label: "stack (Stack Size)" },
{ value: "core", label: "core (Core File Size)" },
{ value: "cpu", label: "cpu (CPU Time)" },
{ value: "data", label: "data (Data Segment)" },
{ value: "fsize", label: "fsize (File Size)" },
{ value: "locks", label: "locks (File Locks)" },
{ value: "msgqueue", label: "msgqueue (Message Queues)" },
{ value: "nice", label: "nice (Nice Priority)" },
{ value: "rtprio", label: "rtprio (Real-time Priority)" },
{ value: "sigpending", label: "sigpending (Pending Signals)" },
];
export type ServiceType =
| "postgres"
| "mongo"
@@ -107,10 +138,16 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: "",
memoryLimit: "",
memoryReservation: "",
ulimitsSwarm: [],
},
resolver: zodResolver(addResourcesSchema),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "ulimitsSwarm",
});
useEffect(() => {
if (data) {
form.reset({
@@ -118,6 +155,7 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
ulimitsSwarm: data?.ulimitsSwarm || [],
});
}
}, [data, form, form.reset]);
@@ -134,6 +172,10 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
memoryReservation: formData.memoryReservation || null,
ulimitsSwarm:
formData.ulimitsSwarm && formData.ulimitsSwarm.length > 0
? formData.ulimitsSwarm
: null,
})
.then(async () => {
toast.success("Resources Updated");
@@ -325,6 +367,145 @@ export const ShowResources = ({ id, type }: Props) => {
}}
/>
</div>
{/* Ulimits Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FormLabel className="text-base">Ulimits</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>
Set resource limits for the container. Each ulimit has
a soft limit (warning threshold) and hard limit
(maximum allowed). Use -1 for unlimited.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
append({ Name: "nofile", Soft: 65535, Hard: 65535 })
}
>
<Plus className="h-4 w-4 mr-1" />
Add Ulimit
</Button>
</div>
{fields.length > 0 && (
<div className="space-y-3">
{fields.map((field, index) => (
<div
key={field.id}
className="flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
>
<FormField
control={form.control}
name={`ulimitsSwarm.${index}.Name`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="text-xs">Type</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select ulimit" />
</SelectTrigger>
</FormControl>
<SelectContent>
{ULIMIT_PRESETS.map((preset) => (
<SelectItem
key={preset.value}
value={preset.value}
>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ulimitsSwarm.${index}.Soft`}
render={({ field }) => (
<FormItem className="w-32">
<FormLabel className="text-xs">
Soft Limit
</FormLabel>
<FormControl>
<Input
type="number"
min={-1}
placeholder="65535"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ulimitsSwarm.${index}.Hard`}
render={({ field }) => (
<FormItem className="w-32">
<FormLabel className="text-xs">
Hard Limit
</FormLabel>
<FormControl>
<Input
type="number"
min={-1}
placeholder="65535"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="mt-6 text-destructive hover:text-destructive"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{fields.length === 0 && (
<p className="text-sm text-muted-foreground">
No ulimits configured. Click &quot;Add Ulimit&quot; to set
resource limits.
</p>
)}
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save

View File

@@ -404,8 +404,7 @@ const MENU: Menu = {
url: "/dashboard/settings/license",
icon: Key,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.role === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,

View File

@@ -166,7 +166,12 @@ export function LicenseKeySettings() {
{!haveValidLicenseKey && (
<Button
variant="secondary"
disabled={isSaving || isValidating || isDeactivating}
disabled={
isSaving ||
isValidating ||
isDeactivating ||
!licenseKey.trim()
}
isLoading={isActivating}
onClick={async () => {
try {

View File

@@ -0,0 +1,6 @@
ALTER TABLE "application" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "ulimitsSwarm" json;

File diff suppressed because it is too large Load Diff

View File

@@ -995,6 +995,13 @@
"when": 1770490719123,
"tag": "0141_plain_earthquake",
"breakpoints": true
},
{
"idx": 142,
"version": "7",
"when": 1770615019498,
"tag": "0142_outstanding_tusk",
"breakpoints": true
}
]
}

View File

@@ -1,4 +1,4 @@
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
@@ -45,7 +45,7 @@ export async function getServerSideProps(
},
};
}
if (user.role === "member") {
if (user.role !== "owner") {
return {
redirect: {
permanent: true,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -12,7 +12,7 @@ import {
export const licenseKeyRouter = createTRPCRouter({
activate: adminProcedure
.input(z.object({ licenseKey: z.string() }))
.input(z.object({ licenseKey: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
try {
const currentUserId = ctx.user.id;
@@ -74,6 +74,13 @@ export const licenseKeyRouter = createTRPCRouter({
});
}
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to validate a license key",
});
}
if (!currentUser.licenseKey) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -164,6 +171,13 @@ export const licenseKeyRouter = createTRPCRouter({
});
}
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to get enterprise settings",
});
}
return {
enableEnterpriseFeatures: !!currentUser.enableEnterpriseFeatures,
licenseKey: currentUser.licenseKey ?? "",
@@ -200,6 +214,13 @@ export const licenseKeyRouter = createTRPCRouter({
});
}
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to update enterprise settings",
});
}
await db
.update(user)
.set({

View File

@@ -1,5 +1,18 @@
import { getPublicIpWithFallback, LICENSE_KEY_URL } from "@dokploy/server";
const LICENSE_SERVER_UNREACHABLE =
"Could not reach the license server. Check your connection or try again later.";
function isNetworkError(error: unknown): boolean {
if (error instanceof Error) {
if (error.message === "fetch failed") return true;
const cause = (error as Error & { cause?: { code?: string } }).cause;
const code = cause?.code;
return code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT";
}
return false;
}
export const validateLicenseKey = async (licenseKey: string) => {
try {
const ip = await getPublicIpWithFallback();
@@ -22,6 +35,9 @@ export const validateLicenseKey = async (licenseKey: string) => {
console.error(
error instanceof Error ? error.message : "Failed to validate license key",
);
if (isNetworkError(error)) {
throw new Error(LICENSE_SERVER_UNREACHABLE);
}
throw error;
}
};
@@ -48,6 +64,9 @@ export const activateLicenseKey = async (licenseKey: string) => {
console.error(
error instanceof Error ? error.message : "Failed to activate license key",
);
if (isNetworkError(error)) {
throw new Error(LICENSE_SERVER_UNREACHABLE);
}
throw error;
}
};
@@ -76,6 +95,9 @@ export const deactivateLicenseKey = async (licenseKey: string) => {
? error.message
: "Failed to deactivate license key",
);
if (isNetworkError(error)) {
throw new Error(LICENSE_SERVER_UNREACHABLE);
}
throw error;
}
};

View File

@@ -43,6 +43,8 @@ import {
type ServiceModeSwarm,
ServiceModeSwarmSchema,
triggerType,
type UlimitsSwarm,
UlimitsSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
@@ -172,6 +174,7 @@ export const applications = pgTable("application", {
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
//
replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus")
@@ -364,6 +367,7 @@ const createSchema = createInsertSchema(applications, {
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
});
export const apiCreateApplication = createSchema.pick({

View File

@@ -23,6 +23,8 @@ import {
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
UlimitsSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
@@ -67,6 +69,7 @@ export const mariadb = pgTable("mariadb", {
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -141,6 +144,7 @@ const createSchema = createInsertSchema(mariadb, {
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
});
export const apiCreateMariaDB = createSchema.pick({

View File

@@ -30,6 +30,8 @@ import {
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
UlimitsSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
@@ -70,6 +72,7 @@ export const mongo = pgTable("mongo", {
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -138,6 +141,7 @@ const createSchema = createInsertSchema(mongo, {
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
});
export const apiCreateMongo = createSchema.pick({

View File

@@ -23,6 +23,8 @@ import {
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
UlimitsSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
@@ -65,6 +67,7 @@ export const mysql = pgTable("mysql", {
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -138,6 +141,7 @@ const createSchema = createInsertSchema(mysql, {
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
});
export const apiCreateMySql = createSchema.pick({

View File

@@ -23,6 +23,8 @@ import {
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
UlimitsSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
@@ -65,6 +67,7 @@ export const postgres = pgTable("postgres", {
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -132,6 +135,7 @@ const createSchema = createInsertSchema(postgres, {
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
});
export const apiCreatePostgres = createSchema.pick({

View File

@@ -22,6 +22,8 @@ import {
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UlimitsSwarm,
UlimitsSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
@@ -64,6 +66,7 @@ export const redis = pgTable("redis", {
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
replicas: integer("replicas").default(1).notNull(),
environmentId: text("environmentId")
@@ -120,6 +123,7 @@ const createSchema = createInsertSchema(redis, {
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
});
export const apiCreateRedis = createSchema.pick({

View File

@@ -86,6 +86,14 @@ export interface EndpointSpecSwarm {
Ports?: EndpointPortConfigSwarm[] | undefined;
}
export interface UlimitSwarm {
Name: string;
Soft: number;
Hard: number;
}
export type UlimitsSwarm = UlimitSwarm[];
export const HealthCheckSwarmSchema = z
.object({
Test: z.array(z.string()).optional(),
@@ -189,3 +197,13 @@ export const EndpointSpecSwarmSchema = z
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
})
.strict();
export const UlimitSwarmSchema = z
.object({
Name: z.string().min(1),
Soft: z.number().int().min(-1),
Hard: z.number().int().min(-1),
})
.strict();
export const UlimitsSwarmSchema = z.array(UlimitSwarmSchema);

View File

@@ -220,6 +220,7 @@ const rollbackApplication = async (
RollbackConfig,
UpdateConfig,
Networks,
Ulimits,
} = generateConfigContainer(fullContext as ApplicationNested);
const bindsMount = generateBindMounts(mounts);
@@ -254,6 +255,7 @@ const rollbackApplication = async (
Args: ["-c", command],
}
: {}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -88,7 +88,7 @@ export const createCommand = (compose: ComposeNested) => {
let command = "";
if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
command = `compose -p ${appName} -f ${path} up -d --build --pull always --remove-orphans`;
} else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
}

View File

@@ -110,6 +110,7 @@ export const mechanizeDockerContainer = async (
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(application);
const bindsMount = generateBindMounts(mounts);
@@ -142,7 +143,7 @@ export const mechanizeDockerContainer = async (
args.length > 0 && {
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -48,6 +48,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(mariadb);
const resources = calculateResources({
memoryLimit,
@@ -83,7 +84,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
args.length > 0 && {
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -94,6 +94,7 @@ ${command ?? "wait $MONGOD_PID"}`;
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(mongo);
const resources = calculateResources({
@@ -139,7 +140,7 @@ ${command ?? "wait $MONGOD_PID"}`;
!replicaSets && {
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -54,6 +54,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(mysql);
const resources = calculateResources({
memoryLimit,
@@ -89,7 +90,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
args.length > 0 && {
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -47,6 +47,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(postgres);
const resources = calculateResources({
memoryLimit,
@@ -82,7 +83,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
args.length > 0 && {
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -45,6 +45,7 @@ export const buildRedis = async (redis: RedisNested) => {
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(redis);
const resources = calculateResources({
memoryLimit,
@@ -87,6 +88,7 @@ export const buildRedis = async (redis: RedisNested) => {
Command: ["/bin/sh"],
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,

View File

@@ -508,6 +508,7 @@ export const generateConfigContainer = (
networkSwarm,
stopGracePeriodSwarm,
endpointSpecSwarm,
ulimitsSwarm,
} = application;
const sanitizedStopGracePeriodSwarm =
@@ -584,6 +585,10 @@ export const generateConfigContainer = (
})) || [],
},
}),
...(ulimitsSwarm &&
ulimitsSwarm.length > 0 && {
Ulimits: ulimitsSwarm,
}),
};
};