Compare commits

...

24 Commits

Author SHA1 Message Date
Mauricio Siu
17da1d5b3c fix: Update LICENSE_KEY_URL for production environment
- Changed the production license key URL from "https://licenses.dokploy.com" to "https://licenses-api.dokploy.com" for improved API access.
2026-02-10 00:31:40 -06:00
Mauricio Siu
f7613d9375 Merge pull request #3664 from AlexDev404/fix/break-project-description-properly
fix: Update text breaking so that it breaks words properly
2026-02-10 00:14:36 -06:00
Mauricio Siu
a43ad106f2 Merge pull request #3665 from Dokploy/3652-fixrepository-loading
refactor(dokploy): improve repository selection UI for version contro…
2026-02-10 00:13:52 -06:00
autofix-ci[bot]
0e26c5023b [autofix.ci] apply automated fixes 2026-02-10 06:13:46 +00:00
autofix-ci[bot]
f4a4530481 [autofix.ci] apply automated fixes 2026-02-10 06:12:28 +00:00
Mauricio Siu
00dc3fae11 refactor(dokploy): improve repository selection UI for version control providers
- Updated repository selection logic across Bitbucket, Gitea, GitHub, and GitLab components to display a placeholder when no repository is selected.
- Enhanced loading state messages for better user experience, ensuring users are prompted to select an account before loading repositories.
- Cleaned up conditional rendering for loading states and account selection prompts in the UI.
2026-02-10 00:11:39 -06:00
Mauricio Siu
1da23f8888 Merge pull request #3650 from Dokploy/feat/add-linking-accounts-cloud-version
Feat/add linking accounts cloud version
2026-02-09 13:29:12 -06:00
Immanuel Daviel A. Garcia
bee4e4639c amend: Apply the proper fix 2026-02-09 13:10:50 -06:00
Immanuel Daviel A. Garcia
bd5b27ad51 fix: Update text breaking so that it breaks words properly 2026-02-09 12:48:28 -06:00
Mauricio Siu
b391abfd5c feat(dokploy): add product IDs for monthly and annual subscriptions in Stripe integration
- Introduced PRODUCT_MONTHLY_ID and PRODUCT_ANNUAL_ID constants to manage subscription product IDs.
- Updated the Stripe API call to fetch only the specified subscription products, enhancing performance and clarity in product management.
2026-02-09 02:42:15 -06:00
autofix-ci[bot]
21a6657e00 [autofix.ci] apply automated fixes 2026-02-09 08:33:23 +00:00
Mauricio Siu
d348ad5556 fix(dokploy): remove console logs from linking account component
- Eliminated unnecessary console log statements in the LinkingAccount component to clean up the code and improve performance.
- Ensured that the account listing functionality remains intact while enhancing code readability.
2026-02-09 02:21:37 -06:00
Mauricio Siu
5d8b7b9b99 feat(dokploy): implement linking account feature for social providers
- Added a new component for linking Google and GitHub accounts to user profiles.
- Integrated account linking functionality with the authentication client, allowing users to link and unlink their social accounts.
- Updated the profile settings page to conditionally display the linking account component based on cloud settings.
- Enhanced error handling and loading states for a better user experience.
2026-02-09 02:21:20 -06: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
44 changed files with 8030 additions and 89 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

@@ -245,13 +245,13 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -263,11 +263,15 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!bitbucketId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Bitbucket account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -258,14 +258,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -277,11 +277,15 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!giteaId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Gitea account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -233,13 +233,13 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -251,11 +251,15 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!githubId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a GitHub account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -254,13 +254,13 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -272,11 +272,15 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!gitlabId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a GitLab account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -247,13 +247,13 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -265,11 +265,15 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!bitbucketId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Bitbucket account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -244,13 +244,13 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
@@ -261,11 +261,15 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!giteaId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Gitea account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -234,13 +234,13 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -252,11 +252,15 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!githubId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a GitHub account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -256,13 +256,13 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -274,11 +274,15 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!gitlabId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a GitLab account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>

View File

@@ -430,7 +430,7 @@ export const ShowProjects = () => {
</DropdownMenu>
) : null}
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2">
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 ">
<div className="flex items-center gap-2">
<BookIcon className="size-4 text-muted-foreground" />
@@ -439,7 +439,7 @@ export const ShowProjects = () => {
</span>
</div>
<span className="text-sm font-medium text-muted-foreground break-all">
<span className="text-sm font-medium text-muted-foreground break-normal">
{project.description}
</span>

View File

@@ -0,0 +1,245 @@
"use client";
import { Link2, Loader2, Unlink } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
const LINKING_CALLBACK_URL = "/dashboard/settings/profile";
const TRUSTED_PROVIDERS = ["google", "github"] as const;
type SocialProvider = (typeof TRUSTED_PROVIDERS)[number];
type AccountItem = {
providerId: string;
accountId?: string;
};
function providerLabel(providerId: string): string {
return providerId.charAt(0).toUpperCase() + providerId.slice(1);
}
export function LinkingAccount() {
const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [accountsLoading, setAccountsLoading] = useState(true);
const [linkingProvider, setLinkingProvider] = useState<SocialProvider | null>(
null,
);
const [unlinkingProviderId, setUnlinkingProviderId] = useState<string | null>(
null,
);
const fetchAccounts = useCallback(async () => {
setAccountsLoading(true);
try {
const { data } = await authClient.listAccounts();
const list = Array.isArray(data)
? data
: ((data && typeof data === "object" && "accounts" in data
? (data as { accounts?: AccountItem[] }).accounts
: null) ?? []);
setAccounts(Array.isArray(list) ? list : []);
} catch {
setAccounts([]);
} finally {
setAccountsLoading(false);
}
}, []);
useEffect(() => {
fetchAccounts();
}, [fetchAccounts]);
const linkedProviderIds = new Set(accounts.map((a) => a.providerId));
const socialAccounts = accounts.filter((a) =>
TRUSTED_PROVIDERS.includes(a.providerId as SocialProvider),
);
const handleLinkSocial = async (provider: SocialProvider) => {
setLinkingProvider(provider);
try {
const { error } = await authClient.linkSocial({
provider,
callbackURL: LINKING_CALLBACK_URL,
});
if (error) {
toast.error(error.message ?? "Failed to link account");
setLinkingProvider(null);
return;
}
} catch (err) {
toast.error(
"Failed to link account",
err instanceof Error ? { description: err.message } : undefined,
);
setLinkingProvider(null);
}
};
const handleUnlink = async (providerId: string, accountId?: string) => {
setUnlinkingProviderId(providerId);
try {
const { error } = await authClient.unlinkAccount({
providerId,
...(accountId && { accountId }),
});
if (error) {
toast.error(error.message ?? "Failed to unlink account");
return;
}
toast.success("Account unlinked");
await fetchAccounts();
} catch (err) {
toast.error(
"Failed to unlink account",
err instanceof Error ? { description: err.message } : undefined,
);
} finally {
setUnlinkingProviderId(null);
}
};
const canUnlink = accounts.length > 1;
return (
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-6xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<div className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl flex flex-row gap-2">
<Link2 className="size-6 text-muted-foreground self-center" />
Linking account
</CardTitle>
<CardDescription>
Link your Google or GitHub account to sign in with them.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6 py-8 border-t">
{/* Linked accounts */}
<div className="space-y-2">
<p className="text-sm font-medium">Linked accounts</p>
{accountsLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground py-2">
<Loader2 className="size-4 animate-spin" />
Loading...
</div>
) : socialAccounts.length === 0 ? (
<p className="text-sm text-muted-foreground py-2">
No social accounts linked yet.
</p>
) : (
<ul className="space-y-2">
{socialAccounts.map((acc) => (
<li
key={acc.accountId ?? acc.providerId}
className="flex items-center justify-between rounded-lg border px-3 py-2 text-sm"
>
<span className="font-medium">
{providerLabel(acc.providerId)}
</span>
{canUnlink && (
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() =>
handleUnlink(acc.providerId, acc.accountId)
}
disabled={unlinkingProviderId === acc.providerId}
isLoading={unlinkingProviderId === acc.providerId}
>
{unlinkingProviderId === acc.providerId ? (
<Loader2 className="size-4 animate-spin" />
) : (
<>
<Unlink className="mr-1.5 size-4" />
Unlink
</>
)}
</Button>
)}
</li>
))}
</ul>
)}
</div>
<p className="text-sm text-muted-foreground">
Click a provider below to link it to your account. You will be
redirected to complete the flow.
</p>
<div className="flex flex-wrap gap-3">
{!linkedProviderIds.has("google") && (
<Button
variant="outline"
type="button"
className="min-w-[180px]"
onClick={() => handleLinkSocial("google")}
disabled={!!linkingProvider}
isLoading={linkingProvider === "google"}
>
{linkingProvider === "google" ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : (
<svg viewBox="0 0 24 24" className="mr-2 size-4">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
)}
Link with Google
</Button>
)}
{!linkedProviderIds.has("github") && (
<Button
variant="outline"
type="button"
className="min-w-[180px]"
onClick={() => handleLinkSocial("github")}
disabled={!!linkingProvider}
isLoading={linkingProvider === "github"}
>
{linkingProvider === "github" ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : (
<svg
viewBox="0 0 24 24"
className="mr-2 size-4"
fill="currentColor"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
)}
Link with GitHub
</Button>
)}
</div>
</CardContent>
</div>
</Card>
);
}

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,

View File

@@ -4,6 +4,7 @@ import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
import { LinkingAccount } from "@/components/dashboard/settings/linking-account/linking-account";
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
@@ -12,17 +13,16 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
const Page = () => {
const { data } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
// const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<ProfileForm />
{isCloud && <LinkingAccount />}
{(data?.canAccessToAPI ||
data?.role === "owner" ||
data?.role === "admin") && <ShowApiKeys />}
{/* {isCloud && <RemoveSelfAccount />} */}
</div>
</div>
);

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

@@ -7,7 +7,12 @@ import {
import { TRPCError } from "@trpc/server";
import Stripe from "stripe";
import { z } from "zod";
import { getStripeItems, WEBSITE_URL } from "@/server/utils/stripe";
import {
getStripeItems,
PRODUCT_ANNUAL_ID,
PRODUCT_MONTHLY_ID,
WEBSITE_URL,
} from "@/server/utils/stripe";
import { adminProcedure, createTRPCRouter } from "../trpc";
export const stripeRouter = createTRPCRouter({
@@ -22,6 +27,7 @@ export const stripeRouter = createTRPCRouter({
const products = await stripe.products.list({
expand: ["data.default_price"],
active: true,
ids: [PRODUCT_MONTHLY_ID, PRODUCT_ANNUAL_ID],
});
if (!stripeCustomerId) {

View File

@@ -1,5 +1,20 @@
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 +37,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 +66,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 +97,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

@@ -3,9 +3,12 @@ export const WEBSITE_URL =
? "http://localhost:3000"
: process.env.SITE_URL;
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID!; // $4.00
export const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID!; // $4.00
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID!; // $7.99
export const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID!; // $7.99
export const PRODUCT_MONTHLY_ID = process.env.PRODUCT_MONTHLY_ID!;
export const PRODUCT_ANNUAL_ID = process.env.PRODUCT_ANNUAL_ID!;
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
const items = [];

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

@@ -43,6 +43,17 @@ const { handler, api } = betterAuth({
},
}
: {}),
...(IS_CLOUD
? {
account: {
accountLinking: {
enabled: true,
trustedProviders: ["github", "google"],
allowDifferentEmails: true,
},
},
}
: {}),
appName: "Dokploy",
socialProviders: {
github: {

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

@@ -7,7 +7,7 @@ import { user as userSchema } from "../../db/schema/user";
export const LICENSE_KEY_URL =
process.env.NODE_ENV === "development"
? "http://localhost:4002"
: "https://licenses.dokploy.com";
: "https://licenses-api.dokploy.com";
export const initEnterpriseBackupCronJobs = async () => {
scheduleJob("enterprise-check", "0 0 */3 * *", async () => {

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,
}),
};
};