Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ Dokploy includes multiple features to make your life easier.
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
- **Custom Wildcard Domains**: Configure wildcard domains at the organization or project level for generated application and preview URLs.
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
- **Docker Management**: Easily deploy and manage Docker containers.
- **CLI/API**: Manage your applications and databases using the command line or through the API.
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
- **Multi Server**: Deploy and manage your applications remotely to external servers.
- **Self-Hosted**: Self-host Dokploy on your VPS.

Custom wildcard domains cascade from organizations down to projects, and preview deployments automatically pick up those settings unless an application-level preview wildcard override is configured. Domain suggestions in the dashboard also respect these wildcard settings when generating preview URLs.

## 🚀 Getting Started

To get started, run the following command on a VPS:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
generateApplicationDomain,
generatePreviewDeploymentDomain,
generateCustomWildcardDomain,
getProjectWildcardDomain,
} from "@dokploy/server";

// Mock the project service
vi.mock("@dokploy/server/services/project", async () => {
const actual = await vi.importActual<
typeof import("@dokploy/server/services/project")
>("@dokploy/server/services/project");
return {
...actual,
getProjectWildcardDomain: vi.fn(),
};
});

// Import after mocking to get the mocked version
import * as projectService from "@dokploy/server/services/project";

afterEach(() => {
vi.restoreAllMocks();
});

describe("generateApplicationDomain", () => {
it("uses project wildcard domains when available", async () => {
vi.mocked(projectService.getProjectWildcardDomain).mockResolvedValue(
"*.apps.example.com",
);

const domain = await generateApplicationDomain(
"my-application",
"user-1",
"project-1",
);

expect(domain).toMatch(/my-application-[0-9a-f]{6}\.apps\.example\.com/);
});

it("falls back to traefik.me when no wildcard domain is configured", async () => {
vi.mocked(projectService.getProjectWildcardDomain).mockResolvedValue(null);
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "development";

const domain = await generateApplicationDomain("app", "user-2");

process.env.NODE_ENV = originalEnv;

expect(domain).toMatch(/app-[0-9a-f]{6}\.traefik\.me/);
});
});

describe("generatePreviewDeploymentDomain", () => {
it("prefers the application preview wildcard when provided", async () => {
const traefikMock = vi.fn();

const domain = await generatePreviewDeploymentDomain(
"preview-app",
"user-1",
"project-1",
"server-1",
"*-preview.example.com",
{ fallbackGenerator: traefikMock },
);

expect(domain).toMatch(/preview-app-[0-9a-f]{6}-preview\.example\.com/);
expect(projectService.getProjectWildcardDomain).not.toHaveBeenCalled();
expect(traefikMock).not.toHaveBeenCalled();
});

it("uses project wildcard domain when no preview wildcard is set", async () => {
vi.mocked(projectService.getProjectWildcardDomain).mockResolvedValue(
"*.apps.example.com",
);
const traefikMock = vi.fn();

const domain = await generatePreviewDeploymentDomain(
"preview-app",
"user-2",
"project-1",
undefined,
undefined,
{ fallbackGenerator: traefikMock },
);

expect(domain).toMatch(/preview-app-[0-9a-f]{6}\.apps\.example\.com/);
expect(traefikMock).not.toHaveBeenCalled();
});

it("falls back to traefik.me when no wildcard domains exist", async () => {
vi.mocked(projectService.getProjectWildcardDomain).mockResolvedValue(null);
const traefikMock = vi
.fn()
.mockResolvedValue("preview-app-a1b2c3.traefik.me");

const domain = await generatePreviewDeploymentDomain(
"preview-app",
"user-3",
undefined,
undefined,
undefined,
{ fallbackGenerator: traefikMock },
);

expect(domain).toBe("preview-app-a1b2c3.traefik.me");
expect(traefikMock).toHaveBeenCalledWith(
"preview-app",
"user-3",
undefined,
);
});
});

describe("generateCustomWildcardDomain", () => {
it("replaces the wildcard token with the app name and a hash", () => {
const domain = generateCustomWildcardDomain({
appName: "blog",
wildcardDomain: "*-apps.example.com",
});

expect(domain).toMatch(/blog-[0-9a-f]{6}-apps\.example\.com/);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getProjectWildcardDomain } from "@dokploy/server";

// Mock the database
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
projects: {
findFirst: vi.fn(),
},
},
},
}));

import { db } from "@dokploy/server/db";

beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

describe("getProjectWildcardDomain", () => {
it("returns the project wildcard when set", async () => {
vi.mocked(db.query.projects.findFirst).mockResolvedValue({
wildcardDomain: "*.project.example.com",
useOrganizationWildcard: true,
organization: { wildcardDomain: "*.org.example.com" },
} as never);

const result = await getProjectWildcardDomain("project-1");

expect(result).toBe("*.project.example.com");
expect(db.query.projects.findFirst).toHaveBeenCalledWith({
where: expect.anything(),
with: { organization: true },
});
});

it("falls back to the organization's wildcard when inheritance is enabled", async () => {
vi.mocked(db.query.projects.findFirst).mockResolvedValue({
wildcardDomain: null,
useOrganizationWildcard: true,
organization: { wildcardDomain: "*.org.example.com" },
} as never);

const result = await getProjectWildcardDomain("project-2");

expect(result).toBe("*.org.example.com");
});

it("returns null when neither project nor organization wildcards are available", async () => {
vi.mocked(db.query.projects.findFirst).mockResolvedValue({
wildcardDomain: null,
useOrganizationWildcard: false,
organization: { wildcardDomain: "*.org.example.com" },
} as never);

const result = await getProjectWildcardDomain("project-3");

expect(result).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,16 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
serverId: application?.serverId || "",
});

// Get the project ID to check for custom wildcard domain
const projectId = application?.environment?.projectId;

// Fetch the effective wildcard domain for the project
const { data: effectiveWildcard } =
api.domain.getEffectiveWildcardDomain.useQuery(
{ projectId: projectId || "" },
{ enabled: !!projectId },
);

const {
data: services,
isFetching: isLoadingServices,
Expand Down Expand Up @@ -527,6 +537,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
generateDomain({
appName: application?.appName || "",
serverId: application?.serverId || "",
projectId: projectId || undefined,
})
.then((domain) => {
field.onChange(domain);
Expand All @@ -542,9 +553,18 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
className="max-w-[12rem]"
>
<p>Generate traefik.me domain</p>
{effectiveWildcard ? (
<p>
Generate domain using: <br />
<code className="text-xs">
{effectiveWildcard}
</code>
</p>
) : (
<p>Generate traefik.me domain</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,22 @@ export const AddPreviewDomain = ({
},
);

const projectId =
previewDeployment?.application.environment.projectId ?? undefined;

const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();

const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();

const { data: effectiveWildcard } =
api.domain.getEffectiveWildcardDomain.useQuery(
{ projectId: projectId || "" },
{ enabled: !!projectId },
);

const form = useForm<Domain>({
resolver: zodResolver(domain),
});
Expand Down Expand Up @@ -185,6 +194,11 @@ export const AddPreviewDomain = ({
serverId:
previewDeployment?.application
?.serverId || "",
projectId,
domainType: "preview",
previewWildcard:
previewDeployment?.application
?.previewWildcard || undefined,
})
.then((domain) => {
field.onChange(domain);
Expand All @@ -200,9 +214,18 @@ export const AddPreviewDomain = ({
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
className="max-w-[12rem]"
>
<p>Generate traefik.me domain</p>
{effectiveWildcard ? (
<p>
Generate domain using: <br />
<code className="text-xs">
{effectiveWildcard}
</code>
</p>
) : (
<p>Generate traefik.me domain</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
Expand Down
Loading
Loading