mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Docker compose support (#111)
* feat(WIP): compose implementation * feat: add volumes, networks, services name hash generate * feat: add compose config test unique * feat: add tests for each unique config * feat: implement lodash for docker compose parsing * feat: add tests for generating compose file * refactor: implement logs docker compose * refactor: composeFile set not empty * feat: implement providers for compose deployments * feat: add Files volumes to compose * feat: add stop compose button * refactor: change strategie of building compose * feat: create .env file in composepath * refactor: simplify git and github function * chore: update deps * refactor: update migrations and add badge to recognize compose type * chore: update lock yaml * refactor: use code editor * feat: add monitoring for app types * refactor: reset stats on change appName * refactor: add option to clean monitoring folder * feat: show current command that will run * feat: add prefix * fix: add missing types * refactor: add docker provider and expose by default as false * refactor: customize error page * refactor: unified deployments to be a single one * feat: add vitest to ci/cd * revert: back to initial version * refactor: add maxconcurrency vitest * refactor: add pool forks to vitest * feat: add pocketbase template * fix: update path resolution compose * removed * feat: add template pocketbase * feat: add pocketbase template * feat: add support button * feat: add plausible template * feat: add calcom template * feat: add version to each template * feat: add code editor to enviroment variables and swarm settings json * refactor: add loader when download the image * fix: use base64 to generate keys plausible * feat: add recognized domain names by enviroment compose * refactor: show alert to redeploy in each card advanced tab * refactor: add validation to prevent create compose if not have permissions * chore: add templates section to contributing * chore: add example contributing
This commit is contained in:
@@ -160,6 +160,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
@@ -291,6 +292,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
278
server/api/routers/compose.ts
Normal file
278
server/api/routers/compose.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
apiCreateCompose,
|
||||
apiCreateComposeByTemplate,
|
||||
apiFindCompose,
|
||||
apiRandomizeCompose,
|
||||
apiUpdateCompose,
|
||||
compose,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createCompose,
|
||||
createComposeByTemplate,
|
||||
findComposeById,
|
||||
loadServices,
|
||||
removeCompose,
|
||||
stopCompose,
|
||||
updateCompose,
|
||||
} from "../services/compose";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import {
|
||||
cleanQueuesByCompose,
|
||||
type DeploymentJob,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
generateSSHKey,
|
||||
readRSAFile,
|
||||
removeRSAFiles,
|
||||
} from "@/server/utils/filesystem/ssh";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
||||
import { nanoid } from "nanoid";
|
||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
|
||||
import { findAdmin } from "../services/admin";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
||||
import { createMount } from "../services/mount";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import { templates } from "@/templates/templates";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
}
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newService.composeId);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the compose",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
|
||||
}
|
||||
|
||||
return await findComposeById(input.composeId);
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
return updateCompose(input.composeId, input);
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
||||
}
|
||||
const composeResult = await findComposeById(input.composeId);
|
||||
|
||||
const result = await db
|
||||
.delete(compose)
|
||||
.where(eq(compose.composeId, input.composeId))
|
||||
.returning();
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => await removeCompose(composeResult),
|
||||
async () => await removeDeploymentsByComposeId(composeResult),
|
||||
async () => await removeComposeDirectory(composeResult.appName),
|
||||
async () => await removeRSAFiles(composeResult.appName),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
}),
|
||||
|
||||
allServices: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input }) => {
|
||||
return await loadServices(input.composeId);
|
||||
}),
|
||||
|
||||
randomizeCompose: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
return await randomizeComposeFile(input.composeId, input.prefix);
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "compose",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
stop: protectedProcedure.input(apiFindCompose).mutation(async ({ input }) => {
|
||||
await stopCompose(input.composeId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const command = createCommand(compose);
|
||||
return `docker ${command}`;
|
||||
}),
|
||||
generateSSHKey: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
try {
|
||||
await generateSSHKey(compose.appName);
|
||||
const file = await readRSAFile(compose.appName);
|
||||
|
||||
await updateCompose(input.composeId, {
|
||||
customGitSSHKey: file,
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
return true;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateCompose(input.composeId, {
|
||||
refreshToken: nanoid(),
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
removeSSHKey: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await removeRSAFiles(compose.appName);
|
||||
await updateCompose(input.composeId, {
|
||||
customGitSSHKey: null,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
deployTemplate: protectedProcedure
|
||||
.input(apiCreateComposeByTemplate)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
}
|
||||
const composeFile = await readComposeFile(input.id);
|
||||
|
||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (!admin.serverIp) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"You need to have a server IP to deploy this template in order to generate domains",
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
||||
const { envs, mounts } = generate({
|
||||
serverIp: admin.serverIp,
|
||||
projectName: projectName,
|
||||
});
|
||||
|
||||
const compose = await createComposeByTemplate({
|
||||
...input,
|
||||
composeFile: composeFile,
|
||||
env: envs.join("\n"),
|
||||
name: input.id,
|
||||
sourceType: "raw",
|
||||
});
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, compose.composeId);
|
||||
}
|
||||
|
||||
if (mounts && mounts?.length > 0) {
|
||||
for (const mount of mounts) {
|
||||
await createMount({
|
||||
mountPath: mount.mountPath,
|
||||
content: mount.content,
|
||||
serviceId: compose.composeId,
|
||||
serviceType: "compose",
|
||||
type: "file",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
templates: protectedProcedure.query(async () => {
|
||||
const templatesData = templates.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
id: t.id,
|
||||
links: t.links,
|
||||
tags: t.tags,
|
||||
logo: t.logo,
|
||||
version: t.version,
|
||||
}));
|
||||
|
||||
return templatesData;
|
||||
}),
|
||||
});
|
||||
@@ -1,11 +1,23 @@
|
||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||
import { findAllDeploymentsByApplicationId } from "../services/deployment";
|
||||
import {
|
||||
apiFindAllByApplication,
|
||||
apiFindAllByCompose,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
findAllDeploymentsByApplicationId,
|
||||
findAllDeploymentsByComposeId,
|
||||
} from "../services/deployment";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
|
||||
allByCompose: protectedProcedure
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByComposeId(input.composeId);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { apiCreateMount, apiRemoveMount } from "@/server/db/schema";
|
||||
import { createMount, deleteMount } from "../services/mount";
|
||||
import {
|
||||
apiCreateMount,
|
||||
apiFindOneMount,
|
||||
apiRemoveMount,
|
||||
apiUpdateMount,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createMount,
|
||||
deleteMount,
|
||||
findMountById,
|
||||
updateMount,
|
||||
} from "../services/mount";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const mountRouter = createTRPCRouter({
|
||||
@@ -14,4 +24,14 @@ export const mountRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteMount(input.mountId);
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
|
||||
return await findMountById(input.mountId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateMount(input.mountId, input);
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "../services/user";
|
||||
import {
|
||||
applications,
|
||||
compose,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -64,6 +65,9 @@ export const projectRouter = createTRPCRouter({
|
||||
const service = await db.query.projects.findFirst({
|
||||
where: eq(projects.projectId, input.projectId),
|
||||
with: {
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accesedServices),
|
||||
},
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
@@ -135,6 +139,9 @@ export const projectRouter = createTRPCRouter({
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accesedServices),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accesedServices),
|
||||
},
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
});
|
||||
@@ -149,6 +156,7 @@ export const projectRouter = createTRPCRouter({
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { docker, MAIN_TRAEFIK_PATH } from "@/server/constants";
|
||||
import { docker, MAIN_TRAEFIK_PATH, MONITORING_PATH } from "@/server/constants";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
cleanStoppedContainers,
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
readDirectory,
|
||||
} from "../services/settings";
|
||||
import { canAccessToTraefikFiles } from "../services/user";
|
||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
@@ -85,6 +86,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
await cleanUpSystemPrune();
|
||||
return true;
|
||||
}),
|
||||
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||
await recreateDirectory(MONITORING_PATH);
|
||||
return true;
|
||||
}),
|
||||
saveSSHPrivateKey: adminProcedure
|
||||
.input(apiSaveSSHKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
Reference in New Issue
Block a user