Compare commits

...

31 Commits

Author SHA1 Message Date
Mauricio Siu
9498fbeff3 Update package.json 2025-12-31 00:28:03 -06:00
Mauricio Siu
d2aa60ddf7 Update package.json 2025-12-30 23:53:30 -06:00
Mauricio Siu
58b75205af Merge pull request #3327 from Dokploy/refactor/separate-settings-from-users-table
refactor(settings): migrate user settings to webServerSettings schema…
2025-12-28 13:21:55 -06:00
Mauricio Siu
9e03625586 refactor(auth): simplify trustedOrigins logic by removing redundant admin check and using optional chaining for settings access 2025-12-28 13:18:20 -06:00
Mauricio Siu
260efdc2bb Merge pull request #3353 from bdkopen/remove-rotating-file-stream
chore: uninstall `rotating-file-stream`
2025-12-28 13:09:34 -06:00
bdkopen
1b5bfe051d chore: uninstall rotating-file-stream 2025-12-27 12:33:39 -05:00
Mauricio Siu
e4384075f2 Merge pull request #3341 from dpulpeiro/fix/stack-registry-auth
fix: pass registry auth to stack deploy
2025-12-25 03:29:33 -06:00
Mauricio Siu
b355d44605 fix(web-server-settings): use optional chaining for safer ID access in update function 2025-12-24 12:24:27 -06:00
Daniel García Pulpeiro
f39aa23803 fix: pass registry auth to stack deploy 2025-12-23 22:37:00 +01:00
Mauricio Siu
3abc4cdc3b refactor(access-log): consolidate web server settings imports and enhance log cleanup status retrieval 2025-12-21 01:46:27 -06:00
Mauricio Siu
ec56062f17 fix(settings): update getIp function to return an empty string for cloud environments 2025-12-21 01:45:49 -06:00
Mauricio Siu
10c4f882a5 Update packages/server/src/services/web-server-settings.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-21 01:44:46 -06:00
Mauricio Siu
f1dfa9c6a2 refactor(preview-deployment): remove dynamic import of getWebServerSettings and streamline IP retrieval logic 2025-12-21 01:43:09 -06:00
Mauricio Siu
6010643d9e refactor(server): update server configuration handling to utilize webServerSettings schema and improve code clarity 2025-12-21 01:41:33 -06:00
Mauricio Siu
1ccb205495 fix(admin): add optional chaining to safely access settings properties 2025-12-21 01:35:21 -06:00
autofix-ci[bot]
b2be5bc09f [autofix.ci] apply automated fixes 2025-12-21 07:33:59 +00:00
Mauricio Siu
babd30a110 refactor(settings): migrate user settings to webServerSettings schema and update related components 2025-12-21 01:33:18 -06:00
Mauricio Siu
e77f276785 refactor(issue-template): remove unnecessary dropdowns for git providers in bug report 2025-12-21 01:06:12 -06:00
Mauricio Siu
78c9a047b0 feat(issue-template): add dropdowns for affected areas and git providers in bug report 2025-12-21 01:05:33 -06:00
Mauricio Siu
84e0f5856b Merge pull request #3164 from difagume/fix/log-warning-detection
fix(docker-logs): fix warning symbol detection
2025-12-20 23:02:00 -06:00
Mauricio Siu
2bfa4643fc Merge pull request #3186 from divaltor/slider-resources
feat(resources): Add number component to have better UX control over Docker resources
2025-12-20 22:56:54 -06:00
Mauricio Siu
8c7bc82712 Merge pull request #3323 from Dokploy/copilot/fix-shell-command-issue
fix: quote registry username in docker login to prevent shell variable expansion
2025-12-20 21:24:57 -06:00
copilot-swe-agent[bot]
44645a6fbe fix: properly quote registry username in docker login to handle special characters like $
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 19:41:56 +00:00
copilot-swe-agent[bot]
771d0dd8ab Initial plan 2025-12-20 19:35:55 +00:00
Mauricio Siu
67725759e6 Merge pull request #3318 from Dokploy/copilot/fix-perplexity-ai-models-endpoint
Fix Perplexity AI provider models endpoint by returning hardcoded model list
2025-12-20 13:34:55 -06:00
Mauricio Siu
2065372d4f fix: update test command in package.json to remove specific test target 2025-12-20 13:34:32 -06:00
copilot-swe-agent[bot]
69d5c6f0cb Fix Perplexity AI provider by adding hardcoded model list
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:43:11 +00:00
copilot-swe-agent[bot]
53f67c6eb2 Initial plan 2025-12-20 06:38:09 +00:00
Mauricio Siu
7c53a3ef75 Merge pull request #3316 from Dokploy/feat/add-admin-creation-projects
fix: update project handling permissions to include admin role
2025-12-19 23:26:44 -06:00
Vlad Vladov
d465fb4da1 feat(resources): Add number component to have better UX control over Docker resources 2025-12-07 13:45:14 +02:00
diego fabricio
698104e7b7 fix(docker-logs): fix warning symbol detection
- Added support for detecting warning symbols (⚠, ⚠️) in log messages
2025-12-04 11:33:29 -05:00
44 changed files with 7725 additions and 358 deletions

View File

@@ -206,4 +206,38 @@ describe("getRegistryTag", () => {
expect(result).toBe("docker.io/myuser/repo");
});
});
describe("special characters in username", () => {
it("should handle Harbor robot account username with $ (e.g. robot$library+dokploy)", () => {
const registry = createMockRegistry({
username: "robot$library+dokploy",
});
const result = getRegistryTag(registry, "nginx");
expect(result).toBe("docker.io/robot$library+dokploy/nginx");
});
it("should handle username with $ and other special characters", () => {
const registry = createMockRegistry({
username: "robot$test+app",
});
const result = getRegistryTag(registry, "myapp:latest");
expect(result).toBe("docker.io/robot$test+app/myapp:latest");
});
it("should handle username with multiple $ symbols", () => {
const registry = createMockRegistry({
username: "user$name$test",
});
const result = getRegistryTag(registry, "app");
expect(result).toBe("docker.io/user$name$test/app");
});
it("should handle username with + and - symbols", () => {
const registry = createMockRegistry({
username: "robot+test-user",
});
const result = getRegistryTag(registry, "nginx:latest");
expect(result).toBe("docker.io/robot+test-user/nginx:latest");
});
});
});

View File

@@ -1,7 +1,7 @@
import type { Domain } from "@dokploy/server";
import { createDomainLabels } from "@dokploy/server";
import { parse, stringify } from "yaml";
import { describe, expect, it } from "vitest";
import { parse, stringify } from "yaml";
/**
* Regression tests for Traefik Host rule label format.

View File

@@ -5,21 +5,27 @@ vi.mock("node:fs", () => ({
default: fs,
}));
import type { FileConfig, User } from "@dokploy/server";
import type { FileConfig } from "@dokploy/server";
import {
createDefaultServerTraefikConfig,
loadOrCreateConfig,
updateServerTraefik,
} from "@dokploy/server";
import type { webServerSettings } from "@dokploy/server/db/schema";
import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = {
type WebServerSettings = typeof webServerSettings.$inferSelect;
const baseSettings: WebServerSettings = {
id: "",
https: false,
enablePaidFeatures: false,
allowImpersonation: false,
role: "user",
firstName: "",
lastName: "",
certificateType: "none",
host: null,
serverIp: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
metricsConfig: {
containers: {
refreshRate: 20,
@@ -45,29 +51,8 @@ const baseAdmin: User = {
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
createdAt: new Date(),
serverIp: null,
certificateType: "none",
host: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
banExpires: new Date(),
banned: true,
banReason: "",
email: "",
expirationDate: "",
id: "",
isRegistered: false,
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
createdAt: null,
updatedAt: new Date(),
twoFactorEnabled: false,
};
beforeEach(() => {
@@ -85,7 +70,7 @@ test("Should read the configuration file", () => {
test("Should apply redirect-to-https", () => {
updateServerTraefik(
{
...baseAdmin,
...baseSettings,
https: true,
certificateType: "letsencrypt",
},
@@ -100,7 +85,7 @@ test("Should apply redirect-to-https", () => {
});
test("Should change only host when no certificate", () => {
updateServerTraefik(baseAdmin, "example.com");
updateServerTraefik(baseSettings, "example.com");
const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -110,7 +95,7 @@ test("Should change only host when no certificate", () => {
test("Should not touch config without host", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(baseAdmin, null);
updateServerTraefik(baseSettings, null);
const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -119,11 +104,14 @@ test("Should not touch config without host", () => {
test("Should remove websecure if https rollback to http", () => {
updateServerTraefik(
{ ...baseAdmin, certificateType: "letsencrypt" },
{ ...baseSettings, certificateType: "letsencrypt" },
"example.com",
);
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
updateServerTraefik(
{ ...baseSettings, certificateType: "none" },
"example.com",
);
const config: FileConfig = loadOrCreateConfig("dokploy");

View File

@@ -21,7 +21,10 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
createConverter,
NumberInputWithSteps,
} from "@/components/ui/number-input";
import {
Tooltip,
TooltipContent,
@@ -30,6 +33,23 @@ import {
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
const CPU_STEP = 0.25;
const MEMORY_STEP_MB = 256;
const formatNumber = (value: number, decimals = 2): string =>
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
);
const memoryConverter = createConverter(1024 * 1024, (mb) => {
if (mb <= 0) return "";
return mb >= 1024
? `${formatNumber(mb / 1024)} GB`
: `${formatNumber(mb)} MB`;
});
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
@@ -51,6 +71,7 @@ interface Props {
}
type AddResources = z.infer<typeof addResourcesSchema>;
export const ShowResources = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
@@ -163,16 +184,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
1073741824 bytes. Use +/- buttons to adjust by
256 MB.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="1073741824 (1GB in bytes)"
{...field}
step={MEMORY_STEP_MB}
converter={memoryConverter}
/>
</FormControl>
<FormMessage />
@@ -198,16 +223,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
268435456 bytes. Use +/- buttons to adjust by 256
MB.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="268435456 (256MB in bytes)"
{...field}
step={MEMORY_STEP_MB}
converter={memoryConverter}
/>
</FormControl>
<FormMessage />
@@ -234,17 +263,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
CPUs = 2000000000. Use +/- buttons to adjust by
0.25 CPU.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
step={CPU_STEP}
converter={cpuConverter}
/>
</FormControl>
<FormMessage />
@@ -271,14 +303,21 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
1000000000. Use +/- buttons to adjust by 0.25
CPU.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input placeholder="1000000000 (1 CPU)" {...field} />
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="1000000000 (1 CPU)"
step={CPU_STEP}
converter={cpuConverter}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -108,7 +108,8 @@ export const getLogType = (message: string): LogStyle => {
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
/\b(?:unstable|experimental)\b/i.test(lowerMessage) ||
/⚠|⚠️/i.test(lowerMessage)
) {
return LOG_STYLES.warning;
}

View File

@@ -1,5 +1,6 @@
import { Activity } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -7,7 +8,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowStorageActions } from "./show-storage-actions";
import { ShowTraefikActions } from "./show-traefik-actions";

View File

@@ -7,9 +7,12 @@ interface Props {
serverId?: string;
}
export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.user.get.useQuery(undefined, {
enabled: !serverId,
});
const { data, refetch } = api.settings.getWebServerSettings.useQuery(
undefined,
{
enabled: !serverId,
},
);
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{
@@ -22,7 +25,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
const enabled = serverId
? server?.enableDockerCleanup
: data?.user.enableDockerCleanup;
: data?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
@@ -30,7 +33,10 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
try {
await mutateAsync({
enableDockerCleanup: checked,
serverId: serverId,
...(serverId && { serverId }),
} as {
enableDockerCleanup: boolean;
serverId?: string;
});
if (serverId) {
await refetchServer();

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Pencil } from "lucide-react";
import { Pencil, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";

View File

@@ -80,7 +80,7 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => {
const { data } = serverId
const { data: serverData } = serverId
? api.server.one.useQuery(
{
serverId: serverId || "",
@@ -89,7 +89,14 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId,
},
)
: api.user.getServerMetrics.useQuery();
: { data: null };
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery(undefined, {
enabled: !serverId,
});
const data = serverId ? serverData : webServerSettings;
const url = useUrl();

View File

@@ -1,17 +1,17 @@
import { format } from "date-fns";
import {
Clock,
Key,
KeyIcon,
Loader2,
MoreHorizontal,
ServerIcon,
Clock,
User,
Key,
Network,
Terminal,
Settings,
Pencil,
ServerIcon,
Settings,
Terminal,
Trash2,
User,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";

View File

@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data, refetch } = api.user.get.useQuery();
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
@@ -82,15 +82,15 @@ export const WebDomain = () => {
});
const https = form.watch("https");
const domain = form.watch("domain") || "";
const host = data?.user?.host || "";
const host = data?.host || "";
const hasChanged = domain !== host;
useEffect(() => {
if (data) {
form.reset({
domain: data?.user?.host || "",
certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
https: data?.user?.https || false,
domain: data?.host || "",
certificateType: data?.certificateType || "none",
letsEncryptEmail: data?.letsEncryptEmail || "",
https: data?.https || false,
});
}
}, [form, form.reset, data]);

View File

@@ -16,7 +16,8 @@ import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => {
const { t } = useTranslation("settings");
const { data } = api.user.get.useQuery();
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -53,7 +54,7 @@ export const WebServer = () => {
<div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground">
Server IP: {data?.user.serverIp}
Server IP: {webServerSettings?.serverIp}
</span>
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}

View File

@@ -46,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.user.get.useQuery();
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.user.update.useMutation();
api.settings.updateServerIp.useMutation();
const form = useForm<Schema>({
defaultValues: {
serverIp: data?.user.serverIp || "",
serverIp: data?.serverIp || "",
},
resolver: zodResolver(schema),
});
@@ -62,13 +62,11 @@ export const UpdateServerIp = ({ children }: Props) => {
useEffect(() => {
if (data) {
form.reset({
serverIp: data.user.serverIp || "",
serverIp: data.serverIp || "",
});
}
}, [form, form.reset, data]);
const utils = api.useUtils();
const setCurrentIp = () => {
if (!ip) return;
form.setValue("serverIp", ip);
@@ -80,7 +78,7 @@ export const UpdateServerIp = ({ children }: Props) => {
})
.then(async () => {
toast.success("Server IP Updated");
await utils.user.get.invalidate();
await refetch();
setIsOpen(false);
})
.catch(() => {

View File

@@ -1,13 +1,13 @@
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import { Fragment } from "react";
import { ChevronDown } from "lucide-react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
DropdownMenu,

View File

@@ -0,0 +1,84 @@
import { MinusIcon, PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export interface UnitConverter {
toValue: (raw: string | undefined) => number;
fromValue: (value: number) => string;
formatDisplay: (value: number) => string;
}
export const createConverter = (
multiplier: number,
formatDisplay: (value: number) => string,
): UnitConverter => ({
toValue: (raw) => {
if (!raw) return 0;
const value = Number.parseInt(raw, 10);
return Number.isNaN(value) ? 0 : value / multiplier;
},
fromValue: (value) =>
value <= 0 ? "" : String(Math.round(value * multiplier)),
formatDisplay,
});
interface NumberInputWithStepsProps {
value: string | undefined;
onChange: (value: string) => void;
placeholder: string;
step: number;
converter: UnitConverter;
}
export const NumberInputWithSteps = ({
value,
onChange,
placeholder,
step,
converter,
}: NumberInputWithStepsProps) => {
const numericValue = converter.toValue(value);
const displayValue = converter.formatDisplay(numericValue);
const handleIncrement = () =>
onChange(converter.fromValue(numericValue + step));
const handleDecrement = () =>
onChange(converter.fromValue(Math.max(0, numericValue - step)));
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleDecrement}
disabled={numericValue <= 0}
>
<MinusIcon className="h-4 w-4" />
</Button>
<Input
placeholder={placeholder}
value={value || ""}
onChange={(e) => onChange(e.target.value)}
className="text-center"
/>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleIncrement}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
{displayValue && (
<span className="text-xs text-muted-foreground text-center">
{displayValue}
</span>
)}
</div>
);
};

View File

@@ -0,0 +1,114 @@
CREATE TABLE "webServerSettings" (
"id" text PRIMARY KEY NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"https" boolean DEFAULT false NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
"logCleanupCron" text DEFAULT '0 0 * * *',
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now() NOT NULL
);
-- Migrate data from user table to webServerSettings
-- Get the owner user's data and insert into webServerSettings
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
u."serverIp",
COALESCE(u."certificateType", 'none') as "certificateType",
COALESCE(u."https", false) as "https",
u."host",
u."letsEncryptEmail",
u."sshPrivateKey",
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
COALESCE(
u."metricsConfig",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
) as "metricsConfig",
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
FROM "user" u
INNER JOIN "member" m ON u."id" = m."user_id"
WHERE m."role" = 'owner'
ORDER BY m."created_at" ASC
LIMIT 1;
-- If no owner found, create a default entry
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
NULL as "serverIp",
'none' as "certificateType",
false as "https",
NULL as "host",
NULL as "letsEncryptEmail",
NULL as "sshPrivateKey",
true as "enableDockerCleanup",
'0 0 * * *' as "logCleanupCron",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
false as "cleanupCacheApplications",
false as "cleanupCacheOnPreviews",
false as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
WHERE NOT EXISTS (
SELECT 1 FROM "webServerSettings"
);
--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";

File diff suppressed because it is too large Load Diff

View File

@@ -932,6 +932,13 @@
"when": 1765346573500,
"tag": "0132_clean_layla_miller",
"breakpoints": true
},
{
"idx": 133,
"version": "7",
"when": 1766301478005,
"tag": "0133_striped_the_order",
"breakpoints": true
}
]
}

View File

@@ -33,7 +33,7 @@
"docker:build:canary": "./docker/build.sh canary",
"docker:push:canary": "./docker/push.sh canary",
"version": "echo $(node -p \"require('./package.json').version\")",
"test": "vitest --config __test__/vitest.config.ts volume-backups",
"test": "vitest --config __test__/vitest.config.ts",
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
},
"dependencies": {
@@ -140,7 +140,6 @@
"react-i18next": "^15.5.2",
"react-markdown": "^9.1.0",
"recharts": "^2.15.3",
"rotating-file-stream": "3.2.3",
"slugify": "^1.6.6",
"sonner": "^1.7.4",
"ssh2": "1.15.0",

View File

@@ -1,8 +1,8 @@
import {
findUserById,
getWebServerSettings,
IS_CLOUD,
setupWebMonitoring,
updateUser,
updateWebServerSettings,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure
.input(apiUpdateWebServerMonitoring)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
try {
if (IS_CLOUD) {
throw new TRPCError({
@@ -19,15 +19,8 @@ export const adminRouter = createTRPCRouter({
message: "Feature disabled on cloud",
});
}
const user = await findUserById(ctx.user.ownerId);
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup the monitoring",
});
}
await updateUser(user.id, {
await updateWebServerSettings({
metricsConfig: {
server: {
type: "Dokploy",
@@ -52,8 +45,9 @@ export const adminRouter = createTRPCRouter({
},
});
const currentServer = await setupWebMonitoring(user.id);
return currentServer;
await setupWebMonitoring();
const settings = await getWebServerSettings();
return settings;
} catch (error) {
throw error;
}

View File

@@ -68,6 +68,40 @@ export const aiRouter = createTRPCRouter({
{ headers: {} },
);
break;
case "perplexity":
// Perplexity doesn't have a /models endpoint, return hardcoded list
return [
{
id: "sonar-deep-research",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-reasoning-pro",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-reasoning",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-pro",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
] as Model[];
default:
if (!input.apiKey)
throw new TRPCError({

View File

@@ -17,8 +17,8 @@ import {
findGitProviderById,
findProjectById,
findServerById,
findUserById,
getComposeContainer,
getWebServerSettings,
IS_CLOUD,
loadServices,
randomizeComposeFile,
@@ -569,8 +569,7 @@ export const composeRouter = createTRPCRouter({
const template = await fetchTemplateFiles(input.id, input.baseUrl);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
let serverIp = "127.0.0.1";
const project = await findProjectById(environment.projectId);
@@ -579,6 +578,9 @@ export const composeRouter = createTRPCRouter({
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
}
const projectName = slugify(`${project.name} ${input.id}`);
@@ -803,14 +805,16 @@ export const composeRouter = createTRPCRouter({
const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8",
);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
let serverIp = "127.0.0.1";
if (compose.serverId) {
const server = await findServerById(compose.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
}
const templateData = JSON.parse(decodedData);
const config = parse(templateData.config) as CompleteTemplate;
@@ -880,14 +884,16 @@ export const composeRouter = createTRPCRouter({
await removeDomainById(domain.domainId);
}
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
let serverIp = "127.0.0.1";
if (compose.serverId) {
const server = await findServerById(compose.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
}
const templateData = JSON.parse(decodedData);

View File

@@ -9,6 +9,7 @@ import {
findPreviewDeploymentById,
findServerById,
generateTraefikMeDomain,
getWebServerSettings,
manageDomain,
removeDomain,
removeDomainById,
@@ -107,16 +108,13 @@ export const domainRouter = createTRPCRouter({
}),
canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() }))
.query(async ({ input, ctx }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
.query(async ({ input }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
return server.ipAddress;
}
return organization?.owner.serverIp;
const settings = await getWebServerSettings();
return settings?.serverIp || "";
}),
update: protectedProcedure

View File

@@ -8,6 +8,7 @@ import {
createSlackNotification,
createTelegramNotification,
findNotificationById,
getWebServerSettings,
IS_CLOUD,
removeNotificationById,
sendCustomNotification,
@@ -66,7 +67,6 @@ import {
apiUpdateTelegram,
notifications,
server,
user,
} from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({
@@ -364,21 +364,20 @@ export const notificationRouter = createTRPCRouter({
let organizationId = "";
let ServerName = "";
if (input.ServerType === "Dokploy") {
const result = await db
.select()
.from(user)
.where(
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
);
if (!result?.[0]?.id) {
const settings = await getWebServerSettings();
if (
!settings?.metricsConfig?.server?.token ||
settings.metricsConfig.server.token !== input.Token
) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Token not found",
});
}
organizationId = result?.[0]?.id;
// For Dokploy server type, we don't have a specific organizationId
// This might need to be adjusted based on your business logic
organizationId = "";
ServerName = "Dokploy";
} else {
const result = await db

View File

@@ -12,11 +12,11 @@ import {
DEFAULT_UPDATE_DATA,
execAsync,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
getWebServerSettings,
IS_CLOUD,
parseRawConfig,
paths,
@@ -40,7 +40,7 @@ import {
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
updateWebServerSettings,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
@@ -77,6 +77,13 @@ import {
} from "../trpc";
export const settingsRouter = createTRPCRouter({
getWebServerSettings: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
return settings;
}),
reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
@@ -209,11 +216,11 @@ export const settingsRouter = createTRPCRouter({
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
await updateUser(ctx.user.ownerId, {
await updateWebServerSettings({
sshPrivateKey: input.sshPrivateKey,
});
@@ -221,36 +228,36 @@ export const settingsRouter = createTRPCRouter({
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ ctx, input }) => {
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const user = await updateUser(ctx.user.ownerId, {
const settings = await updateWebServerSettings({
host: input.host,
letsEncryptEmail: input.letsEncryptEmail,
certificateType: input.certificateType,
https: input.https,
});
if (!user) {
if (!settings) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
message: "Web server settings not found",
});
}
updateServerTraefik(user, input.host);
updateServerTraefik(settings, input.host);
if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail);
}
return user;
return settings;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await updateUser(ctx.user.ownerId, {
await updateWebServerSettings({
sshPrivateKey: null,
});
return true;
@@ -310,11 +317,11 @@ export const settingsRouter = createTRPCRouter({
}
}
} else if (!IS_CLOUD) {
const userUpdated = await updateUser(ctx.user.ownerId, {
const settingsUpdated = await updateWebServerSettings({
enableDockerCleanup: input.enableDockerCleanup,
});
if (userUpdated?.enableDockerCleanup) {
if (settingsUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
@@ -488,13 +495,28 @@ export const settingsRouter = createTRPCRouter({
return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async ({ ctx }) => {
getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return true;
return "";
}
const user = await findUserById(ctx.user.ownerId);
return user.serverIp;
const settings = await getWebServerSettings();
return settings?.serverIp || "";
}),
updateServerIp: adminProcedure
.input(
z.object({
serverIp: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const settings = await updateWebServerSettings({
serverIp: input.serverIp,
});
return settings;
}),
getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => {

View File

@@ -5,6 +5,7 @@ import {
findUserById,
getDokployUrl,
getUserByToken,
getWebServerSettings,
IS_CLOUD,
removeUserById,
sendEmailNotification,
@@ -214,10 +215,11 @@ export const userRouter = createTRPCRouter({
}),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return {
serverIp: user.serverIp,
serverIp: settings?.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig,
metricsConfig: settings?.metricsConfig,
};
}),
remove: protectedProcedure

View File

@@ -75,7 +75,6 @@
"qrcode": "^1.5.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"shell-quote": "^1.8.1",
"slugify": "^1.6.6",
"ssh2": "1.15.0",

View File

@@ -35,3 +35,4 @@ export * from "./ssh-key";
export * from "./user";
export * from "./utils";
export * from "./volume-backups";
export * from "./web-server-settings";

View File

@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
import {
boolean,
integer,
jsonb,
pgTable,
text,
timestamp,
@@ -15,7 +14,6 @@ import { account, apikey, organization } from "./account";
import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -51,73 +49,10 @@ export const user = pgTable("user", {
banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(),
// Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
@@ -203,33 +138,6 @@ export const apiFindOneUserByAuth = createSchema
// authId: true,
})
.required();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
https: true,
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1),
@@ -298,32 +206,6 @@ export const apiReadStatsLogs = z.object({
.optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});
export const apiUpdateUser = createSchema.partial().extend({
email: z
.string()
@@ -334,29 +216,4 @@ export const apiUpdateUser = createSchema.partial().extend({
currentPassword: z.string().optional(),
name: z.string().optional(),
lastName: z.string().optional(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
logCleanupCron: z.string().optional().nullable(),
});

View File

@@ -0,0 +1,178 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { certificateType } from "./shared";
export const webServerSettings = pgTable("webServerSettings", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
// Web Server Configuration
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
// Metrics Configuration
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const webServerSettingsRelations = relations(
webServerSettings,
() => ({}),
);
const createSchema = createInsertSchema(webServerSettings, {
id: z.string().min(1),
});
export const apiUpdateWebServerSettings = createSchema.partial().extend({
serverIp: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
https: z.boolean().optional(),
host: z.string().optional(),
letsEncryptEmail: z.string().email().optional().nullable(),
sshPrivateKey: z.string().optional(),
enableDockerCleanup: z.boolean().optional(),
logCleanupCron: z.string().optional().nullable(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
});
export const apiAssignDomain = z
.object({
host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(),
https: z.boolean().optional(),
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiSaveSSHKey = z
.object({
sshPrivateKey: z.string(),
})
.required();
export const apiUpdateDockerCleanup = z.object({
enableDockerCleanup: z.boolean(),
serverId: z.string().optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});

View File

@@ -41,6 +41,7 @@ export * from "./services/settings";
export * from "./services/ssh-key";
export * from "./services/user";
export * from "./services/volume-backups";
export * from "./services/web-server-settings";
export * from "./setup/config-paths";
export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup";

View File

@@ -9,7 +9,10 @@ import { IS_CLOUD } from "../constants";
import { db } from "../db";
import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin";
import { updateUser } from "../services/user";
import {
getWebServerSettings,
updateWebServerSettings,
} from "../services/web-server-settings";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
@@ -35,22 +38,14 @@ const { handler, api } = betterAuth({
},
...(!IS_CLOUD && {
async trustedOrigins() {
const admin = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
with: {
user: true,
},
});
if (admin?.user) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
];
const settings = await getWebServerSettings();
if (!settings) {
return [];
}
return [];
return [
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
...(settings?.host ? [`https://${settings?.host}`] : []),
];
},
}),
emailVerification: {
@@ -122,7 +117,7 @@ const { handler, api } = betterAuth({
});
if (!IS_CLOUD) {
await updateUser(user.id, {
await updateWebServerSettings({
serverIp: await getPublicIpWithFallback(),
});
}

View File

@@ -8,6 +8,7 @@ import {
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { getWebServerSettings } from "./web-server-settings";
export const findUserById = async (userId: string) => {
const userResult = await db.query.user.findFirst({
@@ -107,11 +108,11 @@ export const getDokployUrl = async () => {
if (IS_CLOUD) {
return "https://app.dokploy.com";
}
const owner = await findOwner();
const settings = await getWebServerSettings();
if (owner.user.host) {
const protocol = owner.user.https ? "https" : "http";
return `${protocol}://${owner.user.host}`;
if (settings?.host) {
const protocol = settings?.https ? "https" : "http";
return `${protocol}://${settings?.host}`;
}
return `http://${owner.user.serverIp}:${process.env.PORT}`;
return `http://${settings?.serverIp}:${process.env.PORT}`;
};

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server";
import { getWebServerSettings } from "./web-server-settings";
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
let ip = "";
if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId);
ip = organization?.owner.serverIp || "";
const settings = await getWebServerSettings();
ip = settings?.serverIp || "";
}
if (serverId) {

View File

@@ -3,10 +3,10 @@ import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server";
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
projectName: appName,
});
}
const admin = await findUserById(userId);
const settings = await getWebServerSettings();
return generateRandomDomain({
serverIp: admin?.serverIp || "",
serverIp: settings?.serverIp || "",
projectName: appName,
});
};

View File

@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
}
if (!ip) {
const admin = await findUserById(userId);
ip = admin?.serverIp || "";
const settings = await getWebServerSettings();
ip = settings?.serverIp || "";
}
const slugIp = ip.replaceAll(".", "-");

View File

@@ -0,0 +1,44 @@
import { db } from "@dokploy/server/db";
import { webServerSettings } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
/**
* Get the web server settings (singleton - only one row should exist)
*/
export const getWebServerSettings = async () => {
const settings = await db.query.webServerSettings.findFirst({
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
});
if (!settings) {
// Create default settings if none exist
const [newSettings] = await db
.insert(webServerSettings)
.values({})
.returning();
return newSettings;
}
return settings;
};
/**
* Update web server settings
*/
export const updateWebServerSettings = async (
updates: Partial<typeof webServerSettings.$inferInsert>,
) => {
const current = await getWebServerSettings();
const [updated] = await db
.update(webServerSettings)
.set({
...updates,
updatedAt: new Date(),
})
.where(eq(webServerSettings.id, current?.id ?? ""))
.returning();
return updated;
};

View File

@@ -1,7 +1,7 @@
import { findServerById } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
}
};
export const setupWebMonitoring = async (userId: string) => {
const user = await findUserById(userId);
export const setupWebMonitoring = async () => {
const webServerSettings = await getWebServerSettings();
const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest";
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
const settings: ContainerCreateOptions = {
name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
Image: imageName,
HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
Name: "always",
},
PortBindings: {
[`${user?.metricsConfig?.server?.port}/tcp`]: [
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
{
HostPort: user?.metricsConfig?.server?.port.toString(),
HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
},
],
},
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
// NetworkMode: "host",
},
ExposedPorts: {
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
},
};
const docker = await getRemoteDocker();

View File

@@ -1,6 +1,8 @@
import { paths } from "@dokploy/server/constants";
import { findOwner } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import {
getWebServerSettings,
updateWebServerSettings,
} from "@dokploy/server/services/web-server-settings";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync";
@@ -29,12 +31,9 @@ export const startLogCleanup = async (
}
});
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
logCleanupCron: cronExpression,
});
}
await updateWebServerSettings({
logCleanupCron: cronExpression,
});
return true;
} catch (error) {
@@ -51,12 +50,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
}
// Update database
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
logCleanupCron: null,
});
}
await updateWebServerSettings({
logCleanupCron: null,
});
return true;
} catch (error) {
@@ -69,8 +65,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean;
cronExpression: string | null;
}> => {
const owner = await findOwner();
const cronExpression = owner?.user.logCleanupCron ?? null;
const settings = await getWebServerSettings();
const cronExpression = settings?.logCleanupCron ?? null;
return {
enabled: cronExpression !== null,
cronExpression,

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
@@ -25,7 +26,9 @@ export const initCronJobs = async () => {
return;
}
if (admin?.user?.enableDockerCleanup) {
const webServerSettings = await getWebServerSettings();
if (webServerSettings?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -82,9 +85,12 @@ export const initCronJobs = async () => {
}
}
if (admin?.user?.logCleanupCron) {
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
await startLogCleanup(admin.user.logCleanupCron);
if (webServerSettings?.logCleanupCron) {
console.log(
"Starting log requests cleanup",
webServerSettings.logCleanupCron,
);
await startLogCleanup(webServerSettings.logCleanupCron);
}
};

View File

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

View File

@@ -117,7 +117,7 @@ const getRegistryCommands = (
): string => {
return `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
echo "❌ DockerHub Failed" ;
exit 1;
}

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user";
import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
import { parse, stringify } from "yaml";
import {
loadOrCreateConfig,
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types";
export const updateServerTraefik = (
user: User | null,
settings: typeof webServerSettings.$inferSelect | null,
newHost: string | null,
) => {
const { https, certificateType } = user || {};
const { https, certificateType } = settings || {};
const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName);

12
pnpm-lock.yaml generated
View File

@@ -406,9 +406,6 @@ importers:
recharts:
specifier: ^2.15.3
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
@@ -732,9 +729,6 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
@@ -7064,10 +7058,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rotating-file-stream@3.2.3:
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
engines: {node: '>=14.0'}
rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
@@ -14660,8 +14650,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3
rotating-file-stream@3.2.3: {}
rou3@0.5.1: {}
run-parallel@1.2.0: