From eb9d140c5de212f0217869f0bd9cf52b4570e7d2 Mon Sep 17 00:00:00 2001 From: bdkopen Date: Mon, 5 Jan 2026 21:12:55 -0500 Subject: [PATCH 01/16] chore: uninstall ununused hi-base32 package --- apps/dokploy/package.json | 1 - packages/server/package.json | 1 - pnpm-lock.yaml | 11 ----------- 3 files changed, 13 deletions(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index c8bb95056..d6e08e16e 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -109,7 +109,6 @@ "drizzle-orm": "^0.39.3", "drizzle-zod": "0.5.1", "fancy-ansi": "^0.1.3", - "hi-base32": "^0.5.1", "i18next": "^23.16.8", "input-otp": "^1.4.2", "js-cookie": "^3.0.5", diff --git a/packages/server/package.json b/packages/server/package.json index 9ce60fcf7..a9c31d5c5 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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0c06df37..fde1f0948 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,9 +313,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 @@ -678,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 @@ -5389,9 +5383,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==} @@ -12824,8 +12815,6 @@ snapshots: help-me@5.0.0: {} - hi-base32@0.5.1: {} - highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} From 016aa0248a81d1cb388d571e3dbf929bed6f9732 Mon Sep 17 00:00:00 2001 From: bdkopen Date: Mon, 5 Jan 2026 22:27:57 -0500 Subject: [PATCH 02/16] chore: uninstall unused `otpauth` package --- apps/dokploy/package.json | 1 - packages/server/package.json | 1 - pnpm-lock.yaml | 13 ------------- 3 files changed, 15 deletions(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index c8bb95056..bd1c2856d 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -126,7 +126,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", diff --git a/packages/server/package.json b/packages/server/package.json index 9ce60fcf7..d6a3ac143 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -67,7 +67,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0c06df37..c478e2aa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,9 +364,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 @@ -705,9 +702,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 @@ -6426,9 +6420,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'} @@ -13962,10 +13953,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: From d12f029e2bd89e21c47efa1fade80a688e19e49a Mon Sep 17 00:00:00 2001 From: bdkopen Date: Sat, 10 Jan 2026 00:11:26 -0500 Subject: [PATCH 03/16] chore: uninstall `@nerimity/mimiqueue` --- apps/api/package.json | 1 - pnpm-lock.yaml | 18 ------------------ 2 files changed, 19 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index dfc2a355d..0f4b1044f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -13,7 +13,6 @@ "@dokploy/server": "workspace:*", "@hono/node-server": "^1.14.3", "@hono/zod-validator": "0.3.0", - "@nerimity/mimiqueue": "1.2.3", "dotenv": "^16.4.5", "hono": "^4.7.10", "pino": "9.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25890f46f..ffa8ee5df 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 @@ -1939,11 +1936,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==} @@ -4290,9 +4282,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==} @@ -8744,11 +8733,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': @@ -11655,8 +11639,6 @@ snapshots: assertion-error@1.1.0: {} - async-await-queue@2.1.4: {} - asynckit@0.4.0: {} atomic-sleep@1.0.0: {} From 1e11f603de421a1c2692eed3b02b8daa091a2c7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:41:46 +0000 Subject: [PATCH 04/16] Initial plan From 14d359dd142b45e62d4d9f789474f08e59238c72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:45:17 +0000 Subject: [PATCH 05/16] Fix GitLab View Repository links to use correct URL and namespace Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- .../general/generic/save-gitlab-provider.tsx | 4 ++-- .../generic/save-gitlab-provider-compose.tsx | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index d6f65caf3..6197fc49f 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -232,9 +232,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
Repository - {field.value.owner && field.value.repo && ( + {field.value.gitlabPathNamespace && ( { const repository = form.watch("repository"); const gitlabId = form.watch("gitlabId"); + const gitlabUrl = useMemo(() => { + const url = gitlabProviders?.find( + (provider) => provider.gitlabId === gitlabId, + )?.gitlabUrl; + + const gitlabUrl = url?.replace(/\/$/, ""); + + return gitlabUrl || "https://gitlab.com"; + }, [gitlabId, gitlabProviders]); + const { data: repositories, isLoading: isLoadingRepositories, @@ -224,9 +234,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
Repository - {field.value.owner && field.value.repo && ( + {field.value.gitlabPathNamespace && ( Date: Sun, 11 Jan 2026 18:17:19 -0600 Subject: [PATCH 06/16] feat(stripe): add customer_email to payment metadata --- apps/dokploy/server/api/routers/stripe.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index d2a000324..2d1556a27 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -81,6 +81,7 @@ export const stripeRouter = createTRPCRouter({ metadata: { adminId: owner.id, }, + customer_email: owner.email, allow_promotion_codes: true, success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`, cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`, From 4001f1d067c7da62d343b794acc554ecc77cf127 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 18:27:19 -0600 Subject: [PATCH 07/16] feat(billing): implement invoice display and retrieval functionality - Added `ShowInvoices` component to display user invoices with status and actions. - Integrated Stripe API to fetch invoices for the authenticated user. - Updated `ShowBilling` component to conditionally render invoices if the user has a Stripe customer ID. --- .../settings/billing/show-billing.tsx | 7 +- .../settings/billing/show-invoices.tsx | 159 ++++++++++++++++++ apps/dokploy/server/api/routers/stripe.ts | 35 ++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index ac211a1c5..faa5bcdcc 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -24,6 +24,7 @@ import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; +import { ShowInvoices } from "./show-invoices"; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, @@ -75,8 +76,8 @@ export const ShowBilling = () => { const safePercentage = Math.min(percentage, 100); return ( -
- +
+
@@ -319,6 +320,8 @@ export const ShowBilling = () => {
+ + {admin?.user.stripeCustomerId && }
); }; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx new file mode 100644 index 000000000..513bb3dc8 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx @@ -0,0 +1,159 @@ +import { Download, ExternalLink, FileText, Loader2 } from "lucide-react"; +import type Stripe from "stripe"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { api } from "@/utils/api"; + +const formatDate = (timestamp: number | null) => { + 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 ( + +
+ + + + Invoices + + + View and download your billing invoices + + + + {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/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 2d1556a27..3354c3311 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -129,4 +129,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 []; + } + }), }); From 4e0cb2a9c7ca4051a019d534c84e4252d3704c6f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 18:34:14 -0600 Subject: [PATCH 08/16] feat(billing): add billing invoices page and update billing components - Introduced `ShowBillingInvoices` component to manage and display billing invoices. - Updated `ShowBilling` component to include navigation for invoices and enhanced subscription management. - Refactored `ShowInvoices` component for improved loading and display logic. - Created a new invoices page with server-side validation and layout integration. --- .../billing/show-billing-invoices.tsx | 74 +++ .../settings/billing/show-billing.tsx | 500 ++++++++++-------- .../settings/billing/show-invoices.tsx | 176 +++--- .../pages/dashboard/settings/invoices.tsx | 63 +++ 4 files changed, 483 insertions(+), 330 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx create mode 100644 apps/dokploy/pages/dashboard/settings/invoices.tsx 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 faa5bcdcc..1d79903cf 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"; @@ -24,7 +26,6 @@ import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { ShowInvoices } from "./show-invoices"; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, @@ -38,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,252 +92,274 @@ export const ShowBilling = () => { const safePercentage = Math.min(percentage, 100); return ( -
- -
- +
+ +
+ Billing - Manage your subscription + + Manage your subscription and invoices + - -
- setIsAnnual(e === "annual")} - > - - Monthly - Annual - - - {admin?.user.stripeSubscriptionId && ( -
-

Servers Plan

-

- You have {servers} server on your plan of{" "} - {admin?.user.serversQuantity} servers -

-
- -
- {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( -
- - - You have reached the maximum number of servers you can - create, please upgrade your plan to add more servers. - + + + +
+ setIsAnnual(e === "annual")} + > + + Monthly + Annual + + + {admin?.user.stripeSubscriptionId && ( +
+

Servers Plan

+

+ You have {servers} server on your plan of{" "} + {admin?.user.serversQuantity} servers +

+
+ +
+ {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( +
+ + + You have reached the maximum number of servers you can + create, please upgrade your plan to add more servers. + +
+ )}
)} -
- )} -
- - Need Help? We are here to help you. - - - Join to our Discord server and we will help you. - - -
- {isLoading ? ( - - Loading... - - - ) : ( - <> - {products?.map((product) => { - const featured = true; - return ( -
-
+ + Need Help? We are here to help you. + + + Join to our Discord server and we will help you. + + - { - setServerQuantity( - e.target.value as unknown as number, - ); - }} - /> - - -
-
0 - ? "justify-between" - : "justify-end", - "flex flex-row items-center gap-2 mt-4", + + + Join Discord + + +
+ {isLoading ? ( + + Loading... + + + ) : ( + <> + {products?.map((product) => { + const featured = true; + return ( +
+
- {admin?.user.stripeCustomerId && ( - - )} - - {data?.subscriptions?.length === 0 && ( -
- + {isAnnual && ( +
+ Recommended 🚀
)} -
+ {isAnnual ? ( +
+

+ ${" "} + {calculatePrice( + serverQuantity, + isAnnual, + ).toFixed(2)}{" "} + USD +

+ | +

+ ${" "} + {( + calculatePrice(serverQuantity, isAnnual) / 12 + ).toFixed(2)}{" "} + / Month USD +

+
+ ) : ( +

+ ${" "} + {calculatePrice(serverQuantity, isAnnual).toFixed( + 2, + )}{" "} + USD +

+ )} +

+ {product.name} +

+

+ {product.description} +

+ +
    + {[ + "All the features of Dokploy", + "Unlimited deployments", + "Self-hosted on your own infrastructure", + "Full access to all deployment features", + "Dokploy integration", + "Backups", + "All Incoming features", + ].map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ + {serverQuantity} Servers + +
+ +
+ + { + setServerQuantity( + e.target.value as unknown as number, + ); + }} + /> + + +
+
0 + ? "justify-between" + : "justify-end", + "flex flex-row items-center gap-2 mt-4", + )} + > + {admin?.user.stripeCustomerId && ( + + )} + + {data?.subscriptions?.length === 0 && ( +
+ +
+ )} +
+
+
- -
- ); - })} - - )} -
+ ); + })} + + )} +
- - {admin?.user.stripeCustomerId && }
); }; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx index 513bb3dc8..73cc82efc 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx @@ -2,13 +2,6 @@ import { Download, ExternalLink, FileText, Loader2 } from "lucide-react"; import type Stripe from "stripe"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Table, TableBody, @@ -63,97 +56,82 @@ export const ShowInvoices = () => { const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery(); return ( - -
- - - - Invoices - - - View and download your billing invoices - - - - {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 -

-
- )} -
-
-
+
+ {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/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(), + }, + }; +} From edc8efe81657e938aae75be1ae8c11b5a10db107 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 19:21:29 -0600 Subject: [PATCH 09/16] refactor(servers): replace DropdownMenuItem with Button for Setup Server action - Updated the SetupServer component to use a Button instead of DropdownMenuItem for better accessibility and user experience. - Enhanced the ShowServers component by adding tooltips for the Setup Server action, providing users with additional context on server configuration. --- .../settings/servers/setup-server.tsx | 11 +++-- .../settings/servers/show-servers.tsx | 45 +++++++++++-------- 2 files changed, 31 insertions(+), 25 deletions(-) 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 85e7f3ee7..92d6fc5c3 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -6,9 +6,7 @@ import { Loader2, MoreHorizontal, Network, - Pencil, ServerIcon, - Settings, Terminal, Trash2, User, @@ -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

-
-
-
From f3039623191ae6f66e810b2a74bd6e560265e238 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 20:21:41 -0600 Subject: [PATCH 10/16] fix(database): update container name query to use exact match - Modified the SQL queries in GetLastNContainerMetrics and GetAllMetricsContainer functions to use an exact match for container names instead of a LIKE clause, improving query accuracy and performance. --- apps/monitoring/database/containers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 2acaaede3733fe0fa83bbef33e4d43040f12cf90 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:22:33 +0000 Subject: [PATCH 11/16] [autofix.ci] apply automated fixes --- .../settings/billing/show-billing.tsx | 438 +++++++++--------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index 1d79903cf..1460244c1 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -128,235 +128,235 @@ export const ShowBilling = () => {
- setIsAnnual(e === "annual")} - > - - Monthly - Annual - - - {admin?.user.stripeSubscriptionId && ( -
-

Servers Plan

-

- You have {servers} server on your plan of{" "} - {admin?.user.serversQuantity} servers -

-
- -
- {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( -
- - - You have reached the maximum number of servers you can - create, please upgrade your plan to add more servers. - -
- )} + setIsAnnual(e === "annual")} + > + + Monthly + Annual + + + {admin?.user.stripeSubscriptionId && ( +
+

Servers Plan

+

+ You have {servers} server on your plan of{" "} + {admin?.user.serversQuantity} servers +

+
+ +
+ {admin && admin.user.serversQuantity! <= (servers ?? 0) && ( +
+ + + You have reached the maximum number of servers you can + create, please upgrade your plan to add more servers. +
)} -
- - Need Help? We are here to help you. - - - Join to our Discord server and we will help you. - - +
+ {isLoading ? ( + + Loading... + + + ) : ( + <> + {products?.map((product) => { + const featured = true; + return ( +
+
- - - Join Discord - - -
- {isLoading ? ( - - Loading... - - - ) : ( - <> - {products?.map((product) => { - const featured = true; - return ( -
-
+ Recommended 🚀 +
+ )} + {isAnnual ? ( +
+

+ ${" "} + {calculatePrice( + serverQuantity, + isAnnual, + ).toFixed(2)}{" "} + USD +

+ | +

+ ${" "} + {( + calculatePrice(serverQuantity, isAnnual) / 12 + ).toFixed(2)}{" "} + / Month USD +

+
+ ) : ( +

+ ${" "} + {calculatePrice(serverQuantity, isAnnual).toFixed( + 2, + )}{" "} + USD +

+ )} +

+ {product.name} +

+

+ {product.description} +

+ +
    + {[ + "All the features of Dokploy", + "Unlimited deployments", + "Self-hosted on your own infrastructure", + "Full access to all deployment features", + "Dokploy integration", + "Backups", + "All Incoming features", + ].map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ + {serverQuantity} Servers + +
+ +
+ + { + setServerQuantity( + e.target.value as unknown as number, + ); + }} + /> + + +
+
0 + ? "justify-between" + : "justify-end", + "flex flex-row items-center gap-2 mt-4", )} > - {isAnnual && ( -
- Recommended 🚀 -
- )} - {isAnnual ? ( -
-

- ${" "} - {calculatePrice( - serverQuantity, - isAnnual, - ).toFixed(2)}{" "} - USD -

- | -

- ${" "} - {( - calculatePrice(serverQuantity, isAnnual) / 12 - ).toFixed(2)}{" "} - / Month USD -

-
- ) : ( -

- ${" "} - {calculatePrice(serverQuantity, isAnnual).toFixed( - 2, - )}{" "} - USD -

- )} -

- {product.name} -

-

- {product.description} -

+ {admin?.user.stripeCustomerId && ( + - { - setServerQuantity( - e.target.value as unknown as number, - ); - }} - /> - - -
-
0 - ? "justify-between" - : "justify-end", - "flex flex-row items-center gap-2 mt-4", - )} + window.open(session.url); + }} > - {admin?.user.stripeCustomerId && ( - + )} - window.open(session.url); - }} - > - Manage Subscription - - )} - - {data?.subscriptions?.length === 0 && ( -
- -
- )} + {data?.subscriptions?.length === 0 && ( +
+
-
- + )} +
- ); - })} - - )} -
+ +
+ ); + })} + + )} +
From 6d94da1dee88129ed2d6c02a6fcc690c130891d5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 20:44:16 -0600 Subject: [PATCH 12/16] feat(backup): add functionality to keep the latest N backups after running a backup --- apps/dokploy/server/api/routers/backup.ts | 1 + 1 file changed, 1 insertion(+) 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 From 85424badcfc818bda3c3e16d368ba52ba6921b1c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 21:51:56 -0600 Subject: [PATCH 13/16] chore(dependencies): update semver to version 7.7.3 and add @types/semver to package.json files; refactor getUpdateData function to accept current version as a parameter --- apps/dokploy/package.json | 4 +- apps/dokploy/server/api/routers/settings.ts | 2 +- packages/server/package.json | 4 +- packages/server/src/services/settings.ts | 102 ++++++++++++-------- pnpm-lock.yaml | 45 +++++---- 5 files changed, 98 insertions(+), 59 deletions(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 375ecbe69..34df486e2 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -153,9 +153,11 @@ "xterm-addon-fit": "^0.8.0", "yaml": "2.8.1", "zod": "^3.25.32", - "zod-form-data": "^2.0.7" + "zod-form-data": "^2.0.7", + "semver": "7.7.3" }, "devDependencies": { + "@types/semver": "7.7.1", "@types/shell-quote": "^1.7.5", "@types/adm-zip": "^0.5.7", "@types/bcrypt": "5.0.2", diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index bd182527a..b681da9ca 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -399,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) { diff --git a/packages/server/package.json b/packages/server/package.json index 789924d51..5a7b0037e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -78,9 +78,11 @@ "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", diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 277008bd3..65172f110 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,82 @@ 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") { + 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 { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25890f46f..156a583d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -400,6 +400,9 @@ importers: recharts: specifier: ^2.15.3 version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + semver: + specifier: 7.7.3 + version: 7.7.3 shell-quote: specifier: ^1.8.1 version: 1.8.2 @@ -485,6 +488,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 @@ -717,6 +723,9 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + semver: + specifier: 7.7.3 + version: 7.7.3 shell-quote: specifier: ^1.8.1 version: 1.8.2 @@ -772,6 +781,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 @@ -4048,6 +4060,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==} @@ -7069,11 +7084,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'} @@ -8101,7 +8111,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: @@ -8718,7 +8728,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 @@ -9309,7 +9319,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 @@ -9510,7 +9520,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 @@ -9655,7 +9665,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': {} @@ -11403,6 +11413,8 @@ snapshots: dependencies: '@types/node': 20.17.51 + '@types/semver@7.7.1': {} + '@types/shell-quote@1.7.5': {} '@types/shimmer@1.2.0': {} @@ -11802,7 +11814,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: @@ -12318,7 +12330,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: {} @@ -12632,7 +12644,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: @@ -13118,7 +13130,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: @@ -14650,10 +14662,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: {} From 11af6a5eb9195a8ea4348b8a264f73b102a29a93 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 21:58:04 -0600 Subject: [PATCH 14/16] feat(docker): enhance reloadDockerResource to accept version parameter for dokploy updates - Updated the reloadDockerResource function to include an optional version parameter. - Modified the command for updating the dokploy service to specify the image version during updates. --- apps/dokploy/server/api/routers/settings.ts | 2 +- packages/server/src/services/settings.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index b681da9ca..c9d21e515 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -88,7 +88,7 @@ export const settingsRouter = createTRPCRouter({ if (IS_CLOUD) { return true; } - await reloadDockerResource("dokploy"); + await reloadDockerResource("dokploy", undefined, packageInfo.version); return true; }), cleanRedis: adminProcedure.mutation(async () => { diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 65172f110..7dd13996f 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -280,11 +280,16 @@ 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") { + command = `docker service update --force --image dokploy/dokploy:${version} ${resourceName}`; + } else { + command = `docker service update --force ${resourceName}`; + } } else if (resourceType === "standalone") { command = `docker restart ${resourceName}`; } else { From 167daccee04d6a246c2c1e92bd26cb432d1c7fd5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 11 Jan 2026 22:12:39 -0600 Subject: [PATCH 15/16] feat(settings): enhance getUpdateData and reloadDockerResource for image digest comparison - Added logic to getUpdateData to compare current and latest image digests for canary and feature tags, indicating if an update is available. - Updated reloadDockerResource to ensure the correct image tag is used during dokploy service updates based on the current image tag. --- packages/server/src/services/settings.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 7dd13996f..4235376d9 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -87,6 +87,19 @@ export const getUpdateData = async ( // 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, @@ -286,7 +299,13 @@ export const reloadDockerResource = async ( let command = ""; if (resourceType === "service") { if (resourceName === "dokploy") { - command = `docker service update --force --image dokploy/dokploy:${version} ${resourceName}`; + 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}`; } From 0c0944d221bf2e48f27d9095fca74a96c4ac7402 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:16:50 -0600 Subject: [PATCH 16/16] Update package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 34df486e2..c33826adb 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.26.3", + "version": "v0.26.4", "private": true, "license": "Apache-2.0", "type": "module",