(activeTab);
const { data } = api.redis.one.useQuery({ redisId });
@@ -68,18 +68,20 @@ const Redis = (
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
- name: data?.project?.name || "",
- href: `/dashboard/project/${projectId}`,
+ name: data?.environment?.project?.name || "",
+ },
+ {
+ name: data?.environment?.name || "",
+ href: `/dashboard/project/${projectId}/environment/${environmentId}`,
},
{
name: data?.name || "",
- href: `/dashboard/project/${projectId}/services/redis/${redisId}`,
},
]}
/>
- Database: {data?.name} - {data?.project.name} | Dokploy
+ Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
@@ -179,7 +181,7 @@ const Redis = (
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
- const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
+ const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/redis/${redisId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
@@ -291,7 +293,11 @@ Redis.getLayout = (page: ReactElement) => {
};
export async function getServerSideProps(
- ctx: GetServerSidePropsContext<{ redisId: string; activeTab: TabState }>,
+ ctx: GetServerSidePropsContext<{
+ redisId: string;
+ activeTab: TabState;
+ environmentId: string;
+ }>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx
index df7b34e11..8127b41fd 100644
--- a/apps/dokploy/pages/index.tsx
+++ b/apps/dokploy/pages/index.tsx
@@ -329,6 +329,7 @@ export default function Home({ IS_CLOUD }: Props) {
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
autoComplete="off"
+ autoFocus
>
diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts
index e930f2264..63ce38d10 100644
--- a/apps/dokploy/server/api/root.ts
+++ b/apps/dokploy/server/api/root.ts
@@ -11,6 +11,7 @@ import { deploymentRouter } from "./routers/deployment";
import { destinationRouter } from "./routers/destination";
import { dockerRouter } from "./routers/docker";
import { domainRouter } from "./routers/domain";
+import { environmentRouter } from "./routers/environment";
import { gitProviderRouter } from "./routers/git-provider";
import { giteaRouter } from "./routers/gitea";
import { githubRouter } from "./routers/github";
@@ -84,6 +85,7 @@ export const appRouter = createTRPCRouter({
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
+ environment: environmentRouter,
});
// export type definition of API
diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts
index ed4277c1d..6fa2e0cdc 100644
--- a/apps/dokploy/server/api/routers/ai.ts
+++ b/apps/dokploy/server/api/routers/ai.ts
@@ -4,7 +4,11 @@ import {
apiUpdateAi,
deploySuggestionSchema,
} from "@dokploy/server/db/schema/ai";
-import { createDomain, createMount } from "@dokploy/server/index";
+import {
+ createDomain,
+ createMount,
+ findEnvironmentById,
+} from "@dokploy/server/index";
import {
deleteAiSettings,
getAiSettingById,
@@ -20,6 +24,7 @@ import {
} from "@dokploy/server/services/user";
import {
getProviderHeaders,
+ getProviderName,
type Model,
} from "@dokploy/server/utils/ai/select-ai-provider";
import { TRPCError } from "@trpc/server";
@@ -47,11 +52,24 @@ export const aiRouter = createTRPCRouter({
}),
getModels: protectedProcedure
- .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) }))
+ .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
.query(async ({ input }) => {
try {
+ const providerName = getProviderName(input.apiUrl);
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
- const response = await fetch(`${input.apiUrl}/models`, { headers });
+ let response = null;
+ switch (providerName) {
+ case "ollama":
+ response = await fetch(`${input.apiUrl}/api/tags`, { headers });
+ break;
+ default:
+ if (!input.apiKey)
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "API key must contain at least 1 character(s)",
+ });
+ response = await fetch(`${input.apiUrl}/models`, { headers });
+ }
if (!response.ok) {
const errorText = await response.text();
@@ -163,10 +181,12 @@ export const aiRouter = createTRPCRouter({
deploy: protectedProcedure
.input(deploySuggestionSchema)
.mutation(async ({ ctx, input }) => {
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.session.activeOrganizationId,
- input.projectId,
+ environment.projectId,
"create",
);
}
@@ -178,8 +198,6 @@ export const aiRouter = createTRPCRouter({
});
}
- const project = await findProjectById(input.projectId);
-
const projectName = slugify(`${project.name} ${input.id}`);
const compose = await createComposeByTemplate({
@@ -191,6 +209,7 @@ export const aiRouter = createTRPCRouter({
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
isolatedDeployment: true,
+ environmentId: input.environmentId,
});
if (input.domains && input.domains?.length > 0) {
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts
index 5299ba6c2..45fe15c6f 100644
--- a/apps/dokploy/server/api/routers/application.ts
+++ b/apps/dokploy/server/api/routers/application.ts
@@ -4,6 +4,7 @@ import {
createApplication,
deleteAllMiddlewares,
findApplicationById,
+ findEnvironmentById,
findGitProviderById,
findProjectById,
getApplicationStats,
@@ -23,6 +24,7 @@ import {
unzipDrop,
updateApplication,
updateApplicationStatus,
+ updateDeploymentStatus,
writeConfig,
writeConfigRemote,
// uploadFileSchema
@@ -39,8 +41,10 @@ import {
import { db } from "@/server/db";
import {
apiCreateApplication,
+ apiDeployApplication,
apiFindMonitoringStats,
apiFindOneApplication,
+ apiRedeployApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
@@ -55,7 +59,7 @@ import {
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
-import { deploy } from "@/server/utils/deploy";
+import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
export const applicationRouter = createTRPCRouter({
@@ -63,10 +67,14 @@ export const applicationRouter = createTRPCRouter({
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -79,13 +87,13 @@ export const applicationRouter = createTRPCRouter({
});
}
- const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
+
const newApplication = await createApplication(input);
if (ctx.user.role === "member") {
@@ -97,6 +105,7 @@ export const applicationRouter = createTRPCRouter({
}
return newApplication;
} catch (error: unknown) {
+ console.log("error", error);
if (error instanceof TRPCError) {
throw error;
}
@@ -120,7 +129,8 @@ export const applicationRouter = createTRPCRouter({
}
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -175,7 +185,7 @@ export const applicationRouter = createTRPCRouter({
try {
if (
- application.project.organizationId !==
+ application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -212,7 +222,8 @@ export const applicationRouter = createTRPCRouter({
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -247,14 +258,17 @@ export const applicationRouter = createTRPCRouter({
} catch (_) {}
}
- return result[0];
+ return application;
}),
stop: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
- if (service.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ service.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this application",
@@ -274,7 +288,10 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const service = await findApplicationById(input.applicationId);
- if (service.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ service.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this application",
@@ -292,11 +309,12 @@ export const applicationRouter = createTRPCRouter({
}),
redeploy: protectedProcedure
- .input(apiFindOneApplication)
+ .input(apiRedeployApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -305,8 +323,8 @@ export const applicationRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
applicationId: input.applicationId,
- titleLog: "Rebuild deployment",
- descriptionLog: "",
+ titleLog: input.title || "Rebuild deployment",
+ descriptionLog: input.description || "",
type: "redeploy",
applicationType: "application",
server: !!application.serverId,
@@ -331,7 +349,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -349,7 +368,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -374,7 +394,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -401,7 +422,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -429,7 +451,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -455,7 +478,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -481,7 +505,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -504,7 +529,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -529,7 +555,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -590,7 +617,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -604,7 +632,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -630,7 +659,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -643,11 +673,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
deploy: protectedProcedure
- .input(apiFindOneApplication)
+ .input(apiDeployApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -656,8 +687,8 @@ export const applicationRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
applicationId: input.applicationId,
- titleLog: "Manual deployment",
- descriptionLog: "",
+ titleLog: input.title || "Manual deployment",
+ descriptionLog: input.description || "",
type: "deploy",
applicationType: "application",
server: !!application.serverId,
@@ -683,7 +714,8 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -698,7 +730,8 @@ export const applicationRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -734,7 +767,10 @@ export const applicationRouter = createTRPCRouter({
const app = await findApplicationById(input.applicationId as string);
- if (app.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ app.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this application",
@@ -777,7 +813,8 @@ export const applicationRouter = createTRPCRouter({
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -813,13 +850,14 @@ export const applicationRouter = createTRPCRouter({
.input(
z.object({
applicationId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -827,11 +865,16 @@ export const applicationRouter = createTRPCRouter({
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
@@ -839,7 +882,7 @@ export const applicationRouter = createTRPCRouter({
const updatedApplication = await db
.update(applications)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(applications.applicationId, input.applicationId))
.returning()
@@ -854,4 +897,55 @@ export const applicationRouter = createTRPCRouter({
return updatedApplication;
}),
+
+ cancelDeployment: protectedProcedure
+ .input(apiFindOneApplication)
+ .mutation(async ({ input, ctx }) => {
+ 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 cancel this deployment",
+ });
+ }
+
+ if (IS_CLOUD && application.serverId) {
+ try {
+ await updateApplicationStatus(input.applicationId, "idle");
+
+ if (application.deployments[0]) {
+ await updateDeploymentStatus(
+ application.deployments[0].deploymentId,
+ "done",
+ );
+ }
+
+ await cancelDeployment({
+ applicationId: input.applicationId,
+ applicationType: "application",
+ });
+
+ return {
+ success: true,
+ message: "Deployment cancellation requested",
+ };
+ } catch (error) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message:
+ error instanceof Error
+ ? error.message
+ : "Failed to cancel deployment",
+ });
+ }
+ }
+
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Deployment cancellation only available in cloud version",
+ });
+ }),
});
diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 6d6b8b55d..64f844faf 100644
--- a/apps/dokploy/server/api/routers/compose.ts
+++ b/apps/dokploy/server/api/routers/compose.ts
@@ -12,6 +12,7 @@ import {
deleteMount,
findComposeById,
findDomainsByComposeId,
+ findEnvironmentById,
findGitProviderById,
findProjectById,
findServerById,
@@ -28,6 +29,7 @@ import {
startCompose,
stopCompose,
updateCompose,
+ updateDeploymentStatus,
} from "@dokploy/server";
import {
type CompleteTemplate,
@@ -47,15 +49,17 @@ import { db } from "@/server/db";
import {
apiCreateCompose,
apiDeleteCompose,
+ apiDeployCompose,
apiFetchServices,
apiFindCompose,
apiRandomizeCompose,
+ apiRedeployCompose,
apiUpdateCompose,
compose as composeTable,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
-import { deploy } from "@/server/utils/deploy";
+import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -64,10 +68,14 @@ export const composeRouter = createTRPCRouter({
.input(apiCreateCompose)
.mutation(async ({ ctx, input }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -79,14 +87,15 @@ export const composeRouter = createTRPCRouter({
message: "You need to use a server to create a compose",
});
}
- const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
- const newService = await createCompose(input);
+ const newService = await createCompose({
+ ...input,
+ });
if (ctx.user.role === "member") {
await addNewService(
@@ -115,7 +124,10 @@ export const composeRouter = createTRPCRouter({
}
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -166,7 +178,10 @@ export const composeRouter = createTRPCRouter({
.input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
@@ -188,7 +203,7 @@ export const composeRouter = createTRPCRouter({
const composeResult = await findComposeById(input.composeId);
if (
- composeResult.project.organizationId !==
+ composeResult.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -196,7 +211,6 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to delete this compose",
});
}
- 4;
const result = await db
.delete(composeTable)
@@ -215,13 +229,16 @@ export const composeRouter = createTRPCRouter({
} catch (_) {}
}
- return result[0];
+ return composeResult;
}),
cleanQueues: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this compose",
@@ -234,7 +251,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFetchServices)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
@@ -251,7 +271,10 @@ export const composeRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
@@ -270,7 +293,8 @@ export const composeRouter = createTRPCRouter({
const compose = await findComposeById(input.composeId);
if (
- compose.project.organizationId !== ctx.session.activeOrganizationId
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -296,7 +320,10 @@ export const composeRouter = createTRPCRouter({
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
@@ -308,7 +335,10 @@ export const composeRouter = createTRPCRouter({
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
@@ -323,7 +353,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
@@ -337,11 +370,14 @@ export const composeRouter = createTRPCRouter({
}),
deploy: protectedProcedure
- .input(apiFindCompose)
+ .input(apiDeployCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this compose",
@@ -349,10 +385,10 @@ export const composeRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
composeId: input.composeId,
- titleLog: "Manual deployment",
+ titleLog: input.title || "Manual deployment",
type: "deploy",
applicationType: "compose",
- descriptionLog: "",
+ descriptionLog: input.description || "",
server: !!compose.serverId,
};
@@ -371,10 +407,13 @@ export const composeRouter = createTRPCRouter({
);
}),
redeploy: protectedProcedure
- .input(apiFindCompose)
+ .input(apiRedeployCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this compose",
@@ -382,10 +421,10 @@ export const composeRouter = createTRPCRouter({
}
const jobData: DeploymentJob = {
composeId: input.composeId,
- titleLog: "Rebuild deployment",
+ titleLog: input.title || "Rebuild deployment",
type: "redeploy",
applicationType: "compose",
- descriptionLog: "",
+ descriptionLog: input.description || "",
server: !!compose.serverId,
};
if (IS_CLOUD && compose.serverId) {
@@ -406,7 +445,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
@@ -420,7 +462,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
@@ -435,7 +480,10 @@ export const composeRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
@@ -448,7 +496,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this compose",
@@ -462,17 +513,19 @@ export const composeRouter = createTRPCRouter({
deployTemplate: protectedProcedure
.input(
z.object({
- projectId: z.string(),
+ environmentId: z.string(),
serverId: z.string().optional(),
id: z.string(),
baseUrl: z.string().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
+ const environment = await findEnvironmentById(input.environmentId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ environment.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -490,7 +543,7 @@ export const composeRouter = createTRPCRouter({
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
- const project = await findProjectById(input.projectId);
+ const project = await findProjectById(environment.projectId);
if (input.serverId) {
const server = await findServerById(input.serverId);
@@ -591,7 +644,10 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to disconnect this git provider",
@@ -647,30 +703,38 @@ export const composeRouter = createTRPCRouter({
.input(
z.object({
composeId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
const updatedCompose = await db
.update(composeTable)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(composeTable.composeId, input.composeId))
.returning()
@@ -698,7 +762,8 @@ export const composeRouter = createTRPCRouter({
const compose = await findComposeById(input.composeId);
if (
- compose.project.organizationId !== ctx.session.activeOrganizationId
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -769,7 +834,8 @@ export const composeRouter = createTRPCRouter({
);
if (
- compose.project.organizationId !== ctx.session.activeOrganizationId
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -863,4 +929,57 @@ export const composeRouter = createTRPCRouter({
});
}
}),
+
+ cancelDeployment: protectedProcedure
+ .input(apiFindCompose)
+ .mutation(async ({ input, ctx }) => {
+ 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 cancel this deployment",
+ });
+ }
+
+ if (IS_CLOUD && compose.serverId) {
+ try {
+ await updateCompose(input.composeId, {
+ composeStatus: "idle",
+ });
+
+ if (compose.deployments[0]) {
+ await updateDeploymentStatus(
+ compose.deployments[0].deploymentId,
+ "done",
+ );
+ }
+
+ await cancelDeployment({
+ composeId: input.composeId,
+ applicationType: "compose",
+ });
+
+ return {
+ success: true,
+ message: "Deployment cancellation requested",
+ };
+ } catch (error) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message:
+ error instanceof Error
+ ? error.message
+ : "Failed to cancel deployment",
+ });
+ }
+ }
+
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Deployment cancellation only available in cloud version",
+ });
+ }),
});
diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts
index 40a0834ff..9004a0a05 100644
--- a/apps/dokploy/server/api/routers/deployment.ts
+++ b/apps/dokploy/server/api/routers/deployment.ts
@@ -29,7 +29,8 @@ export const deploymentRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -43,7 +44,10 @@ export const deploymentRouter = createTRPCRouter({
.input(apiFindAllByCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts
index 7c16baf34..1f6264351 100644
--- a/apps/dokploy/server/api/routers/domain.ts
+++ b/apps/dokploy/server/api/routers/domain.ts
@@ -34,7 +34,8 @@ export const domainRouter = createTRPCRouter({
if (input.domainType === "compose" && input.composeId) {
const compose = await findComposeById(input.composeId);
if (
- compose.project.organizationId !== ctx.session.activeOrganizationId
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -44,7 +45,7 @@ export const domainRouter = createTRPCRouter({
} else if (input.domainType === "application" && input.applicationId) {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !==
+ application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -70,7 +71,8 @@ export const domainRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -83,7 +85,10 @@ export const domainRouter = createTRPCRouter({
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -122,7 +127,8 @@ export const domainRouter = createTRPCRouter({
if (currentDomain.applicationId) {
const newApp = await findApplicationById(currentDomain.applicationId);
if (
- newApp.project.organizationId !== ctx.session.activeOrganizationId
+ newApp.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -132,7 +138,8 @@ export const domainRouter = createTRPCRouter({
} else if (currentDomain.composeId) {
const newCompose = await findComposeById(currentDomain.composeId);
if (
- newCompose.project.organizationId !== ctx.session.activeOrganizationId
+ newCompose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -144,8 +151,8 @@ export const domainRouter = createTRPCRouter({
currentDomain.previewDeploymentId,
);
if (
- newPreviewDeployment.application.project.organizationId !==
- ctx.session.activeOrganizationId
+ newPreviewDeployment.application.environment.project
+ .organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -175,7 +182,8 @@ export const domainRouter = createTRPCRouter({
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -184,7 +192,10 @@ export const domainRouter = createTRPCRouter({
}
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
- if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
@@ -200,7 +211,7 @@ export const domainRouter = createTRPCRouter({
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (
- application.project.organizationId !==
+ application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -211,7 +222,8 @@ export const domainRouter = createTRPCRouter({
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (
- compose.project.organizationId !== ctx.session.activeOrganizationId
+ compose.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
diff --git a/apps/dokploy/server/api/routers/environment.ts b/apps/dokploy/server/api/routers/environment.ts
new file mode 100644
index 000000000..98c565134
--- /dev/null
+++ b/apps/dokploy/server/api/routers/environment.ts
@@ -0,0 +1,343 @@
+import {
+ addNewEnvironment,
+ checkEnvironmentAccess,
+ createEnvironment,
+ deleteEnvironment,
+ duplicateEnvironment,
+ findEnvironmentById,
+ findEnvironmentsByProjectId,
+ findMemberById,
+ updateEnvironmentById,
+} from "@dokploy/server";
+import { TRPCError } from "@trpc/server";
+import { z } from "zod";
+import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
+import {
+ apiCreateEnvironment,
+ apiDuplicateEnvironment,
+ apiFindOneEnvironment,
+ apiRemoveEnvironment,
+ apiUpdateEnvironment,
+} from "@/server/db/schema";
+
+// Helper function to filter services within an environment based on user permissions
+const filterEnvironmentServices = (
+ environment: any,
+ accessedServices: string[],
+) => ({
+ ...environment,
+ applications: environment.applications.filter((app: any) =>
+ accessedServices.includes(app.applicationId),
+ ),
+ mariadb: environment.mariadb.filter((db: any) =>
+ accessedServices.includes(db.mariadbId),
+ ),
+ mongo: environment.mongo.filter((db: any) =>
+ accessedServices.includes(db.mongoId),
+ ),
+ mysql: environment.mysql.filter((db: any) =>
+ accessedServices.includes(db.mysqlId),
+ ),
+ postgres: environment.postgres.filter((db: any) =>
+ accessedServices.includes(db.postgresId),
+ ),
+ redis: environment.redis.filter((db: any) =>
+ accessedServices.includes(db.redisId),
+ ),
+ compose: environment.compose.filter((comp: any) =>
+ accessedServices.includes(comp.composeId),
+ ),
+});
+
+export const environmentRouter = createTRPCRouter({
+ create: protectedProcedure
+ .input(apiCreateEnvironment)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ // Check if user has access to the project
+ // This would typically involve checking project ownership/membership
+ // For now, we'll use a basic organization check
+
+ if (input.name === "production") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Environment name cannot be production",
+ });
+ }
+
+ const environment = await createEnvironment(input);
+
+ if (ctx.user.role === "member") {
+ await addNewEnvironment(
+ ctx.user.id,
+ environment.environmentId,
+ ctx.session.activeOrganizationId,
+ );
+ }
+ return environment;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Error creating the environment: ${error instanceof Error ? error.message : error}`,
+ cause: error,
+ });
+ }
+ }),
+
+ one: protectedProcedure
+ .input(apiFindOneEnvironment)
+ .query(async ({ input, ctx }) => {
+ try {
+ if (ctx.user.role === "member") {
+ await checkEnvironmentAccess(
+ ctx.user.id,
+ input.environmentId,
+ ctx.session.activeOrganizationId,
+ "access",
+ );
+ }
+ const environment = await findEnvironmentById(input.environmentId);
+ if (
+ environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to access this environment",
+ });
+ }
+
+ // Check environment access and filter services for members
+ if (ctx.user.role === "member") {
+ const { accessedEnvironments, accessedServices } =
+ await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
+
+ if (!accessedEnvironments.includes(environment.environmentId)) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to access this environment",
+ });
+ }
+
+ // Filter services based on member permissions
+ const filteredEnvironment = filterEnvironmentServices(
+ environment,
+ accessedServices,
+ );
+
+ return filteredEnvironment;
+ }
+
+ return environment;
+ } catch (error) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Environment not found",
+ });
+ }
+ }),
+
+ byProjectId: protectedProcedure
+ .input(z.object({ projectId: z.string() }))
+ .query(async ({ input, ctx }) => {
+ try {
+ const environments = await findEnvironmentsByProjectId(input.projectId);
+
+ // Check organization access
+ if (
+ environments.some(
+ (environment) =>
+ environment.project.organizationId !==
+ ctx.session.activeOrganizationId,
+ )
+ ) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to access this environment",
+ });
+ }
+
+ // Filter environments for members based on their permissions
+ if (ctx.user.role === "member") {
+ const { accessedEnvironments, accessedServices } =
+ await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
+
+ // Filter environments to only show those the member has access to
+ const filteredEnvironments = environments
+ .filter((environment) =>
+ accessedEnvironments.includes(environment.environmentId),
+ )
+ .map((environment) =>
+ filterEnvironmentServices(environment, accessedServices),
+ );
+
+ return filteredEnvironments;
+ }
+
+ return environments;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Error fetching environments: ${error instanceof Error ? error.message : error}`,
+ });
+ }
+ }),
+
+ remove: protectedProcedure
+ .input(apiRemoveEnvironment)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ if (ctx.user.role === "member") {
+ await checkEnvironmentAccess(
+ ctx.user.id,
+ input.environmentId,
+ ctx.session.activeOrganizationId,
+ "access",
+ );
+ }
+ const environment = await findEnvironmentById(input.environmentId);
+ if (
+ environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to access this environment",
+ });
+ }
+
+ // Check environment access for members
+ if (ctx.user.role === "member") {
+ const { accessedEnvironments } = await findMemberById(
+ ctx.user.id,
+ ctx.session.activeOrganizationId,
+ );
+
+ if (!accessedEnvironments.includes(environment.environmentId)) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to delete this environment",
+ });
+ }
+ }
+
+ const deletedEnvironment = await deleteEnvironment(input.environmentId);
+ return deletedEnvironment;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`,
+ });
+ }
+ }),
+
+ update: protectedProcedure
+ .input(apiUpdateEnvironment)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { environmentId, ...updateData } = input;
+
+ if (updateData.name === "production") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Environment name cannot be production",
+ });
+ }
+
+ if (ctx.user.role === "member") {
+ await checkEnvironmentAccess(
+ ctx.user.id,
+ environmentId,
+ ctx.session.activeOrganizationId,
+ "access",
+ );
+ }
+ const currentEnvironment = await findEnvironmentById(environmentId);
+ if (
+ currentEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to access this environment",
+ });
+ }
+
+ // Check environment access for members
+ if (ctx.user.role === "member") {
+ const { accessedEnvironments } = await findMemberById(
+ ctx.user.id,
+ ctx.session.activeOrganizationId,
+ );
+
+ if (
+ !accessedEnvironments.includes(currentEnvironment.environmentId)
+ ) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to update this environment",
+ });
+ }
+ }
+
+ const environment = await updateEnvironmentById(
+ environmentId,
+ updateData,
+ );
+ return environment;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Error updating the environment: ${error instanceof Error ? error.message : error}`,
+ });
+ }
+ }),
+
+ duplicate: protectedProcedure
+ .input(apiDuplicateEnvironment)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ if (ctx.user.role === "member") {
+ await checkEnvironmentAccess(
+ ctx.user.id,
+ input.environmentId,
+ ctx.session.activeOrganizationId,
+ "access",
+ );
+ }
+ const environment = await findEnvironmentById(input.environmentId);
+ if (
+ environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to access this environment",
+ });
+ }
+
+ // Check environment access for members
+ if (ctx.user.role === "member") {
+ const { accessedEnvironments } = await findMemberById(
+ ctx.user.id,
+ ctx.session.activeOrganizationId,
+ );
+
+ if (!accessedEnvironments.includes(environment.environmentId)) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to duplicate this environment",
+ });
+ }
+ }
+
+ const duplicatedEnvironment = await duplicateEnvironment(input);
+ return duplicatedEnvironment;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Error duplicating the environment: ${error instanceof Error ? error.message : error}`,
+ });
+ }
+ }),
+});
diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts
index 9fc95a35e..dc811e0ca 100644
--- a/apps/dokploy/server/api/routers/mariadb.ts
+++ b/apps/dokploy/server/api/routers/mariadb.ts
@@ -6,6 +6,7 @@ import {
deployMariadb,
findBackupsByDbId,
findMariadbById,
+ findEnvironmentById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
@@ -41,10 +42,14 @@ export const mariadbRouter = createTRPCRouter({
.input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,14 +62,15 @@ export const mariadbRouter = createTRPCRouter({
});
}
- const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
- const newMariadb = await createMariadb(input);
+ const newMariadb = await createMariadb({
+ ...input,
+ });
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -101,7 +107,10 @@ export const mariadbRouter = createTRPCRouter({
);
}
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this Mariadb",
@@ -114,7 +123,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
const service = await findMariadbById(input.mariadbId);
- if (service.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ service.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Mariadb",
@@ -151,7 +163,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiSaveExternalPortMariaDB)
.mutation(async ({ input, ctx }) => {
const mongo = await findMariadbById(input.mariadbId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -167,7 +182,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiDeployMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Mariadb",
@@ -188,7 +206,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiDeployMariaDB)
.subscription(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Mariadb",
@@ -205,7 +226,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiChangeMariaDBStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMariadbById(input.mariadbId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Mariadb status",
@@ -229,7 +253,10 @@ export const mariadbRouter = createTRPCRouter({
}
const mongo = await findMariadbById(input.mariadbId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this Mariadb",
@@ -255,7 +282,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -278,7 +308,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiResetMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Mariadb",
@@ -308,7 +341,10 @@ export const mariadbRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { mariadbId, ...rest } = input;
const mariadb = await findMariadbById(mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this Mariadb",
@@ -331,23 +367,31 @@ export const mariadbRouter = createTRPCRouter({
.input(
z.object({
mariadbId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
@@ -355,7 +399,7 @@ export const mariadbRouter = createTRPCRouter({
const updatedMariadb = await db
.update(mariadbTable)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(mariadbTable.mariadbId, input.mariadbId))
.returning()
@@ -374,7 +418,10 @@ export const mariadbRouter = createTRPCRouter({
.input(apiRebuildMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
- if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mariadb.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MariaDB database",
diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts
index 026a7fe20..1f054a1c9 100644
--- a/apps/dokploy/server/api/routers/mongo.ts
+++ b/apps/dokploy/server/api/routers/mongo.ts
@@ -6,6 +6,7 @@ import {
deployMongo,
findBackupsByDbId,
findMongoById,
+ findEnvironmentById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
@@ -41,10 +42,14 @@ export const mongoRouter = createTRPCRouter({
.input(apiCreateMongo)
.mutation(async ({ input, ctx }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,14 +62,15 @@ export const mongoRouter = createTRPCRouter({
});
}
- const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
- const newMongo = await createMongo(input);
+ const newMongo = await createMongo({
+ ...input,
+ });
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -106,7 +112,10 @@ export const mongoRouter = createTRPCRouter({
}
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mongo",
@@ -120,7 +129,10 @@ export const mongoRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const service = await findMongoById(input.mongoId);
- if (service.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ service.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this mongo",
@@ -143,7 +155,10 @@ export const mongoRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this mongo",
@@ -165,7 +180,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiSaveExternalPortMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -181,7 +199,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiDeployMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
@@ -201,7 +222,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiDeployMongo)
.subscription(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
@@ -218,7 +242,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiChangeMongoStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this mongo status",
@@ -233,7 +260,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiResetMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this mongo",
@@ -272,7 +302,10 @@ export const mongoRouter = createTRPCRouter({
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mongo",
@@ -298,7 +331,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -322,7 +358,10 @@ export const mongoRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { mongoId, ...rest } = input;
const mongo = await findMongoById(mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mongo",
@@ -345,23 +384,31 @@ export const mongoRouter = createTRPCRouter({
.input(
z.object({
mongoId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
@@ -369,7 +416,7 @@ export const mongoRouter = createTRPCRouter({
const updatedMongo = await db
.update(mongoTable)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(mongoTable.mongoId, input.mongoId))
.returning()
@@ -388,7 +435,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiRebuildMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MongoDB database",
diff --git a/apps/dokploy/server/api/routers/mount.ts b/apps/dokploy/server/api/routers/mount.ts
index 4ffec8c19..814d3d392 100644
--- a/apps/dokploy/server/api/routers/mount.ts
+++ b/apps/dokploy/server/api/routers/mount.ts
@@ -3,9 +3,11 @@ import {
deleteMount,
findApplicationById,
findMountById,
+ findMountOrganizationId,
getServiceContainer,
updateMount,
} from "@dokploy/server";
+import { TRPCError } from "@trpc/server";
import { z } from "zod";
import {
apiCreateMount,
@@ -24,16 +26,39 @@ export const mountRouter = createTRPCRouter({
}),
remove: protectedProcedure
.input(apiRemoveMount)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
+ const organizationId = await findMountOrganizationId(input.mountId);
+ if (organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to delete this mount",
+ });
+ }
return await deleteMount(input.mountId);
}),
- one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
- return await findMountById(input.mountId);
- }),
+ one: protectedProcedure
+ .input(apiFindOneMount)
+ .query(async ({ input, ctx }) => {
+ const organizationId = await findMountOrganizationId(input.mountId);
+ if (organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this mount",
+ });
+ }
+ return await findMountById(input.mountId);
+ }),
update: protectedProcedure
.input(apiUpdateMount)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
+ const organizationId = await findMountOrganizationId(input.mountId);
+ if (organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to update this mount",
+ });
+ }
return await updateMount(input.mountId, input);
}),
allNamedByApplicationId: protectedProcedure
diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts
index 254bd836a..5edb27da4 100644
--- a/apps/dokploy/server/api/routers/mysql.ts
+++ b/apps/dokploy/server/api/routers/mysql.ts
@@ -5,6 +5,7 @@ import {
createMysql,
deployMySql,
findBackupsByDbId,
+ findEnvironmentById,
findMySqlById,
findProjectById,
IS_CLOUD,
@@ -42,10 +43,14 @@ export const mysqlRouter = createTRPCRouter({
.input(apiCreateMySql)
.mutation(async ({ input, ctx }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,8 +62,7 @@ export const mysqlRouter = createTRPCRouter({
message: "You need to use a server to create a MySQL",
});
}
- 1;
- const project = await findProjectById(input.projectId);
+
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -66,7 +70,9 @@ export const mysqlRouter = createTRPCRouter({
});
}
- const newMysql = await createMysql(input);
+ const newMysql = await createMysql({
+ ...input,
+ });
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -107,7 +113,10 @@ export const mysqlRouter = createTRPCRouter({
);
}
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this MySQL",
@@ -120,7 +129,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
const service = await findMySqlById(input.mysqlId);
- if (service.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ service.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this MySQL",
@@ -142,7 +154,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this MySQL",
@@ -163,7 +178,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiSaveExternalPortMySql)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -179,7 +197,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiDeployMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this MySQL",
@@ -199,7 +220,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiDeployMySql)
.subscription(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this MySQL",
@@ -216,7 +240,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiChangeMySqlStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findMySqlById(input.mysqlId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this MySQL status",
@@ -231,7 +258,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiResetMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this MySQL",
@@ -267,7 +297,10 @@ export const mysqlRouter = createTRPCRouter({
);
}
const mongo = await findMySqlById(input.mysqlId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this MySQL",
@@ -293,7 +326,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -317,7 +353,10 @@ export const mysqlRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { mysqlId, ...rest } = input;
const mysql = await findMySqlById(mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this MySQL",
@@ -340,23 +379,31 @@ export const mysqlRouter = createTRPCRouter({
.input(
z.object({
mysqlId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
@@ -364,7 +411,7 @@ export const mysqlRouter = createTRPCRouter({
const updatedMysql = await db
.update(mysqlTable)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(mysqlTable.mysqlId, input.mysqlId))
.returning()
@@ -383,7 +430,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiRebuildMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
- if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mysql.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MySQL database",
diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts
index 59812fbb3..09a2aed2a 100644
--- a/apps/dokploy/server/api/routers/notification.ts
+++ b/apps/dokploy/server/api/routers/notification.ts
@@ -2,6 +2,7 @@ import {
createDiscordNotification,
createEmailNotification,
createGotifyNotification,
+ createNtfyNotification,
createSlackNotification,
createTelegramNotification,
findNotificationById,
@@ -10,12 +11,14 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
+ sendNtfyNotification,
sendServerThresholdNotifications,
sendSlackNotification,
sendTelegramNotification,
updateDiscordNotification,
updateEmailNotification,
updateGotifyNotification,
+ updateNtfyNotification,
updateSlackNotification,
updateTelegramNotification,
} from "@dokploy/server";
@@ -33,17 +36,20 @@ import {
apiCreateDiscord,
apiCreateEmail,
apiCreateGotify,
+ apiCreateNtfy,
apiCreateSlack,
apiCreateTelegram,
apiFindOneNotification,
apiTestDiscordConnection,
apiTestEmailConnection,
apiTestGotifyConnection,
+ apiTestNtfyConnection,
apiTestSlackConnection,
apiTestTelegramConnection,
apiUpdateDiscord,
apiUpdateEmail,
apiUpdateGotify,
+ apiUpdateNtfy,
apiUpdateSlack,
apiUpdateTelegram,
notifications,
@@ -321,6 +327,7 @@ export const notificationRouter = createTRPCRouter({
discord: true,
email: true,
gotify: true,
+ ntfy: true,
},
orderBy: desc(notifications.createdAt),
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
@@ -446,6 +453,64 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
+ createNtfy: adminProcedure
+ .input(apiCreateNtfy)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ return await createNtfyNotification(
+ input,
+ ctx.session.activeOrganizationId,
+ );
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error creating the notification",
+ cause: error,
+ });
+ }
+ }),
+ updateNtfy: adminProcedure
+ .input(apiUpdateNtfy)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const notification = await findNotificationById(input.notificationId);
+ if (
+ IS_CLOUD &&
+ notification.organizationId !== ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to update this notification",
+ });
+ }
+ return await updateNtfyNotification({
+ ...input,
+ organizationId: ctx.session.activeOrganizationId,
+ });
+ } catch (error) {
+ throw error;
+ }
+ }),
+ testNtfyConnection: adminProcedure
+ .input(apiTestNtfyConnection)
+ .mutation(async ({ input }) => {
+ try {
+ await sendNtfyNotification(
+ input,
+ "Test Notification",
+ "",
+ "view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;",
+ "Hi, From Dokploy 👋",
+ );
+ return true;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error testing the notification",
+ cause: error,
+ });
+ }
+ }),
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
return await db.query.notifications.findMany({
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
diff --git a/apps/dokploy/server/api/routers/port.ts b/apps/dokploy/server/api/routers/port.ts
index 40ba23d91..bbd949804 100644
--- a/apps/dokploy/server/api/routers/port.ts
+++ b/apps/dokploy/server/api/routers/port.ts
@@ -27,22 +27,44 @@ export const portRouter = createTRPCRouter({
});
}
}),
- one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => {
- try {
- return await finPortById(input.portId);
- } catch (error) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Port not found",
- cause: error,
- });
- }
- }),
+ one: protectedProcedure
+ .input(apiFindOnePort)
+ .query(async ({ input, ctx }) => {
+ try {
+ const port = await finPortById(input.portId);
+ if (
+ port.application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to access this port",
+ });
+ }
+ return port;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Port not found",
+ cause: error,
+ });
+ }
+ }),
delete: protectedProcedure
.input(apiFindOnePort)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
+ const port = await finPortById(input.portId);
+ if (
+ port.application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to delete this port",
+ });
+ }
try {
- return removePortById(input.portId);
+ return await removePortById(input.portId);
} catch (error) {
const message =
error instanceof Error ? error.message : "Error input: Deleting port";
@@ -54,9 +76,19 @@ export const portRouter = createTRPCRouter({
}),
update: protectedProcedure
.input(apiUpdatePort)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
+ const port = await finPortById(input.portId);
+ if (
+ port.application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to update this port",
+ });
+ }
try {
- return updatePortById(input.portId, input);
+ return await updatePortById(input.portId, input);
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating the port";
diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts
index 12d244241..a05072ab7 100644
--- a/apps/dokploy/server/api/routers/postgres.ts
+++ b/apps/dokploy/server/api/routers/postgres.ts
@@ -5,6 +5,7 @@ import {
createPostgres,
deployPostgres,
findBackupsByDbId,
+ findEnvironmentById,
findPostgresById,
findProjectById,
IS_CLOUD,
@@ -41,10 +42,14 @@ export const postgresRouter = createTRPCRouter({
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -57,14 +62,15 @@ export const postgresRouter = createTRPCRouter({
});
}
- const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
- const newPostgres = await createPostgres(input);
+ const newPostgres = await createPostgres({
+ ...input,
+ });
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -107,7 +113,8 @@ export const postgresRouter = createTRPCRouter({
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -122,7 +129,10 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const service = await findPostgresById(input.postgresId);
- if (service.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ service.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Postgres",
@@ -145,7 +155,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -169,7 +180,8 @@ export const postgresRouter = createTRPCRouter({
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -187,7 +199,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -210,7 +223,8 @@ export const postgresRouter = createTRPCRouter({
.subscription(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -229,7 +243,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -255,7 +270,8 @@ export const postgresRouter = createTRPCRouter({
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -280,7 +296,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -305,7 +322,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -337,7 +355,8 @@ export const postgresRouter = createTRPCRouter({
const { postgresId, ...rest } = input;
const postgres = await findPostgresById(postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -361,13 +380,14 @@ export const postgresRouter = createTRPCRouter({
.input(
z.object({
postgresId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -375,11 +395,16 @@ export const postgresRouter = createTRPCRouter({
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
@@ -387,7 +412,7 @@ export const postgresRouter = createTRPCRouter({
const updatedPostgres = await db
.update(postgresTable)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(postgresTable.postgresId, input.postgresId))
.returning()
@@ -407,7 +432,8 @@ export const postgresRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
- postgres.project.organizationId !== ctx.session.activeOrganizationId
+ postgres.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
diff --git a/apps/dokploy/server/api/routers/preview-deployment.ts b/apps/dokploy/server/api/routers/preview-deployment.ts
index d4cf16f4e..49b781101 100644
--- a/apps/dokploy/server/api/routers/preview-deployment.ts
+++ b/apps/dokploy/server/api/routers/preview-deployment.ts
@@ -15,7 +15,8 @@ export const previewDeploymentRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -31,7 +32,7 @@ export const previewDeploymentRouter = createTRPCRouter({
input.previewDeploymentId,
);
if (
- previewDeployment.application.project.organizationId !==
+ previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
@@ -49,7 +50,7 @@ export const previewDeploymentRouter = createTRPCRouter({
input.previewDeploymentId,
);
if (
- previewDeployment.application.project.organizationId !==
+ previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts
index 2d1bd5af1..5dd2ee695 100644
--- a/apps/dokploy/server/api/routers/project.ts
+++ b/apps/dokploy/server/api/routers/project.ts
@@ -19,6 +19,7 @@ import {
deleteProject,
findApplicationById,
findComposeById,
+ findEnvironmentById,
findMariadbById,
findMemberById,
findMongoById,
@@ -43,6 +44,7 @@ import {
apiUpdateProject,
applications,
compose,
+ environments,
mariadb,
mongo,
mysql,
@@ -80,7 +82,7 @@ export const projectRouter = createTRPCRouter({
if (ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
- project.projectId,
+ project.project.projectId,
ctx.session.activeOrganizationId,
);
}
@@ -117,29 +119,42 @@ export const projectRouter = createTRPCRouter({
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
with: {
- applications: {
- where: buildServiceFilter(
- applications.applicationId,
- accessedServices,
- ),
- },
- compose: {
- where: buildServiceFilter(compose.composeId, accessedServices),
- },
- mariadb: {
- where: buildServiceFilter(mariadb.mariadbId, accessedServices),
- },
- mongo: {
- where: buildServiceFilter(mongo.mongoId, accessedServices),
- },
- mysql: {
- where: buildServiceFilter(mysql.mysqlId, accessedServices),
- },
- postgres: {
- where: buildServiceFilter(postgres.postgresId, accessedServices),
- },
- redis: {
- where: buildServiceFilter(redis.redisId, accessedServices),
+ environments: {
+ with: {
+ applications: {
+ where: buildServiceFilter(
+ applications.applicationId,
+ accessedServices,
+ ),
+ },
+ compose: {
+ where: buildServiceFilter(
+ compose.composeId,
+ accessedServices,
+ ),
+ },
+ mariadb: {
+ where: buildServiceFilter(
+ mariadb.mariadbId,
+ accessedServices,
+ ),
+ },
+ mongo: {
+ where: buildServiceFilter(mongo.mongoId, accessedServices),
+ },
+ mysql: {
+ where: buildServiceFilter(mysql.mysqlId, accessedServices),
+ },
+ postgres: {
+ where: buildServiceFilter(
+ postgres.postgresId,
+ accessedServices,
+ ),
+ },
+ redis: {
+ where: buildServiceFilter(redis.redisId, accessedServices),
+ },
+ },
},
},
});
@@ -164,15 +179,22 @@ export const projectRouter = createTRPCRouter({
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "member") {
- const { accessedProjects, accessedServices } = await findMemberById(
- ctx.user.id,
- ctx.session.activeOrganizationId,
- );
+ const { accessedProjects, accessedEnvironments, accessedServices } =
+ await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
if (accessedProjects.length === 0) {
return [];
}
+ // Build environment filter
+ const environmentFilter =
+ accessedEnvironments.length === 0
+ ? sql`false`
+ : sql`${environments.environmentId} IN (${sql.join(
+ accessedEnvironments.map((envId) => sql`${envId}`),
+ sql`, `,
+ )})`;
+
return await db.query.projects.findMany({
where: and(
sql`${projects.projectId} IN (${sql.join(
@@ -182,31 +204,39 @@ export const projectRouter = createTRPCRouter({
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
with: {
- applications: {
- where: buildServiceFilter(
- applications.applicationId,
- accessedServices,
- ),
- with: { domains: true },
- },
- mariadb: {
- where: buildServiceFilter(mariadb.mariadbId, accessedServices),
- },
- mongo: {
- where: buildServiceFilter(mongo.mongoId, accessedServices),
- },
- mysql: {
- where: buildServiceFilter(mysql.mysqlId, accessedServices),
- },
- postgres: {
- where: buildServiceFilter(postgres.postgresId, accessedServices),
- },
- redis: {
- where: buildServiceFilter(redis.redisId, accessedServices),
- },
- compose: {
- where: buildServiceFilter(compose.composeId, accessedServices),
- with: { domains: true },
+ environments: {
+ where: environmentFilter,
+ with: {
+ applications: {
+ where: buildServiceFilter(
+ applications.applicationId,
+ accessedServices,
+ ),
+ with: { domains: true },
+ },
+ mariadb: {
+ where: buildServiceFilter(mariadb.mariadbId, accessedServices),
+ },
+ mongo: {
+ where: buildServiceFilter(mongo.mongoId, accessedServices),
+ },
+ mysql: {
+ where: buildServiceFilter(mysql.mysqlId, accessedServices),
+ },
+ postgres: {
+ where: buildServiceFilter(
+ postgres.postgresId,
+ accessedServices,
+ ),
+ },
+ redis: {
+ where: buildServiceFilter(redis.redisId, accessedServices),
+ },
+ compose: {
+ where: buildServiceFilter(compose.composeId, accessedServices),
+ with: { domains: true },
+ },
+ },
},
},
orderBy: desc(projects.createdAt),
@@ -215,19 +245,23 @@ export const projectRouter = createTRPCRouter({
return await db.query.projects.findMany({
with: {
- applications: {
+ environments: {
with: {
- domains: true,
- },
- },
- mariadb: true,
- mongo: true,
- mysql: true,
- postgres: true,
- redis: true,
- compose: {
- with: {
- domains: true,
+ applications: {
+ with: {
+ domains: true,
+ },
+ },
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ postgres: true,
+ redis: true,
+ compose: {
+ with: {
+ domains: true,
+ },
+ },
},
},
},
@@ -288,7 +322,7 @@ export const projectRouter = createTRPCRouter({
duplicate: protectedProcedure
.input(
z.object({
- sourceProjectId: z.string(),
+ sourceEnvironmentId: z.string(),
name: z.string(),
description: z.string().optional(),
includeServices: z.boolean().default(true),
@@ -322,9 +356,15 @@ export const projectRouter = createTRPCRouter({
}
// Get source project
- const sourceProject = await findProjectById(input.sourceProjectId);
+ const sourceEnvironment = input.duplicateInSameProject
+ ? await findEnvironmentById(input.sourceEnvironmentId)
+ : null;
- if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ input.duplicateInSameProject &&
+ sourceEnvironment?.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
@@ -333,15 +373,17 @@ export const projectRouter = createTRPCRouter({
// Create new project or use existing one
const targetProject = input.duplicateInSameProject
- ? sourceProject
+ ? sourceEnvironment
: await createProject(
{
name: input.name,
description: input.description,
- env: sourceProject.env,
+ env: sourceEnvironment?.project.env,
},
ctx.session.activeOrganizationId,
- );
+ ).then((value) => value.environment);
+
+ console.log("targetProject", targetProject);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
@@ -374,7 +416,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const domain of domains) {
@@ -444,7 +486,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -480,7 +522,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -516,7 +558,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -552,7 +594,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -588,7 +630,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -623,7 +665,7 @@ export const projectRouter = createTRPCRouter({
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
- projectId: targetProject.projectId,
+ environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
@@ -658,7 +700,7 @@ export const projectRouter = createTRPCRouter({
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
- targetProject.projectId,
+ targetProject?.projectId || "",
ctx.session.activeOrganizationId,
);
}
diff --git a/apps/dokploy/server/api/routers/redirects.ts b/apps/dokploy/server/api/routers/redirects.ts
index 6c002262d..f8b7014f2 100644
--- a/apps/dokploy/server/api/routers/redirects.ts
+++ b/apps/dokploy/server/api/routers/redirects.ts
@@ -19,7 +19,8 @@ export const redirectsRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -34,7 +35,8 @@ export const redirectsRouter = createTRPCRouter({
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -49,7 +51,8 @@ export const redirectsRouter = createTRPCRouter({
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -64,7 +67,8 @@ export const redirectsRouter = createTRPCRouter({
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts
index a403f8768..d377b1707 100644
--- a/apps/dokploy/server/api/routers/redis.ts
+++ b/apps/dokploy/server/api/routers/redis.ts
@@ -4,6 +4,7 @@ import {
createMount,
createRedis,
deployRedis,
+ findEnvironmentById,
findProjectById,
findRedisById,
IS_CLOUD,
@@ -40,10 +41,14 @@ export const redisRouter = createTRPCRouter({
.input(apiCreateRedis)
.mutation(async ({ input, ctx }) => {
try {
+ // Get project from environment
+ const environment = await findEnvironmentById(input.environmentId);
+ const project = await findProjectById(environment.projectId);
+
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
- input.projectId,
+ project.projectId,
ctx.session.activeOrganizationId,
"create",
);
@@ -56,14 +61,15 @@ export const redisRouter = createTRPCRouter({
});
}
- const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
- const newRedis = await createRedis(input);
+ const newRedis = await createRedis({
+ ...input,
+ });
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
@@ -80,7 +86,7 @@ export const redisRouter = createTRPCRouter({
type: "volume",
});
- return true;
+ return newRedis;
} catch (error) {
throw error;
}
@@ -98,7 +104,10 @@ export const redisRouter = createTRPCRouter({
}
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this Redis",
@@ -111,7 +120,10 @@ export const redisRouter = createTRPCRouter({
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Redis",
@@ -133,7 +145,10 @@ export const redisRouter = createTRPCRouter({
.input(apiResetRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Redis",
@@ -163,7 +178,10 @@ export const redisRouter = createTRPCRouter({
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this Redis",
@@ -184,7 +202,10 @@ export const redisRouter = createTRPCRouter({
.input(apiSaveExternalPortRedis)
.mutation(async ({ input, ctx }) => {
const mongo = await findRedisById(input.redisId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
@@ -200,7 +221,10 @@ export const redisRouter = createTRPCRouter({
.input(apiDeployRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Redis",
@@ -220,7 +244,10 @@ export const redisRouter = createTRPCRouter({
.input(apiDeployRedis)
.subscription(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Redis",
@@ -236,7 +263,10 @@ export const redisRouter = createTRPCRouter({
.input(apiChangeRedisStatus)
.mutation(async ({ input, ctx }) => {
const mongo = await findRedisById(input.redisId);
- if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ mongo.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Redis status",
@@ -261,7 +291,10 @@ export const redisRouter = createTRPCRouter({
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this Redis",
@@ -284,7 +317,10 @@ export const redisRouter = createTRPCRouter({
.input(apiSaveEnvironmentVariablesRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
@@ -324,23 +360,31 @@ export const redisRouter = createTRPCRouter({
.input(
z.object({
redisId: z.string(),
- targetProjectId: z.string(),
+ targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
- const targetProject = await findProjectById(input.targetProjectId);
- if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
+ const targetEnvironment = await findEnvironmentById(
+ input.targetEnvironmentId,
+ );
+ if (
+ targetEnvironment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
- message: "You are not authorized to move to this project",
+ message: "You are not authorized to move to this environment",
});
}
@@ -348,7 +392,7 @@ export const redisRouter = createTRPCRouter({
const updatedRedis = await db
.update(redisTable)
.set({
- projectId: input.targetProjectId,
+ environmentId: input.targetEnvironmentId,
})
.where(eq(redisTable.redisId, input.redisId))
.returning()
@@ -367,7 +411,10 @@ export const redisRouter = createTRPCRouter({
.input(apiRebuildRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
- if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
+ if (
+ redis.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
+ ) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Redis database",
diff --git a/apps/dokploy/server/api/routers/rollbacks.ts b/apps/dokploy/server/api/routers/rollbacks.ts
index b8b904172..d9e6180fb 100644
--- a/apps/dokploy/server/api/routers/rollbacks.ts
+++ b/apps/dokploy/server/api/routers/rollbacks.ts
@@ -1,4 +1,8 @@
-import { removeRollbackById, rollback } from "@dokploy/server";
+import {
+ findRollbackById,
+ removeRollbackById,
+ rollback,
+} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { apiFindOneRollback } from "@/server/db/schema";
@@ -22,8 +26,18 @@ export const rollbackRouter = createTRPCRouter({
}),
rollback: protectedProcedure
.input(apiFindOneRollback)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
try {
+ const currentRollback = await findRollbackById(input.rollbackId);
+ if (
+ currentRollback?.deployment?.application?.environment?.project
+ .organizationId !== ctx.session.activeOrganizationId
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to rollback this deployment",
+ });
+ }
return await rollback(input.rollbackId);
} catch (error) {
console.error(error);
diff --git a/apps/dokploy/server/api/routers/security.ts b/apps/dokploy/server/api/routers/security.ts
index d973ecc8d..3d8374b4c 100644
--- a/apps/dokploy/server/api/routers/security.ts
+++ b/apps/dokploy/server/api/routers/security.ts
@@ -19,7 +19,8 @@ export const securityRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -34,7 +35,8 @@ export const securityRouter = createTRPCRouter({
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -49,7 +51,8 @@ export const securityRouter = createTRPCRouter({
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -64,7 +67,8 @@ export const securityRouter = createTRPCRouter({
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
- application.project.organizationId !== ctx.session.activeOrganizationId
+ application.environment.project.organizationId !==
+ ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index 7ae0b6e85..02678b990 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -45,7 +45,7 @@ import {
} from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
-import { sql } from "drizzle-orm";
+import { eq, sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { z } from "zod";
@@ -60,6 +60,8 @@ import {
apiServerSchema,
apiTraefikConfig,
apiUpdateDockerCleanup,
+ projects,
+ server,
} from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup";
import packageInfo from "../../../package.json";
@@ -706,6 +708,18 @@ export const settingsRouter = createTRPCRouter({
isCloud: publicProcedure.query(async () => {
return IS_CLOUD;
}),
+ isUserSubscribed: protectedProcedure.query(async ({ ctx }) => {
+ const haveServers = await db.query.server.findMany({
+ where: eq(server.organizationId, ctx.session?.activeOrganizationId || ""),
+ });
+ const haveProjects = await db.query.projects.findMany({
+ where: eq(
+ projects.organizationId,
+ ctx.session?.activeOrganizationId || "",
+ ),
+ });
+ return haveServers.length > 0 || haveProjects.length > 0;
+ }),
health: publicProcedure.query(async () => {
if (IS_CLOUD) {
try {
diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts
index 3ac6e3940..55d6d3a46 100644
--- a/apps/dokploy/server/db/index.ts
+++ b/apps/dokploy/server/db/index.ts
@@ -6,14 +6,18 @@ declare global {
var db: PostgresJsDatabase | undefined;
}
+const dbUrl =
+ process.env.DATABASE_URL ||
+ "postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
+
export let db: PostgresJsDatabase;
if (process.env.NODE_ENV === "production") {
- db = drizzle(postgres(process.env.DATABASE_URL!), {
+ db = drizzle(postgres(dbUrl!), {
schema,
});
} else {
if (!global.db)
- global.db = drizzle(postgres(process.env.DATABASE_URL!), {
+ global.db = drizzle(postgres(dbUrl!), {
schema,
});
diff --git a/apps/dokploy/server/db/seed.ts b/apps/dokploy/server/db/seed.ts
deleted file mode 100644
index 5b3eb6c62..000000000
--- a/apps/dokploy/server/db/seed.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { drizzle } from "drizzle-orm/postgres-js";
-import postgres from "postgres";
-
-const connectionString = process.env.DATABASE_URL!;
-
-const pg = postgres(connectionString, { max: 1 });
-const _db = drizzle(pg);
-
-async function seed() {
- console.log("> Seed:", process.env.DATABASE_PATH, "\n");
-
- // const authenticationR = await db
- // .insert(users)
- // .values([
- // {
- // email: "user1@hotmail.com",
- // password: password("12345671"),
- // },
- // ])
- // .onConflictDoNothing()
- // .returning();
-
- // console.log("\nSemillas Update:", authenticationR.length);
-}
-
-seed().catch((e) => {
- console.error(e);
- process.exit(1);
-});
diff --git a/apps/dokploy/server/utils/deploy.ts b/apps/dokploy/server/utils/deploy.ts
index df8fc8041..f4591e3b3 100644
--- a/apps/dokploy/server/utils/deploy.ts
+++ b/apps/dokploy/server/utils/deploy.ts
@@ -23,3 +23,30 @@ export const deploy = async (jobData: DeploymentJob) => {
throw error;
}
};
+
+type CancelDeploymentData =
+ | { applicationId: string; applicationType: "application" }
+ | { composeId: string; applicationType: "compose" };
+
+export const cancelDeployment = async (cancelData: CancelDeploymentData) => {
+ try {
+ const result = await fetch(`${process.env.SERVER_URL}/cancel-deployment`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": process.env.API_KEY || "NO-DEFINED",
+ },
+ body: JSON.stringify(cancelData),
+ });
+
+ if (!result.ok) {
+ const errorData = await result.json().catch(() => ({}));
+ throw new Error(errorData.message || "Failed to cancel deployment");
+ }
+
+ const data = await result.json();
+ return data;
+ } catch (error) {
+ throw error;
+ }
+};
diff --git a/apps/dokploy/setup.ts b/apps/dokploy/setup.ts
index 7abf9fa2d..13590e4e7 100644
--- a/apps/dokploy/setup.ts
+++ b/apps/dokploy/setup.ts
@@ -21,7 +21,7 @@ import {
await initializeNetwork();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
- await execAsync("docker pull traefik:v3.1.2");
+ await execAsync("docker pull traefik:v3.5.0");
await initializeStandaloneTraefik();
await initializeRedis();
await initializePostgres();
diff --git a/packages/server/package.json b/packages/server/package.json
index eeee8f831..3b249a65b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -28,13 +28,13 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@ai-sdk/anthropic": "^1.2.12",
- "@ai-sdk/azure": "^1.3.23",
- "@ai-sdk/cohere": "^1.2.10",
- "@ai-sdk/deepinfra": "^0.0.4",
- "@ai-sdk/mistral": "^1.2.8",
- "@ai-sdk/openai": "^1.3.22",
- "@ai-sdk/openai-compatible": "^0.0.13",
+ "@ai-sdk/anthropic": "^2.0.5",
+ "@ai-sdk/azure": "^2.0.16",
+ "@ai-sdk/cohere": "^2.0.4",
+ "@ai-sdk/deepinfra": "^1.0.10",
+ "@ai-sdk/mistral": "^2.0.7",
+ "@ai-sdk/openai": "^2.0.16",
+ "@ai-sdk/openai-compatible": "^1.0.10",
"@better-auth/utils": "0.2.4",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.1.3",
@@ -44,7 +44,8 @@
"@react-email/components": "^0.0.21",
"@trpc/server": "^10.45.2",
"adm-zip": "^0.5.16",
- "ai": "^4.3.16",
+ "ai": "^5.0.17",
+ "ai-sdk-ollama": "^0.5.1",
"bcrypt": "5.1.1",
"better-auth": "v1.2.8-beta.7",
"bl": "6.0.11",
@@ -65,7 +66,6 @@
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
- "ollama-ai-provider": "^1.2.0",
"otpauth": "^9.4.0",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
@@ -111,4 +111,4 @@
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
-}
+}
\ No newline at end of file
diff --git a/packages/server/src/db/drizzle.config.ts b/packages/server/src/db/drizzle.config.ts
deleted file mode 100644
index 60a3bb937..000000000
--- a/packages/server/src/db/drizzle.config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { defineConfig } from "drizzle-kit";
-
-export default defineConfig({
- schema: "./server/db/schema/index.ts",
- dialect: "postgresql",
- dbCredentials: {
- url: process.env.DATABASE_URL!,
- },
- out: "drizzle",
- migrations: {
- table: "migrations",
- schema: "public",
- },
-});
diff --git a/packages/server/src/db/migration.ts b/packages/server/src/db/migration.ts
deleted file mode 100644
index 6fada0833..000000000
--- a/packages/server/src/db/migration.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-// import { drizzle } from "drizzle-orm/postgres-js";
-// import { migrate } from "drizzle-orm/postgres-js/migrator";
-// import postgres from "postgres";
-
-// const connectionString = process.env.DATABASE_URL!;
-
-// const sql = postgres(connectionString, { max: 1 });
-// const db = drizzle(sql);
-
-// export const migration = async () =>
-// await migrate(db, { migrationsFolder: "drizzle" })
-// .then(() => {
-// console.log("Migration complete");
-// sql.end();
-// })
-// .catch((error) => {
-// console.log("Migration failed", error);
-// })
-// .finally(() => {
-// sql.end();
-// });
diff --git a/packages/server/src/db/reset.ts b/packages/server/src/db/reset.ts
deleted file mode 100644
index c22291478..000000000
--- a/packages/server/src/db/reset.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { sql } from "drizzle-orm";
-// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
-import { drizzle } from "drizzle-orm/postgres-js";
-import postgres from "postgres";
-
-const connectionString = process.env.DATABASE_URL!;
-
-const pg = postgres(connectionString, { max: 1 });
-const db = drizzle(pg);
-
-const clearDb = async (): Promise => {
- try {
- const tablesQuery = sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
- const tables = await db.execute(tablesQuery);
- console.log(tables);
- await pg.end();
- } catch (error) {
- console.error("Error cleaning database", error);
- } finally {
- }
-};
-
-clearDb();
diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts
index 8291ea4d6..3eb57b552 100644
--- a/packages/server/src/db/schema/account.ts
+++ b/packages/server/src/db/schema/account.ts
@@ -112,6 +112,10 @@ export const member = pgTable("member", {
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
+ accessedEnvironments: text("accessedEnvironments")
+ .array()
+ .notNull()
+ .default(sql`ARRAY[]::text[]`),
accessedServices: text("accesedServices")
.array()
.notNull()
diff --git a/packages/server/src/db/schema/ai.ts b/packages/server/src/db/schema/ai.ts
index 3704c3dd9..558f2648e 100644
--- a/packages/server/src/db/schema/ai.ts
+++ b/packages/server/src/db/schema/ai.ts
@@ -32,7 +32,7 @@ export const aiRelations = relations(ai, ({ one }) => ({
const createSchema = createInsertSchema(ai, {
name: z.string().min(1, { message: "Name is required" }),
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
- apiKey: z.string().min(1, { message: "API Key is required" }),
+ apiKey: z.string(),
model: z.string().min(1, { message: "Model is required" }),
isEnabled: z.boolean().optional(),
});
@@ -55,7 +55,7 @@ export const apiUpdateAi = createSchema
.omit({ organizationId: true });
export const deploySuggestionSchema = z.object({
- projectId: z.string().min(1),
+ environmentId: z.string().min(1),
id: z.string().min(1),
dockerCompose: z.string().min(1),
envVariables: z.string(),
diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts
index 1b03989ef..6d176e737 100644
--- a/packages/server/src/db/schema/application.ts
+++ b/packages/server/src/db/schema/application.ts
@@ -13,6 +13,7 @@ import { z } from "zod";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
+import { environments } from "./environment";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -79,6 +80,7 @@ export const applications = pgTable("application", {
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
+ previewLabels: text("previewLabels").array(),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
previewHttps: boolean("previewHttps").notNull().default(false),
@@ -178,9 +180,9 @@ export const applications = pgTable("application", {
registryId: text("registryId").references(() => registry.registryId, {
onDelete: "set null",
}),
- projectId: text("projectId")
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
@@ -201,9 +203,9 @@ export const applications = pgTable("application", {
export const applicationsRelations = relations(
applications,
({ one, many }) => ({
- project: one(projects, {
- fields: [applications.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [applications.environmentId],
+ references: [environments.environmentId],
}),
deployments: many(deployments),
customGitSSHKey: one(sshKeys, {
@@ -272,7 +274,7 @@ const createSchema = createInsertSchema(applications, {
customGitBuildPath: z.string().optional(),
customGitUrl: z.string().optional(),
buildPath: z.string().optional(),
- projectId: z.string(),
+ environmentId: z.string(),
sourceType: z
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
.optional(),
@@ -308,6 +310,7 @@ const createSchema = createInsertSchema(applications, {
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(),
+ previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
});
@@ -315,7 +318,7 @@ export const apiCreateApplication = createSchema.pick({
name: true,
appName: true,
description: true,
- projectId: true,
+ environmentId: true,
serverId: true,
});
@@ -325,6 +328,26 @@ export const apiFindOneApplication = createSchema
})
.required();
+export const apiDeployApplication = createSchema
+ .pick({
+ applicationId: true,
+ })
+ .extend({
+ applicationId: z.string().min(1),
+ title: z.string().optional(),
+ description: z.string().optional(),
+ });
+
+export const apiRedeployApplication = createSchema
+ .pick({
+ applicationId: true,
+ })
+ .extend({
+ applicationId: z.string().min(1),
+ title: z.string().optional(),
+ description: z.string().optional(),
+ });
+
export const apiReloadApplication = createSchema
.pick({
appName: true,
diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts
index 57d8d9f1e..958c2c32c 100644
--- a/packages/server/src/db/schema/compose.ts
+++ b/packages/server/src/db/schema/compose.ts
@@ -7,6 +7,7 @@ import { backups } from "./backups";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
+import { environments } from "./environment";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -84,9 +85,9 @@ export const compose = pgTable("compose", {
.default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
- projectId: text("projectId")
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -109,9 +110,9 @@ export const compose = pgTable("compose", {
});
export const composeRelations = relations(compose, ({ one, many }) => ({
- project: one(projects, {
- fields: [compose.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [compose.environmentId],
+ references: [environments.environmentId],
}),
deployments: many(deployments),
mounts: many(mounts),
@@ -149,7 +150,7 @@ const createSchema = createInsertSchema(compose, {
description: z.string(),
env: z.string().optional(),
composeFile: z.string().optional(),
- projectId: z.string(),
+ environmentId: z.string(),
customGitSSHKeyId: z.string().optional(),
command: z.string().optional(),
composePath: z.string().min(1),
@@ -160,7 +161,7 @@ const createSchema = createInsertSchema(compose, {
export const apiCreateCompose = createSchema.pick({
name: true,
description: true,
- projectId: true,
+ environmentId: true,
composeType: true,
appName: true,
serverId: true,
@@ -169,7 +170,7 @@ export const apiCreateCompose = createSchema.pick({
export const apiCreateComposeByTemplate = createSchema
.pick({
- projectId: true,
+ environmentId: true,
})
.extend({
id: z.string().min(1),
@@ -180,6 +181,18 @@ export const apiFindCompose = z.object({
composeId: z.string().min(1),
});
+export const apiDeployCompose = z.object({
+ composeId: z.string().min(1),
+ title: z.string().optional(),
+ description: z.string().optional(),
+});
+
+export const apiRedeployCompose = z.object({
+ composeId: z.string().min(1),
+ title: z.string().optional(),
+ description: z.string().optional(),
+});
+
export const apiDeleteCompose = z.object({
composeId: z.string().min(1),
deleteVolumes: z.boolean(),
diff --git a/packages/server/src/db/schema/environment.ts b/packages/server/src/db/schema/environment.ts
new file mode 100644
index 000000000..a284a2264
--- /dev/null
+++ b/packages/server/src/db/schema/environment.ts
@@ -0,0 +1,85 @@
+import { relations } from "drizzle-orm";
+import { pgTable, text } from "drizzle-orm/pg-core";
+import { createInsertSchema } from "drizzle-zod";
+import { nanoid } from "nanoid";
+import { z } from "zod";
+import { applications } from "./application";
+import { compose } from "./compose";
+import { mariadb } from "./mariadb";
+import { mongo } from "./mongo";
+import { mysql } from "./mysql";
+import { postgres } from "./postgres";
+import { projects } from "./project";
+import { redis } from "./redis";
+
+export const environments = pgTable("environment", {
+ environmentId: text("environmentId")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => nanoid()),
+ name: text("name").notNull(),
+ description: text("description"),
+ createdAt: text("createdAt")
+ .notNull()
+ .$defaultFn(() => new Date().toISOString()),
+ env: text("env").notNull().default(""),
+ projectId: text("projectId")
+ .notNull()
+ .references(() => projects.projectId, { onDelete: "cascade" }),
+});
+
+export const environmentRelations = relations(
+ environments,
+ ({ one, many }) => ({
+ project: one(projects, {
+ fields: [environments.projectId],
+ references: [projects.projectId],
+ }),
+ applications: many(applications),
+ mariadb: many(mariadb),
+ postgres: many(postgres),
+ mysql: many(mysql),
+ redis: many(redis),
+ mongo: many(mongo),
+ compose: many(compose),
+ }),
+);
+
+const createSchema = createInsertSchema(environments, {
+ environmentId: z.string().min(1),
+ name: z.string().min(1),
+ description: z.string().optional(),
+});
+
+export const apiCreateEnvironment = createSchema.pick({
+ name: true,
+ description: true,
+ projectId: true,
+});
+
+export const apiFindOneEnvironment = createSchema
+ .pick({
+ environmentId: true,
+ })
+ .required();
+
+export const apiRemoveEnvironment = createSchema
+ .pick({
+ environmentId: true,
+ })
+ .required();
+
+export const apiUpdateEnvironment = createSchema.partial().extend({
+ environmentId: z.string().min(1),
+});
+
+export const apiDuplicateEnvironment = createSchema
+ .pick({
+ environmentId: true,
+ name: true,
+ description: true,
+ })
+ .required({
+ environmentId: true,
+ name: true,
+ });
diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts
index 67f145c19..c16ef1452 100644
--- a/packages/server/src/db/schema/index.ts
+++ b/packages/server/src/db/schema/index.ts
@@ -8,6 +8,7 @@ export * from "./compose";
export * from "./deployment";
export * from "./destination";
export * from "./domain";
+export * from "./environment";
export * from "./git-provider";
export * from "./gitea";
export * from "./github";
diff --git a/packages/server/src/db/schema/mariadb.ts b/packages/server/src/db/schema/mariadb.ts
index 039836d77..416c66e1c 100644
--- a/packages/server/src/db/schema/mariadb.ts
+++ b/packages/server/src/db/schema/mariadb.ts
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
+import { environments } from "./environment";
import { mounts } from "./mount";
-import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -66,18 +66,19 @@ export const mariadb = pgTable("mariadb", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
- projectId: text("projectId")
+
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
- project: one(projects, {
- fields: [mariadb.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [mariadb.environmentId],
+ references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -94,8 +95,19 @@ const createSchema = createInsertSchema(mariadb, {
createdAt: z.string(),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
- databasePassword: z.string(),
- databaseRootPassword: z.string().optional(),
+ databasePassword: z
+ .string()
+ .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
+ message:
+ "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ }),
+ databaseRootPassword: z
+ .string()
+ .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
+ message:
+ "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ })
+ .optional(),
dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(),
env: z.string().optional(),
@@ -103,7 +115,7 @@ const createSchema = createInsertSchema(mariadb, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
- projectId: z.string(),
+ environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -124,7 +136,7 @@ export const apiCreateMariaDB = createSchema
appName: true,
dockerImage: true,
databaseRootPassword: true,
- projectId: true,
+ environmentId: true,
description: true,
databaseName: true,
databaseUser: true,
diff --git a/packages/server/src/db/schema/mongo.ts b/packages/server/src/db/schema/mongo.ts
index eb6103a36..eb4661066 100644
--- a/packages/server/src/db/schema/mongo.ts
+++ b/packages/server/src/db/schema/mongo.ts
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
+import { environments } from "./environment";
import { mounts } from "./mount";
-import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -62,9 +62,10 @@ export const mongo = pgTable("mongo", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
- projectId: text("projectId")
+
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
@@ -72,9 +73,9 @@ export const mongo = pgTable("mongo", {
});
export const mongoRelations = relations(mongo, ({ one, many }) => ({
- project: one(projects, {
- fields: [mongo.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [mongo.environmentId],
+ references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -89,7 +90,12 @@ const createSchema = createInsertSchema(mongo, {
createdAt: z.string(),
mongoId: z.string(),
name: z.string().min(1),
- databasePassword: z.string(),
+ databasePassword: z
+ .string()
+ .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
+ message:
+ "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ }),
databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
@@ -98,7 +104,7 @@ const createSchema = createInsertSchema(mongo, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
- projectId: z.string(),
+ environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -119,7 +125,7 @@ export const apiCreateMongo = createSchema
name: true,
appName: true,
dockerImage: true,
- projectId: true,
+ environmentId: true,
description: true,
databaseUser: true,
databasePassword: true,
diff --git a/packages/server/src/db/schema/mysql.ts b/packages/server/src/db/schema/mysql.ts
index 03d360b3d..8f87bff1e 100644
--- a/packages/server/src/db/schema/mysql.ts
+++ b/packages/server/src/db/schema/mysql.ts
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
+import { environments } from "./environment";
import { mounts } from "./mount";
-import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -64,18 +64,19 @@ export const mysql = pgTable("mysql", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
- projectId: text("projectId")
+
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
- project: one(projects, {
- fields: [mysql.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [mysql.environmentId],
+ references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -92,8 +93,19 @@ const createSchema = createInsertSchema(mysql, {
name: z.string().min(1),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
- databasePassword: z.string(),
- databaseRootPassword: z.string().optional(),
+ databasePassword: z
+ .string()
+ .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
+ message:
+ "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ }),
+ databaseRootPassword: z
+ .string()
+ .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
+ message:
+ "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ })
+ .optional(),
dockerImage: z.string().default("mysql:8"),
command: z.string().optional(),
env: z.string().optional(),
@@ -101,7 +113,6 @@ const createSchema = createInsertSchema(mysql, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
- projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -121,7 +132,7 @@ export const apiCreateMySql = createSchema
name: true,
appName: true,
dockerImage: true,
- projectId: true,
+ environmentId: true,
description: true,
databaseName: true,
databaseUser: true,
diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts
index 1c8a2d8f3..b5e871c35 100644
--- a/packages/server/src/db/schema/notification.ts
+++ b/packages/server/src/db/schema/notification.ts
@@ -11,6 +11,7 @@ export const notificationType = pgEnum("notificationType", [
"discord",
"email",
"gotify",
+ "ntfy",
]);
export const notifications = pgTable("notification", {
@@ -44,6 +45,9 @@ export const notifications = pgTable("notification", {
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
+ ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
+ onDelete: "cascade",
+ }),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -101,6 +105,17 @@ export const gotify = pgTable("gotify", {
decoration: boolean("decoration"),
});
+export const ntfy = pgTable("ntfy", {
+ ntfyId: text("ntfyId")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => nanoid()),
+ serverUrl: text("serverUrl").notNull(),
+ topic: text("topic").notNull(),
+ accessToken: text("accessToken").notNull(),
+ priority: integer("priority").notNull().default(3),
+});
+
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -122,6 +137,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
+ ntfy: one(ntfy, {
+ fields: [notifications.ntfyId],
+ references: [ntfy.ntfyId],
+ }),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
@@ -284,6 +303,36 @@ export const apiTestGotifyConnection = apiCreateGotify
decoration: z.boolean().optional(),
});
+export const apiCreateNtfy = notificationsSchema
+ .pick({
+ appBuildError: true,
+ databaseBackup: true,
+ dokployRestart: true,
+ name: true,
+ appDeploy: true,
+ dockerCleanup: true,
+ })
+ .extend({
+ serverUrl: z.string().min(1),
+ topic: z.string().min(1),
+ accessToken: z.string().min(1),
+ priority: z.number().min(1),
+ })
+ .required();
+
+export const apiUpdateNtfy = apiCreateNtfy.partial().extend({
+ notificationId: z.string().min(1),
+ ntfyId: z.string().min(1),
+ organizationId: z.string().optional(),
+});
+
+export const apiTestNtfyConnection = apiCreateNtfy.pick({
+ serverUrl: true,
+ topic: true,
+ accessToken: true,
+ priority: true,
+});
+
export const apiFindOneNotification = notificationsSchema
.pick({
notificationId: true,
@@ -303,7 +352,9 @@ export const apiSendTest = notificationsSchema
password: z.string(),
toAddresses: z.array(z.string()),
serverUrl: z.string(),
+ topic: z.string(),
appToken: z.string(),
+ accessToken: z.string(),
priority: z.number(),
})
.partial();
diff --git a/packages/server/src/db/schema/postgres.ts b/packages/server/src/db/schema/postgres.ts
index df0202094..961371b5c 100644
--- a/packages/server/src/db/schema/postgres.ts
+++ b/packages/server/src/db/schema/postgres.ts
@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
+import { environments } from "./environment";
import { mounts } from "./mount";
-import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -64,18 +64,19 @@ export const postgres = pgTable("postgres", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
- projectId: text("projectId")
+
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const postgresRelations = relations(postgres, ({ one, many }) => ({
- project: one(projects, {
- fields: [postgres.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [postgres.environmentId],
+ references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -88,7 +89,12 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
const createSchema = createInsertSchema(postgres, {
postgresId: z.string(),
name: z.string().min(1),
- databasePassword: z.string(),
+ databasePassword: z
+ .string()
+ .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
+ message:
+ "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ }),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"),
@@ -98,7 +104,7 @@ const createSchema = createInsertSchema(postgres, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
- projectId: z.string(),
+ environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
createdAt: z.string(),
@@ -122,7 +128,7 @@ export const apiCreatePostgres = createSchema
databaseUser: true,
databasePassword: true,
dockerImage: true,
- projectId: true,
+ environmentId: true,
description: true,
serverId: true,
})
diff --git a/packages/server/src/db/schema/project.ts b/packages/server/src/db/schema/project.ts
index e40b362f9..abba26a7d 100644
--- a/packages/server/src/db/schema/project.ts
+++ b/packages/server/src/db/schema/project.ts
@@ -4,13 +4,7 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
-import { applications } from "./application";
-import { compose } from "./compose";
-import { mariadb } from "./mariadb";
-import { mongo } from "./mongo";
-import { mysql } from "./mysql";
-import { postgres } from "./postgres";
-import { redis } from "./redis";
+import { environments } from "./environment";
export const projects = pgTable("project", {
projectId: text("projectId")
@@ -30,13 +24,7 @@ export const projects = pgTable("project", {
});
export const projectRelations = relations(projects, ({ many, one }) => ({
- mysql: many(mysql),
- postgres: many(postgres),
- mariadb: many(mariadb),
- applications: many(applications),
- mongo: many(mongo),
- redis: many(redis),
- compose: many(compose),
+ environments: many(environments),
organization: one(organization, {
fields: [projects.organizationId],
references: [organization.id],
diff --git a/packages/server/src/db/schema/redis.ts b/packages/server/src/db/schema/redis.ts
index 1bfa53510..29e40282d 100644
--- a/packages/server/src/db/schema/redis.ts
+++ b/packages/server/src/db/schema/redis.ts
@@ -3,6 +3,7 @@ import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
+import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
@@ -60,18 +61,19 @@ export const redis = pgTable("redis", {
labelsSwarm: json("labelsSwarm").$type(),
networkSwarm: json("networkSwarm").$type(),
replicas: integer("replicas").default(1).notNull(),
- projectId: text("projectId")
+
+ environmentId: text("environmentId")
.notNull()
- .references(() => projects.projectId, { onDelete: "cascade" }),
+ .references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const redisRelations = relations(redis, ({ one, many }) => ({
- project: one(projects, {
- fields: [redis.projectId],
- references: [projects.projectId],
+ environment: one(environments, {
+ fields: [redis.environmentId],
+ references: [environments.environmentId],
}),
mounts: many(mounts),
server: one(server, {
@@ -93,7 +95,7 @@ const createSchema = createInsertSchema(redis, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
- projectId: z.string(),
+ environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -114,7 +116,7 @@ export const apiCreateRedis = createSchema
appName: true,
databasePassword: true,
dockerImage: true,
- projectId: true,
+ environmentId: true,
description: true,
serverId: true,
})
diff --git a/packages/server/src/db/schema/rollbacks.ts b/packages/server/src/db/schema/rollbacks.ts
index 92690dd93..ec27d683f 100644
--- a/packages/server/src/db/schema/rollbacks.ts
+++ b/packages/server/src/db/schema/rollbacks.ts
@@ -27,7 +27,9 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type<
Application & {
- project: Project;
+ environment: {
+ project: Project;
+ };
mounts: Mount[];
ports: Port[];
registry?: Registry | null;
diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts
index e7fb4cbf4..933a7490c 100644
--- a/packages/server/src/db/schema/user.ts
+++ b/packages/server/src/db/schema/user.ts
@@ -175,6 +175,7 @@ export const apiAssignPermissions = createSchema
})
.extend({
accessedProjects: z.array(z.string()).optional(),
+ accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),
diff --git a/packages/server/src/db/seed.ts b/packages/server/src/db/seed.ts
deleted file mode 100644
index 7e2736b00..000000000
--- a/packages/server/src/db/seed.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-// import bc from "bcrypt";
-// import { drizzle } from "drizzle-orm/postgres-js";
-// import postgres from "postgres";
-// import { users } from "./schema";
-
-// const connectionString = process.env.DATABASE_URL!;
-
-// const pg = postgres(connectionString, { max: 1 });
-// const db = drizzle(pg);
-
-// function password(txt: string) {
-// return bc.hashSync(txt, 10);
-// }
-
-// async function seed() {
-// console.log("> Seed:", process.env.DATABASE_PATH, "\n");
-
-// // const authenticationR = await db
-// // .insert(users)
-// // .values([
-// // {
-// // email: "user1@hotmail.com",
-// // password: password("12345671"),
-// // },
-// // ])
-// // .onConflictDoNothing()
-// // .returning();
-
-// // console.log("\nSemillas Update:", authenticationR.length);
-// }
-
-// seed().catch((e) => {
-// console.error(e);
-// process.exit(1);
-// });
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 775236455..c845dc0bd 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -16,6 +16,7 @@ export * from "./services/deployment";
export * from "./services/destination";
export * from "./services/docker";
export * from "./services/domain";
+export * from "./services/environment";
export * from "./services/git-provider";
export * from "./services/gitea";
export * from "./services/github";
diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts
index a8effba98..e9cde27c5 100644
--- a/packages/server/src/services/application.ts
+++ b/packages/server/src/services/application.ts
@@ -105,7 +105,11 @@ export const findApplicationById = async (applicationId: string) => {
const application = await db.query.applications.findFirst({
where: eq(applications.applicationId, applicationId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
domains: true,
deployments: true,
mounts: true,
@@ -180,7 +184,7 @@ export const deployApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
- const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
+ const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -227,11 +231,11 @@ export const deployApplication = async ({
}
await sendBuildSuccessNotifications({
- projectName: application.project.name,
+ projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
- organizationId: application.project.organizationId,
+ organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -239,13 +243,13 @@ export const deployApplication = async ({
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
- projectName: application.project.name,
+ projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
- organizationId: application.project.organizationId,
+ organizationId: application.environment.project.organizationId,
});
throw error;
@@ -307,7 +311,7 @@ export const deployRemoteApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
- const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
+ const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -363,11 +367,11 @@ export const deployRemoteApplication = async ({
}
await sendBuildSuccessNotifications({
- projectName: application.project.name,
+ projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
- organizationId: application.project.organizationId,
+ organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -387,12 +391,12 @@ export const deployRemoteApplication = async ({
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
- projectName: application.project.name,
+ projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
- organizationId: application.project.organizationId,
+ organizationId: application.environment.project.organizationId,
});
throw error;
diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts
index bb1b2e8a0..bfe8d01d0 100644
--- a/packages/server/src/services/compose.ts
+++ b/packages/server/src/services/compose.ts
@@ -126,7 +126,11 @@ export const findComposeById = async (composeId: string) => {
const result = await db.query.compose.findFirst({
where: eq(compose.composeId, composeId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
deployments: true,
mounts: true,
domains: true,
@@ -222,7 +226,7 @@ export const deployCompose = async ({
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
- compose.projectId
+ compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -255,11 +259,11 @@ export const deployCompose = async ({
});
await sendBuildSuccessNotifications({
- projectName: compose.project.name,
+ projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
- organizationId: compose.project.organizationId,
+ organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -268,13 +272,13 @@ export const deployCompose = async ({
composeStatus: "error",
});
await sendBuildErrorNotifications({
- projectName: compose.project.name,
+ projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
- organizationId: compose.project.organizationId,
+ organizationId: compose.environment.project.organizationId,
});
throw error;
}
@@ -330,7 +334,7 @@ export const deployRemoteCompose = async ({
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
- compose.projectId
+ compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -387,11 +391,11 @@ export const deployRemoteCompose = async ({
});
await sendBuildSuccessNotifications({
- projectName: compose.project.name,
+ projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
- organizationId: compose.project.organizationId,
+ organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -410,13 +414,13 @@ export const deployRemoteCompose = async ({
composeStatus: "error",
});
await sendBuildErrorNotifications({
- projectName: compose.project.name,
+ projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
- organizationId: compose.project.organizationId,
+ organizationId: compose.environment.project.organizationId,
});
throw error;
}
diff --git a/packages/server/src/services/environment.ts b/packages/server/src/services/environment.ts
new file mode 100644
index 000000000..1d77510be
--- /dev/null
+++ b/packages/server/src/services/environment.ts
@@ -0,0 +1,140 @@
+import { db } from "@dokploy/server/db";
+import {
+ type apiCreateEnvironment,
+ type apiDuplicateEnvironment,
+ environments,
+} from "@dokploy/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { asc, eq } from "drizzle-orm";
+
+export type Environment = typeof environments.$inferSelect;
+
+export const createEnvironment = async (
+ input: typeof apiCreateEnvironment._type,
+) => {
+ const newEnvironment = await db
+ .insert(environments)
+ .values({
+ ...input,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newEnvironment) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error creating the environment",
+ });
+ }
+
+ return newEnvironment;
+};
+
+export const findEnvironmentById = async (environmentId: string) => {
+ const environment = await db.query.environments.findFirst({
+ where: eq(environments.environmentId, environmentId),
+ with: {
+ applications: true,
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ postgres: true,
+ redis: true,
+ compose: true,
+ project: true,
+ },
+ });
+ if (!environment) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Environment not found",
+ });
+ }
+ return environment;
+};
+
+export const findEnvironmentsByProjectId = async (projectId: string) => {
+ const projectEnvironments = await db.query.environments.findMany({
+ where: eq(environments.projectId, projectId),
+ orderBy: asc(environments.createdAt),
+ with: {
+ applications: true,
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ postgres: true,
+ redis: true,
+ compose: true,
+ project: true,
+ },
+ });
+ return projectEnvironments;
+};
+
+export const deleteEnvironment = async (environmentId: string) => {
+ const currentEnvironment = await findEnvironmentById(environmentId);
+ if (currentEnvironment.name === "production") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "You cannot delete the production environment",
+ });
+ }
+ const deletedEnvironment = await db
+ .delete(environments)
+ .where(eq(environments.environmentId, environmentId))
+ .returning()
+ .then((value) => value[0]);
+
+ return deletedEnvironment;
+};
+
+export const updateEnvironmentById = async (
+ environmentId: string,
+ environmentData: Partial,
+) => {
+ const result = await db
+ .update(environments)
+ .set({
+ ...environmentData,
+ })
+ .where(eq(environments.environmentId, environmentId))
+ .returning()
+ .then((res) => res[0]);
+
+ return result;
+};
+
+export const duplicateEnvironment = async (
+ input: typeof apiDuplicateEnvironment._type,
+) => {
+ // Find the original environment
+ const originalEnvironment = await findEnvironmentById(input.environmentId);
+
+ // Create a new environment with the provided name and description
+ const newEnvironment = await db
+ .insert(environments)
+ .values({
+ name: input.name,
+ description: input.description || originalEnvironment.description,
+ projectId: originalEnvironment.projectId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newEnvironment) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error duplicating the environment",
+ });
+ }
+
+ return newEnvironment;
+};
+
+export const createProductionEnvironment = async (projectId: string) => {
+ return createEnvironment({
+ name: "production",
+ description: "Production environment",
+ projectId,
+ });
+};
diff --git a/packages/server/src/services/mariadb.ts b/packages/server/src/services/mariadb.ts
index b664e1593..8aac45346 100644
--- a/packages/server/src/services/mariadb.ts
+++ b/packages/server/src/services/mariadb.ts
@@ -56,7 +56,11 @@ export const findMariadbById = async (mariadbId: string) => {
const result = await db.query.mariadb.findFirst({
where: eq(mariadb.mariadbId, mariadbId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
mounts: true,
server: true,
backups: {
diff --git a/packages/server/src/services/mongo.ts b/packages/server/src/services/mongo.ts
index a760a1669..d52b2445e 100644
--- a/packages/server/src/services/mongo.ts
+++ b/packages/server/src/services/mongo.ts
@@ -53,7 +53,11 @@ export const findMongoById = async (mongoId: string) => {
const result = await db.query.mongo.findFirst({
where: eq(mongo.mongoId, mongoId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
mounts: true,
server: true,
backups: {
diff --git a/packages/server/src/services/mount.ts b/packages/server/src/services/mount.ts
index d64fef6f1..f08a32312 100644
--- a/packages/server/src/services/mount.ts
+++ b/packages/server/src/services/mount.ts
@@ -105,13 +105,69 @@ export const findMountById = async (mountId: string) => {
const mount = await db.query.mounts.findFirst({
where: eq(mounts.mountId, mountId),
with: {
- application: true,
- postgres: true,
- mariadb: true,
- mongo: true,
- mysql: true,
- redis: true,
- compose: true,
+ application: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ postgres: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ mariadb: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ mongo: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ mysql: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ redis: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ compose: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
},
});
if (!mount) {
@@ -123,6 +179,34 @@ export const findMountById = async (mountId: string) => {
return mount;
};
+export const findMountOrganizationId = async (mountId: string) => {
+ const mount = await findMountById(mountId);
+
+ if (mount.application) {
+ return mount.application.environment.project.organizationId;
+ }
+ if (mount.postgres) {
+ return mount.postgres.environment.project.organizationId;
+ }
+ if (mount.mariadb) {
+ return mount.mariadb.environment.project.organizationId;
+ }
+ if (mount.mongo) {
+ return mount.mongo.environment.project.organizationId;
+ }
+ if (mount.mysql) {
+ return mount.mysql.environment.project.organizationId;
+ }
+ if (mount.redis) {
+ return mount.redis.environment.project.organizationId;
+ }
+
+ if (mount.compose) {
+ return mount.compose.environment.project.organizationId;
+ }
+ return null;
+};
+
export const updateMount = async (
mountId: string,
mountData: Partial,
diff --git a/packages/server/src/services/mysql.ts b/packages/server/src/services/mysql.ts
index bb838eb97..f25664e01 100644
--- a/packages/server/src/services/mysql.ts
+++ b/packages/server/src/services/mysql.ts
@@ -56,7 +56,11 @@ export const findMySqlById = async (mysqlId: string) => {
const result = await db.query.mysql.findFirst({
where: eq(mysql.mysqlId, mysqlId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
mounts: true,
server: true,
backups: {
diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts
index 16ba2085b..efd52061c 100644
--- a/packages/server/src/services/notification.ts
+++ b/packages/server/src/services/notification.ts
@@ -3,17 +3,20 @@ import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateGotify,
+ type apiCreateNtfy,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateGotify,
+ type apiUpdateNtfy,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
gotify,
notifications,
+ ntfy,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -482,6 +485,96 @@ export const updateGotifyNotification = async (
});
};
+export const createNtfyNotification = async (
+ input: typeof apiCreateNtfy._type,
+ organizationId: string,
+) => {
+ await db.transaction(async (tx) => {
+ const newNtfy = await tx
+ .insert(ntfy)
+ .values({
+ serverUrl: input.serverUrl,
+ topic: input.topic,
+ accessToken: input.accessToken,
+ priority: input.priority,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newNtfy) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting ntfy",
+ });
+ }
+
+ const newDestination = await tx
+ .insert(notifications)
+ .values({
+ ntfyId: newNtfy.ntfyId,
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ notificationType: "ntfy",
+ organizationId: organizationId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting notification",
+ });
+ }
+
+ return newDestination;
+ });
+};
+
+export const updateNtfyNotification = async (
+ input: typeof apiUpdateNtfy._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newDestination = await tx
+ .update(notifications)
+ .set({
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ organizationId: input.organizationId,
+ })
+ .where(eq(notifications.notificationId, input.notificationId))
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error Updating notification",
+ });
+ }
+
+ await tx
+ .update(ntfy)
+ .set({
+ serverUrl: input.serverUrl,
+ topic: input.topic,
+ accessToken: input.accessToken,
+ priority: input.priority,
+ })
+ .where(eq(ntfy.ntfyId, input.ntfyId));
+
+ return newDestination;
+ });
+};
+
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -491,6 +584,7 @@ export const findNotificationById = async (notificationId: string) => {
discord: true,
email: true,
gotify: true,
+ ntfy: true,
},
});
if (!notification) {
diff --git a/packages/server/src/services/port.ts b/packages/server/src/services/port.ts
index 1f66c0143..afafba29b 100644
--- a/packages/server/src/services/port.ts
+++ b/packages/server/src/services/port.ts
@@ -27,6 +27,17 @@ export const createPort = async (input: typeof apiCreatePort._type) => {
export const finPortById = async (portId: string) => {
const result = await db.query.ports.findFirst({
where: eq(ports.portId, portId),
+ with: {
+ application: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ },
});
if (!result) {
throw new TRPCError({
diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts
index 47ea3fafc..0d900443e 100644
--- a/packages/server/src/services/postgres.ts
+++ b/packages/server/src/services/postgres.ts
@@ -51,7 +51,11 @@ export const findPostgresById = async (postgresId: string) => {
const result = await db.query.postgres.findFirst({
where: eq(postgres.postgresId, postgresId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
mounts: true,
server: true,
backups: {
diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts
index 1b358946f..5ee763b08 100644
--- a/packages/server/src/services/preview-deployment.ts
+++ b/packages/server/src/services/preview-deployment.ts
@@ -31,7 +31,11 @@ export const findPreviewDeploymentById = async (
application: {
with: {
server: true,
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
},
},
},
@@ -45,68 +49,43 @@ export const findPreviewDeploymentById = async (
return application;
};
-export const findApplicationByPreview = async (applicationId: string) => {
- const application = await db.query.applications.findFirst({
- with: {
- previewDeployments: {
- where: eq(previewDeployments.applicationId, applicationId),
- },
- project: true,
- domains: true,
- deployments: true,
- mounts: true,
- redirects: true,
- security: true,
- ports: true,
- registry: true,
- gitlab: true,
- github: true,
- bitbucket: true,
- gitea: true,
- server: true,
- },
- });
-
- if (!application) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Applicationnot found",
- });
- }
- return application;
-};
-
export const removePreviewDeployment = async (previewDeploymentId: string) => {
try {
- const application = await findApplicationByPreview(previewDeploymentId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
-
- const deployment = await db
- .delete(previewDeployments)
- .where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
- .returning();
+ const application = await findApplicationById(
+ previewDeployment.applicationId,
+ );
application.appName = previewDeployment.appName;
const cleanupOperations = [
+ async () =>
+ await removeService(application?.appName, application?.serverId),
async () =>
await removeDeploymentsByPreviewDeploymentId(
previewDeployment,
- application.serverId,
+ application?.serverId,
),
async () =>
- await removeDirectoryCode(application.appName, application.serverId),
+ await removeDirectoryCode(application?.appName, application?.serverId),
async () =>
- await removeTraefikConfig(application.appName, application.serverId),
+ await removeTraefikConfig(application?.appName, application?.serverId),
async () =>
- await removeService(application?.appName, application.serverId),
+ await db
+ .delete(previewDeployments)
+ .where(
+ eq(previewDeployments.previewDeploymentId, previewDeploymentId),
+ )
+ .returning(),
];
for (const operation of cleanupOperations) {
try {
await operation();
- } catch {}
+ } catch (error) {
+ console.error(error);
+ }
}
- return deployment[0];
+ return previewDeployment;
} catch (error) {
const message =
error instanceof Error
@@ -157,7 +136,7 @@ export const createPreviewDeployment = async (
const appName = `preview-${application.appName}-${generatePassword(6)}`;
const org = await db.query.organization.findFirst({
- where: eq(organization.id, application.project.organizationId),
+ where: eq(organization.id, application.environment.project.organizationId),
});
const generateDomain = await generateWildcardDomain(
application.previewWildcard || "*.traefik.me",
diff --git a/packages/server/src/services/project.ts b/packages/server/src/services/project.ts
index b740834b5..cf58b18fa 100644
--- a/packages/server/src/services/project.ts
+++ b/packages/server/src/services/project.ts
@@ -11,6 +11,7 @@ import {
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
+import { createProductionEnvironment } from "./environment";
export type Project = typeof projects.$inferSelect;
@@ -34,20 +35,31 @@ export const createProject = async (
});
}
- return newProject;
+ // Automatically create a production environment
+ const newEnvironment = await createProductionEnvironment(
+ newProject.projectId,
+ );
+ return {
+ project: newProject,
+ environment: newEnvironment,
+ };
};
export const findProjectById = async (projectId: string) => {
const project = await db.query.projects.findFirst({
where: eq(projects.projectId, projectId),
with: {
- applications: true,
- mariadb: true,
- mongo: true,
- mysql: true,
- postgres: true,
- redis: true,
- compose: true,
+ environments: {
+ with: {
+ applications: true,
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ postgres: true,
+ redis: true,
+ compose: true,
+ },
+ },
},
});
if (!project) {
@@ -86,7 +98,7 @@ export const updateProjectById = async (
};
export const validUniqueServerAppName = async (appName: string) => {
- const query = await db.query.projects.findMany({
+ const query = await db.query.environments.findMany({
with: {
applications: {
where: eq(applications.appName, appName),
diff --git a/packages/server/src/services/redis.ts b/packages/server/src/services/redis.ts
index 7b9721cb8..b382cbfd1 100644
--- a/packages/server/src/services/redis.ts
+++ b/packages/server/src/services/redis.ts
@@ -52,7 +52,11 @@ export const findRedisById = async (redisId: string) => {
const result = await db.query.redis.findFirst({
where: eq(redis.redisId, redisId),
with: {
- project: true,
+ environment: {
+ with: {
+ project: true,
+ },
+ },
mounts: true,
server: true,
},
diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts
index 3e96325a6..16877e7d9 100644
--- a/packages/server/src/services/rollbacks.ts
+++ b/packages/server/src/services/rollbacks.ts
@@ -76,9 +76,24 @@ export const createRollback = async (
});
};
-const findRollbackById = async (rollbackId: string) => {
+export const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId),
+ with: {
+ deployment: {
+ with: {
+ application: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
});
if (!result) {
@@ -179,7 +194,9 @@ const rollbackApplication = async (
image: string,
serverId?: string | null,
fullContext?: Application & {
- project: Project;
+ environment: {
+ project: Project;
+ };
mounts: Mount[];
ports: Port[];
},
@@ -225,7 +242,7 @@ const rollbackApplication = async (
const bindsMount = generateBindMounts(mounts);
const envVariables = prepareEnvironmentVariables(
env,
- fullContext.project.env,
+ fullContext.environment.project.env,
);
// For rollback, we use the provided image instead of calculating it
diff --git a/packages/server/src/services/schedule.ts b/packages/server/src/services/schedule.ts
index b311ba760..4dace4b67 100644
--- a/packages/server/src/services/schedule.ts
+++ b/packages/server/src/services/schedule.ts
@@ -35,9 +35,29 @@ export const findScheduleById = async (scheduleId: string) => {
const schedule = await db.query.schedules.findFirst({
where: eq(schedules.scheduleId, scheduleId),
with: {
- application: true,
- compose: true,
- server: true,
+ application: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ compose: {
+ with: {
+ environment: {
+ with: {
+ project: true,
+ },
+ },
+ },
+ },
+ server: {
+ with: {
+ organization: true,
+ },
+ },
},
});
@@ -50,6 +70,21 @@ export const findScheduleById = async (scheduleId: string) => {
return schedule;
};
+export const findScheduleOrganizationId = async (scheduleId: string) => {
+ const schedule = await findScheduleById(scheduleId);
+
+ if (schedule?.application) {
+ return schedule?.application?.environment?.project?.organizationId;
+ }
+ if (schedule?.compose) {
+ return schedule?.compose?.environment?.project?.organizationId;
+ }
+ if (schedule?.server) {
+ return schedule?.server?.organization?.id;
+ }
+ return null;
+};
+
export const deleteSchedule = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId);
const serverId =
diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts
index e4402892f..f39064523 100644
--- a/packages/server/src/services/settings.ts
+++ b/packages/server/src/services/settings.ts
@@ -253,37 +253,36 @@ export const getDockerResourceType = async (
resourceName: string,
serverId?: string,
) => {
- let result = "";
- const command = `
- RESOURCE_NAME="${resourceName}"
- if docker service inspect "$RESOURCE_NAME" &>/dev/null; then
- echo "service"
- exit 0
- fi
+ try {
+ let result = "";
+ const command = `
+RESOURCE_NAME="${resourceName}"
+if docker service inspect "$RESOURCE_NAME" >/dev/null 2>&1; then
+ echo "service"
+elif docker inspect "$RESOURCE_NAME" >/dev/null 2>&1; then
+ echo "standalone"
+else
+ echo "unknown"
+fi`;
- if docker inspect "$RESOURCE_NAME" &>/dev/null; then
- echo "standalone"
- exit 0
- fi
-
- echo "unknown"
- exit 0
- `;
-
- if (serverId) {
- const { stdout } = await execAsyncRemote(serverId, command);
- result = stdout.trim();
- } else {
- const { stdout } = await execAsync(command);
- result = stdout.trim();
+ if (serverId) {
+ const { stdout } = await execAsyncRemote(serverId, command);
+ result = stdout.trim();
+ } else {
+ const { stdout } = await execAsync(command);
+ result = stdout.trim();
+ }
+ if (result === "service") {
+ return "service";
+ }
+ if (result === "standalone") {
+ return "standalone";
+ }
+ return "unknown";
+ } catch (error) {
+ console.error(error);
+ return "unknown";
}
- if (result === "service") {
- return "service";
- }
- if (result === "standalone") {
- return "standalone";
- }
- return "unknown";
};
export const reloadDockerResource = async (
@@ -294,8 +293,10 @@ export const reloadDockerResource = async (
let command = "";
if (resourceType === "service") {
command = `docker service update --force ${resourceName}`;
- } else {
+ } else if (resourceType === "standalone") {
command = `docker restart ${resourceName}`;
+ } else {
+ throw new Error("Resource type not found");
}
if (serverId) {
await execAsyncRemote(serverId, command);
@@ -312,7 +313,7 @@ export const readEnvironmentVariables = async (
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}'`;
- } else {
+ } else if (resourceType === "standalone") {
command = `docker container inspect ${resourceName} --format '{{json .Config.Env}}'`;
}
let result = "";
@@ -339,7 +340,7 @@ export const readPorts = async (
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
- } else {
+ } else if (resourceType === "standalone") {
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
}
let result = "";
diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts
index 39ac95cef..728d5b8ee 100644
--- a/packages/server/src/services/user.ts
+++ b/packages/server/src/services/user.ts
@@ -23,6 +23,23 @@ export const addNewProject = async (
);
};
+export const addNewEnvironment = async (
+ userId: string,
+ environmentId: string,
+ organizationId: string,
+) => {
+ const userR = await findMemberById(userId, organizationId);
+
+ await db
+ .update(member)
+ .set({
+ accessedEnvironments: [...userR.accessedEnvironments, environmentId],
+ })
+ .where(
+ and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
+ );
+};
+
export const addNewService = async (
userId: string,
serviceId: string,
@@ -131,6 +148,21 @@ export const canPerformAccessProject = async (
return false;
};
+export const canPerformAccessEnvironment = async (
+ userId: string,
+ environmentId: string,
+ organizationId: string,
+) => {
+ const { accessedEnvironments } = await findMemberById(userId, organizationId);
+ const haveAccessToEnvironment = accessedEnvironments.includes(environmentId);
+
+ if (haveAccessToEnvironment) {
+ return true;
+ }
+
+ return false;
+};
+
export const canAccessToTraefikFiles = async (
userId: string,
organizationId: string,
@@ -182,6 +214,32 @@ export const checkServiceAccess = async (
}
};
+export const checkEnvironmentAccess = async (
+ userId: string,
+ environmentId: string,
+ organizationId: string,
+ action = "access" as const,
+) => {
+ let hasPermission = false;
+ switch (action) {
+ case "access":
+ hasPermission = await canPerformAccessEnvironment(
+ userId,
+ environmentId,
+ organizationId,
+ );
+ break;
+ default:
+ hasPermission = false;
+ }
+ if (!hasPermission) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "Permission denied",
+ });
+ }
+};
+
export const checkProjectAccess = async (
authId: string,
action: "create" | "delete" | "access",
diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts
index a9ca1c371..8128b57e0 100644
--- a/packages/server/src/setup/server-setup.ts
+++ b/packages/server/src/setup/server-setup.ts
@@ -578,8 +578,7 @@ export const createTraefikInstance = () => {
TRAEFIK_VERSION=${TRAEFIK_VERSION}
docker run -d \
--name dokploy-traefik \
- --network dokploy-network \
- --restart unless-stopped \
+ --restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
-v /var/run/docker.sock:/var/run/docker.sock \
@@ -587,6 +586,8 @@ export const createTraefikInstance = () => {
-p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
traefik:v$TRAEFIK_VERSION
+
+ docker network connect dokploy-network dokploy-traefik;
echo "Traefik version $TRAEFIK_VERSION installed ✅"
fi
`;
diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts
index ccdfa30f8..17c48d0ff 100644
--- a/packages/server/src/setup/traefik-setup.ts
+++ b/packages/server/src/setup/traefik-setup.ts
@@ -13,7 +13,7 @@ export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
-export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
+export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0";
export interface TraefikOptions {
env?: string[];
diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts
index 39d7ae13c..c0715030b 100644
--- a/packages/server/src/utils/ai/select-ai-provider.ts
+++ b/packages/server/src/utils/ai/select-ai-provider.ts
@@ -5,17 +5,16 @@ import { createDeepInfra } from "@ai-sdk/deepinfra";
import { createMistral } from "@ai-sdk/mistral";
import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
-import { createOllama } from "ollama-ai-provider";
+import { createOllama } from "ai-sdk-ollama";
-function getProviderName(apiUrl: string) {
+export function getProviderName(apiUrl: string) {
if (apiUrl.includes("api.openai.com")) return "openai";
if (apiUrl.includes("azure.com")) return "azure";
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
if (apiUrl.includes("api.cohere.ai")) return "cohere";
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
if (apiUrl.includes("api.mistral.ai")) return "mistral";
- if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
- return "ollama";
+ if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
return "custom";
}
diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts
index f260ffa08..1963f2c91 100644
--- a/packages/server/src/utils/backups/compose.ts
+++ b/packages/server/src/utils/backups/compose.ts
@@ -4,6 +4,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
+import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runComposeBackup = async (
compose: Compose,
backup: BackupSchedule,
) => {
- const { projectId, name } = compose;
- const project = await findProjectById(projectId);
+ const { environmentId, name } = compose;
+ const environment = await findEnvironmentById(environmentId);
+ const project = await findProjectById(environment.projectId);
const { prefix, databaseType } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts
index 8760095c8..2353821e5 100644
--- a/packages/server/src/utils/backups/mariadb.ts
+++ b/packages/server/src/utils/backups/mariadb.ts
@@ -4,6 +4,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
+import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runMariadbBackup = async (
mariadb: Mariadb,
backup: BackupSchedule,
) => {
- const { projectId, name } = mariadb;
- const project = await findProjectById(projectId);
+ const { environmentId, name } = mariadb;
+ const environment = await findEnvironmentById(environmentId);
+ const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts
index 6a74f1d10..429de7d4d 100644
--- a/packages/server/src/utils/backups/mongo.ts
+++ b/packages/server/src/utils/backups/mongo.ts
@@ -4,14 +4,16 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
+import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
- const { projectId, name } = mongo;
- const project = await findProjectById(projectId);
+ const { environmentId, name } = mongo;
+ const environment = await findEnvironmentById(environmentId);
+ const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts
index 6f6678421..90919f24c 100644
--- a/packages/server/src/utils/backups/mysql.ts
+++ b/packages/server/src/utils/backups/mysql.ts
@@ -4,14 +4,16 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
+import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
- const { projectId, name } = mysql;
- const project = await findProjectById(projectId);
+ const { environmentId, name } = mysql;
+ const environment = await findEnvironmentById(environmentId);
+ const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts
index 1f7b6a2d2..9aa5d8f5f 100644
--- a/packages/server/src/utils/backups/postgres.ts
+++ b/packages/server/src/utils/backups/postgres.ts
@@ -4,6 +4,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
+import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runPostgresBackup = async (
postgres: Postgres,
backup: BackupSchedule,
) => {
- const { name, projectId } = postgres;
- const project = await findProjectById(projectId);
+ const { name, environmentId } = postgres;
+ const environment = await findEnvironmentById(environmentId);
+ const project = await findProjectById(environment.projectId);
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts
index 923f03fd8..4d13ae31a 100644
--- a/packages/server/src/utils/backups/web-server.ts
+++ b/packages/server/src/utils/backups/web-server.ts
@@ -80,7 +80,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
writeStream.write("Zipped database and filesystem\n");
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
- writeStream.write(`Running command: ${uploadCommand}\n`);
+ writeStream.write("Running command to upload backup to S3\n");
await execAsync(uploadCommand);
writeStream.write("Uploaded backup to S3 ✅\n");
writeStream.end();
diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts
index d93c9395e..667b46b74 100644
--- a/packages/server/src/utils/builders/compose.ts
+++ b/packages/server/src/utils/builders/compose.ts
@@ -22,7 +22,7 @@ import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType<
"compose",
- { project: true; mounts: true; domains: true }
+ { environment: { with: { project: true } }; mounts: true; domains: true }
>;
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
@@ -72,7 +72,10 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
NODE_ENV: process.env.NODE_ENV,
PATH: process.env.PATH,
...(composeType === "stack" && {
- ...getEnviromentVariablesObject(compose.env, compose.project.env),
+ ...getEnviromentVariablesObject(
+ compose.env,
+ compose.environment.project.env,
+ ),
}),
},
},
@@ -202,7 +205,8 @@ const createEnvFile = (compose: ComposeNested) => {
const envFileContent = prepareEnvironmentVariables(
envContent,
- compose.project.env,
+ compose.environment.project.env,
+ compose.environment.env,
).join("\n");
if (!existsSync(dirname(envFilePath))) {
@@ -232,7 +236,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFileContent = prepareEnvironmentVariables(
envContent,
- compose.project.env,
+ compose.environment.project.env,
+ compose.environment.env,
).join("\n");
const encodedContent = encodeBase64(envFileContent);
@@ -247,7 +252,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
const envVars = getEnviromentVariablesObject(
compose.env,
- compose.project.env,
+ compose.environment.project.env,
);
const exports = Object.entries(envVars)
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
diff --git a/packages/server/src/utils/builders/docker-file.ts b/packages/server/src/utils/builders/docker-file.ts
index 6799b0810..b218590c8 100644
--- a/packages/server/src/utils/builders/docker-file.ts
+++ b/packages/server/src/utils/builders/docker-file.ts
@@ -28,7 +28,8 @@ export const buildCustomDocker = async (
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const dockerContextPath = getDockerContextPath(application);
@@ -51,7 +52,12 @@ export const buildCustomDocker = async (
as it could be publicly exposed.
*/
if (!publishDirectory) {
- createEnvFile(dockerFilePath, env, application.project.env);
+ createEnvFile(
+ dockerFilePath,
+ env,
+ application.environment.project.env,
+ application.environment.env,
+ );
}
await spawnAsync(
@@ -92,7 +98,8 @@ export const getDockerCommand = (
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const dockerContextPath =
@@ -121,7 +128,8 @@ export const getDockerCommand = (
command += createEnvFileCommand(
dockerFilePath,
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
}
diff --git a/packages/server/src/utils/builders/heroku.ts b/packages/server/src/utils/builders/heroku.ts
index e634ce45c..3306f2fc2 100644
--- a/packages/server/src/utils/builders/heroku.ts
+++ b/packages/server/src/utils/builders/heroku.ts
@@ -13,7 +13,8 @@ export const buildHeroku = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
try {
const args = [
@@ -53,7 +54,8 @@ export const getHerokuCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const args = [
diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts
index b779558ef..492f799a8 100644
--- a/packages/server/src/utils/builders/index.ts
+++ b/packages/server/src/utils/builders/index.ts
@@ -30,7 +30,7 @@ export type ApplicationNested = InferResultType<
redirects: true;
ports: true;
registry: true;
- project: true;
+ environment: { with: { project: true } };
}
>;
@@ -148,7 +148,8 @@ export const mechanizeDockerContainer = async (
const filesMount = generateFileMounts(appName, application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const image = getImageName(application);
diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts
index 373bbafb0..76905d0e7 100644
--- a/packages/server/src/utils/builders/nixpacks.ts
+++ b/packages/server/src/utils/builders/nixpacks.ts
@@ -20,7 +20,8 @@ export const buildNixpacks = async (
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const writeToStream = (data: string) => {
@@ -101,7 +102,8 @@ export const getNixpacksCommand = (
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const args = ["build", buildAppDirectory, "--name", appName];
diff --git a/packages/server/src/utils/builders/paketo.ts b/packages/server/src/utils/builders/paketo.ts
index c9c6c774e..b95a1bb31 100644
--- a/packages/server/src/utils/builders/paketo.ts
+++ b/packages/server/src/utils/builders/paketo.ts
@@ -12,7 +12,8 @@ export const buildPaketo = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
try {
const args = [
@@ -52,7 +53,8 @@ export const getPaketoCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
const args = [
diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts
index c91a30ab1..4adc9ca1c 100644
--- a/packages/server/src/utils/builders/railpack.ts
+++ b/packages/server/src/utils/builders/railpack.ts
@@ -26,7 +26,8 @@ export const buildRailpack = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
try {
@@ -123,7 +124,8 @@ export const getRailpackCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
- application.project.env,
+ application.environment.project.env,
+ application.environment.env,
);
// Prepare command
diff --git a/packages/server/src/utils/builders/utils.ts b/packages/server/src/utils/builders/utils.ts
index 8eb5bbb07..cce8dfd95 100644
--- a/packages/server/src/utils/builders/utils.ts
+++ b/packages/server/src/utils/builders/utils.ts
@@ -6,14 +6,17 @@ export const createEnvFile = (
directory: string,
env: string | null,
projectEnv?: string | null,
+ environmentEnv?: string | null,
) => {
const envFilePath = join(dirname(directory), ".env");
if (!existsSync(dirname(envFilePath))) {
mkdirSync(dirname(envFilePath), { recursive: true });
}
- const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
- "\n",
- );
+ const envFileContent = prepareEnvironmentVariables(
+ env,
+ projectEnv,
+ environmentEnv,
+ ).join("\n");
writeFileSync(envFilePath, envFileContent);
};
@@ -21,10 +24,13 @@ export const createEnvFileCommand = (
directory: string,
env: string | null,
projectEnv?: string | null,
+ environmentEnv?: string | null,
) => {
- const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
- "\n",
- );
+ const envFileContent = prepareEnvironmentVariables(
+ env,
+ projectEnv,
+ environmentEnv,
+ ).join("\n");
const encodedContent = encodeBase64(envFileContent || "");
const envFilePath = join(dirname(directory), ".env");
diff --git a/packages/server/src/utils/databases/mariadb.ts b/packages/server/src/utils/databases/mariadb.ts
index 46e9dac67..1671de454 100644
--- a/packages/server/src/utils/databases/mariadb.ts
+++ b/packages/server/src/utils/databases/mariadb.ts
@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MariadbNested = InferResultType<
"mariadb",
- { mounts: true; project: true }
+ { mounts: true; environment: { with: { project: true } } }
>;
export const buildMariadb = async (mariadb: MariadbNested) => {
const {
@@ -54,7 +54,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultMariadbEnv,
- mariadb.project.env,
+ mariadb.environment.project.env,
+ mariadb.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
diff --git a/packages/server/src/utils/databases/mongo.ts b/packages/server/src/utils/databases/mongo.ts
index bf188c184..b4c36e6eb 100644
--- a/packages/server/src/utils/databases/mongo.ts
+++ b/packages/server/src/utils/databases/mongo.ts
@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MongoNested = InferResultType<
"mongo",
- { mounts: true; project: true }
+ { mounts: true; environment: { with: { project: true } } }
>;
export const buildMongo = async (mongo: MongoNested) => {
@@ -102,7 +102,8 @@ ${command ?? "wait $MONGOD_PID"}`;
const envVariables = prepareEnvironmentVariables(
defaultMongoEnv,
- mongo.project.env,
+ mongo.environment.project.env,
+ mongo.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
diff --git a/packages/server/src/utils/databases/mysql.ts b/packages/server/src/utils/databases/mysql.ts
index 51deb7e6a..a3ed72afc 100644
--- a/packages/server/src/utils/databases/mysql.ts
+++ b/packages/server/src/utils/databases/mysql.ts
@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MysqlNested = InferResultType<
"mysql",
- { mounts: true; project: true }
+ { mounts: true; environment: { with: { project: true } } }
>;
export const buildMysql = async (mysql: MysqlNested) => {
@@ -60,7 +60,8 @@ export const buildMysql = async (mysql: MysqlNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultMysqlEnv,
- mysql.project.env,
+ mysql.environment.project.env,
+ mysql.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
diff --git a/packages/server/src/utils/databases/postgres.ts b/packages/server/src/utils/databases/postgres.ts
index b017bc3e1..83f65f1c2 100644
--- a/packages/server/src/utils/databases/postgres.ts
+++ b/packages/server/src/utils/databases/postgres.ts
@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type PostgresNested = InferResultType<
"postgres",
- { mounts: true; project: true }
+ { mounts: true; environment: { with: { project: true } } }
>;
export const buildPostgres = async (postgres: PostgresNested) => {
const {
@@ -53,7 +53,8 @@ export const buildPostgres = async (postgres: PostgresNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultPostgresEnv,
- postgres.project.env,
+ postgres.environment.project.env,
+ postgres.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
diff --git a/packages/server/src/utils/databases/redis.ts b/packages/server/src/utils/databases/redis.ts
index 7cb209de4..85ae873d4 100644
--- a/packages/server/src/utils/databases/redis.ts
+++ b/packages/server/src/utils/databases/redis.ts
@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type RedisNested = InferResultType<
"redis",
- { mounts: true; project: true }
+ { mounts: true; environment: { with: { project: true } } }
>;
export const buildRedis = async (redis: RedisNested) => {
const {
@@ -51,7 +51,8 @@ export const buildRedis = async (redis: RedisNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultRedisEnv,
- redis.project.env,
+ redis.environment.project.env,
+ redis.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts
index 0ce138d70..4bd38f874 100644
--- a/packages/server/src/utils/docker/domain.ts
+++ b/packages/server/src/utils/docker/domain.ts
@@ -254,6 +254,9 @@ export const addDomainToCompose = async (
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
+ if (!labels.includes("traefik.swarm.network=dokploy-network")) {
+ labels.unshift("traefik.swarm.network=dokploy-network");
+ }
}
}
diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts
index 12f46218f..ae14bcf40 100644
--- a/packages/server/src/utils/docker/utils.ts
+++ b/packages/server/src/utils/docker/utils.ts
@@ -259,20 +259,51 @@ export const removeService = async (
export const prepareEnvironmentVariables = (
serviceEnv: string | null,
projectEnv?: string | null,
+ environmentEnv?: string | null,
) => {
const projectVars = parse(projectEnv ?? "");
+ const environmentVars = parse(environmentEnv ?? "");
const serviceVars = parse(serviceEnv ?? "");
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
let resolvedValue = value;
+
+ // Replace project variables
if (projectVars) {
- resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
- if (projectVars[ref] !== undefined) {
- return projectVars[ref];
- }
- throw new Error(`Invalid project environment variable: project.${ref}`);
- });
+ resolvedValue = resolvedValue.replace(
+ /\$\{\{project\.(.*?)\}\}/g,
+ (_, ref) => {
+ if (projectVars[ref] !== undefined) {
+ return projectVars[ref];
+ }
+ throw new Error(
+ `Invalid project environment variable: project.${ref}`,
+ );
+ },
+ );
}
+
+ // Replace environment variables
+ if (environmentVars) {
+ resolvedValue = resolvedValue.replace(
+ /\$\{\{environment\.(.*?)\}\}/g,
+ (_, ref) => {
+ if (environmentVars[ref] !== undefined) {
+ return environmentVars[ref];
+ }
+ throw new Error(`Invalid environment variable: environment.${ref}`);
+ },
+ );
+ }
+
+ // Replace self-references (service variables)
+ resolvedValue = resolvedValue.replace(/\$\{\{(.*?)\}\}/g, (_, ref) => {
+ if (serviceVars[ref] !== undefined) {
+ return serviceVars[ref];
+ }
+ throw new Error(`Invalid service environment variable: ${ref}`);
+ });
+
return `${key}=${resolvedValue}`;
});
@@ -293,8 +324,9 @@ export const parseEnvironmentKeyValuePair = (
export const getEnviromentVariablesObject = (
input: string | null,
projectEnv?: string | null,
+ environmentEnv?: string | null,
) => {
- const envs = prepareEnvironmentVariables(input, projectEnv);
+ const envs = prepareEnvironmentVariables(input, projectEnv, environmentEnv);
const jsonObject: Record = {};
diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts
index 47fa4de77..0d2c2108b 100644
--- a/packages/server/src/utils/notifications/build-error.ts
+++ b/packages/server/src/utils/notifications/build-error.ts
@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
+ sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -42,11 +43,12 @@ export const sendBuildErrorNotifications = async ({
telegram: true,
slack: true,
gotify: true,
+ ntfy: true,
},
});
for (const notification of notificationList) {
- const { email, discord, telegram, slack, gotify } = notification;
+ const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
@@ -132,6 +134,20 @@ export const sendBuildErrorNotifications = async ({
);
}
+ if (ntfy) {
+ await sendNtfyNotification(
+ ntfy,
+ "Build Failed",
+ "warning",
+ `view, Build details, ${buildLink}, clear=true;`,
+ `🛠️Project: ${projectName}\n` +
+ `⚙️Application: ${applicationName}\n` +
+ `❔Type: ${applicationType}\n` +
+ `🕒Date: ${date.toLocaleString()}\n` +
+ `⚠️Error:\n${errorMessage}`,
+ );
+ }
+
if (telegram) {
const inlineButton = [
[
diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts
index ac470c49f..fb8b89f76 100644
--- a/packages/server/src/utils/notifications/build-success.ts
+++ b/packages/server/src/utils/notifications/build-success.ts
@@ -9,6 +9,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
+ sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -43,11 +44,12 @@ export const sendBuildSuccessNotifications = async ({
telegram: true,
slack: true,
gotify: true,
+ ntfy: true,
},
});
for (const notification of notificationList) {
- const { email, discord, telegram, slack, gotify } = notification;
+ const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -126,6 +128,19 @@ export const sendBuildSuccessNotifications = async ({
);
}
+ if (ntfy) {
+ await sendNtfyNotification(
+ ntfy,
+ "Build Success",
+ "white_check_mark",
+ `view, Build details, ${buildLink}, clear=true;`,
+ `🛠Project: ${projectName}\n` +
+ `⚙️Application: ${applicationName}\n` +
+ `❔Type: ${applicationType}\n` +
+ `🕒Date: ${date.toLocaleString()}`,
+ );
+ }
+
if (telegram) {
const chunkArray = (array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts
index 044e3a0cf..f3c5cd5f4 100644
--- a/packages/server/src/utils/notifications/database-backup.ts
+++ b/packages/server/src/utils/notifications/database-backup.ts
@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
+ sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -42,11 +43,12 @@ export const sendDatabaseBackupNotifications = async ({
telegram: true,
slack: true,
gotify: true,
+ ntfy: true,
},
});
for (const notification of notificationList) {
- const { email, discord, telegram, slack, gotify } = notification;
+ const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -149,6 +151,21 @@ export const sendDatabaseBackupNotifications = async ({
);
}
+ if (ntfy) {
+ await sendNtfyNotification(
+ ntfy,
+ `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
+ `${type === "success" ? "white_check_mark" : "x"}`,
+ "",
+ `🛠Project: ${projectName}\n` +
+ `⚙️Application: ${applicationName}\n` +
+ `❔Type: ${databaseType}\n` +
+ `📂Database Name: ${databaseName}` +
+ `🕒Date: ${date.toLocaleString()}\n` +
+ `${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
+ );
+ }
+
if (telegram) {
const isError = type === "error" && errorMessage;
diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts
index b3959cccd..15b1c347a 100644
--- a/packages/server/src/utils/notifications/docker-cleanup.ts
+++ b/packages/server/src/utils/notifications/docker-cleanup.ts
@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
+ sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -29,11 +30,12 @@ export const sendDockerCleanupNotifications = async (
telegram: true,
slack: true,
gotify: true,
+ ntfy: true,
},
});
for (const notification of notificationList) {
- const { email, discord, telegram, slack, gotify } = notification;
+ const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -93,6 +95,16 @@ export const sendDockerCleanupNotifications = async (
);
}
+ if (ntfy) {
+ await sendNtfyNotification(
+ ntfy,
+ "Docker Cleanup",
+ "white_check_mark",
+ "",
+ `🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
+ );
+ }
+
if (telegram) {
await sendTelegramNotification(
telegram,
diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts
index 53ccf05d9..a6ade6f11 100644
--- a/packages/server/src/utils/notifications/dokploy-restart.ts
+++ b/packages/server/src/utils/notifications/dokploy-restart.ts
@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
+ sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -23,11 +24,12 @@ export const sendDokployRestartNotifications = async () => {
telegram: true,
slack: true,
gotify: true,
+ ntfy: true,
},
});
for (const notification of notificationList) {
- const { email, discord, telegram, slack, gotify } = notification;
+ const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -85,6 +87,20 @@ export const sendDokployRestartNotifications = async () => {
}
}
+ if (ntfy) {
+ try {
+ await sendNtfyNotification(
+ ntfy,
+ "Dokploy Server Restarted",
+ "white_check_mark",
+ "",
+ `🕒Date: ${date.toLocaleString()}`,
+ );
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
if (telegram) {
try {
await sendTelegramNotification(
diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts
index dd552cf3e..ec1c020ad 100644
--- a/packages/server/src/utils/notifications/utils.ts
+++ b/packages/server/src/utils/notifications/utils.ts
@@ -2,6 +2,7 @@ import type {
discord,
email,
gotify,
+ ntfy,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -126,3 +127,27 @@ export const sendGotifyNotification = async (
);
}
};
+
+export const sendNtfyNotification = async (
+ connection: typeof ntfy.$inferInsert,
+ title: string,
+ tags: string,
+ actions: string,
+ message: string,
+) => {
+ const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${connection.accessToken}`,
+ "X-Priority": connection.priority?.toString() || "3",
+ "X-Title": title,
+ "X-Tags": tags,
+ "X-Actions": actions,
+ },
+ body: message,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
+ }
+};
diff --git a/packages/server/src/utils/providers/docker.ts b/packages/server/src/utils/providers/docker.ts
index 88c457767..56341b7d6 100644
--- a/packages/server/src/utils/providers/docker.ts
+++ b/packages/server/src/utils/providers/docker.ts
@@ -42,7 +42,9 @@ export const buildDocker = async (
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
- writeStream.write("❌ Error");
+ writeStream.write(
+ `❌ Error: ${error instanceof Error ? error.message : String(error)}`,
+ );
throw error;
} finally {
writeStream.end();
diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts
index 5e21fc67e..5b55c240c 100644
--- a/packages/server/src/utils/volume-backups/utils.ts
+++ b/packages/server/src/utils/volume-backups/utils.ts
@@ -2,11 +2,14 @@ import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduledJobs, scheduleJob } from "node-schedule";
import {
createDeploymentVolumeBackup,
+ updateDeploymentStatus,
+} from "@dokploy/server/services/deployment";
+import {
execAsync,
execAsyncRemote,
- updateDeploymentStatus,
-} from "../..";
+} from "@dokploy/server/utils/process/execAsync";
import { backupVolume } from "./backup";
+import { getS3Credentials, normalizeS3Path } from "../backups/utils";
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
@@ -20,6 +23,33 @@ export const removeVolumeBackupJob = async (volumeBackupId: string) => {
currentJob?.cancel();
};
+const cleanupOldVolumeBackups = async (
+ volumeBackup: Awaited>,
+ serverId?: string | null,
+) => {
+ const { keepLatestCount, destination, prefix, volumeName } = volumeBackup;
+
+ if (!keepLatestCount) return;
+
+ try {
+ const rcloneFlags = getS3Credentials(destination);
+ const normalizedPrefix = normalizeS3Path(prefix);
+ const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`;
+ const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`;
+ const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
+ const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
+ const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
+
+ if (serverId) {
+ await execAsyncRemote(serverId, fullCommand);
+ } else {
+ await execAsync(fullCommand);
+ }
+ } catch (error) {
+ console.error("Volume backup retention error", error);
+ }
+};
+
export const runVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
const serverId =
@@ -40,6 +70,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
await execAsync(commandWithLog);
}
+ if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) {
+ await cleanupOldVolumeBackups(volumeBackup, serverId);
+ }
+
await updateDeploymentStatus(deployment.deploymentId, "done");
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index db93bb4da..94dedb322 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,26 +101,26 @@ importers:
apps/dokploy:
dependencies:
'@ai-sdk/anthropic':
- specifier: ^1.2.12
- version: 1.2.12(zod@3.25.32)
+ specifier: ^2.0.5
+ version: 2.0.5(zod@3.25.32)
'@ai-sdk/azure':
- specifier: ^1.3.23
- version: 1.3.23(zod@3.25.32)
+ specifier: ^2.0.16
+ version: 2.0.16(zod@3.25.32)
'@ai-sdk/cohere':
- specifier: ^1.2.10
- version: 1.2.10(zod@3.25.32)
+ specifier: ^2.0.4
+ version: 2.0.4(zod@3.25.32)
'@ai-sdk/deepinfra':
- specifier: ^0.0.4
- version: 0.0.4(zod@3.25.32)
+ specifier: ^1.0.10
+ version: 1.0.10(zod@3.25.32)
'@ai-sdk/mistral':
- specifier: ^1.2.8
- version: 1.2.8(zod@3.25.32)
+ specifier: ^2.0.7
+ version: 2.0.7(zod@3.25.32)
'@ai-sdk/openai':
- specifier: ^1.3.22
- version: 1.3.22(zod@3.25.32)
+ specifier: ^2.0.16
+ version: 2.0.16(zod@3.25.32)
'@ai-sdk/openai-compatible':
- specifier: ^0.0.13
- version: 0.0.13(zod@3.25.32)
+ specifier: ^1.0.10
+ version: 1.0.10(zod@3.25.32)
'@codemirror/autocomplete':
specifier: ^6.18.6
version: 6.18.6
@@ -263,8 +263,11 @@ importers:
specifier: ^0.5.16
version: 0.5.16
ai:
- specifier: ^4.3.16
- version: 4.3.16(react@18.2.0)(zod@3.25.32)
+ specifier: ^5.0.17
+ version: 5.0.17(zod@3.25.32)
+ ai-sdk-ollama:
+ specifier: ^0.5.1
+ version: 0.5.1(zod@3.25.32)
bcrypt:
specifier: 5.1.1
version: 5.1.1
@@ -361,9 +364,6 @@ importers:
octokit:
specifier: 3.1.2
version: 3.1.2
- ollama-ai-provider:
- specifier: ^1.2.0
- version: 1.2.0(zod@3.25.32)
otpauth:
specifier: ^9.4.0
version: 9.4.0
@@ -595,26 +595,26 @@ importers:
packages/server:
dependencies:
'@ai-sdk/anthropic':
- specifier: ^1.2.12
- version: 1.2.12(zod@3.25.32)
+ specifier: ^2.0.5
+ version: 2.0.5(zod@3.25.32)
'@ai-sdk/azure':
- specifier: ^1.3.23
- version: 1.3.23(zod@3.25.32)
+ specifier: ^2.0.16
+ version: 2.0.16(zod@3.25.32)
'@ai-sdk/cohere':
- specifier: ^1.2.10
- version: 1.2.10(zod@3.25.32)
+ specifier: ^2.0.4
+ version: 2.0.4(zod@3.25.32)
'@ai-sdk/deepinfra':
- specifier: ^0.0.4
- version: 0.0.4(zod@3.25.32)
+ specifier: ^1.0.10
+ version: 1.0.10(zod@3.25.32)
'@ai-sdk/mistral':
- specifier: ^1.2.8
- version: 1.2.8(zod@3.25.32)
+ specifier: ^2.0.7
+ version: 2.0.7(zod@3.25.32)
'@ai-sdk/openai':
- specifier: ^1.3.22
- version: 1.3.22(zod@3.25.32)
+ specifier: ^2.0.16
+ version: 2.0.16(zod@3.25.32)
'@ai-sdk/openai-compatible':
- specifier: ^0.0.13
- version: 0.0.13(zod@3.25.32)
+ specifier: ^1.0.10
+ version: 1.0.10(zod@3.25.32)
'@better-auth/utils':
specifier: 0.2.4
version: 0.2.4
@@ -643,8 +643,11 @@ importers:
specifier: ^0.5.16
version: 0.5.16
ai:
- specifier: ^4.3.16
- version: 4.3.16(react@18.2.0)(zod@3.25.32)
+ specifier: ^5.0.17
+ version: 5.0.17(zod@3.25.32)
+ ai-sdk-ollama:
+ specifier: ^0.5.1
+ version: 0.5.1(zod@3.25.32)
bcrypt:
specifier: 5.1.1
version: 5.1.1
@@ -705,9 +708,6 @@ importers:
octokit:
specifier: 3.1.2
version: 3.1.2
- ollama-ai-provider:
- specifier: ^1.2.0
- version: 1.2.0(zod@3.25.32)
otpauth:
specifier: ^9.4.0
version: 9.4.0
@@ -823,87 +823,64 @@ importers:
packages:
- '@ai-sdk/anthropic@1.2.12':
- resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==}
+ '@ai-sdk/anthropic@2.0.5':
+ resolution: {integrity: sha512-f0+mD3c5D+ImCWqxFxkT3buGeBg9vFOd2aTaLd1jjIJmWO8O4INLxBC2ETif7z0BfegTIw528B6acBRIeg3jIw==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/azure@1.3.23':
- resolution: {integrity: sha512-vpsaPtU24RBVk/IMM5UylR/N4RtAuL2NZLWc7LJ3tvMTHu6pI46a7w+1qIwR3F6yO9ehWR8qvfLaBefJNFxaVw==}
+ '@ai-sdk/azure@2.0.16':
+ resolution: {integrity: sha512-Q8Fq7aJP9tJOCNicfEUDxU763NkX646zCePayy2Nse+5Gz6ElQEJ9MApIdC4LAyR/IsEuY8G5iY477GAF+iBjg==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/cohere@1.2.10':
- resolution: {integrity: sha512-OaUwd5xj4bxSO8hdCbX1a5uUlTouU8FcodSuRON6xDSsmjZIvQL4O2G1XzcidxiQVL8JQuA+M0tHZOwGxSL96A==}
+ '@ai-sdk/cohere@2.0.4':
+ resolution: {integrity: sha512-GkQsTmhDNVDv8OiwWEGBhkjWYXFzBSbRtmyaM5y4yr9h6rqNwRiwueJG89/aX4mqpGf431mnJCPYxD+eSV6vgQ==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/deepinfra@0.0.4':
- resolution: {integrity: sha512-0YZpe7bFWpSZpk2swBhYsKyd2DjxyMa0bJTajJjwec4UGUpuiDyhjXkBgEHY85JjlTubEPby8Ix+FgU0E3ofnw==}
+ '@ai-sdk/deepinfra@1.0.10':
+ resolution: {integrity: sha512-bAvg29LkPI04o9ehRyqwjkKAUlX6W8AXlBTzX/k/gMez9qBbiW6uDpRRXTNYbaEuMONrEHgOQLh2OQ5WYJLDPg==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/mistral@1.2.8':
- resolution: {integrity: sha512-lv857D9UJqCVxiq2Fcu7mSPTypEHBUqLl1K+lCaP6X/7QAkcaxI36QDONG+tOhGHJOXTsS114u8lrUTaEiGXbg==}
+ '@ai-sdk/gateway@1.0.8':
+ resolution: {integrity: sha512-yiHYz0bAHEvhL+fSUBI2dNmyj0LOI8zw5qrYpa4gp1ojPgZq/7T1WXoIWRmVdjQwvT4PzSmRKLtbMPfz+umgfw==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/openai-compatible@0.0.13':
- resolution: {integrity: sha512-fuauXYKac6PBuf8m52tWcWQW0UCScEkwTaOyr00TcPeK3dd8nPP+ZJzSYE5QhFg7rwi9EH3ahIFqSX1biXhdkQ==}
+ '@ai-sdk/mistral@2.0.7':
+ resolution: {integrity: sha512-wWBacWHZHx+WxwjSqb5iIdtK76tmBiEb35ZBLmjODFEdh/dMIaj+g/qMVFT9PY7hHxZ1sT9C58KS671l8nAISw==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/openai@1.3.22':
- resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==}
+ '@ai-sdk/openai-compatible@1.0.10':
+ resolution: {integrity: sha512-NInkII/DOvrMvO/mS0BxGUGi3r+wuXxbdzAh9k2gFGT+xVoP6OePikhogQu8qZuti8loUZJGYq3GJ/DCftOzhQ==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
+ zod: ^3.25.76 || ^4
- '@ai-sdk/provider-utils@2.0.5':
- resolution: {integrity: sha512-2M7vLhYN0ThGjNlzow7oO/lsL+DyMxvGMIYmVQvEYaCWhDzxH5dOp78VNjJIVwHzVLMbBDigX3rJuzAs853idw==}
+ '@ai-sdk/openai@2.0.16':
+ resolution: {integrity: sha512-Boe715q4SkSJedFfAtbP0yuo8DmF9iYElAaDH2g4YgqJqqkskIJJx4hlCYGMMk1eMesRrB2NqZvtOeyTZ/u4fA==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.0.0
- peerDependenciesMeta:
- zod:
- optional: true
+ zod: ^3.25.76 || ^4
- '@ai-sdk/provider-utils@2.2.8':
- resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
+ '@ai-sdk/provider-utils@3.0.4':
+ resolution: {integrity: sha512-/3Z6lfUp8r+ewFd9yzHkCmPlMOJUXup2Sx3aoUyrdXLhOmAfHRl6Z4lDbIdV0uvw/QYoBcVLJnvXN7ncYeS3uQ==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.23.8
+ zod: ^3.25.76 || ^4
- '@ai-sdk/provider@1.0.3':
- resolution: {integrity: sha512-WiuJEpHTrltOIzv3x2wx4gwksAHW0h6nK3SoDzjqCOJLu/2OJ1yASESTIX+f07ChFykHElVoP80Ol/fe9dw6tQ==}
+ '@ai-sdk/provider@2.0.0':
+ resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
engines: {node: '>=18'}
- '@ai-sdk/provider@1.1.3':
- resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
- engines: {node: '>=18'}
-
- '@ai-sdk/react@1.2.12':
- resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
- engines: {node: '>=18'}
- peerDependencies:
- react: ^18 || ^19 || ^19.0.0-rc
- zod: ^3.23.8
- peerDependenciesMeta:
- zod:
- optional: true
-
- '@ai-sdk/ui-utils@1.2.11':
- resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
- engines: {node: '>=18'}
- peerDependencies:
- zod: ^3.23.8
-
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -3716,6 +3693,9 @@ packages:
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
engines: {node: '>=14.16'}
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
'@stepperize/react@4.0.1':
resolution: {integrity: sha512-LAOcfi3d2mM/Jn740Xy35qsuTwmoLIuitvWZTZRURYeGsc7a6sIKAkk3+L1joZGkLFvf5q4I6V7LxWWfB5hDvg==}
peerDependencies:
@@ -3958,9 +3938,6 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
- '@types/diff-match-patch@1.0.36':
- resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
-
'@types/docker-modem@3.0.6':
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
@@ -4201,15 +4178,15 @@ packages:
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
engines: {node: '>=12'}
- ai@4.3.16:
- resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
+ ai-sdk-ollama@0.5.1:
+ resolution: {integrity: sha512-VPE2yagxtowepiPROaP/7YUpiZxqZO4SDHHM2Tdw0wyatPCggct142eQI34UO2/PPJ1iXKpraWI5+n0/pcz69Q==}
+ engines: {node: '>=22'}
+
+ ai@5.0.17:
+ resolution: {integrity: sha512-DLZikqZZJdwSkRhFikw6Mt7pUmPZ7Ue38TjdOcw2U6iZtBbuiyWGIhHyJXlUpLcZrtBE5yqPTozyZri1lRjduw==}
engines: {node: '>=18'}
peerDependencies:
- react: ^18 || ^19 || ^19.0.0-rc
- zod: ^3.23.8
- peerDependenciesMeta:
- react:
- optional: true
+ zod: ^3.25.76 || ^4
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
@@ -4846,9 +4823,6 @@ packages:
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
- diff-match-patch@1.0.5:
- resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
-
diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -5135,9 +5109,9 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
- eventsource-parser@3.0.2:
- resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==}
- engines: {node: '>=18.0.0'}
+ eventsource-parser@3.0.5:
+ resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==}
+ engines: {node: '>=20.0.0'}
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
@@ -5730,11 +5704,6 @@ packages:
json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
- jsondiffpatch@0.6.0:
- resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
- engines: {node: ^18.0.0 || >=20.0.0}
- hasBin: true
-
jsonparse@1.3.1:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
@@ -6395,14 +6364,8 @@ packages:
resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==}
engines: {node: '>= 18'}
- ollama-ai-provider@1.2.0:
- resolution: {integrity: sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==}
- engines: {node: '>=18'}
- peerDependencies:
- zod: ^3.0.0
- peerDependenciesMeta:
- zod:
- optional: true
+ ollama@0.5.17:
+ resolution: {integrity: sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
@@ -6485,9 +6448,6 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
- partial-json@0.1.7:
- resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
-
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -7349,11 +7309,6 @@ packages:
react: '>=16.8.0 <19'
react-dom: '>=16.8.0 <19'
- swr@2.3.3:
- resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
- peerDependencies:
- react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
symbol-observable@1.2.0:
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
engines: {node: '>=0.10.0'}
@@ -7414,10 +7369,6 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
- throttleit@2.1.0:
- resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
- engines: {node: '>=18'}
-
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -7735,6 +7686,9 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ whatwg-fetch@3.6.20:
+ resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -7873,91 +7827,68 @@ packages:
snapshots:
- '@ai-sdk/anthropic@1.2.12(zod@3.25.32)':
+ '@ai-sdk/anthropic@2.0.5(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/azure@1.3.23(zod@3.25.32)':
+ '@ai-sdk/azure@2.0.16(zod@3.25.32)':
dependencies:
- '@ai-sdk/openai': 1.3.22(zod@3.25.32)
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
+ '@ai-sdk/openai': 2.0.16(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/cohere@1.2.10(zod@3.25.32)':
+ '@ai-sdk/cohere@2.0.4(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/deepinfra@0.0.4(zod@3.25.32)':
+ '@ai-sdk/deepinfra@1.0.10(zod@3.25.32)':
dependencies:
- '@ai-sdk/openai-compatible': 0.0.13(zod@3.25.32)
- '@ai-sdk/provider': 1.0.3
- '@ai-sdk/provider-utils': 2.0.5(zod@3.25.32)
+ '@ai-sdk/openai-compatible': 1.0.10(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/mistral@1.2.8(zod@3.25.32)':
+ '@ai-sdk/gateway@1.0.8(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/openai-compatible@0.0.13(zod@3.25.32)':
+ '@ai-sdk/mistral@2.0.7(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.0.3
- '@ai-sdk/provider-utils': 2.0.5(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/openai@1.3.22(zod@3.25.32)':
+ '@ai-sdk/openai-compatible@1.0.10(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/provider-utils@2.0.5(zod@3.25.32)':
+ '@ai-sdk/openai@2.0.16(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.0.3
- eventsource-parser: 3.0.2
- nanoid: 3.3.11
- secure-json-parse: 2.7.0
- optionalDependencies:
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
zod: 3.25.32
- '@ai-sdk/provider-utils@2.2.8(zod@3.25.32)':
+ '@ai-sdk/provider-utils@3.0.4(zod@3.25.32)':
dependencies:
- '@ai-sdk/provider': 1.1.3
- nanoid: 3.3.11
- secure-json-parse: 2.7.0
- zod: 3.25.32
-
- '@ai-sdk/provider@1.0.3':
- dependencies:
- json-schema: 0.4.0
-
- '@ai-sdk/provider@1.1.3':
- dependencies:
- json-schema: 0.4.0
-
- '@ai-sdk/react@1.2.12(react@18.2.0)(zod@3.25.32)':
- dependencies:
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
- '@ai-sdk/ui-utils': 1.2.11(zod@3.25.32)
- react: 18.2.0
- swr: 2.3.3(react@18.2.0)
- throttleit: 2.1.0
- optionalDependencies:
- zod: 3.25.32
-
- '@ai-sdk/ui-utils@1.2.11(zod@3.25.32)':
- dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@standard-schema/spec': 1.0.0
+ eventsource-parser: 3.0.5
zod: 3.25.32
zod-to-json-schema: 3.24.5(zod@3.25.32)
+ '@ai-sdk/provider@2.0.0':
+ dependencies:
+ json-schema: 0.4.0
+
'@alloc/quick-lru@5.2.0': {}
'@babel/code-frame@7.27.1':
@@ -10767,6 +10698,8 @@ snapshots:
'@sindresorhus/is@5.6.0': {}
+ '@standard-schema/spec@1.0.0': {}
+
'@stepperize/react@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -11257,8 +11190,6 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
- '@types/diff-match-patch@1.0.36': {}
-
'@types/docker-modem@3.0.6':
dependencies:
'@types/node': 20.17.51
@@ -11347,7 +11278,7 @@ snapshots:
'@types/pg@8.6.1':
dependencies:
- '@types/node': 20.17.51
+ '@types/node': 18.19.104
pg-protocol: 1.10.3
pg-types: 2.2.0
@@ -11534,17 +11465,22 @@ snapshots:
clean-stack: 4.2.0
indent-string: 5.0.0
- ai@4.3.16(react@18.2.0)(zod@3.25.32):
+ ai-sdk-ollama@0.5.1(zod@3.25.32):
dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
- '@ai-sdk/react': 1.2.12(react@18.2.0)(zod@3.25.32)
- '@ai-sdk/ui-utils': 1.2.11(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
+ ai: 5.0.17(zod@3.25.32)
+ ollama: 0.5.17
+ transitivePeerDependencies:
+ - zod
+
+ ai@5.0.17(zod@3.25.32):
+ dependencies:
+ '@ai-sdk/gateway': 1.0.8(zod@3.25.32)
+ '@ai-sdk/provider': 2.0.0
+ '@ai-sdk/provider-utils': 3.0.4(zod@3.25.32)
'@opentelemetry/api': 1.9.0
- jsondiffpatch: 0.6.0
zod: 3.25.32
- optionalDependencies:
- react: 18.2.0
ajv@8.17.1:
dependencies:
@@ -12181,8 +12117,6 @@ snapshots:
didyoumean@1.2.2: {}
- diff-match-patch@1.0.5: {}
-
diff-sequences@29.6.3: {}
dijkstrajs@1.0.3: {}
@@ -12469,7 +12403,7 @@ snapshots:
events@3.3.0: {}
- eventsource-parser@3.0.2: {}
+ eventsource-parser@3.0.5: {}
execa@8.0.1:
dependencies:
@@ -13088,12 +13022,6 @@ snapshots:
json-stringify-safe@5.0.1: {}
- jsondiffpatch@0.6.0:
- dependencies:
- '@types/diff-match-patch': 1.0.36
- chalk: 5.4.1
- diff-match-patch: 1.0.5
-
jsonparse@1.3.1: {}
jsonwebtoken@9.0.2:
@@ -13895,13 +13823,9 @@ snapshots:
'@octokit/request-error': 5.1.1
'@octokit/types': 12.6.0
- ollama-ai-provider@1.2.0(zod@3.25.32):
+ ollama@0.5.17:
dependencies:
- '@ai-sdk/provider': 1.1.3
- '@ai-sdk/provider-utils': 2.2.8(zod@3.25.32)
- partial-json: 0.1.7
- optionalDependencies:
- zod: 3.25.32
+ whatwg-fetch: 3.6.20
on-exit-leak-free@2.1.2: {}
@@ -13994,8 +13918,6 @@ snapshots:
parseurl@1.3.3: {}
- partial-json@0.1.7: {}
-
path-exists@4.0.0: {}
path-exists@5.0.0: {}
@@ -14957,12 +14879,6 @@ snapshots:
- '@types/react'
- debug
- swr@2.3.3(react@18.2.0):
- dependencies:
- dequal: 2.0.3
- react: 18.2.0
- use-sync-external-store: 1.5.0(react@18.2.0)
-
symbol-observable@1.2.0: {}
tailwind-merge@2.6.0: {}
@@ -15054,8 +14970,6 @@ snapshots:
dependencies:
real-require: 0.2.0
- throttleit@2.1.0: {}
-
through@2.3.8: {}
tiny-invariant@1.3.3: {}
@@ -15359,6 +15273,8 @@ snapshots:
webidl-conversions@3.0.1: {}
+ whatwg-fetch@3.6.20: {}
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3