+
+
{data?.name}
diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 34f05e9ed..24c8dee1a 100644
--- a/apps/dokploy/server/api/routers/compose.ts
+++ b/apps/dokploy/server/api/routers/compose.ts
@@ -43,6 +43,7 @@ import {
import {
type CompleteTemplate,
fetchTemplateFiles,
+ fetchTemplateLogo,
fetchTemplatesList,
} from "@dokploy/server/templates/github";
import { processTemplate } from "@dokploy/server/templates/processors";
@@ -601,7 +602,10 @@ export const composeRouter = createTRPCRouter({
}
}
- const template = await fetchTemplateFiles(input.id, input.baseUrl);
+ const [template, templateLogo] = await Promise.all([
+ fetchTemplateFiles(input.id, input.baseUrl),
+ fetchTemplateLogo(input.id, input.baseUrl),
+ ]);
let serverIp = "127.0.0.1";
@@ -639,6 +643,7 @@ export const composeRouter = createTRPCRouter({
name: input.id,
sourceType: "raw",
appName: appName,
+ icon: templateLogo,
});
await addNewService(ctx, compose.composeId);
diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts
index ce55fa8e1..495048d18 100644
--- a/packages/server/src/db/schema/compose.ts
+++ b/packages/server/src/db/schema/compose.ts
@@ -98,6 +98,7 @@ export const compose = pgTable("compose", {
.default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
+ icon: text("icon"),
environmentId: text("environmentId")
.notNull()
.references(() => environments.environmentId, { onDelete: "cascade" }),
@@ -190,6 +191,11 @@ const createSchema = createInsertSchema(compose, {
.optional(),
triggerType: z.enum(["push", "tag"]).optional(),
composeStatus: z.enum(["idle", "running", "done", "error"]).optional(),
+ icon: z
+ .string()
+ .max(2 * 1024 * 1024, "Icon must be less than 2MB")
+ .nullable()
+ .optional(),
serviceNetworks: z
.array(
z.object({
diff --git a/packages/server/src/services/environment.ts b/packages/server/src/services/environment.ts
index 5f500efb1..c4c08ac02 100644
--- a/packages/server/src/services/environment.ts
+++ b/packages/server/src/services/environment.ts
@@ -168,6 +168,7 @@ export const findEnvironmentById = async (environmentId: string) => {
composeStatus: true,
description: true,
serverId: true,
+ icon: true,
},
},
libsql: {
@@ -239,7 +240,12 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
with: { server: { columns: { name: true } } },
},
compose: {
- columns: { ...serviceColumns, composeId: true, composeStatus: true },
+ columns: {
+ ...serviceColumns,
+ composeId: true,
+ composeStatus: true,
+ icon: true,
+ },
with: { server: { columns: { name: true } } },
},
libsql: {
diff --git a/packages/server/src/templates/github.ts b/packages/server/src/templates/github.ts
index 5b5c4ade8..f3bdca2ba 100644
--- a/packages/server/src/templates/github.ts
+++ b/packages/server/src/templates/github.ts
@@ -74,6 +74,49 @@ export async function fetchTemplatesList(
}));
}
+const LOGO_MIME_TYPES: Record
= {
+ png: "image/png",
+ jpg: "image/jpeg",
+ jpeg: "image/jpeg",
+ svg: "image/svg+xml",
+ webp: "image/webp",
+ gif: "image/gif",
+};
+
+export async function fetchTemplateLogo(
+ templateId: string,
+ baseUrl = "https://templates.dokploy.com",
+): Promise {
+ try {
+ const templates = await fetchTemplatesList(baseUrl);
+ const template = templates.find((t) => t.id === templateId);
+ if (!template?.logo) return null;
+
+ const response = await fetch(
+ `${baseUrl}/blueprints/${templateId}/${template.logo}`,
+ { signal: AbortSignal.timeout(10000) },
+ );
+ if (!response.ok) return null;
+
+ const buffer = Buffer.from(await response.arrayBuffer());
+ if (buffer.length === 0) return null;
+
+ const contentType = response.headers.get("content-type")?.split(";")[0];
+ const extension = template.logo.split(".").pop()?.toLowerCase() ?? "";
+ const mimeType = contentType?.startsWith("image/")
+ ? contentType
+ : LOGO_MIME_TYPES[extension];
+ if (!mimeType) return null;
+
+ const dataUrl = `data:${mimeType};base64,${buffer.toString("base64")}`;
+ if (dataUrl.length > 2 * 1024 * 1024) return null;
+
+ return dataUrl;
+ } catch {
+ return null;
+ }
+}
+
/**
* Fetches a specific template's files
*/