Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
},
);

const { data: certificateResolvers } =
api.domain.certificateResolvers.useQuery(
{
serverId: application?.serverId || undefined,
},
{
enabled: isOpen,
},
);

const {
data: services,
isFetching: isLoadingServices,
Expand Down Expand Up @@ -234,12 +244,23 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
});

const certificateType = form.watch("certificateType");
const customCertResolver = form.watch("customCertResolver");
const useCustomEntrypoint = form.watch("useCustomEntrypoint");
const https = form.watch("https");
const domainType = form.watch("domainType");
const host = form.watch("host");
const isTraefikMeDomain = host?.includes("sslip.io") || false;

// Synthetic value for the certificate provider Select: detected resolvers
// from traefik.yml are stored as certificateType="custom" +
// customCertResolver=<name>, but displayed as their own option.
const certSelectValue =
certificateType === "custom" &&
customCertResolver &&
certificateResolvers?.includes(customCertResolver)
? `resolver:${customCertResolver}`
: (certificateType ?? "");

useEffect(() => {
if (data) {
form.reset({
Expand Down Expand Up @@ -750,15 +771,29 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
if (value !== "custom") {
if (value.startsWith("resolver:")) {
field.onChange("custom");
form.setValue(
"customCertResolver",
undefined,
value.slice("resolver:".length),
);
} else {
field.onChange(value);
if (
value !== "custom" ||
(customCertResolver &&
certificateResolvers?.includes(
customCertResolver,
))
) {
form.setValue(
"customCertResolver",
undefined,
);
}
}
}}
value={field.value}
value={certSelectValue}
>
<FormControl>
<SelectTrigger>
Expand All @@ -771,6 +806,18 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
Let's Encrypt
</SelectItem>
<SelectItem value={"custom"}>Custom</SelectItem>
{certificateResolvers
?.filter(
(resolver) => resolver !== "letsencrypt",
)
.map((resolver) => (
<SelectItem
key={resolver}
value={`resolver:${resolver}`}
>
{resolver}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Expand Down Expand Up @@ -810,7 +857,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
}}
/>

{certificateType === "custom" && (
{certSelectValue === "custom" && (
<FormField
control={form.control}
name="customCertResolver"
Expand Down
21 changes: 21 additions & 0 deletions apps/dokploy/server/api/routers/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
findPreviewDeploymentById,
findServerById,
generateTraefikMeDomain,
getCertificateResolvers,
getServerIpCandidates,
getWebServerSettings,
IS_CLOUD,
manageDomain,
removeDomain,
removeDomainById,
Expand Down Expand Up @@ -101,6 +103,25 @@ export const domainRouter = createTRPCRouter({
return settings?.serverIp || "";
}),

certificateResolvers: withPermission("domain", "read")
.input(z.object({ serverId: z.string().optional() }))
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this server",
});
}
return getCertificateResolvers(input.serverId);
}
if (IS_CLOUD) {
return [];
}
return getCertificateResolvers();
}),

update: protectedProcedure
.input(apiUpdateDomain)
.mutation(async ({ input, ctx }) => {
Expand Down
24 changes: 24 additions & 0 deletions packages/server/src/utils/traefik/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
import { parse, stringify } from "yaml";
import { execAsyncRemote } from "../process/execAsync";
import {
loadOrCreateConfig,
removeTraefikConfig,
Expand Down Expand Up @@ -109,6 +110,29 @@ export const readMainConfig = () => {
return null;
};

export const getCertificateResolvers = async (
serverId?: string | null,
): Promise<string[]> => {
let yamlStr: string | null = null;
if (serverId) {
const { MAIN_TRAEFIK_PATH } = paths(true);
const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml");
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
yamlStr = stdout || null;
} else {
yamlStr = readMainConfig();
}
if (!yamlStr) return [];
const config = parse(yamlStr) as MainTraefikConfig;
if (
!config?.certificatesResolvers ||
typeof config.certificatesResolvers !== "object"
) {
return [];
}
return Object.keys(config.certificatesResolvers);
};

export const writeMainConfig = (traefikConfig: string) => {
try {
const { MAIN_TRAEFIK_PATH } = paths();
Expand Down