Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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