diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
index 9575c59c42..5444ee1b9d 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.type === "file" && (
Content
-
+
{mount.content}
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
)}
-
- Mount Path
-
- {mount.mountPath}
-
-
+ {mount.type === "file" ? (
+
+ File Path
+
+ {mount.filePath}
+
+
+ ) : (
+
+ Mount Path
+
+ {mount.mountPath}
+
+
+ )}
{
- Add a project
+ {projectId ? "Update" : "Add a"} project
The home of something big!
{isError && {error?.message}}
diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx
index c4b2f672de..afeabf309f 100644
--- a/apps/dokploy/components/dashboard/projects/show.tsx
+++ b/apps/dokploy/components/dashboard/projects/show.tsx
@@ -87,9 +87,12 @@ export const ShowProjects = () => {
Create and manage your projects
-
-
-
+
+ {(auth?.rol === "admin" || user?.canCreateProjects) && (
+
+
+
+ )}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx
index b2f87e4173..c711b862a4 100644
--- a/apps/dokploy/components/layouts/side.tsx
+++ b/apps/dokploy/components/layouts/side.tsx
@@ -1,7 +1,6 @@
"use client";
import {
Activity,
- AudioWaveform,
BarChartHorizontalBigIcon,
Bell,
BlocksIcon,
@@ -9,7 +8,6 @@ import {
Boxes,
ChevronRight,
CircleHelp,
- Command,
CreditCard,
Database,
Folder,
@@ -27,8 +25,8 @@ import {
Users,
} from "lucide-react";
import { usePathname } from "next/navigation";
-import { useEffect, useState } from "react";
import type * as React from "react";
+import { useEffect, useState } from "react";
import {
Breadcrumb,
@@ -65,243 +63,290 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
+import type { AppRouter } from "@/server/api/root";
import { api } from "@/utils/api";
+import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { useRouter } from "next/router";
import { Logo } from "../shared/logo";
import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav";
-// This is sample data.
-interface NavItem {
+
+// The types of the queries we are going to use
+type AuthQueryOutput = inferRouterOutputs["auth"]["get"];
+type UserQueryOutput = inferRouterOutputs["user"]["byAuthId"];
+
+type SingleNavItem = {
+ isSingle?: true;
title: string;
url: string;
- icon: LucideIcon;
- isSingle: boolean;
- isActive: boolean;
- items?: {
- title: string;
- url: string;
- icon?: LucideIcon;
- }[];
-}
+ icon?: LucideIcon;
+ isEnabled?: (opts: {
+ auth?: AuthQueryOutput;
+ user?: UserQueryOutput;
+ isCloud: boolean;
+ }) => boolean;
+};
-interface ExternalLink {
+// NavItem type
+// Consists of a single item or a group of items
+// If `isSingle` is true or undefined, the item is a single item
+// If `isSingle` is false, the item is a group of items
+type NavItem =
+ | SingleNavItem
+ | {
+ isSingle: false;
+ title: string;
+ icon: LucideIcon;
+ items: SingleNavItem[];
+ isEnabled?: (opts: {
+ auth?: AuthQueryOutput;
+ user?: UserQueryOutput;
+ isCloud: boolean;
+ }) => boolean;
+ };
+
+// ExternalLink type
+// Represents an external link item (used for the help section)
+type ExternalLink = {
name: string;
url: string;
icon: React.ComponentType<{ className?: string }>;
-}
+ isEnabled?: (opts: {
+ auth?: AuthQueryOutput;
+ user?: UserQueryOutput;
+ isCloud: boolean;
+ }) => boolean;
+};
-const data = {
- user: {
- name: "shadcn",
- email: "m@example.com",
- avatar: "/avatars/shadcn.jpg",
- },
- teams: [
- {
- name: "Dokploy",
- logo: Logo,
- plan: "Enterprise",
- },
- {
- name: "Acme Corp.",
- logo: AudioWaveform,
- plan: "Startup",
- },
- {
- name: "Evil Corp.",
- logo: Command,
- plan: "Free",
- },
- ],
+// Menu type
+// Consists of home, settings, and help items
+type Menu = {
+ home: NavItem[];
+ settings: NavItem[];
+ help: ExternalLink[];
+};
+
+// Menu items
+// Consists of unfiltered home, settings, and help items
+// The items are filtered based on the user's role and permissions
+// The `isEnabled` function is called to determine if the item should be displayed
+const MENU: Menu = {
home: [
{
+ isSingle: true,
title: "Projects",
url: "/dashboard/projects",
icon: Folder,
- isSingle: true,
- isActive: false,
},
{
+ isSingle: true,
title: "Monitoring",
url: "/dashboard/monitoring",
icon: BarChartHorizontalBigIcon,
- isSingle: true,
- isActive: false,
+ // Only enabled in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) => !isCloud,
},
{
+ isSingle: true,
title: "Traefik File System",
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins and users with access to Traefik files in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!(
+ (auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
+ !isCloud
+ ),
},
{
+ isSingle: true,
title: "Docker",
url: "/dashboard/docker",
icon: BlocksIcon,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins and users with access to Docker in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
{
+ isSingle: true,
title: "Swarm",
url: "/dashboard/swarm",
icon: PieChart,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins and users with access to Docker in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
{
+ isSingle: true,
title: "Requests",
url: "/dashboard/requests",
icon: Forward,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins and users with access to Docker in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
+ // Legacy unused menu, adjusted to the new structure
// {
+ // isSingle: true,
// title: "Projects",
// url: "/dashboard/projects",
// icon: Folder,
- // isSingle: true,
// },
// {
+ // isSingle: true,
// title: "Monitoring",
// icon: BarChartHorizontalBigIcon,
// url: "/dashboard/settings/monitoring",
- // isSingle: true,
// },
-
// {
- // title: "Settings",
- // url: "#",
- // icon: Settings2,
- // isActive: true,
- // items: [
- // {
- // title: "Profile",
- // url: "/dashboard/settings/profile",
- // },
- // {
- // title: "Users",
- // url: "/dashboard/settings/users",
- // },
- // {
- // title: "SSH Key",
- // url: "/dashboard/settings/ssh-keys",
- // },
- // {
- // title: "Git",
- // url: "/dashboard/settings/git-providers",
- // },
- // ],
+ // isSingle: false,
+ // title: "Settings",
+ // icon: Settings2,
+ // items: [
+ // {
+ // title: "Profile",
+ // url: "/dashboard/settings/profile",
+ // },
+ // {
+ // title: "Users",
+ // url: "/dashboard/settings/users",
+ // },
+ // {
+ // title: "SSH Key",
+ // url: "/dashboard/settings/ssh-keys",
+ // },
+ // {
+ // title: "Git",
+ // url: "/dashboard/settings/git-providers",
+ // },
+ // ],
// },
-
// {
- // title: "Integrations",
- // icon: BlocksIcon,
- // items: [
- // {
- // title: "S3 Destinations",
- // url: "/dashboard/settings/destinations",
- // },
- // {
- // title: "Registry",
- // url: "/dashboard/settings/registry",
- // },
- // {
- // title: "Notifications",
- // url: "/dashboard/settings/notifications",
- // },
- // ],
- ] as NavItem[],
+ // isSingle: false,
+ // title: "Integrations",
+ // icon: BlocksIcon,
+ // items: [
+ // {
+ // title: "S3 Destinations",
+ // url: "/dashboard/settings/destinations",
+ // },
+ // {
+ // title: "Registry",
+ // url: "/dashboard/settings/registry",
+ // },
+ // {
+ // title: "Notifications",
+ // url: "/dashboard/settings/notifications",
+ // },
+ // ],
+ // },
+ ],
+
settings: [
{
+ isSingle: true,
title: "Server",
url: "/dashboard/settings/server",
icon: Activity,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!(auth?.rol === "admin" && !isCloud),
},
{
+ isSingle: true,
title: "Profile",
url: "/dashboard/settings/profile",
icon: User,
- isSingle: true,
- isActive: false,
},
{
+ isSingle: true,
title: "Servers",
url: "/dashboard/settings/servers",
icon: Server,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins
+ isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
+ isSingle: true,
title: "Users",
icon: Users,
url: "/dashboard/settings/users",
- isSingle: true,
- isActive: false,
+ // Only enabled for admins
+ isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
+ isSingle: true,
title: "SSH Keys",
icon: KeyRound,
url: "/dashboard/settings/ssh-keys",
- isSingle: true,
- isActive: false,
+ // Only enabled for admins and users with access to SSH keys
+ isEnabled: ({ auth, user }) =>
+ !!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
},
-
{
+ isSingle: true,
title: "Git",
url: "/dashboard/settings/git-providers",
icon: GitBranch,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins and users with access to Git providers
+ isEnabled: ({ auth, user }) =>
+ !!(auth?.rol === "admin" || user?.canAccessToGitProviders),
},
{
+ isSingle: true,
title: "Registry",
url: "/dashboard/settings/registry",
icon: Package,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins
+ isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
+ isSingle: true,
title: "S3 Destinations",
url: "/dashboard/settings/destinations",
icon: Database,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins
+ isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
+ isSingle: true,
title: "Certificates",
url: "/dashboard/settings/certificates",
icon: ShieldCheck,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins
+ isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
+ isSingle: true,
title: "Cluster",
url: "/dashboard/settings/cluster",
icon: Boxes,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins in non-cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!(auth?.rol === "admin" && !isCloud),
},
{
+ isSingle: true,
title: "Notifications",
url: "/dashboard/settings/notifications",
icon: Bell,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins
+ isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
+ isSingle: true,
title: "Billing",
url: "/dashboard/settings/billing",
icon: CreditCard,
- isSingle: true,
- isActive: false,
+ // Only enabled for admins in cloud environments
+ isEnabled: ({ auth, user, isCloud }) =>
+ !!(auth?.rol === "admin" && isCloud),
},
- ] as NavItem[],
+ ],
+
help: [
{
name: "Documentation",
@@ -325,8 +370,108 @@ const data = {
/>
),
},
- ] as ExternalLink[],
-};
+ ],
+} as const;
+
+/**
+ * Creates a menu based on the current user's role and permissions
+ * @returns a menu object with the home, settings, and help items
+ */
+function createMenuForAuthUser(opts: {
+ auth?: AuthQueryOutput;
+ user?: UserQueryOutput;
+ isCloud: boolean;
+}): Menu {
+ return {
+ // Filter the home items based on the user's role and permissions
+ // Calls the `isEnabled` function if it exists to determine if the item should be displayed
+ home: MENU.home.filter((item) =>
+ !item.isEnabled
+ ? true
+ : item.isEnabled({
+ auth: opts.auth,
+ user: opts.user,
+ isCloud: opts.isCloud,
+ }),
+ ),
+ // Filter the settings items based on the user's role and permissions
+ // Calls the `isEnabled` function if it exists to determine if the item should be displayed
+ settings: MENU.settings.filter((item) =>
+ !item.isEnabled
+ ? true
+ : item.isEnabled({
+ auth: opts.auth,
+ user: opts.user,
+ isCloud: opts.isCloud,
+ }),
+ ),
+ // Filter the help items based on the user's role and permissions
+ // Calls the `isEnabled` function if it exists to determine if the item should be displayed
+ help: MENU.help.filter((item) =>
+ !item.isEnabled
+ ? true
+ : item.isEnabled({
+ auth: opts.auth,
+ user: opts.user,
+ isCloud: opts.isCloud,
+ }),
+ ),
+ };
+}
+
+/**
+ * Determines if an item url is active based on the current pathname
+ * @returns true if the item url is active, false otherwise
+ */
+function isActiveRoute(opts: {
+ /** The url of the item. Usually obtained from `item.url` */
+ itemUrl: string;
+ /** The current pathname. Usually obtained from `usePathname()` */
+ pathname: string;
+}): boolean {
+ const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project");
+ const normalizedPathname = opts.pathname?.replace("/projects", "/project");
+
+ if (!normalizedPathname) return false;
+
+ if (normalizedPathname === normalizedItemUrl) return true;
+
+ if (normalizedPathname.startsWith(normalizedItemUrl)) {
+ const nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
+ return nextChar === "/";
+ }
+
+ return false;
+}
+
+/**
+ * Finds the active nav item based on the current pathname
+ * @returns the active nav item with `SingleNavItem` type or undefined if none is active
+ */
+function findActiveNavItem(
+ navItems: NavItem[],
+ pathname: string,
+): SingleNavItem | undefined {
+ const found = navItems.find((item) =>
+ item.isSingle !== false
+ ? // The current item is single, so check if the item url is active
+ isActiveRoute({ itemUrl: item.url, pathname })
+ : // The current item is not single, so check if any of the sub items are active
+ item.items.some((item) =>
+ isActiveRoute({ itemUrl: item.url, pathname }),
+ ),
+ );
+
+ if (found?.isSingle !== false) {
+ // The found item is single, so return it
+ return found;
+ }
+
+ // The found item is not single, so find the active sub item
+ return found?.items.find((item) =>
+ isActiveRoute({ itemUrl: item.url, pathname }),
+ );
+}
interface Props {
children: React.ReactNode;
@@ -398,64 +543,21 @@ export default function Page({ children }: Props) {
const includesProjects = pathname?.includes("/dashboard/project");
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
- const isActiveRoute = (itemUrl: string) => {
- const normalizedItemUrl = itemUrl?.replace("/projects", "/project");
- const normalizedPathname = pathname?.replace("/projects", "/project");
-
- if (!normalizedPathname) return false;
- if (normalizedPathname === normalizedItemUrl) return true;
+ const {
+ home: filteredHome,
+ settings: filteredSettings,
+ help,
+ } = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
- if (normalizedPathname.startsWith(normalizedItemUrl)) {
- const nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
- return nextChar === "/";
- }
-
- return false;
- };
+ const activeItem = findActiveNavItem(
+ [...filteredHome, ...filteredSettings],
+ pathname,
+ );
- let filteredHome = isCloud
- ? data.home.filter(
- (item) =>
- ![
- "/dashboard/monitoring",
- "/dashboard/traefik",
- "/dashboard/docker",
- "/dashboard/swarm",
- "/dashboard/requests",
- ].includes(item.url),
- )
- : data.home;
-
- let filteredSettings = isCloud
- ? data.settings.filter(
- (item) =>
- ![
- "/dashboard/settings/server",
- "/dashboard/settings/cluster",
- ].includes(item.url),
- )
- : data.settings.filter(
- (item) => !["/dashboard/settings/billing"].includes(item.url),
- );
-
- filteredHome = filteredHome.map((item) => ({
- ...item,
- isActive: isActiveRoute(item.url),
- }));
-
- filteredSettings = filteredSettings.map((item) => ({
- ...item,
- isActive: isActiveRoute(item.url),
- }));
-
- const activeItem =
- filteredHome.find((item) => item.isActive) ||
- filteredSettings.find((item) => item.isActive);
-
- const showProjectsButton =
- currentPath === "/dashboard/projects" &&
- (auth?.rol === "admin" || user?.canCreateProjects);
+ // const showProjectsButton =
+ // currentPath === "/dashboard/projects" &&
+ // (auth?.rol === "admin" || user?.canCreateProjects);
return (
Home
- {filteredHome.map((item) => (
-
-
- {item.isSingle ? (
-
- {
+ const isSingle = item.isSingle !== false;
+ const isActive = isSingle
+ ? isActiveRoute({ itemUrl: item.url, pathname })
+ : item.items.some((item) =>
+ isActiveRoute({ itemUrl: item.url, pathname }),
+ );
+
+ return (
+
+
+ {isSingle ? (
+
-
- {item.title}
-
-
- ) : (
- <>
-
-
- {item.icon && }
-
- {item.title}
- {item.items?.length && (
-
+ {item.icon && (
+
)}
-
-
-
-
- {item.items?.map((subItem) => (
-
-
- {item.title}
+
+
+ ) : (
+ <>
+
+
+ {item.icon && }
+
+ {item.title}
+ {item.items?.length && (
+
+ )}
+
+
+
+
+ {item.items?.map((subItem) => (
+
+
- {subItem.icon && (
-
-
-
- )}
- {subItem.title}
-
-
-
- ))}
-
-
- >
- )}
-
-
- ))}
+
+ {subItem.icon && (
+
+
+
+ )}
+ {subItem.title}
+
+
+
+ ))}
+
+
+ >
+ )}
+
+
+ );
+ })}
Settings
- {filteredSettings.map((item) => (
-
-
- {item.isSingle ? (
-
- {
+ const isSingle = item.isSingle !== false;
+ const isActive = isSingle
+ ? isActiveRoute({ itemUrl: item.url, pathname })
+ : item.items.some((item) =>
+ isActiveRoute({ itemUrl: item.url, pathname }),
+ );
+
+ return (
+
+
+ {isSingle ? (
+
-
- {item.title}
-
-
- ) : (
- <>
-
-
- {item.icon && }
-
- {item.title}
- {item.items?.length && (
-
+ {item.icon && (
+
)}
-
-
-
-
- {item.items?.map((subItem) => (
-
-
- {item.title}
+
+
+ ) : (
+ <>
+
+
+ {item.icon && }
+
+ {item.title}
+ {item.items?.length && (
+
+ )}
+
+
+
+
+ {item.items?.map((subItem) => (
+
+
- {subItem.icon && (
-
-
-
- )}
- {subItem.title}
-
-
-
- ))}
-
-
- >
- )}
-
-
- ))}
+
+ {subItem.icon && (
+
+
+
+ )}
+ {subItem.title}
+
+
+
+ ))}
+
+
+ >
+ )}
+
+
+ );
+ })}
Extra
- {data.help.map((item: ExternalLink) => (
+ {help.map((item: ExternalLink) => (
+
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index 449a223336..cb0e32d9a7 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -345,7 +345,7 @@ export const settingsRouter = createTRPCRouter({
writeConfig("middlewares", input.traefikConfig);
return true;
}),
- getUpdateData: adminProcedure.mutation(async () => {
+ getUpdateData: protectedProcedure.mutation(async () => {
if (IS_CLOUD) {
return DEFAULT_UPDATE_DATA;
}
@@ -373,10 +373,10 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
- getDokployVersion: adminProcedure.query(() => {
+ getDokployVersion: protectedProcedure.query(() => {
return packageInfo.version;
}),
- getReleaseTag: adminProcedure.query(() => {
+ getReleaseTag: protectedProcedure.query(() => {
return getDokployImageTag();
}),
readDirectories: protectedProcedure
diff --git a/apps/dokploy/templates/superset/docker-compose.yml b/apps/dokploy/templates/superset/docker-compose.yml
new file mode 100644
index 0000000000..1766b86b7c
--- /dev/null
+++ b/apps/dokploy/templates/superset/docker-compose.yml
@@ -0,0 +1,62 @@
+# Note: this is an UNOFFICIAL production docker image build for Superset:
+# - https://github.com/amancevice/docker-superset
+#
+# After deploying this image, you will need to run one of the two
+# commands below in a terminal within the superset container:
+# $ superset-demo # Initialise database + load demo charts/datasets
+# $ superset-init # Initialise database only
+#
+# You will be prompted to enter the credentials for the admin user.
+
+services:
+ superset:
+ image: amancevice/superset
+ restart: always
+ depends_on:
+ - db
+ - redis
+ environment:
+ SECRET_KEY: ${SECRET_KEY}
+ MAPBOX_API_KEY: ${MAPBOX_API_KEY}
+ POSTGRES_USER: ${POSTGRES_USER}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ POSTGRES_DB: ${POSTGRES_DB}
+ REDIS_PASSWORD: ${REDIS_PASSWORD}
+ volumes:
+ # Note: superset_config.py can be edited in Dokploy's UI Volume Mount
+ - ../files/superset/superset_config.py:/etc/superset/superset_config.py
+
+ db:
+ image: postgres
+ restart: always
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ POSTGRES_DB: ${POSTGRES_DB}
+ volumes:
+ - postgres:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ networks:
+ - dokploy-network
+
+ redis:
+ image: redis
+ restart: always
+ volumes:
+ - redis:/data
+ command: redis-server --requirepass ${REDIS_PASSWORD}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ networks:
+ - dokploy-network
+
+volumes:
+ postgres:
+ redis:
diff --git a/apps/dokploy/templates/superset/index.ts b/apps/dokploy/templates/superset/index.ts
new file mode 100644
index 0000000000..6132f978f6
--- /dev/null
+++ b/apps/dokploy/templates/superset/index.ts
@@ -0,0 +1,67 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generatePassword,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mapboxApiKey = "";
+ const secretKey = generatePassword(30);
+ const postgresDb = "superset";
+ const postgresUser = "superset";
+ const postgresPassword = generatePassword(30);
+ const redisPassword = generatePassword(30);
+
+ const domains: DomainSchema[] = [
+ {
+ host: generateRandomDomain(schema),
+ port: 8088,
+ serviceName: "superset",
+ },
+ ];
+
+ const envs = [
+ `SECRET_KEY=${secretKey}`,
+ `MAPBOX_API_KEY=${mapboxApiKey}`,
+ `POSTGRES_DB=${postgresDb}`,
+ `POSTGRES_USER=${postgresUser}`,
+ `POSTGRES_PASSWORD=${postgresPassword}`,
+ `REDIS_PASSWORD=${redisPassword}`,
+ ];
+
+ const mounts: Template["mounts"] = [
+ {
+ filePath: "./superset/superset_config.py",
+ content: `
+import os
+
+SECRET_KEY = os.getenv("SECRET_KEY")
+MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "")
+
+CACHE_CONFIG = {
+ "CACHE_TYPE": "RedisCache",
+ "CACHE_DEFAULT_TIMEOUT": 300,
+ "CACHE_KEY_PREFIX": "superset_",
+ "CACHE_REDIS_HOST": "redis",
+ "CACHE_REDIS_PORT": 6379,
+ "CACHE_REDIS_DB": 1,
+ "CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@redis:6379/1",
+}
+
+FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"}
+EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"}
+
+SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}"
+SQLALCHEMY_TRACK_MODIFICATIONS = True
+ `.trim(),
+ },
+ ];
+
+ return {
+ envs,
+ domains,
+ mounts,
+ };
+}
diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts
index 9531eb7aec..4cd167a501 100644
--- a/apps/dokploy/templates/templates.ts
+++ b/apps/dokploy/templates/templates.ts
@@ -1298,4 +1298,18 @@ export const templates: TemplateData[] = [
tags: ["developer", "tools"],
load: () => import("./it-tools/index").then((m) => m.generate),
},
+ {
+ id: "superset",
+ name: "Superset (Unofficial)",
+ version: "latest",
+ description: "Data visualization and data exploration platform.",
+ logo: "superset.svg",
+ links: {
+ github: "https://github.com/amancevice/docker-superset",
+ website: "https://superset.apache.org",
+ docs: "https://superset.apache.org/docs/intro",
+ },
+ tags: ["analytics", "bi", "dashboard", "database", "sql"],
+ load: () => import("./superset/index").then((m) => m.generate),
+ },
];