Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
86f941d
feat(sidebar): add enterprise badge for valid license in sidebar
Siumauricio Jul 6, 2026
8f9be16
feat(whitelabeling): implement public whitelabeling configuration ret…
Siumauricio Jul 6, 2026
d7b9f01
fix(ui): update project name display in environment page
Siumauricio Jul 6, 2026
6e5ac41
refactor(sso): remove deprecated enterprise authentication card from …
Siumauricio Jul 7, 2026
c9ac723
fix(requests): remove nested ResponsiveContainer breaking chart height
Siumauricio Jul 7, 2026
0d79a0a
Merge pull request #4751 from Dokploy/fix/requests-chart-not-visible
Siumauricio Jul 7, 2026
a296407
fix(ui): enable vertical scroll on collapsed sidebar
imrja8 Jul 7, 2026
d3e0b10
feat(ci): attach install.sh from website repo to each release
Siumauricio Jul 7, 2026
12a9cce
Merge pull request #4757 from Dokploy/feat/attach-install-script-to-r…
Siumauricio Jul 7, 2026
9749d86
feat(ci): pin install.sh release asset to the released version
Siumauricio Jul 7, 2026
215c466
Merge pull request #4758 from Dokploy/feat/pin-install-script-version
Siumauricio Jul 7, 2026
17fdd64
fix(ui): resolve CommandDialog crash on ⌘J shortcut
imrja8 Jul 7, 2026
4e3a6db
fix(databases): resolve crash when opening rebuild database dialog
Siumauricio Jul 7, 2026
82fcdc9
Merge pull request #4763 from Dokploy/fix/rebuild-database-dialog-crash
Siumauricio Jul 7, 2026
98b8630
Merge pull request #4761 from imrja8/fix/command-dialog-crash
Siumauricio Jul 7, 2026
7871ce7
Merge pull request #4755 from imrja8/fix/sidebar-collapsed-scroll
Siumauricio Jul 7, 2026
532c8d0
Bump version from v0.29.10 to v0.29.11
Siumauricio Jul 8, 2026
bb30dc1
chore(ui): fix biome formatting in command.tsx to resolve CI failure
imrja8 Jul 8, 2026
3912c1a
Merge branch 'Dokploy:canary' into fix/command-dialog-crash
imrja8 Jul 8, 2026
e6bfaa2
Merge pull request #4768 from imrja8/fix/command-dialog-crash
Siumauricio Jul 8, 2026
995a04d
chore(ui): reorganize imports across multiple components and tests fo…
Siumauricio Jul 8, 2026
d831607
feat(scim): implement SCIM 2.0 user provisioning support
Siumauricio Jul 8, 2026
b4574aa
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 8, 2026
b3621bc
fix(sso): apply trusted origin changes without server restart
Siumauricio Jul 9, 2026
cffd846
Merge pull request #4776 from Dokploy/fix/sso-trusted-origins-restart
Siumauricio Jul 9, 2026
3c114e2
Merge pull request #4771 from Dokploy/feat/scim
Siumauricio Jul 9, 2026
7924794
perf: share db, docker and auth singletons across duplicated bundles
Siumauricio Jul 9, 2026
856cf33
Merge pull request #4777 from Dokploy/perf/dedupe-module-singletons
Siumauricio Jul 9, 2026
8d0ae19
feat: make concurrent builds an OSS feature without license gating
Siumauricio Jul 9, 2026
6224d57
Merge pull request #4778 from Dokploy/feat/oss-concurrent-builds
Siumauricio Jul 9, 2026
93b7942
chore: update dependencies in pnpm-lock.yaml and package.json
Siumauricio Jul 9, 2026
1c44141
feat: enhance container dashboard with new features
Siumauricio Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/dokploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ jobs:
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Fetch install.sh
run: |
curl -fsSL https://raw.githubusercontent.com/Dokploy/website/main/apps/website/public/install.sh -o install.sh
head -1 install.sh | grep -q '^#!' || { echo "Downloaded install.sh is not a shell script"; exit 1; }
grep -q 'DOKPLOY_VERSION' install.sh || { echo "install.sh no longer supports DOKPLOY_VERSION pinning"; exit 1; }
{ head -1 install.sh; echo "DOKPLOY_VERSION=\"\${DOKPLOY_VERSION:-${{ steps.get_version.outputs.version }}}\""; tail -n +2 install.sh; } > install-pinned.sh
mv install-pinned.sh install.sh

- name: Create Release
uses: softprops/action-gh-release@v2
with:
Expand All @@ -160,6 +168,7 @@ jobs:
generate_release_notes: true
draft: false
prerelease: false
files: install.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,5 @@ EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \
CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1

CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]
# Ejecutar node directamente: pnpm como wrapper queda residente (~100MB RSS)
CMD ["sh", "-c", "node -r dotenv/config dist/wait-for-postgres.mjs && node -r dotenv/config dist/migration.mjs && exec node -r dotenv/config dist/server.mjs"]
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
canEditDeployGitSource,
getAccessibleGitProviderIds,
} from "@dokploy/server/services/git-provider";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockDb = vi.hoisted(() => ({
query: {
Expand Down
91 changes: 12 additions & 79 deletions apps/dokploy/__test__/queues/concurrency.test.ts
Original file line number Diff line number Diff line change
@@ -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),
},
Expand All @@ -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 () => {
Expand All @@ -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,
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Tooltip as TooltipPrimitive } from "radix-ui";
import {
Ban,
CheckCircle2,
Expand All @@ -8,6 +7,7 @@ import {
Terminal,
} from "lucide-react";
import { useRouter } from "next/router";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { toast } from "sonner";
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Tooltip as TooltipPrimitive } from "radix-ui";
import {
ExternalLink,
FileText,
Expand All @@ -9,6 +8,7 @@ import {
RocketIcon,
Trash2,
} from "lucide-react";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { DateTooltip } from "@/components/shared/date-tooltip";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
Expand Down
36 changes: 35 additions & 1 deletion apps/dokploy/components/dashboard/docker/show/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { ColumnDef } from "@tanstack/react-table";
import copy from "copy-to-clipboard";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Expand Down Expand Up @@ -37,6 +40,7 @@ export const columns: ColumnDef<Container>[] = [
},
{
accessorKey: "state",
filterFn: "equals",
header: ({ column }) => {
return (
<Button
Expand All @@ -56,7 +60,7 @@ export const columns: ColumnDef<Container>[] = [
variant={
value === "running"
? "default"
: value === "failed"
: value === "exited" || value === "dead"
? "destructive"
: "secondary"
}
Expand Down Expand Up @@ -99,6 +103,28 @@ export const columns: ColumnDef<Container>[] = [
},
cell: ({ row }) => <div className="lowercase">{row.getValue("image")}</div>,
},
{
accessorKey: "ports",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Ports
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const value = row.getValue("ports") as string;
return (
<div className="max-w-[16rem] truncate lowercase" title={value}>
{value}
</div>
);
},
},
{
id: "actions",
enableHiding: false,
Expand All @@ -115,6 +141,14 @@ export const columns: ColumnDef<Container>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
copy(container.containerId);
toast.success("Container ID copied to clipboard");
}}
>
Copy Container ID
</DropdownMenuItem>
<ShowDockerModalLogs
containerId={container.containerId}
serverId={container.serverId}
Expand Down
Loading
Loading