Repository
- {field.value.owner && field.value.repo && (
+ {field.value.gitlabPathNamespace && (
{
/(?: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;
}
diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx
index 3176b9589..278707cef 100644
--- a/apps/dokploy/components/dashboard/project/add-database.tsx
+++ b/apps/dokploy/components/dashboard/project/add-database.tsx
@@ -559,6 +559,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
type="password"
placeholder="******************"
autoComplete="one-time-code"
+ enablePasswordGenerator={true}
{...field}
/>
@@ -578,6 +579,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx
new file mode 100644
index 000000000..67c15ee63
--- /dev/null
+++ b/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx
@@ -0,0 +1,74 @@
+import { CreditCard, FileText } from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/router";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+import { ShowInvoices } from "./show-invoices";
+
+const navigationItems = [
+ {
+ name: "Subscription",
+ href: "/dashboard/settings/billing",
+ icon: CreditCard,
+ },
+ {
+ name: "Invoices",
+ href: "/dashboard/settings/invoices",
+ icon: FileText,
+ },
+];
+
+export const ShowBillingInvoices = () => {
+ const router = useRouter();
+
+ return (
+
+
+
+
+
+
+ Billing
+
+
+ Manage your subscription and invoices
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
index ac211a1c5..1460244c1 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
@@ -4,11 +4,13 @@ import {
AlertTriangle,
CheckIcon,
CreditCard,
+ FileText,
Loader2,
MinusIcon,
PlusIcon,
} from "lucide-react";
import Link from "next/link";
+import { useRouter } from "next/router";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -37,7 +39,22 @@ export const calculatePrice = (count: number, isAnnual = false) => {
if (count <= 1) return 4.5;
return count * 3.5;
};
+
+const navigationItems = [
+ {
+ name: "Subscription",
+ href: "/dashboard/settings/billing",
+ icon: CreditCard,
+ },
+ {
+ name: "Invoices",
+ href: "/dashboard/settings/invoices",
+ icon: FileText,
+ },
+];
+
export const ShowBilling = () => {
+ const router = useRouter();
const { data: servers } = api.server.count.useQuery();
const { data: admin } = api.user.get.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery();
@@ -76,17 +93,41 @@ export const ShowBilling = () => {
return (
-
-
-
+
+
+
Billing
- Manage your subscription
+
+ Manage your subscription and invoices
+
-
-
+
+
+
+
{
+ if (!timestamp) return "-";
+ return new Date(timestamp * 1000).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ });
+};
+
+const formatAmount = (amount: number, currency: string) => {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: currency.toUpperCase(),
+ }).format(amount / 100);
+};
+
+const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
+ const statusConfig: Record<
+ Stripe.Invoice.Status,
+ { label: string; variant: "default" | "secondary" | "destructive" }
+ > = {
+ paid: { label: "Paid", variant: "default" },
+ open: { label: "Open", variant: "secondary" },
+ draft: { label: "Draft", variant: "secondary" },
+ void: { label: "Void", variant: "destructive" },
+ uncollectible: { label: "Uncollectible", variant: "destructive" },
+ };
+
+ if (!status) {
+ return Unknown;
+ }
+
+ const config = statusConfig[status] || {
+ label: status,
+ variant: "secondary" as const,
+ };
+
+ return {config.label};
+};
+
+export const ShowInvoices = () => {
+ const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
+
+ return (
+
+ {isLoading ? (
+
+
+ Loading invoices...
+
+
+
+ ) : invoices && invoices.length > 0 ? (
+
+
+
+
+ Invoice
+ Date
+ Due Date
+ Amount
+ Status
+ Actions
+
+
+
+ {invoices.map((invoice) => (
+
+
+ {invoice.number || invoice.id.slice(0, 12)}
+
+ {formatDate(invoice.created)}
+ {formatDate(invoice.dueDate)}
+
+ {formatAmount(invoice.amountDue, invoice.currency)}
+
+ {getStatusBadge(invoice.status)}
+
+
+ {invoice.hostedInvoiceUrl && (
+
+ )}
+ {invoice.invoicePdf && (
+
+ )}
+
+
+
+ ))}
+
+
+
+ ) : (
+
+
+
No invoices found
+
+ Your invoices will appear here once you have a subscription
+
+
+ )}
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx
index 47219620f..ad66aa0fa 100644
--- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx
+++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx
@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
-import { Loader2, User } from "lucide-react";
+import { Loader2, Palette, User } from "lucide-react";
import { useTranslation } from "next-i18next";
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -27,6 +27,7 @@ import {
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
+import { getAvatarType, isSolidColorAvatar } from "@/lib/avatar-utils";
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api";
import { Configure2FA } from "./configure-2fa";
@@ -74,6 +75,7 @@ export const ProfileForm = () => {
} = api.user.update.useMutation();
const { t } = useTranslation("settings");
const [gravatarHash, setGravatarHash] = useState(null);
+ const colorInputRef = useRef(null);
const availableAvatars = useMemo(() => {
if (gravatarHash === null) return randomImages;
@@ -274,16 +276,8 @@ export const ProfileForm = () => {
onValueChange={(e) => {
field.onChange(e);
}}
- defaultValue={
- field.value?.startsWith("data:")
- ? "upload"
- : field.value
- }
- value={
- field.value?.startsWith("data:")
- ? "upload"
- : field.value
- }
+ defaultValue={getAvatarType(field.value)}
+ value={getAvatarType(field.value)}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
>
@@ -370,6 +364,40 @@ export const ProfileForm = () => {
/>
+
+
+
+
+
+
+ colorInputRef.current?.click()
+ }
+ >
+ {!isSolidColorAvatar(field.value) && (
+
+ )}
+
+
+
+
{availableAvatars.map((image) => (
diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx
index ea2300cc5..334d25b20 100644
--- a/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx
@@ -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";
diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx
index 4021ddaf5..97cf3f6be 100644
--- a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx
@@ -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();
diff --git a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx
index d9d1977a7..99804ba6b 100644
--- a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx
@@ -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";
diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-monitoring.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-monitoring.tsx
index fb7b23f78..09260b1a2 100644
--- a/apps/dokploy/components/dashboard/settings/servers/setup-monitoring.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/setup-monitoring.tsx
@@ -80,7 +80,7 @@ const Schema = z.object({
type Schema = z.infer;
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();
diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
index d88e9a3e4..13ff2d6e4 100644
--- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
@@ -22,7 +22,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
-import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
@@ -89,15 +88,15 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
) : (
- {
- e.preventDefault();
+ size="sm"
+ onClick={() => {
setIsOpen(true);
}}
>
- Setup Server
-
+ Setup Server
+
)}
diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
index 111f10e28..92d6fc5c3 100644
--- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
@@ -1,17 +1,15 @@
import { format } from "date-fns";
import {
+ Clock,
+ Key,
KeyIcon,
Loader2,
MoreHorizontal,
- ServerIcon,
- Clock,
- User,
- Key,
Network,
+ ServerIcon,
Terminal,
- Settings,
- Pencil,
Trash2,
+ User,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -31,9 +29,7 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
- DropdownMenuItem,
DropdownMenuLabel,
- DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -285,7 +281,32 @@ export const ShowServers = () => {
{/* Compact Actions */}
{isActive && (
-
+
+
+
+
+
+
+
+
+
+ Setup Server
+
+
+ Configure and initialize your
+ server with Docker, Traefik, and
+ other essential services
+
+
+
+
+
+
{server.sshKeyId && (
@@ -311,20 +332,6 @@ export const ShowServers = () => {
)}
-
-
-
-
-
-
-
- Setup Server
-
-
-
diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx
index c889708c3..e0be5c7f3 100644
--- a/apps/dokploy/components/dashboard/settings/web-domain.tsx
+++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx
@@ -67,7 +67,7 @@ type AddServerDomain = z.infer
;
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]);
diff --git a/apps/dokploy/components/dashboard/settings/web-server.tsx b/apps/dokploy/components/dashboard/settings/web-server.tsx
index 2a2ce4ab1..c9cb7985b 100644
--- a/apps/dokploy/components/dashboard/settings/web-server.tsx
+++ b/apps/dokploy/components/dashboard/settings/web-server.tsx
@@ -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 = () => {
- Server IP: {data?.user.serverIp}
+ Server IP: {webServerSettings?.serverIp}
Version: {dokployVersion}
diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx
index 3c49873de..eb8c4a665 100644
--- a/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx
+++ b/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx
@@ -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({
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(() => {
diff --git a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx
index c3428e301..4b8dc9112 100644
--- a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx
+++ b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx
@@ -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,
diff --git a/apps/dokploy/components/shared/toggle-visibility-input.tsx b/apps/dokploy/components/shared/toggle-visibility-input.tsx
index aea173fbc..660fd753a 100644
--- a/apps/dokploy/components/shared/toggle-visibility-input.tsx
+++ b/apps/dokploy/components/shared/toggle-visibility-input.tsx
@@ -10,7 +10,7 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
return (
-
+
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx
index 56b4b5d0c..b03392c45 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx
@@ -182,7 +182,9 @@ const Service = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && (
+ {(auth?.role === "owner" ||
+ auth?.role === "admin" ||
+ auth?.canDeleteServices) && (
)}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx
index d47fbd14d..e496dd928 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx
@@ -156,7 +156,9 @@ const Mariadb = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && (
+ {(auth?.role === "owner" ||
+ auth?.role === "admin" ||
+ auth?.canDeleteServices) && (
)}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx
index 660315d5a..077add05b 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx
@@ -155,7 +155,9 @@ const Mongo = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && (
+ {(auth?.role === "owner" ||
+ auth?.role === "admin" ||
+ auth?.canDeleteServices) && (
)}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx
index 7f4cc791c..acf7280aa 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx
@@ -156,7 +156,9 @@ const MySql = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && (
+ {(auth?.role === "owner" ||
+ auth?.role === "admin" ||
+ auth?.canDeleteServices) && (
)}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx
index a34f7b7ee..d8bd94ca2 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx
@@ -154,7 +154,9 @@ const Postgresql = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && (
+ {(auth?.role === "owner" ||
+ auth?.role === "admin" ||
+ auth?.canDeleteServices) && (
)}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx
index 72a513fba..0f4bd4a88 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx
@@ -154,7 +154,9 @@ const Redis = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && (
+ {(auth?.role === "owner" ||
+ auth?.role === "admin" ||
+ auth?.canDeleteServices) && (
)}
diff --git a/apps/dokploy/pages/dashboard/settings/invoices.tsx b/apps/dokploy/pages/dashboard/settings/invoices.tsx
new file mode 100644
index 000000000..a37c3607c
--- /dev/null
+++ b/apps/dokploy/pages/dashboard/settings/invoices.tsx
@@ -0,0 +1,63 @@
+import { IS_CLOUD } from "@dokploy/server/constants";
+import { validateRequest } from "@dokploy/server/lib/auth";
+import { createServerSideHelpers } from "@trpc/react-query/server";
+import type { GetServerSidePropsContext } from "next";
+import type { ReactElement } from "react";
+import superjson from "superjson";
+import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
+import { DashboardLayout } from "@/components/layouts/dashboard-layout";
+import { appRouter } from "@/server/api/root";
+
+const Page = () => {
+ return ;
+};
+
+export default Page;
+
+Page.getLayout = (page: ReactElement) => {
+ return {page};
+};
+export async function getServerSideProps(
+ ctx: GetServerSidePropsContext<{ serviceId: string }>,
+) {
+ if (!IS_CLOUD) {
+ return {
+ redirect: {
+ permanent: true,
+ destination: "/dashboard/projects",
+ },
+ };
+ }
+ const { req, res } = ctx;
+ const { user, session } = await validateRequest(req);
+ if (!user || user.role !== "owner") {
+ return {
+ redirect: {
+ permanent: true,
+ destination: "/",
+ },
+ };
+ }
+
+ const helpers = createServerSideHelpers({
+ router: appRouter,
+ ctx: {
+ req: req as any,
+ res: res as any,
+ db: null as any,
+ session: session as any,
+ user: user as any,
+ },
+ transformer: superjson,
+ });
+
+ await helpers.user.get.prefetch();
+
+ await helpers.settings.isCloud.prefetch();
+
+ return {
+ props: {
+ trpcState: helpers.dehydrate(),
+ },
+ };
+}
diff --git a/apps/dokploy/server/api/routers/admin.ts b/apps/dokploy/server/api/routers/admin.ts
index 6a3be31b4..4323d9e47 100644
--- a/apps/dokploy/server/api/routers/admin.ts
+++ b/apps/dokploy/server/api/routers/admin.ts
@@ -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;
}
diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts
index 937505bb1..1f08fc7dd 100644
--- a/apps/dokploy/server/api/routers/ai.ts
+++ b/apps/dokploy/server/api/routers/ai.ts
@@ -94,6 +94,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:
response = await fetch(`${apiUrl}/models`, { headers });
}
diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts
index 68067f9df..600fa5f51 100644
--- a/apps/dokploy/server/api/routers/backup.ts
+++ b/apps/dokploy/server/api/routers/backup.ts
@@ -285,6 +285,7 @@ export const backupRouter = createTRPCRouter({
.mutation(async ({ input }) => {
const backup = await findBackupById(input.backupId);
await runWebServerBackup(backup);
+ await keepLatestNBackups(backup);
return true;
}),
listBackupFiles: protectedProcedure
diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 3261f61fa..9354988a8 100644
--- a/apps/dokploy/server/api/routers/compose.ts
+++ b/apps/dokploy/server/api/routers/compose.ts
@@ -17,8 +17,8 @@ import {
findGitProviderById,
findProjectById,
findServerById,
- findUserById,
getComposeContainer,
+ getWebServerSettings,
IS_CLOUD,
loadServices,
randomizeComposeFile,
@@ -430,7 +430,11 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true,
},
);
- return { success: true, message: "Deployment queued" };
+ return {
+ success: true,
+ message: "Deployment queued",
+ composeId: compose.composeId,
+ };
}),
redeploy: protectedProcedure
.input(apiRedeployCompose)
@@ -468,7 +472,11 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true,
},
);
- return { success: true, message: "Redeployment queued" };
+ return {
+ success: true,
+ message: "Redeployment queued",
+ composeId: compose.composeId,
+ };
}),
stop: protectedProcedure
.input(apiFindCompose)
@@ -569,8 +577,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 +586,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 +813,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 +892,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);
diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts
index 1f6264351..5767a38a9 100644
--- a/apps/dokploy/server/api/routers/domain.ts
+++ b/apps/dokploy/server/api/routers/domain.ts
@@ -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
diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts
index b32278465..303168b9f 100644
--- a/apps/dokploy/server/api/routers/notification.ts
+++ b/apps/dokploy/server/api/routers/notification.ts
@@ -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
diff --git a/apps/dokploy/server/api/routers/preview-deployment.ts b/apps/dokploy/server/api/routers/preview-deployment.ts
index 49b781101..0c325a9c6 100644
--- a/apps/dokploy/server/api/routers/preview-deployment.ts
+++ b/apps/dokploy/server/api/routers/preview-deployment.ts
@@ -2,11 +2,15 @@ import {
findApplicationById,
findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId,
+ IS_CLOUD,
removePreviewDeployment,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { apiFindAllByApplication } from "@/server/db/schema";
+import type { DeploymentJob } from "@/server/queues/queue-types";
+import { myQueue } from "@/server/queues/queueSetup";
+import { deploy } from "@/server/utils/deploy";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const previewDeploymentRouter = createTRPCRouter({
@@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
}
return previewDeployment;
}),
+ redeploy: protectedProcedure
+ .input(
+ z.object({
+ previewDeploymentId: z.string(),
+ title: z.string().optional(),
+ description: z.string().optional(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const previewDeployment = await findPreviewDeploymentById(
+ input.previewDeploymentId,
+ );
+ if (
+ previewDeployment.application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to redeploy this preview deployment",
+ });
+ }
+ const application = await findApplicationById(
+ previewDeployment.applicationId,
+ );
+ const jobData: DeploymentJob = {
+ applicationId: previewDeployment.applicationId,
+ titleLog: input.title || "Rebuild Preview Deployment",
+ descriptionLog: input.description || "",
+ type: "redeploy",
+ applicationType: "application-preview",
+ previewDeploymentId: input.previewDeploymentId,
+ server: !!application.serverId,
+ };
+
+ if (IS_CLOUD && application.serverId) {
+ jobData.serverId = application.serverId;
+ deploy(jobData).catch((error) => {
+ console.error("Background deployment failed:", error);
+ });
+ return true;
+ }
+ await myQueue.add(
+ "deployments",
+ { ...jobData },
+ {
+ removeOnComplete: true,
+ removeOnFail: true,
+ },
+ );
+ return true;
+ }),
});
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index a6154ec1c..c9d21e515 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -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,11 +77,18 @@ 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;
}
- await reloadDockerResource("dokploy");
+ await reloadDockerResource("dokploy", undefined, packageInfo.version);
return true;
}),
cleanRedis: adminProcedure.mutation(async () => {
@@ -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...`,
@@ -392,7 +399,7 @@ export const settingsRouter = createTRPCRouter({
return DEFAULT_UPDATE_DATA;
}
- return await getUpdateData();
+ return await getUpdateData(packageInfo.version);
}),
updateServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
@@ -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 => {
diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts
index d2a000324..be1e94d4a 100644
--- a/apps/dokploy/server/api/routers/stripe.ts
+++ b/apps/dokploy/server/api/routers/stripe.ts
@@ -75,9 +75,9 @@ export const stripeRouter = createTRPCRouter({
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: items,
- ...(stripeCustomerId && {
- customer: stripeCustomerId,
- }),
+ ...(stripeCustomerId
+ ? { customer: stripeCustomerId }
+ : { customer_email: owner.email }),
metadata: {
adminId: owner.id,
},
@@ -128,4 +128,39 @@ export const stripeRouter = createTRPCRouter({
return servers.length < user.serversQuantity;
}),
+
+ getInvoices: adminProcedure.query(async ({ ctx }) => {
+ const user = await findUserById(ctx.user.ownerId);
+ const stripeCustomerId = user.stripeCustomerId;
+
+ if (!stripeCustomerId) {
+ return [];
+ }
+
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ apiVersion: "2024-09-30.acacia",
+ });
+
+ try {
+ const invoices = await stripe.invoices.list({
+ customer: stripeCustomerId,
+ limit: 100,
+ });
+
+ return invoices.data.map((invoice) => ({
+ id: invoice.id,
+ number: invoice.number,
+ status: invoice.status,
+ amountDue: invoice.amount_due,
+ amountPaid: invoice.amount_paid,
+ currency: invoice.currency,
+ created: invoice.created,
+ dueDate: invoice.due_date,
+ hostedInvoiceUrl: invoice.hosted_invoice_url,
+ invoicePdf: invoice.invoice_pdf,
+ }));
+ } catch (_) {
+ return [];
+ }
+ }),
});
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index a6bd81a01..e801a5adb 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -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
diff --git a/apps/dokploy/server/queues/deployments-queue.ts b/apps/dokploy/server/queues/deployments-queue.ts
index 4c117e7e3..0474b63e2 100644
--- a/apps/dokploy/server/queues/deployments-queue.ts
+++ b/apps/dokploy/server/queues/deployments-queue.ts
@@ -4,6 +4,7 @@ import {
deployPreviewApplication,
rebuildApplication,
rebuildCompose,
+ rebuildPreviewApplication,
updateApplicationStatus,
updateCompose,
updatePreviewDeployment,
@@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
previewStatus: "running",
});
- if (job.data.type === "deploy") {
+ if (job.data.type === "redeploy") {
+ await rebuildPreviewApplication({
+ applicationId: job.data.applicationId,
+ titleLog: job.data.titleLog,
+ descriptionLog: job.data.descriptionLog,
+ previewDeploymentId: job.data.previewDeploymentId,
+ });
+ } else if (job.data.type === "deploy") {
await deployPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
diff --git a/apps/dokploy/server/queues/queue-types.ts b/apps/dokploy/server/queues/queue-types.ts
index ef8df6943..1000725ad 100644
--- a/apps/dokploy/server/queues/queue-types.ts
+++ b/apps/dokploy/server/queues/queue-types.ts
@@ -22,7 +22,7 @@ type DeployJob =
titleLog: string;
descriptionLog: string;
server?: boolean;
- type: "deploy";
+ type: "deploy" | "redeploy";
applicationType: "application-preview";
previewDeploymentId: string;
serverId?: string;
diff --git a/apps/monitoring/database/containers.go b/apps/monitoring/database/containers.go
index 568ad12e5..4e41f5fae 100644
--- a/apps/monitoring/database/containers.go
+++ b/apps/monitoring/database/containers.go
@@ -58,7 +58,7 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
WITH recent_metrics AS (
SELECT metrics_json
FROM container_metrics
- WHERE container_name LIKE ? || '%'
+ WHERE container_name = ?
ORDER BY timestamp DESC
LIMIT ?
)
@@ -98,7 +98,7 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
WITH recent_metrics AS (
SELECT metrics_json
FROM container_metrics
- WHERE container_name LIKE ? || '%'
+ WHERE container_name = ?
ORDER BY timestamp DESC
)
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
diff --git a/packages/server/package.json b/packages/server/package.json
index e23fa6d8b..820300b15 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -57,7 +57,6 @@
"drizzle-dbml-generator": "0.10.0",
"drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1",
- "hi-base32": "^0.5.1",
"yaml": "2.8.1",
"lodash": "4.17.21",
"micromatch": "4.0.8",
@@ -67,7 +66,6 @@
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
- "otpauth": "^9.4.0",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"postgres": "3.4.4",
@@ -75,15 +73,16 @@
"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",
"toml": "3.0.0",
"ws": "8.16.0",
- "zod": "^3.25.32"
+ "zod": "^3.25.32",
+ "semver": "7.7.3"
},
"devDependencies": {
+ "@types/semver": "7.7.1",
"@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
@@ -112,4 +111,4 @@
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
-}
\ No newline at end of file
+}
diff --git a/packages/server/schema.dbml b/packages/server/schema.dbml
index ef1814c00..0fe7c05e8 100644
--- a/packages/server/schema.dbml
+++ b/packages/server/schema.dbml
@@ -277,7 +277,7 @@ table application {
replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks']
- railpackVersion text [default: '0.2.2']
+ railpackVersion text [default: '0.15.4']
herokuVersion text [default: '24']
publishDirectory text
isStaticSpa boolean
diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts
index 787ec55b3..90ce58403 100644
--- a/packages/server/src/db/schema/application.ts
+++ b/packages/server/src/db/schema/application.ts
@@ -177,7 +177,7 @@ export const applications = pgTable("application", {
.notNull()
.default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"),
- railpackVersion: text("railpackVersion").default("0.2.2"),
+ railpackVersion: text("railpackVersion").default("0.15.4"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts
index c16ef1452..ee3c03e93 100644
--- a/packages/server/src/db/schema/index.ts
+++ b/packages/server/src/db/schema/index.ts
@@ -35,3 +35,4 @@ export * from "./ssh-key";
export * from "./user";
export * from "./utils";
export * from "./volume-backups";
+export * from "./web-server-settings";
diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts
index 5a96aa3eb..51be7a7ea 100644
--- a/packages/server/src/db/schema/user.ts
+++ b/packages/server/src/db/schema/user.ts
@@ -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(),
});
diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts
new file mode 100644
index 000000000..92219091d
--- /dev/null
+++ b/packages/server/src/db/schema/web-server-settings.ts
@@ -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(),
+});
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index e6d753293..f28711dbf 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -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";
diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts
index 16cf3e58c..d952e1f6a 100644
--- a/packages/server/src/lib/auth.ts
+++ b/packages/server/src/lib/auth.ts
@@ -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,20 @@ 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}`] : []),
+ ...(process.env.NODE_ENV === "development"
+ ? [
+ "http://localhost:3000",
+ "https://absolutely-handy-falcon.ngrok-free.app",
+ ]
+ : []),
+ ];
},
}),
emailVerification: {
@@ -122,7 +123,7 @@ const { handler, api } = betterAuth({
});
if (!IS_CLOUD) {
- await updateUser(user.id, {
+ await updateWebServerSettings({
serverIp: await getPublicIpWithFallback(),
});
}
diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts
index 0e8612415..323d0177e 100644
--- a/packages/server/src/services/admin.ts
+++ b/packages/server/src/services/admin.ts
@@ -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}`;
};
diff --git a/packages/server/src/services/ai.ts b/packages/server/src/services/ai.ts
index 277ec6c39..fefc82eae 100644
--- a/packages/server/src/services/ai.ts
+++ b/packages/server/src/services/ai.ts
@@ -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) {
diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts
index 61a77ae5a..335ffbf77 100644
--- a/packages/server/src/services/application.ts
+++ b/packages/server/src/services/application.ts
@@ -452,6 +452,137 @@ export const deployPreviewApplication = async ({
return true;
};
+export const rebuildPreviewApplication = async ({
+ applicationId,
+ titleLog = "Rebuild Preview Deployment",
+ descriptionLog = "",
+ previewDeploymentId,
+}: {
+ applicationId: string;
+ titleLog: string;
+ descriptionLog: string;
+ previewDeploymentId: string;
+}) => {
+ const application = await findApplicationById(applicationId);
+ const previewDeployment =
+ await findPreviewDeploymentById(previewDeploymentId);
+
+ const deployment = await createDeploymentPreview({
+ title: titleLog,
+ description: descriptionLog,
+ previewDeploymentId: previewDeploymentId,
+ });
+
+ const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
+ const issueParams = {
+ owner: application?.owner || "",
+ repository: application?.repository || "",
+ issue_number: previewDeployment.pullRequestNumber,
+ comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
+ githubId: application?.githubId || "",
+ };
+
+ try {
+ const commentExists = await issueCommentExists({
+ ...issueParams,
+ });
+ if (!commentExists) {
+ const result = await createPreviewDeploymentComment({
+ ...issueParams,
+ previewDomain,
+ appName: previewDeployment.appName,
+ githubId: application?.githubId || "",
+ previewDeploymentId,
+ });
+
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Pull request comment not found",
+ });
+ }
+
+ issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
+ }
+
+ const buildingComment = getIssueComment(
+ application.name,
+ "running",
+ previewDomain,
+ );
+ await updateIssueComment({
+ ...issueParams,
+ body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
+ });
+
+ // Set application properties for preview deployment
+ application.appName = previewDeployment.appName;
+ application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
+ application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
+ application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
+ application.rollbackActive = false;
+ application.buildRegistry = null;
+ application.rollbackRegistry = null;
+ application.registry = null;
+
+ const serverId = application.serverId;
+ let command = "set -e;";
+ // Only rebuild, don't clone repository
+ command += await getBuildCommand(application);
+ const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
+ if (serverId) {
+ await execAsyncRemote(serverId, commandWithLog);
+ } else {
+ await execAsync(commandWithLog);
+ }
+ await mechanizeDockerContainer(application);
+
+ const successComment = getIssueComment(
+ application.name,
+ "success",
+ previewDomain,
+ );
+ await updateIssueComment({
+ ...issueParams,
+ body: `### Dokploy Preview Deployment\n\n${successComment}`,
+ });
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updatePreviewDeployment(previewDeploymentId, {
+ previewStatus: "done",
+ });
+ } catch (error) {
+ let command = "";
+
+ // Only log details for non-ExecError errors
+ if (!(error instanceof ExecError)) {
+ const message = error instanceof Error ? error.message : String(error);
+ const encodedMessage = encodeBase64(message);
+ command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
+ }
+
+ command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
+ const serverId = application.buildServerId || application.serverId;
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command);
+ }
+
+ const comment = getIssueComment(application.name, "error", previewDomain);
+ await updateIssueComment({
+ ...issueParams,
+ body: `### Dokploy Preview Deployment\n\n${comment}`,
+ });
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updatePreviewDeployment(previewDeploymentId, {
+ previewStatus: "error",
+ });
+ throw error;
+ }
+
+ return true;
+};
+
export const getApplicationStats = async (appName: string) => {
if (appName === "dokploy") {
return await getAdvancedStats(appName);
diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts
index 50888e546..b2e15ed91 100644
--- a/packages/server/src/services/domain.ts
+++ b/packages/server/src/services/domain.ts
@@ -1,12 +1,12 @@
import dns from "node:dns";
import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
+import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
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,
});
};
diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts
index 5ee763b08..1ece3bc53 100644
--- a/packages/server/src/services/preview-deployment.ts
+++ b/packages/server/src/services/preview-deployment.ts
@@ -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(".", "-");
diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts
index 277008bd3..4235376d9 100644
--- a/packages/server/src/services/settings.ts
+++ b/packages/server/src/services/settings.ts
@@ -5,12 +5,12 @@ import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
+import semver from "semver";
import {
initializeStandaloneTraefik,
initializeTraefikService,
type TraefikOptions,
} from "../setup/traefik-setup";
-
export interface IUpdateData {
latestVersion: string | null;
updateAvailable: boolean;
@@ -55,56 +55,95 @@ export const getServiceImageDigest = async () => {
};
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
-export const getUpdateData = async (): Promise => {
- let currentDigest: string;
+export const getUpdateData = async (
+ currentVersion: string,
+): Promise => {
try {
- currentDigest = await getServiceImageDigest();
- } catch (error) {
- // TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
- return DEFAULT_UPDATE_DATA;
- }
+ const baseUrl =
+ "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
+ let url: string | null = `${baseUrl}?page_size=100`;
+ let allResults: { digest: string; name: string }[] = [];
- const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
- let url: string | null = `${baseUrl}?page_size=100`;
- let allResults: { digest: string; name: string }[] = [];
- while (url) {
- const response = await fetch(url, {
- method: "GET",
- headers: { "Content-Type": "application/json" },
- });
+ // Fetch all tags from Docker Hub
+ while (url) {
+ const response = await fetch(url, {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ });
- const data = (await response.json()) as {
- next: string | null;
- results: { digest: string; name: string }[];
- };
+ const data = (await response.json()) as {
+ next: string | null;
+ results: { digest: string; name: string }[];
+ };
- allResults = allResults.concat(data.results);
- url = data?.next;
- }
+ allResults = allResults.concat(data.results);
+ url = data?.next;
+ }
- const imageTag = getDokployImageTag();
- const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
+ const currentImageTag = getDokployImageTag();
- if (!searchedDigest) {
- return DEFAULT_UPDATE_DATA;
- }
+ // Special handling for canary and feature branches
+ // For development versions (canary/feature), don't perform update checks
+ // These are unstable versions that change frequently, and users on these
+ // branches are expected to manually manage updates
+ if (currentImageTag === "canary" || currentImageTag === "feature") {
+ const currentDigest = await getServiceImageDigest();
+ const latestDigest = allResults.find(
+ (t) => t.name === currentImageTag,
+ )?.digest;
+ if (!latestDigest) {
+ return DEFAULT_UPDATE_DATA;
+ }
+ if (currentDigest !== latestDigest) {
+ return {
+ latestVersion: currentImageTag,
+ updateAvailable: true,
+ };
+ }
+ return {
+ latestVersion: currentImageTag,
+ updateAvailable: false,
+ };
+ }
- if (imageTag === "latest") {
- const versionedTag = allResults.find(
- (t) => t.digest === searchedDigest && t.name.startsWith("v"),
- );
+ // For stable versions, use semver comparison
+ // Find the "latest" tag and get its digest
+ const latestTag = allResults.find((t) => t.name === "latest");
- if (!versionedTag) {
+ if (!latestTag) {
return DEFAULT_UPDATE_DATA;
}
- const { name: latestVersion, digest } = versionedTag;
- const updateAvailable = digest !== currentDigest;
+ // Find the versioned tag (v0.x.x) that has the same digest as "latest"
+ const latestVersionTag = allResults.find(
+ (t) => t.digest === latestTag.digest && t.name.startsWith("v"),
+ );
- return { latestVersion, updateAvailable };
+ if (!latestVersionTag) {
+ return DEFAULT_UPDATE_DATA;
+ }
+
+ const latestVersion = latestVersionTag.name;
+
+ // Use semver to compare versions for stable releases
+ const cleanedCurrent = semver.clean(currentVersion);
+ const cleanedLatest = semver.clean(latestVersion);
+
+ if (!cleanedCurrent || !cleanedLatest) {
+ return DEFAULT_UPDATE_DATA;
+ }
+
+ // Check if the latest version is greater than the current version
+ const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
+
+ return {
+ latestVersion,
+ updateAvailable,
+ };
+ } catch (error) {
+ console.error("Error fetching update data:", error);
+ return DEFAULT_UPDATE_DATA;
}
- const updateAvailable = searchedDigest !== currentDigest;
- return { latestVersion: imageTag, updateAvailable };
};
interface TreeDataItem {
@@ -254,11 +293,22 @@ fi`;
export const reloadDockerResource = async (
resourceName: string,
serverId?: string,
+ version?: string,
) => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
- command = `docker service update --force ${resourceName}`;
+ if (resourceName === "dokploy") {
+ const currentImageTag = getDokployImageTag();
+ let imageTag = version;
+ if (currentImageTag === "canary" || currentImageTag === "feature") {
+ imageTag = currentImageTag;
+ }
+
+ command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
+ } else {
+ command = `docker service update --force ${resourceName}`;
+ }
} else if (resourceType === "standalone") {
command = `docker restart ${resourceName}`;
} else {
diff --git a/packages/server/src/services/web-server-settings.ts b/packages/server/src/services/web-server-settings.ts
new file mode 100644
index 000000000..289d119c9
--- /dev/null
+++ b/packages/server/src/services/web-server-settings.ts
@@ -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,
+) => {
+ const current = await getWebServerSettings();
+
+ const [updated] = await db
+ .update(webServerSettings)
+ .set({
+ ...updates,
+ updatedAt: new Date(),
+ })
+ .where(eq(webServerSettings.id, current?.id ?? ""))
+ .returning();
+
+ return updated;
+};
diff --git a/packages/server/src/setup/monitoring-setup.ts b/packages/server/src/setup/monitoring-setup.ts
index 20055be9a..287894c99 100644
--- a/packages/server/src/setup/monitoring-setup.ts
+++ b/packages/server/src/setup/monitoring-setup.ts
@@ -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();
diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts
index 54e740583..32e5e4a7e 100644
--- a/packages/server/src/setup/server-setup.ts
+++ b/packages/server/src/setup/server-setup.ts
@@ -1,10 +1,14 @@
import path from "node:path";
-import { paths } from "@dokploy/server/constants";
+import { IS_CLOUD, paths } from "@dokploy/server/constants";
+import { getDokployUrl } from "@dokploy/server/services/admin";
import {
createServerDeployment,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
-import { findServerById } from "@dokploy/server/services/server";
+import {
+ findServerById,
+ updateServerById,
+} from "@dokploy/server/services/server";
import {
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
@@ -16,6 +20,15 @@ import {
import slug from "slugify";
import { Client } from "ssh2";
import { recreateDirectory } from "../utils/filesystem/directory";
+import { setupMonitoring } from "./monitoring-setup";
+
+const generateToken = () => {
+ const array = new Uint8Array(64);
+ crypto.getRandomValues(array);
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
+ "",
+ );
+};
export const slugify = (text: string | undefined) => {
if (!text) {
@@ -59,6 +72,29 @@ export const serverSetup = async (
);
await installRequirements(serverId, onData);
+ if (IS_CLOUD) {
+ onData?.("\nConfiguring Monitoring: 🔄\n");
+
+ const baseUrl = await getDokployUrl();
+ const token = generateToken();
+ const urlCallback = `${baseUrl}/api/trpc/notification.receiveNotification`;
+
+ // Update server with monitoring configuration
+ await updateServerById(serverId, {
+ metricsConfig: {
+ server: {
+ ...server.metricsConfig.server,
+ token: token,
+ urlCallback: urlCallback,
+ },
+ containers: server.metricsConfig.containers,
+ },
+ });
+
+ await setupMonitoring(serverId);
+ onData?.("\nMonitoring Configured: ✅\n");
+ }
+
await updateDeploymentStatus(deployment.deploymentId, "done");
onData?.("\nSetup Server: ✅\n");
@@ -629,7 +665,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
- export NIXPACKS_VERSION=1.39.0
+ export NIXPACKS_VERSION=1.41.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi
@@ -639,7 +675,7 @@ const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
- export RAILPACK_VERSION=0.2.2
+ export RAILPACK_VERSION=0.15.4
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi
@@ -653,8 +689,8 @@ const installBuildpacks = () => `
if command_exists pack; then
echo "Buildpacks already installed ✅"
else
- BUILDPACKS_VERSION=0.35.0
- curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
+ BUILDPACKS_VERSION=0.39.1
+ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
fi
`;
diff --git a/packages/server/src/utils/access-log/handler.ts b/packages/server/src/utils/access-log/handler.ts
index 237a68f17..13a52c4b1 100644
--- a/packages/server/src/utils/access-log/handler.ts
+++ b/packages/server/src/utils/access-log/handler.ts
@@ -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 => {
}
// 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,
diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts
index c2e74a6bd..ede5c59b5 100644
--- a/packages/server/src/utils/ai/select-ai-provider.ts
+++ b/packages/server/src/utils/ai/select-ai-provider.ts
@@ -103,8 +103,9 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
return createOpenAICompatible({
name: "gemini",
baseURL: config.apiUrl,
- queryParams: { key: config.apiKey },
- headers: {},
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ },
});
case "custom":
return createOpenAICompatible({
diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts
index dfdcd2cac..14d38ddf0 100644
--- a/packages/server/src/utils/backups/index.ts
+++ b/packages/server/src/utils/backups/index.ts
@@ -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);
}
};
diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts
index fe5417ea5..5eede59d5 100644
--- a/packages/server/src/utils/builders/compose.ts
+++ b/packages/server/src/utils/builders/compose.ts
@@ -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;
@@ -134,6 +134,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
const envVars = getEnviromentVariablesObject(
compose.env,
compose.environment.project.env,
+ compose.environment.env,
);
const exports = Object.entries(envVars)
.map(([key, value]) => `${key}=${quote([value])}`)
diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts
index e2cf4a4a4..aa014a05c 100644
--- a/packages/server/src/utils/cluster/upload.ts
+++ b/packages/server/src/utils/cluster/upload.ts
@@ -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;
}
diff --git a/packages/server/src/utils/traefik/web-server.ts b/packages/server/src/utils/traefik/web-server.ts
index 0209d9a21..e5315dab4 100644
--- a/packages/server/src/utils/traefik/web-server.ts
+++ b/packages/server/src/utils/traefik/web-server.ts
@@ -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);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a1d8e5c0d..cd5ef0a96 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -51,9 +51,6 @@ importers:
'@hono/zod-validator':
specifier: 0.3.0
version: 0.3.0(hono@4.7.10)(zod@3.25.32)
- '@nerimity/mimiqueue':
- specifier: 1.2.3
- version: 1.2.3(redis@4.7.0)
dotenv:
specifier: ^16.4.5
version: 16.4.5
@@ -313,9 +310,6 @@ importers:
fancy-ansi:
specifier: ^0.1.3
version: 0.1.3
- hi-base32:
- specifier: ^0.5.1
- version: 0.5.1
i18next:
specifier: ^23.16.8
version: 23.16.8
@@ -364,9 +358,6 @@ importers:
octokit:
specifier: 3.1.2
version: 3.1.2
- otpauth:
- specifier: ^9.4.0
- version: 9.4.0
pino:
specifier: 9.4.0
version: 9.4.0
@@ -406,9 +397,9 @@ 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
+ semver:
+ specifier: 7.7.3
+ version: 7.7.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
@@ -494,6 +485,9 @@ importers:
'@types/react-dom':
specifier: 18.3.0
version: 18.3.0
+ '@types/semver':
+ specifier: 7.7.1
+ version: 7.7.1
'@types/shell-quote':
specifier: ^1.7.5
version: 1.7.5
@@ -681,9 +675,6 @@ importers:
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32)
- hi-base32:
- specifier: ^0.5.1
- version: 0.5.1
lodash:
specifier: 4.17.21
version: 4.17.21
@@ -708,9 +699,6 @@ importers:
octokit:
specifier: 3.1.2
version: 3.1.2
- otpauth:
- specifier: ^9.4.0
- version: 9.4.0
pino:
specifier: 9.4.0
version: 9.4.0
@@ -732,9 +720,9 @@ 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
+ semver:
+ specifier: 7.7.3
+ version: 7.7.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
@@ -790,6 +778,9 @@ importers:
'@types/react-dom':
specifier: 18.3.0
version: 18.3.0
+ '@types/semver':
+ specifier: 7.7.1
+ version: 7.7.1
'@types/shell-quote':
specifier: ^1.7.5
version: 1.7.5
@@ -1957,11 +1948,6 @@ packages:
cpu: [x64]
os: [win32]
- '@nerimity/mimiqueue@1.2.3':
- resolution: {integrity: sha512-WPoGe417P+S0FLfl3psRBI5adcAWXb917vCF1qD2yGZ1ggBEnMH6UrUK464gzJEOpAlGt8BBbIp0tgCEazZ47A==}
- peerDependencies:
- redis: ^4.7.0
-
'@next/env@16.0.10':
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
@@ -4066,6 +4052,9 @@ packages:
'@types/readable-stream@4.0.20':
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
+ '@types/semver@7.7.1':
+ resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
+
'@types/shell-quote@1.7.5':
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
@@ -4308,9 +4297,6 @@ packages:
assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
- async-await-queue@2.1.4:
- resolution: {integrity: sha512-3DpDtxkKO0O/FPlWbk/CrbexjuSxWm1CH1bXlVNVyMBIkKHhT5D85gzHmGJokG3ibNGWQ7pHBmStxUW/z/0LYQ==}
-
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -5395,9 +5381,6 @@ packages:
help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
- hi-base32@0.5.1:
- resolution: {integrity: sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==}
-
highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@@ -6432,9 +6415,6 @@ packages:
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
- otpauth@9.4.0:
- resolution: {integrity: sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==}
-
p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'}
@@ -7064,10 +7044,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==}
@@ -7097,11 +7073,6 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
- semver@7.7.2:
- resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
- engines: {node: '>=10'}
- hasBin: true
-
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
@@ -8129,7 +8100,7 @@ snapshots:
'@commitlint/is-ignored@19.8.1':
dependencies:
'@commitlint/types': 19.8.1
- semver: 7.7.2
+ semver: 7.7.3
'@commitlint/lint@19.8.1':
dependencies:
@@ -8746,7 +8717,7 @@ snapshots:
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
- semver: 7.7.2
+ semver: 7.7.3
tar: 6.2.1
transitivePeerDependencies:
- encoding
@@ -8772,11 +8743,6 @@ snapshots:
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true
- '@nerimity/mimiqueue@1.2.3(redis@4.7.0)':
- dependencies:
- async-await-queue: 2.1.4
- redis: 4.7.0
-
'@next/env@16.0.10': {}
'@next/swc-darwin-arm64@16.0.10':
@@ -9337,7 +9303,7 @@ snapshots:
'@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.28.0
forwarded-parse: 2.1.2
- semver: 7.7.2
+ semver: 7.7.3
transitivePeerDependencies:
- supports-color
@@ -9538,7 +9504,7 @@ snapshots:
'@types/shimmer': 1.2.0
import-in-the-middle: 1.14.2
require-in-the-middle: 7.5.2
- semver: 7.7.2
+ semver: 7.7.3
shimmer: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -9683,7 +9649,7 @@ snapshots:
'@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
- semver: 7.7.2
+ semver: 7.7.3
'@opentelemetry/semantic-conventions@1.28.0': {}
@@ -11431,6 +11397,8 @@ snapshots:
dependencies:
'@types/node': 20.17.51
+ '@types/semver@7.7.1': {}
+
'@types/shell-quote@1.7.5': {}
'@types/shimmer@1.2.0': {}
@@ -11683,8 +11651,6 @@ snapshots:
assertion-error@1.1.0: {}
- async-await-queue@2.1.4: {}
-
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
@@ -11830,7 +11796,7 @@ snapshots:
lodash: 4.17.21
msgpackr: 1.11.4
node-abort-controller: 3.1.1
- semver: 7.7.2
+ semver: 7.7.3
tslib: 2.8.1
uuid: 9.0.1
transitivePeerDependencies:
@@ -12346,7 +12312,7 @@ snapshots:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
- semver: 7.7.2
+ semver: 7.7.3
electron-to-chromium@1.5.159: {}
@@ -12660,7 +12626,7 @@ snapshots:
'@petamoriken/float16': 3.9.2
debug: 4.4.1
env-paths: 3.0.0
- semver: 7.7.2
+ semver: 7.7.3
shell-quote: 1.8.2
which: 4.0.0
transitivePeerDependencies:
@@ -12834,8 +12800,6 @@ snapshots:
help-me@5.0.0: {}
- hi-base32@0.5.1: {}
-
highlight.js@10.7.3: {}
highlightjs-vue@1.0.0: {}
@@ -13148,7 +13112,7 @@ snapshots:
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
- semver: 7.7.2
+ semver: 7.7.3
jss-plugin-camel-case@10.10.0:
dependencies:
@@ -13972,10 +13936,6 @@ snapshots:
openapi-types@12.1.3: {}
- otpauth@9.4.0:
- dependencies:
- '@noble/hashes': 1.7.1
-
p-cancelable@3.0.0: {}
p-limit@2.3.0:
@@ -14660,8 +14620,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:
@@ -14686,10 +14644,7 @@ snapshots:
semver@6.3.1: {}
- semver@7.7.2: {}
-
- semver@7.7.3:
- optional: true
+ semver@7.7.3: {}
serialize-error-cjs@0.1.4: {}
diff --git a/schema.dbml b/schema.dbml
index 5823a8ff3..d0845f3ed 100644
--- a/schema.dbml
+++ b/schema.dbml
@@ -276,7 +276,7 @@ table application {
replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks']
- railpackVersion text [default: '0.2.2']
+ railpackVersion text [default: '0.15.4']
herokuVersion text [default: '24']
publishDirectory text
isStaticSpa boolean