diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index c00514715..228c98656 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -6,7 +6,9 @@ import { findEnvironmentById, findGitProviderById, findProjectById, + getAccessibleServerIds, getApplicationStats, + getContainerLogs, IS_CLOUD, mechanizeDockerContainer, readConfig, @@ -26,7 +28,6 @@ import { updateDeploymentStatus, writeConfig, writeConfigRemote, - getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -1101,4 +1102,39 @@ export const applicationRouter = createTRPCRouter({ total: countResult[0]?.count ?? 0, }; }), + + readLogs: protectedProcedure + .input( + apiFindOneApplication.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.applicationId, "read"); + const application = await findApplicationById(input.applicationId); + if ( + application.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await getContainerLogs( + application.appName, + input.tail, + input.since, + input.search, + application.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index be5a836d7..dd632466b 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -16,7 +16,9 @@ import { findGitProviderById, findProjectById, findServerById, + getAccessibleServerIds, getComposeContainer, + getContainerLogs, getWebServerSettings, IS_CLOUD, loadServices, @@ -30,7 +32,6 @@ import { stopCompose, updateCompose, updateDeploymentStatus, - getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -1130,4 +1131,43 @@ export const composeRouter = createTRPCRouter({ total: countResult[0]?.count ?? 0, }; }), + + readLogs: protectedProcedure + .input( + apiFindCompose.extend({ + containerId: z + .string() + .min(1) + .regex(/^[a-zA-Z0-9.\-_]+$/, "Invalid container id."), + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.composeId, "read"); + const compose = await findComposeById(input.composeId); + if ( + compose.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + return await getContainerLogs( + input.containerId, + input.tail, + input.since, + input.search, + compose.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/libsql.ts b/apps/dokploy/server/api/routers/libsql.ts index 65052097e..47798393e 100644 --- a/apps/dokploy/server/api/routers/libsql.ts +++ b/apps/dokploy/server/api/routers/libsql.ts @@ -6,6 +6,7 @@ import { findEnvironmentById, findLibsqlById, findProjectById, + getContainerLogs, IS_CLOUD, rebuildDatabase, removeLibsqlById, @@ -466,4 +467,39 @@ export const libsqlRouter = createTRPCRouter({ }); return true; }), + + readLogs: protectedProcedure + .input( + apiFindOneLibsql.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.libsqlId, "read"); + const libsql = await findLibsqlById(input.libsqlId); + if ( + libsql.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this LibSQL", + }); + } + return await getContainerLogs( + libsql.appName, + input.tail, + input.since, + input.search, + libsql.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index a31554bec..a58a33a9c 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -9,6 +9,8 @@ import { findEnvironmentById, findMariadbById, findProjectById, + getAccessibleServerIds, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -19,7 +21,6 @@ import { stopService, stopServiceRemote, updateMariadbById, - getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { @@ -590,4 +591,39 @@ export const mariadbRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneMariaDB.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.mariadbId, "read"); + const mariadb = await findMariadbById(input.mariadbId); + if ( + mariadb.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this MariaDB", + }); + } + return await getContainerLogs( + mariadb.appName, + input.tail, + input.since, + input.search, + mariadb.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index f0f5c593a..222917b9f 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -10,6 +10,7 @@ import { findMongoById, findProjectById, getAccessibleServerIds, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -601,4 +602,39 @@ export const mongoRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneMongo.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.mongoId, "read"); + const mongo = await findMongoById(input.mongoId); + if ( + mongo.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this MongoDB", + }); + } + return await getContainerLogs( + mongo.appName, + input.tail, + input.since, + input.search, + mongo.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index f2a750c04..263fa53f0 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -9,6 +9,7 @@ import { findEnvironmentById, findMySqlById, findProjectById, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -604,4 +605,39 @@ export const mysqlRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneMySql.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.mysqlId, "read"); + const mysql = await findMySqlById(input.mysqlId); + if ( + mysql.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this MySQL", + }); + } + return await getContainerLogs( + mysql.appName, + input.tail, + input.since, + input.search, + mysql.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 0baeb3f0e..33d8fd3f4 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -9,6 +9,7 @@ import { findEnvironmentById, findPostgresById, findProjectById, + getContainerLogs, getMountPath, getServiceContainerCommand, IS_CLOUD, @@ -614,4 +615,39 @@ export const postgresRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOnePostgres.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.postgresId, "read"); + const postgres = await findPostgresById(input.postgresId); + if ( + postgres.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this Postgres", + }); + } + return await getContainerLogs( + postgres.appName, + input.tail, + input.since, + input.search, + postgres.serverId, + ); + }), }); diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 2d7b20674..a1e912e0b 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -8,6 +8,7 @@ import { findEnvironmentById, findProjectById, findRedisById, + getContainerLogs, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, @@ -587,4 +588,39 @@ export const redisRouter = createTRPCRouter({ ]); return { items, total: countResult[0]?.count ?? 0 }; }), + + readLogs: protectedProcedure + .input( + apiFindOneRedis.extend({ + tail: z.number().int().min(1).max(10000).default(100), + since: z + .string() + .regex(/^(all|\d+[smhd])$/, "Invalid since format") + .default("all"), + search: z + .string() + .regex(/^[a-zA-Z0-9 ._-]{0,500}$/) + .optional(), + }), + ) + .query(async ({ input, ctx }) => { + await checkServiceAccess(ctx, input.redisId, "read"); + const redis = await findRedisById(input.redisId); + if ( + redis.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this Redis", + }); + } + return await getContainerLogs( + redis.appName, + input.tail, + input.since, + input.search, + redis.serverId, + ); + }), }); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 1205e62c2..2b14d0b23 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -354,6 +354,87 @@ export const getContainersByAppLabel = async ( return []; }; +export const getContainerLogs = async ( + appName: string, + tail = 100, + since = "all", + search?: string, + serverId?: string | null, +): Promise => { + // First, find the real container ID by appName filter + const findCommand = `docker ps -q --filter "name=^${appName}" | head -1`; + const findResult = serverId + ? await execAsyncRemote(serverId, findCommand) + : await execAsync(findCommand); + + const containerId = findResult.stdout.trim(); + if (!containerId) { + // Fallback: try as a swarm service + const svcCommand = `docker service ls -q --filter "name=${appName}" | head -1`; + const svcResult = serverId + ? await execAsyncRemote(serverId, svcCommand) + : await execAsync(svcCommand); + + const serviceId = svcResult.stdout.trim(); + if (!serviceId) { + throw new Error(`No container or service found for: ${appName}`); + } + + // Use docker service logs for swarm + const sinceFlag = since === "all" ? "" : `--since ${since}`; + const baseCommand = `docker service logs --timestamps --raw --tail ${tail} ${sinceFlag} ${appName}`; + const escapedSearch = search?.replace(/'/g, "'\\''") ?? ""; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${escapedSearch}'` + : `${baseCommand} 2>&1`; + + try { + const result = serverId + ? await execAsyncRemote(serverId, command) + : await execAsync(command); + return result.stdout; + } catch (error: unknown) { + if ( + error && + typeof error === "object" && + "stdout" in error && + typeof (error as { stdout: string }).stdout === "string" && + (error as { stdout: string }).stdout.length > 0 + ) { + return (error as { stdout: string }).stdout; + } + throw error; + } + } + + const sinceFlag = since === "all" ? "" : `--since ${since}`; + const baseCommand = `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${containerId}`; + + const escapedSearch = search?.replace(/'/g, "'\\''") ?? ""; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${escapedSearch}'` + : `${baseCommand} 2>&1`; + + try { + const result = serverId + ? await execAsyncRemote(serverId, command) + : await execAsync(command); + + return result.stdout; + } catch (error: unknown) { + if ( + error && + typeof error === "object" && + "stdout" in error && + typeof (error as { stdout: string }).stdout === "string" && + (error as { stdout: string }).stdout.length > 0 + ) { + return (error as { stdout: string }).stdout; + } + throw error; + } +}; + export const containerRestart = async (containerId: string) => { try { const { stdout, stderr } = await execAsync(