diff --git a/apps/dokploy/__test__/queues/concurrency.test.ts b/apps/dokploy/__test__/queues/concurrency.test.ts
index c4e9d319f..210789da1 100644
--- a/apps/dokploy/__test__/queues/concurrency.test.ts
+++ b/apps/dokploy/__test__/queues/concurrency.test.ts
@@ -1,16 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
-const hasValidLicense = vi.fn();
const getWebServerSettings = vi.fn();
-const findFirstOrg = vi.fn();
const findFirstServer = vi.fn();
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
- organization: {
- findFirst: (...args: unknown[]) => findFirstOrg(...args),
- },
server: {
findFirst: (...args: unknown[]) => findFirstServer(...args),
},
@@ -19,91 +14,56 @@ vi.mock("@dokploy/server/db", () => ({
}));
vi.mock("@dokploy/server/db/schema", () => ({
- organization: {},
server: {},
}));
-vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
- hasValidLicense: (...args: unknown[]) => hasValidLicense(...args),
-}));
-
vi.mock("@dokploy/server/services/web-server-settings", () => ({
getWebServerSettings: (...args: unknown[]) => getWebServerSettings(...args),
}));
vi.mock("drizzle-orm", () => ({ eq: vi.fn() }));
-import {
- assertBuildsConcurrencyAllowed,
- resolveBuildsConcurrency,
-} from "../../server/queues/concurrency";
+import { resolveBuildsConcurrency } from "../../server/queues/concurrency";
import { LOCAL_PARTITION } from "../../server/queues/in-memory-queue";
-describe("resolveBuildsConcurrency (enterprise gating)", () => {
+describe("resolveBuildsConcurrency", () => {
beforeEach(() => {
vi.clearAllMocks();
- findFirstOrg.mockResolvedValue({ id: "org-1" });
});
describe("local web server partition", () => {
- it("returns the configured concurrency when licensed", async () => {
+ it("returns the configured concurrency", async () => {
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 5 });
- hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(5);
});
- it("clamps to the free max (2) when there is no valid license", async () => {
- getWebServerSettings.mockResolvedValue({ buildsConcurrency: 10 });
- hasValidLicense.mockResolvedValue(false);
-
- await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(2);
- });
-
- it("allows the free max (2) without a license", async () => {
- getWebServerSettings.mockResolvedValue({ buildsConcurrency: 2 });
- hasValidLicense.mockResolvedValue(false);
-
- await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(2);
- });
-
- it("does not cap the value when licensed (N allowed)", async () => {
+ it("does not cap high values", async () => {
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 999 });
- hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(
999,
);
});
+ it("floors values below 1 to 1", async () => {
+ getWebServerSettings.mockResolvedValue({ buildsConcurrency: 0 });
+
+ await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
+ });
+
it("defaults to 1 when settings are missing", async () => {
getWebServerSettings.mockResolvedValue(undefined);
- hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
});
});
describe("remote server partition", () => {
- it("returns the server concurrency when its org is licensed", async () => {
- findFirstServer.mockResolvedValue({
- buildsConcurrency: 4,
- organizationId: "org-1",
- });
- hasValidLicense.mockResolvedValue(true);
+ it("returns the server concurrency", async () => {
+ findFirstServer.mockResolvedValue({ buildsConcurrency: 4 });
await expect(resolveBuildsConcurrency("server-1")).resolves.toBe(4);
- expect(hasValidLicense).toHaveBeenCalledWith("org-1");
- });
-
- it("clamps to the free max (2) when the server org is not licensed", async () => {
- findFirstServer.mockResolvedValue({
- buildsConcurrency: 8,
- organizationId: "org-1",
- });
- hasValidLicense.mockResolvedValue(false);
-
- await expect(resolveBuildsConcurrency("server-1")).resolves.toBe(2);
});
it("defaults to 1 for an unknown server", async () => {
@@ -119,30 +79,3 @@ describe("resolveBuildsConcurrency (enterprise gating)", () => {
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
});
});
-
-describe("assertBuildsConcurrencyAllowed", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it("allows up to the free max (2) without checking the license", async () => {
- await expect(
- assertBuildsConcurrencyAllowed(2, "org-1"),
- ).resolves.toBeUndefined();
- expect(hasValidLicense).not.toHaveBeenCalled();
- });
-
- it("allows more than 2 when licensed", async () => {
- hasValidLicense.mockResolvedValue(true);
- await expect(
- assertBuildsConcurrencyAllowed(5, "org-1"),
- ).resolves.toBeUndefined();
- });
-
- it("rejects more than 2 without a license", async () => {
- hasValidLicense.mockResolvedValue(false);
- await expect(assertBuildsConcurrencyAllowed(3, "org-1")).rejects.toThrow(
- /enterprise license/i,
- );
- });
-});
diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/builds-concurrency.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/builds-concurrency.tsx
index d53709ea1..85c5dad02 100644
--- a/apps/dokploy/components/dashboard/settings/servers/actions/builds-concurrency.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/actions/builds-concurrency.tsx
@@ -4,9 +4,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
-// Free tier may set up to 2 concurrent builds; enterprise unlocks more.
-const FREE_MAX_CONCURRENCY = 2;
-const ENTERPRISE_MAX_CONCURRENCY = 100;
+const MAX_CONCURRENCY = 100;
interface Props {
/**
@@ -20,14 +18,10 @@ interface Props {
/**
* Control to set the number of concurrent builds, either for a remote server
- * (`serverId` provided) or the local web server (omitted). Available to
- * everyone self-hosted up to FREE_MAX_CONCURRENCY; higher values require a
- * valid enterprise license. Not shown in cloud.
+ * (`serverId` provided) or the local web server (omitted). Not shown in cloud.
*/
export const BuildsConcurrency = ({ serverId, label }: Props) => {
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { data: haveValidLicense } =
- api.licenseKey.haveValidLicenseKey.useQuery();
const serverQuery = api.server.one.useQuery(
{ serverId: serverId ?? "" },
@@ -59,10 +53,7 @@ export const BuildsConcurrency = ({ serverId, label }: Props) => {
// Concurrent builds are a self-hosted feature; not shown in cloud.
if (isCloud) return null;
- const max = haveValidLicense
- ? ENTERPRISE_MAX_CONCURRENCY
- : FREE_MAX_CONCURRENCY;
- const clamp = (n: number) => Math.min(max, Math.max(1, n));
+ const clamp = (n: number) => Math.min(MAX_CONCURRENCY, Math.max(1, n));
const handleSave = async () => {
const parsed = clamp(Number.parseInt(value, 10) || 1);
@@ -101,7 +92,7 @@ export const BuildsConcurrency = ({ serverId, label }: Props) => {
setValue(e.target.value)}
className="w-20"
diff --git a/apps/dokploy/pages/dashboard/settings/deployments.tsx b/apps/dokploy/pages/dashboard/settings/deployments.tsx
index 2c40a1b8f..6cc0fc1da 100644
--- a/apps/dokploy/pages/dashboard/settings/deployments.tsx
+++ b/apps/dokploy/pages/dashboard/settings/deployments.tsx
@@ -29,8 +29,6 @@ const Page = () => {
Configure how many deployments can build at the same time on
each server. Builds of the same service are always serialized.
- Free plan allows up to 2 concurrent builds; an enterprise
- license unlocks more.
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index 80e3b2f75..0233173fa 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -46,7 +46,6 @@ import {
redis,
server,
} from "@/server/db/schema";
-import { assertBuildsConcurrencyAllowed } from "@/server/queues/concurrency";
import { applyDockerCleanupSchedule } from "@/server/utils/docker-cleanup";
export const serverRouter = createTRPCRouter({
@@ -491,10 +490,6 @@ export const serverRouter = createTRPCRouter({
message: "You are not authorized to update this server",
});
}
- await assertBuildsConcurrencyAllowed(
- input.buildsConcurrency,
- ctx.session.activeOrganizationId,
- );
return await updateServerById(input.serverId, {
buildsConcurrency: input.buildsConcurrency,
});
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index 9f8bc1fe7..fbc41c459 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -71,7 +71,6 @@ import {
projects,
server,
} from "@/server/db/schema";
-import { assertBuildsConcurrencyAllowed } from "@/server/queues/concurrency";
import { cleanAllDeploymentQueue } from "@/server/queues/queueSetup";
import { removeJob, schedule } from "@/server/utils/backup";
import packageInfo from "../../../package.json";
@@ -480,11 +479,6 @@ export const settingsRouter = createTRPCRouter({
});
}
- await assertBuildsConcurrencyAllowed(
- input.buildsConcurrency,
- ctx.session.activeOrganizationId,
- );
-
await updateWebServerSettings({
buildsConcurrency: input.buildsConcurrency,
});
diff --git a/apps/dokploy/server/queues/concurrency.ts b/apps/dokploy/server/queues/concurrency.ts
index 72c8c6003..0aba01c7e 100644
--- a/apps/dokploy/server/queues/concurrency.ts
+++ b/apps/dokploy/server/queues/concurrency.ts
@@ -1,32 +1,30 @@
import { db } from "@dokploy/server/db";
-import { organization, server } from "@dokploy/server/db/schema";
-import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
+import { server } from "@dokploy/server/db/schema";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
-import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { LOCAL_PARTITION } from "./in-memory-queue";
/**
* Resolve the effective builds concurrency for a queue partition.
*
- * Concurrent deployments (concurrency > 1) are an enterprise feature: without a
- * valid license the effective concurrency is always clamped to 1, so the
- * community experience is unchanged and an expired license degrades gracefully
- * back to sequential deployments instead of breaking anything.
- *
* - `LOCAL_PARTITION` -> concurrency stored on the web server settings (the
- * local Dokploy web server), gated by the owner organization's license.
- * - any other partition -> concurrency stored on the matching `server` row,
- * gated by that server's organization license.
+ * local Dokploy web server).
+ * - any other partition -> concurrency stored on the matching `server` row.
*/
export const resolveBuildsConcurrency = async (
partition: string,
): Promise => {
try {
if (partition === LOCAL_PARTITION) {
- return await resolveLocalConcurrency();
+ const settings = await getWebServerSettings();
+ return normalize(settings?.buildsConcurrency ?? 1);
}
- return await resolveServerConcurrency(partition);
+
+ const currentServer = await db.query.server.findFirst({
+ where: eq(server.serverId, partition),
+ columns: { buildsConcurrency: true },
+ });
+ return normalize(currentServer?.buildsConcurrency ?? 1);
} catch (error) {
console.error(
"Failed to resolve builds concurrency, defaulting to 1",
@@ -36,55 +34,4 @@ export const resolveBuildsConcurrency = async (
}
};
-// Max concurrent builds allowed without an enterprise license. With a valid
-// license the value is unbounded (N) — only the free tier is capped.
-export const FREE_MAX_CONCURRENCY = 2;
-
-const clamp = (value: number, licensed: boolean): number => {
- const min = Math.max(1, Math.floor(value));
- return licensed ? min : Math.min(FREE_MAX_CONCURRENCY, min);
-};
-
-/**
- * Validate a requested builds-concurrency value before persisting it. Free tier
- * may set up to FREE_MAX_CONCURRENCY; anything higher requires a valid
- * enterprise license. Throws a TRPCError when the value is not allowed.
- */
-export const assertBuildsConcurrencyAllowed = async (
- value: number,
- organizationId: string,
-): Promise => {
- if (value <= FREE_MAX_CONCURRENCY) return;
- const licensed = await hasValidLicense(organizationId);
- if (!licensed) {
- throw new TRPCError({
- code: "FORBIDDEN",
- message: `A valid enterprise license is required to set more than ${FREE_MAX_CONCURRENCY} concurrent builds.`,
- });
- }
-};
-
-const resolveLocalConcurrency = async (): Promise => {
- const settings = await getWebServerSettings();
- const buildsConcurrency = settings?.buildsConcurrency ?? 1;
-
- // Self-hosted is single-tenant; gate on any organization's license.
- const anyOrg = await db.query.organization.findFirst({
- columns: { id: true },
- });
- const licensed = anyOrg ? await hasValidLicense(anyOrg.id) : false;
-
- return clamp(buildsConcurrency, licensed);
-};
-
-const resolveServerConcurrency = async (serverId: string): Promise => {
- const currentServer = await db.query.server.findFirst({
- where: eq(server.serverId, serverId),
- columns: { buildsConcurrency: true, organizationId: true },
- });
-
- if (!currentServer) return 1;
-
- const licensed = await hasValidLicense(currentServer.organizationId);
- return clamp(currentServer.buildsConcurrency, licensed);
-};
+const normalize = (value: number): number => Math.max(1, Math.floor(value));
diff --git a/apps/dokploy/server/queues/queueSetup.ts b/apps/dokploy/server/queues/queueSetup.ts
index 0749c364a..b104ef0d0 100644
--- a/apps/dokploy/server/queues/queueSetup.ts
+++ b/apps/dokploy/server/queues/queueSetup.ts
@@ -12,8 +12,8 @@ import type { DeploymentJob } from "./queue-types";
* Deployment queue.
*
* Self-hosted uses an in-memory, per-group FIFO queue with configurable
- * concurrency per server (enterprise-gated). Cloud does not use the queue at
- * all — deployments run directly in the background — so we expose a no-op.
+ * concurrency per server. Cloud does not use the queue at all — deployments
+ * run directly in the background — so we expose a no-op.
*/
interface DeploymentQueue {
diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts
index e9745b533..c5e84d7b7 100644
--- a/packages/server/src/db/schema/web-server-settings.ts
+++ b/packages/server/src/db/schema/web-server-settings.ts
@@ -105,7 +105,7 @@ export const webServerSettings = pgTable("webServerSettings", {
}),
// Deployment Configuration (self-hosted only)
remoteServersOnly: boolean("remoteServersOnly").notNull().default(false),
- // Concurrent builds on the local web server (enterprise-gated to > 1)
+ // Concurrent builds on the local web server
buildsConcurrency: integer("buildsConcurrency").notNull().default(1),
// Auth Configuration (self-hosted only)
enforceSSO: boolean("enforceSSO").notNull().default(false),