diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index dba3666c7..1100a4bf9 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -15,6 +15,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; @@ -35,34 +36,72 @@ interface Props { } export const ShowDockerLogs = ({ appName, serverId }: Props) => { - const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery( - { - appName, - serverId, - }, - { - enabled: !!appName, - }, - ); const [containerId, setContainerId] = useState(); + const [option, setOption] = useState<"swarm" | "native">("native"); + + const { data: services, isLoading: servicesLoading } = + api.docker.getServiceContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "swarm", + }, + ); + + const { data: containers, isLoading: containersLoading } = + api.docker.getContainersByAppNameMatch.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "native", + }, + ); useEffect(() => { - if (data && data?.length > 0) { - setContainerId(data[0]?.containerId); + if (option === "native") { + if (containers && containers?.length > 0) { + setContainerId(containers[0]?.containerId); + } + } else { + if (services && services?.length > 0) { + setContainerId(services[0]?.containerId); + } } - }, [data]); + }, [option, services, containers]); + + const isLoading = option === "native" ? containersLoading : servicesLoading; + const containersLenght = + option === "native" ? containers?.length : services?.length; return ( - Logs + Logssss Watch the logs of the application in real time - +
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + /> +
+
+
diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx new file mode 100644 index 000000000..cec1e5af2 --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -0,0 +1,156 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; +export const DockerLogs = dynamic( + () => + import("@/components/dashboard/docker/logs/docker-logs-id").then( + (e) => e.DockerLogsId, + ), + { + ssr: false, + }, +); + +interface Props { + appName: string; + serverId?: string; +} + +export const ShowDockerLogsStack = ({ appName, serverId }: Props) => { + const [option, setOption] = useState<"swarm" | "native">("native"); + const [containerId, setContainerId] = useState(); + + const { data: services, isLoading: servicesLoading } = + api.docker.getStackContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "swarm", + }, + ); + + const { data: containers, isLoading: containersLoading } = + api.docker.getContainersByAppNameMatch.useQuery( + { + appName, + appType: "stack", + serverId, + }, + { + enabled: !!appName && option === "native", + }, + ); + + useEffect(() => { + if (option === "native") { + if (containers && containers?.length > 0) { + setContainerId(containers[0]?.containerId); + } + } else { + if (services && services?.length > 0) { + setContainerId(services[0]?.containerId); + } + } + }, [option, services, containers]); + + const isLoading = option === "native" ? containersLoading : servicesLoading; + const containersLenght = + option === "native" ? containers?.length : services?.length; + + return ( + + + Logs + + Watch the logs of the application in real time + + + + +
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + /> +
+
+ + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 6b39f4137..bf7e2993e 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -97,6 +97,7 @@ export const ShowDockerLogsCompose = ({ diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 3f30c292c..1fd8cea48 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -12,6 +12,7 @@ import { type LogLine, getLogType, parseLogs } from "./utils"; interface Props { containerId: string; serverId?: string | null; + runType: "swarm" | "native"; } export const priorities = [ @@ -37,7 +38,11 @@ export const priorities = [ }, ]; -export const DockerLogsId: React.FC = ({ containerId, serverId }) => { +export const DockerLogsId: React.FC = ({ + containerId, + serverId, + runType, +}) => { const { data } = api.docker.getConfig.useQuery( { containerId, @@ -104,6 +109,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { tail: lines.toString(), since, search, + runType, }); if (serverId) { diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx index f8531d774..619b25d0c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx @@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({ View the logs for {containerId}
- +
diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx index 92401dc35..12e7b6704 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx @@ -91,7 +91,11 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => { - + diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index a4cb855cc..6b828dc7d 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -6,6 +6,7 @@ import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show- import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; +import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack"; import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show"; import { UpdateCompose } from "@/components/dashboard/compose/update-compose"; import { ProjectLayout } from "@/components/layouts/project-layout"; @@ -251,11 +252,18 @@ const Service = (
- + {data?.composeType === "docker-compose" ? ( + + ) : ( + + )}
diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts index cb6b2712e..f6972e16b 100644 --- a/apps/dokploy/server/api/routers/docker.ts +++ b/apps/dokploy/server/api/routers/docker.ts @@ -4,6 +4,8 @@ import { getContainers, getContainersByAppLabel, getContainersByAppNameMatch, + getServiceContainersByAppName, + getStackContainersByAppName, } from "@dokploy/server"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; @@ -68,4 +70,26 @@ export const dockerRouter = createTRPCRouter({ .query(async ({ input }) => { return await getContainersByAppLabel(input.appName, input.serverId); }), + + getStackContainersByAppName: protectedProcedure + .input( + z.object({ + appName: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getStackContainersByAppName(input.appName, input.serverId); + }), + + getServiceContainersByAppName: protectedProcedure + .input( + z.object({ + appName: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getServiceContainersByAppName(input.appName, input.serverId); + }), }); diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 0c1e66a4d..4a89e42b2 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -34,6 +34,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const search = url.searchParams.get("search"); const since = url.searchParams.get("since"); const serverId = url.searchParams.get("serverId"); + const runType = url.searchParams.get("runType"); const { user, session } = await validateWebSocketRequest(req); if (!containerId) { @@ -53,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const client = new Client(); client .once("ready", () => { - const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps --tail ${tail} ${ since === "all" ? "" : `--since ${since}` } --follow ${containerId}`; const escapedSearch = search ? search.replace(/'/g, "'\\''") : ""; @@ -97,7 +98,7 @@ export const setupDockerContainerLogsWebSocketServer = ( }); } else { const shell = getShell(); - const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps --tail ${tail} ${ since === "all" ? "" : `--since ${since}` } --follow ${containerId}`; const command = search diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 60262ba13..55764230b 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -157,6 +157,124 @@ export const getContainersByAppNameMatch = async ( return []; }; +export const getStackContainersByAppName = async ( + appName: string, + serverId?: string, +) => { + try { + let result: string[] = []; + + const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`; + if (serverId) { + const { stdout, stderr } = await execAsyncRemote(serverId, command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + result = stdout.trim().split("\n"); + } else { + const { stdout, stderr } = await execAsync(command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + + result = stdout.trim().split("\n"); + } + + const containers = result.map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + + const state = parts[2] + ? parts[2].replace("State: ", "").trim() + : "No state"; + const node = parts[3] + ? parts[3].replace("Node: ", "").trim() + : "No specific node"; + return { + containerId, + name, + state, + node, + }; + }); + + return containers || []; + } catch (error) {} + + return []; +}; + +export const getServiceContainersByAppName = async ( + appName: string, + serverId?: string, +) => { + try { + let result: string[] = []; + + const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`; + + if (serverId) { + const { stdout, stderr } = await execAsyncRemote(serverId, command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + result = stdout.trim().split("\n"); + } else { + const { stdout, stderr } = await execAsync(command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + + result = stdout.trim().split("\n"); + } + + const containers = result.map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + + const state = parts[2] + ? parts[2].replace("State: ", "").trim() + : "No state"; + + const node = parts[3] + ? parts[3].replace("Node: ", "").trim() + : "No specific node"; + return { + containerId, + name, + state, + node, + }; + }); + + return containers || []; + } catch (error) {} + + return []; +}; + export const getContainersByAppLabel = async ( appName: string, serverId?: string,