mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/stack-env-support
This commit is contained in:
@@ -53,6 +53,8 @@ const createSchema = createInsertSchema(admins, {
|
||||
letsEncryptEmail: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateAdmin = createSchema.partial();
|
||||
|
||||
export const apiSaveSSHKey = createSchema
|
||||
.pick({
|
||||
sshPrivateKey: true,
|
||||
|
||||
@@ -22,9 +22,10 @@ import { redirects } from "./redirects";
|
||||
import { registry } from "./registry";
|
||||
import { security } from "./security";
|
||||
import { server } from "./server";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { applicationStatus, certificateType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
@@ -114,6 +115,19 @@ export const applications = pgTable("application", {
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
env: text("env"),
|
||||
previewEnv: text("previewEnv"),
|
||||
previewBuildArgs: text("previewBuildArgs"),
|
||||
previewWildcard: text("previewWildcard"),
|
||||
previewPort: integer("previewPort").default(3000),
|
||||
previewHttps: boolean("previewHttps").notNull().default(false),
|
||||
previewPath: text("previewPath").default("/"),
|
||||
previewCertificateType: certificateType("certificateType")
|
||||
.notNull()
|
||||
.default("none"),
|
||||
previewLimit: integer("previewLimit").default(3),
|
||||
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||
false,
|
||||
),
|
||||
buildArgs: text("buildArgs"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
@@ -147,6 +161,7 @@ export const applications = pgTable("application", {
|
||||
username: text("username"),
|
||||
password: text("password"),
|
||||
dockerImage: text("dockerImage"),
|
||||
registryUrl: text("registryUrl"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
@@ -177,6 +192,7 @@ export const applications = pgTable("application", {
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -238,6 +254,7 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
previewDeployments: many(previewDeployments),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -347,7 +364,9 @@ const createSchema = createInsertSchema(applications, {
|
||||
subtitle: z.string().optional(),
|
||||
dockerImage: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
isPreviewDeploymentsActive: z.boolean().optional(),
|
||||
password: z.string().optional(),
|
||||
registryUrl: z.string().optional(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
repository: z.string().optional(),
|
||||
dockerfile: z.string().optional(),
|
||||
@@ -366,6 +385,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
"nixpacks",
|
||||
"static",
|
||||
]),
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
@@ -376,6 +396,14 @@ const createSchema = createInsertSchema(applications, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
previewPort: z.number().optional(),
|
||||
previewEnv: z.string().optional(),
|
||||
previewBuildArgs: z.string().optional(),
|
||||
previewWildcard: z.string().optional(),
|
||||
previewLimit: z.number().optional(),
|
||||
previewHttps: z.boolean().optional(),
|
||||
previewPath: z.string().optional(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
@@ -406,6 +434,7 @@ export const apiSaveBuildType = createSchema
|
||||
dockerfile: true,
|
||||
dockerContextPath: true,
|
||||
dockerBuildStage: true,
|
||||
herokuVersion: true,
|
||||
})
|
||||
.required()
|
||||
.merge(createSchema.pick({ publishDirectory: true }));
|
||||
@@ -451,6 +480,7 @@ export const apiSaveDockerProvider = createSchema
|
||||
applicationId: true,
|
||||
username: true,
|
||||
password: true,
|
||||
registryUrl: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { is, relations } from "drizzle-orm";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
pgEnum,
|
||||
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 { server } from "./server";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
@@ -32,6 +39,11 @@ export const deployments = pgTable("deployment", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
isPreviewDeployment: boolean("isPreviewDeployment").default(false),
|
||||
previewDeploymentId: text("previewDeploymentId").references(
|
||||
(): AnyPgColumn => previewDeployments.previewDeploymentId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -50,6 +62,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
fields: [deployments.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
previewDeployment: one(previewDeployments, {
|
||||
fields: [deployments.previewDeploymentId],
|
||||
references: [previewDeployments.previewDeploymentId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
@@ -59,6 +75,7 @@ const schema = createInsertSchema(deployments, {
|
||||
applicationId: z.string(),
|
||||
composeId: z.string(),
|
||||
description: z.string().optional(),
|
||||
previewDeploymentId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
@@ -68,11 +85,24 @@ export const apiCreateDeployment = schema
|
||||
logPath: true,
|
||||
applicationId: true,
|
||||
description: true,
|
||||
previewDeploymentId: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentPreview = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
description: true,
|
||||
previewDeploymentId: true,
|
||||
})
|
||||
.extend({
|
||||
previewDeploymentId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentCompose = schema
|
||||
.pick({
|
||||
title: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const destinations = pgTable("destination", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
provider: text("provider"),
|
||||
accessKey: text("accessKey").notNull(),
|
||||
secretAccessKey: text("secretAccessKey").notNull(),
|
||||
bucket: text("bucket").notNull(),
|
||||
@@ -37,6 +38,7 @@ export const destinationsRelations = relations(
|
||||
const createSchema = createInsertSchema(destinations, {
|
||||
destinationId: z.string(),
|
||||
name: z.string().min(1),
|
||||
provider: z.string(),
|
||||
accessKey: z.string(),
|
||||
bucket: z.string(),
|
||||
endpoint: z.string(),
|
||||
@@ -47,6 +49,7 @@ const createSchema = createInsertSchema(destinations, {
|
||||
export const apiCreateDestination = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
provider: true,
|
||||
accessKey: true,
|
||||
bucket: true,
|
||||
region: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
integer,
|
||||
pgEnum,
|
||||
@@ -14,8 +15,13 @@ import { domain } from "../validations/domain";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { certificateType } from "./shared";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
|
||||
export const domainType = pgEnum("domainType", ["compose", "application"]);
|
||||
export const domainType = pgEnum("domainType", [
|
||||
"compose",
|
||||
"application",
|
||||
"preview",
|
||||
]);
|
||||
|
||||
export const domains = pgTable("domain", {
|
||||
domainId: text("domainId")
|
||||
@@ -39,6 +45,10 @@ export const domains = pgTable("domain", {
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
previewDeploymentId: text("previewDeploymentId").references(
|
||||
(): AnyPgColumn => previewDeployments.previewDeploymentId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
});
|
||||
|
||||
@@ -51,6 +61,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
fields: [domains.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
previewDeployment: one(previewDeployments, {
|
||||
fields: [domains.previewDeploymentId],
|
||||
references: [previewDeployments.previewDeploymentId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
||||
@@ -65,6 +79,7 @@ export const apiCreateDomain = createSchema.pick({
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
previewDeploymentId: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
|
||||
@@ -29,3 +29,4 @@ export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
74
packages/server/src/db/schema/preview-deployments.ts
Normal file
74
packages/server/src/db/schema/preview-deployments.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applications } from "./application";
|
||||
import { domains } from "./domain";
|
||||
import { deployments } from "./deployment";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { generateAppName } from "./utils";
|
||||
import { applicationStatus } from "./shared";
|
||||
|
||||
export const previewDeployments = pgTable("preview_deployments", {
|
||||
previewDeploymentId: text("previewDeploymentId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
branch: text("branch").notNull(),
|
||||
pullRequestId: text("pullRequestId").notNull(),
|
||||
pullRequestNumber: text("pullRequestNumber").notNull(),
|
||||
pullRequestURL: text("pullRequestURL").notNull(),
|
||||
pullRequestTitle: text("pullRequestTitle").notNull(),
|
||||
pullRequestCommentId: text("pullRequestCommentId").notNull(),
|
||||
previewStatus: applicationStatus("previewStatus").notNull().default("idle"),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("preview"))
|
||||
.unique(),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
domainId: text("domainId").references(() => domains.domainId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
expiresAt: text("expiresAt"),
|
||||
});
|
||||
|
||||
export const previewDeploymentsRelations = relations(
|
||||
previewDeployments,
|
||||
({ one, many }) => ({
|
||||
deployments: many(deployments),
|
||||
domain: one(domains, {
|
||||
fields: [previewDeployments.domainId],
|
||||
references: [domains.domainId],
|
||||
}),
|
||||
application: one(applications, {
|
||||
fields: [previewDeployments.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const createSchema = createInsertSchema(previewDeployments, {
|
||||
applicationId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreatePreviewDeployment = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
domainId: true,
|
||||
branch: true,
|
||||
pullRequestId: true,
|
||||
pullRequestNumber: true,
|
||||
pullRequestURL: true,
|
||||
pullRequestTitle: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
// deploymentId: z.string().min(1),
|
||||
});
|
||||
@@ -26,6 +26,7 @@ export const projects = pgTable("project", {
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
env: text("env").notNull().default(""),
|
||||
});
|
||||
|
||||
export const projectRelations = relations(projects, ({ many, one }) => ({
|
||||
@@ -65,10 +66,16 @@ export const apiRemoveProject = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateProject = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
})
|
||||
.required();
|
||||
// export const apiUpdateProject = createSchema
|
||||
// .pick({
|
||||
// name: true,
|
||||
// description: true,
|
||||
// projectId: true,
|
||||
// env: true,
|
||||
// })
|
||||
// .required();
|
||||
|
||||
export const apiUpdateProject = createSchema.partial().extend({
|
||||
projectId: z.string().min(1),
|
||||
});
|
||||
// .omit({ serverId: true });
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { boolean, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { admins } from "./admin";
|
||||
import { applications } from "./application";
|
||||
import { auth } from "./auth";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
@@ -48,7 +47,7 @@ const createSchema = createInsertSchema(registry, {
|
||||
registryUrl: z.string(),
|
||||
adminId: z.string().min(1),
|
||||
registryId: z.string().min(1),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
registryType: z.enum(["cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
@@ -59,7 +58,7 @@ export const apiCreateRegistry = createSchema
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
registryType: z.enum(["cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
})
|
||||
.required()
|
||||
@@ -72,7 +71,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
registryType: z.enum(["cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ export const BuildFailedEmail = ({
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
|
||||
@@ -50,7 +50,7 @@ export const BuildSuccessEmail = ({
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
|
||||
@@ -51,7 +51,7 @@ export const DatabaseBackupEmail = ({
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
|
||||
@@ -44,7 +44,7 @@ export const DockerCleanupEmail = ({
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
|
||||
@@ -41,7 +41,7 @@ export const DokployRestartEmail = ({
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
|
||||
@@ -49,7 +49,7 @@ export const InvitationEmail = ({
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
|
||||
@@ -20,6 +20,7 @@ export * from "./services/mount";
|
||||
export * from "./services/certificate";
|
||||
export * from "./services/redirect";
|
||||
export * from "./services/security";
|
||||
export * from "./services/preview-deployment";
|
||||
export * from "./services/port";
|
||||
export * from "./services/redis";
|
||||
export * from "./services/compose";
|
||||
@@ -116,3 +117,4 @@ export * from "./monitoring/utilts";
|
||||
|
||||
export * from "./db/validations/domain";
|
||||
export * from "./db/validations/index";
|
||||
export * from "./utils/gpu-setup";
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
getCustomGitCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
authGithub,
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
@@ -40,8 +41,23 @@ import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { createDeployment, updateDeploymentStatus } from "./deployment";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentPreview,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import {
|
||||
createPreviewDeploymentComment,
|
||||
getIssueComment,
|
||||
issueCommentExists,
|
||||
updateIssueComment,
|
||||
} from "./github";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -100,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
@@ -168,7 +185,10 @@ export const deployApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository(application, deployment.logPath);
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
@@ -193,6 +213,7 @@ export const deployApplication = async ({
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -204,16 +225,9 @@ export const deployApplication = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Error on ",
|
||||
application.buildType,
|
||||
"/",
|
||||
application.sourceType,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -282,7 +296,11 @@ export const deployRemoteApplication = async ({
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand(application, deployment.logPath);
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
@@ -314,6 +332,7 @@ export const deployRemoteApplication = async ({
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -336,6 +355,7 @@ export const deployRemoteApplication = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
@@ -352,6 +372,225 @@ export const deployRemoteApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployPreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
// 4eef09efc46009187d668cf1c25f768d0bde4f91
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemotePreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
}
|
||||
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildRemoteApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
|
||||
@@ -214,7 +214,11 @@ export const deployCompose = async ({
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
||||
await cloneGithubRepository({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
});
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
@@ -235,6 +239,7 @@ export const deployCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -248,6 +253,7 @@ export const deployCompose = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -312,11 +318,12 @@ export const deployRemoteCompose = async ({
|
||||
let command = "set -e;";
|
||||
|
||||
if (compose.sourceType === "github") {
|
||||
command += await getGithubCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
command += await getGithubCloneCommand({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
serverId: compose.serverId,
|
||||
});
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(
|
||||
compose,
|
||||
@@ -353,6 +360,7 @@ export const deployRemoteCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -376,6 +384,7 @@ export const deployRemoteCompose = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -459,6 +468,36 @@ export const removeCompose = async (compose: Compose) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const startCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
if (compose.composeType === "docker-compose") {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName, "code"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "idle",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const stopCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
|
||||
@@ -5,13 +5,14 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateDeployment,
|
||||
type apiCreateDeploymentCompose,
|
||||
type apiCreateDeploymentPreview,
|
||||
type apiCreateDeploymentServer,
|
||||
deployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { format } from "date-fns";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
import {
|
||||
type Application,
|
||||
findApplicationById,
|
||||
@@ -21,6 +22,11 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
|
||||
import { type Server, findServerById } from "./server";
|
||||
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
type PreviewDeployment,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
@@ -101,6 +107,74 @@ export const createDeployment = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentPreview = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentPreview._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
deployment.previewDeploymentId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenPreviewDeploymenById(
|
||||
deployment.previewDeploymentId,
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
const appName = `${previewDeployment.appName}`;
|
||||
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, appName, fileName);
|
||||
|
||||
if (previewDeployment?.application?.serverId) {
|
||||
const server = await findServerById(
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await updatePreviewDeployment(deployment.previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentCompose = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentCompose._type,
|
||||
@@ -257,6 +331,41 @@ const removeLastTenComposeDeployments = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const removeLastTenPreviewDeploymenById = async (
|
||||
previewDeploymentId: string,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.previewDeploymentId, previewDeploymentId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
const { appName, applicationId } = application;
|
||||
const { LOGS_PATH } = paths(!!application.serverId);
|
||||
@@ -269,6 +378,30 @@ export const removeDeployments = async (application: Application) => {
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
export const removeDeploymentsByPreviewDeploymentId = async (
|
||||
previewDeployment: PreviewDeployment,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const { appName } = previewDeployment;
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, `rm -rf ${logsPath}`);
|
||||
} else {
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(deployments)
|
||||
.where(
|
||||
eq(
|
||||
deployments.previewDeploymentId,
|
||||
previewDeployment.previewDeploymentId,
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
};
|
||||
|
||||
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
||||
const { appName } = compose;
|
||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||
|
||||
@@ -110,7 +110,7 @@ export const getContainersByAppNameMatch = async (
|
||||
const command =
|
||||
appType === "docker-compose"
|
||||
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
||||
: `${cmd} | grep ${appName}`;
|
||||
: `${cmd} | grep '^.*Name: ${appName}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
|
||||
@@ -134,3 +134,7 @@ export const removeDomainById = async (domainId: string) => {
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const getDomainHost = (domain: Domain) => {
|
||||
return `${domain.https ? "https" : "http"}://${domain.host}`;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { updatePreviewDeployment } from "./preview-deployment";
|
||||
|
||||
export type Github = typeof github.$inferSelect;
|
||||
export const createGithub = async (
|
||||
@@ -72,3 +74,119 @@ export const updateGithub = async (
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
};
|
||||
|
||||
export const getIssueComment = (
|
||||
appName: string,
|
||||
status: "success" | "error" | "running" | "initializing",
|
||||
previewDomain: string,
|
||||
) => {
|
||||
let statusMessage = "";
|
||||
if (status === "success") {
|
||||
statusMessage = "✅ Done";
|
||||
} else if (status === "error") {
|
||||
statusMessage = "❌ Failed";
|
||||
} else if (status === "initializing") {
|
||||
statusMessage = "🔄 Building";
|
||||
} else {
|
||||
statusMessage = "🔄 Building";
|
||||
}
|
||||
const finished = `
|
||||
| Name | Status | Preview | Updated (UTC) |
|
||||
|------------|--------------|-------------------------------------|-----------------------|
|
||||
| ${appName} | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} |
|
||||
`;
|
||||
|
||||
return finished;
|
||||
};
|
||||
interface CommentExists {
|
||||
owner: string;
|
||||
repository: string;
|
||||
comment_id: number;
|
||||
githubId: string;
|
||||
}
|
||||
export const issueCommentExists = async ({
|
||||
owner,
|
||||
repository,
|
||||
comment_id,
|
||||
githubId,
|
||||
}: CommentExists) => {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
try {
|
||||
await octokit.rest.issues.getComment({
|
||||
owner: owner || "",
|
||||
repo: repository || "",
|
||||
comment_id: comment_id,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
interface Comment {
|
||||
owner: string;
|
||||
repository: string;
|
||||
issue_number: string;
|
||||
body: string;
|
||||
comment_id: number;
|
||||
githubId: string;
|
||||
}
|
||||
export const updateIssueComment = async ({
|
||||
owner,
|
||||
repository,
|
||||
issue_number,
|
||||
body,
|
||||
comment_id,
|
||||
githubId,
|
||||
}: Comment) => {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
|
||||
await octokit.rest.issues.updateComment({
|
||||
owner: owner || "",
|
||||
repo: repository || "",
|
||||
issue_number: issue_number,
|
||||
body,
|
||||
comment_id: comment_id,
|
||||
});
|
||||
};
|
||||
|
||||
interface CommentCreate {
|
||||
appName: string;
|
||||
owner: string;
|
||||
repository: string;
|
||||
issue_number: string;
|
||||
previewDomain: string;
|
||||
githubId: string;
|
||||
previewDeploymentId: string;
|
||||
}
|
||||
|
||||
export const createPreviewDeploymentComment = async ({
|
||||
owner,
|
||||
repository,
|
||||
issue_number,
|
||||
previewDomain,
|
||||
appName,
|
||||
githubId,
|
||||
previewDeploymentId,
|
||||
}: CommentCreate) => {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
|
||||
const runningComment = getIssueComment(
|
||||
appName,
|
||||
"initializing",
|
||||
previewDomain,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
owner: owner || "",
|
||||
repo: repository || "",
|
||||
issue_number: Number.parseInt(issue_number),
|
||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||
});
|
||||
|
||||
return await updatePreviewDeployment(previewDeploymentId, {
|
||||
pullRequestCommentId: `${issue.data.id}`,
|
||||
}).then((response) => response[0]);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
@@ -72,12 +72,12 @@ export const findMongoById = async (mongoId: string) => {
|
||||
|
||||
export const updateMongoById = async (
|
||||
mongoId: string,
|
||||
postgresData: Partial<Mongo>,
|
||||
mongoData: Partial<Mongo>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(mongo)
|
||||
.set({
|
||||
...postgresData,
|
||||
...mongoData,
|
||||
})
|
||||
.where(eq(mongo.mongoId, mongoId))
|
||||
.returning();
|
||||
|
||||
283
packages/server/src/services/preview-deployment.ts
Normal file
283
packages/server/src/services/preview-deployment.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreatePreviewDeployment,
|
||||
deployments,
|
||||
previewDeployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { slugify } from "../setup/server-setup";
|
||||
import { findApplicationById } from "./application";
|
||||
import { createDomain } from "./domain";
|
||||
import { generatePassword, generateRandomDomain } from "../templates/utils";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import {
|
||||
removeDeployments,
|
||||
removeDeploymentsByPreviewDeploymentId,
|
||||
} from "./deployment";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { removeService } from "../utils/docker/utils";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { getIssueComment, type Github } from "./github";
|
||||
import { findAdminById } from "./admin";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
export const findPreviewDeploymentById = async (
|
||||
previewDeploymentId: string,
|
||||
) => {
|
||||
const application = await db.query.previewDeployments.findFirst({
|
||||
where: eq(previewDeployments.previewDeploymentId, previewDeploymentId),
|
||||
with: {
|
||||
domain: true,
|
||||
application: {
|
||||
with: {
|
||||
server: true,
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Preview Deployment not found",
|
||||
});
|
||||
}
|
||||
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,
|
||||
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();
|
||||
|
||||
application.appName = previewDeployment.appName;
|
||||
const cleanupOperations = [
|
||||
async () =>
|
||||
await removeDeploymentsByPreviewDeploymentId(
|
||||
previewDeployment,
|
||||
application.serverId,
|
||||
),
|
||||
async () =>
|
||||
await removeDirectoryCode(application.appName, application.serverId),
|
||||
async () =>
|
||||
await removeTraefikConfig(application.appName, application.serverId),
|
||||
async () =>
|
||||
await removeService(application?.appName, application.serverId),
|
||||
];
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
return deployment[0];
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this preview deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
// testing-tesoitnmg-ddq0ul-preview-ihl44o
|
||||
export const updatePreviewDeployment = async (
|
||||
previewDeploymentId: string,
|
||||
previewDeploymentData: Partial<PreviewDeployment>,
|
||||
) => {
|
||||
const application = await db
|
||||
.update(previewDeployments)
|
||||
.set({
|
||||
...previewDeploymentData,
|
||||
})
|
||||
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
|
||||
.returning();
|
||||
|
||||
return application;
|
||||
};
|
||||
|
||||
export const findPreviewDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
const deploymentsList = await db.query.previewDeployments.findMany({
|
||||
where: eq(previewDeployments.applicationId, applicationId),
|
||||
orderBy: desc(previewDeployments.createdAt),
|
||||
with: {
|
||||
deployments: {
|
||||
orderBy: desc(deployments.createdAt),
|
||||
},
|
||||
domain: true,
|
||||
},
|
||||
});
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const createPreviewDeployment = async (
|
||||
schema: typeof apiCreatePreviewDeployment._type,
|
||||
) => {
|
||||
const application = await findApplicationById(schema.applicationId);
|
||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||
|
||||
const generateDomain = await generateWildcardDomain(
|
||||
application.previewWildcard || "*.traefik.me",
|
||||
appName,
|
||||
application.server?.ipAddress || "",
|
||||
application.project.adminId,
|
||||
);
|
||||
|
||||
const octokit = authGithub(application?.github as Github);
|
||||
|
||||
const runningComment = getIssueComment(
|
||||
application.name,
|
||||
"initializing",
|
||||
generateDomain,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
owner: application?.owner || "",
|
||||
repo: application?.repository || "",
|
||||
issue_number: Number.parseInt(schema.pullRequestNumber),
|
||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||
});
|
||||
|
||||
const previewDeployment = await db
|
||||
.insert(previewDeployments)
|
||||
.values({
|
||||
...schema,
|
||||
appName: appName,
|
||||
pullRequestCommentId: `${issue.data.id}`,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!previewDeployment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the preview deployment",
|
||||
});
|
||||
}
|
||||
|
||||
const newDomain = await createDomain({
|
||||
host: generateDomain,
|
||||
path: application.previewPath,
|
||||
port: application.previewPort,
|
||||
https: application.previewHttps,
|
||||
certificateType: application.previewCertificateType,
|
||||
domainType: "preview",
|
||||
previewDeploymentId: previewDeployment.previewDeploymentId,
|
||||
});
|
||||
|
||||
application.appName = appName;
|
||||
|
||||
await manageDomain(application, newDomain);
|
||||
|
||||
await db
|
||||
.update(previewDeployments)
|
||||
.set({
|
||||
domainId: newDomain.domainId,
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
previewDeployments.previewDeploymentId,
|
||||
previewDeployment.previewDeploymentId,
|
||||
),
|
||||
);
|
||||
|
||||
return previewDeployment;
|
||||
};
|
||||
|
||||
export const findPreviewDeploymentsByPullRequestId = async (
|
||||
pullRequestId: string,
|
||||
) => {
|
||||
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
||||
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
||||
});
|
||||
|
||||
return previewDeploymentResult;
|
||||
};
|
||||
|
||||
export const findPreviewDeploymentByApplicationId = async (
|
||||
applicationId: string,
|
||||
pullRequestId: string,
|
||||
) => {
|
||||
const previewDeploymentResult = await db.query.previewDeployments.findFirst({
|
||||
where: and(
|
||||
eq(previewDeployments.applicationId, applicationId),
|
||||
eq(previewDeployments.pullRequestId, pullRequestId),
|
||||
),
|
||||
});
|
||||
|
||||
return previewDeploymentResult;
|
||||
};
|
||||
|
||||
const generateWildcardDomain = async (
|
||||
baseDomain: string,
|
||||
appName: string,
|
||||
serverIp: string,
|
||||
adminId: string,
|
||||
): Promise<string> => {
|
||||
if (!baseDomain.startsWith("*.")) {
|
||||
throw new Error('The base domain must start with "*."');
|
||||
}
|
||||
const hash = `${appName}`;
|
||||
if (baseDomain.includes("traefik.me")) {
|
||||
let ip = "";
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
|
||||
if (serverIp) {
|
||||
ip = serverIp;
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
const admin = await findAdminById(adminId);
|
||||
ip = admin?.serverIp || "";
|
||||
}
|
||||
|
||||
const slugIp = ip.replaceAll(".", "-");
|
||||
return baseDomain.replace(
|
||||
"*",
|
||||
`${hash}${slugIp === "" ? "" : `-${slugIp}`}`,
|
||||
);
|
||||
}
|
||||
|
||||
return baseDomain.replace("*", hash);
|
||||
};
|
||||
@@ -74,25 +74,104 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
# Thanks to coolify <3
|
||||
|
||||
DOCKER_VERSION=27.0.3
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
${installUtilities()}
|
||||
|
||||
echo -e "2. Validating ports. "
|
||||
${validatePorts()}
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
echo -e "3. Installing RClone. "
|
||||
${installRClone()}
|
||||
|
||||
echo -e "4. Installing Docker. "
|
||||
${installDocker()}
|
||||
|
||||
echo -e "5. Setting up Docker Swarm"
|
||||
${setupSwarm()}
|
||||
|
||||
echo -e "6. Setting up Network"
|
||||
${setupNetwork()}
|
||||
|
||||
echo -e "7. Setting up Directories"
|
||||
${setupMainDirectory()}
|
||||
${setupDirectories()}
|
||||
|
||||
echo -e "8. Setting up Traefik"
|
||||
${createTraefikConfig()}
|
||||
|
||||
echo -e "9. Setting up Middlewares"
|
||||
${createDefaultMiddlewares()}
|
||||
|
||||
echo -e "10. Setting up Traefik Instance"
|
||||
${createTraefikInstance()}
|
||||
|
||||
echo -e "11. Installing Nixpacks"
|
||||
${installNixpacks()}
|
||||
|
||||
echo -e "12. Installing Buildpacks"
|
||||
${installBuildpacks()}
|
||||
`;
|
||||
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
if (err) {
|
||||
writeStream.write(err);
|
||||
@@ -101,7 +180,6 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
}
|
||||
stream
|
||||
.on("close", () => {
|
||||
writeStream.write("Connection closed ✅");
|
||||
client.end();
|
||||
resolve();
|
||||
})
|
||||
@@ -205,17 +283,12 @@ const setupNetwork = () => `
|
||||
echo "Network dokploy-network already exists ✅"
|
||||
else
|
||||
# Create the dokploy-network if it doesn't exist
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
echo "Network created ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const installDocker = () => `
|
||||
if command_exists docker; then
|
||||
echo "Docker already installed ✅"
|
||||
else
|
||||
echo "Installing Docker ✅"
|
||||
curl -sSL https://get.docker.com | sh -s -- --version 27.2.0
|
||||
if docker network create --driver overlay --attachable dokploy-network; then
|
||||
echo "Network created ✅"
|
||||
else
|
||||
echo "Failed to create dokploy-network ❌" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -231,6 +304,155 @@ const validatePorts = () => `
|
||||
fi
|
||||
`;
|
||||
|
||||
const installUtilities = () => `
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl unzip >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
`;
|
||||
|
||||
const installDocker = () => `
|
||||
|
||||
# Detect if docker is installed via snap
|
||||
if [ -x "$(command -v snap)" ]; then
|
||||
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
|
||||
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
|
||||
echo " - Docker is installed via snap."
|
||||
echo " Please note that Dokploy does not support Docker installed via snap."
|
||||
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=/usr/local/lib/docker
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"fedora")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/$DOCKER_VERSION.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$OS_TYPE" = "rocky" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "centos" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
echo " - Docker is installed."
|
||||
fi
|
||||
`;
|
||||
|
||||
const createTraefikConfig = () => {
|
||||
const config = getDefaultServerTraefikConfig();
|
||||
|
||||
@@ -261,7 +483,12 @@ const createDefaultMiddlewares = () => {
|
||||
};
|
||||
|
||||
export const installRClone = () => `
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
if command_exists rclone; then
|
||||
echo "RClone already installed ✅"
|
||||
else
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
echo "RClone installed successfully ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const createTraefikInstance = () => {
|
||||
|
||||
@@ -49,6 +49,7 @@ export const runMariadbBackup = async (
|
||||
projectName: project.name,
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -59,6 +60,7 @@ export const runMariadbBackup = async (
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -56,6 +57,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
projectName: project.name,
|
||||
databaseType: "mysql",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -56,6 +57,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const runPostgresBackup = async (
|
||||
projectName: project.name,
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -58,6 +59,7 @@ export const runPostgresBackup = async (
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -28,9 +28,9 @@ export const removeScheduleBackup = (backupId: string) => {
|
||||
};
|
||||
|
||||
export const getS3Credentials = (destination: Destination) => {
|
||||
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination;
|
||||
const { accessKey, secretAccessKey, bucket, region, endpoint, provider } =
|
||||
destination;
|
||||
const rcloneFlags = [
|
||||
// `--s3-provider=Cloudflare`,
|
||||
`--s3-access-key-id=${accessKey}`,
|
||||
`--s3-secret-access-key=${secretAccessKey}`,
|
||||
`--s3-region=${region}`,
|
||||
@@ -39,5 +39,9 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
"--s3-force-path-style",
|
||||
];
|
||||
|
||||
if (provider) {
|
||||
rcloneFlags.unshift(`--s3-provider=${provider}`);
|
||||
}
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
|
||||
@@ -183,7 +183,10 @@ const createEnvFile = (compose: ComposeNested) => {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
envContent,
|
||||
compose.project.env,
|
||||
).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
@@ -241,7 +244,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
envContent,
|
||||
compose.project.env,
|
||||
).join("\n");
|
||||
|
||||
const encodedContent = encodeBase64(envFileContent);
|
||||
return `
|
||||
|
||||
@@ -20,7 +20,10 @@ export const buildCustomDocker = async (
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const dockerContextPath = getDockerContextPath(application);
|
||||
|
||||
@@ -38,7 +41,7 @@ export const buildCustomDocker = async (
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
if (!publishDirectory) {
|
||||
createEnvFile(dockerFilePath, env);
|
||||
createEnvFile(dockerFilePath, env, application.project.env);
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
@@ -71,7 +74,10 @@ export const getDockerCommand = (
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const dockerContextPath =
|
||||
getDockerContextPath(application) || defaultContextPath;
|
||||
@@ -92,7 +98,11 @@ export const getDockerCommand = (
|
||||
*/
|
||||
let command = "";
|
||||
if (!publishDirectory) {
|
||||
command += createEnvFileCommand(dockerFilePath, env);
|
||||
command += createEnvFileCommand(
|
||||
dockerFilePath,
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
}
|
||||
|
||||
command += `
|
||||
|
||||
@@ -11,7 +11,10 @@ export const buildHeroku = async (
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
@@ -19,7 +22,7 @@ export const buildHeroku = async (
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"heroku/builder:24",
|
||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
@@ -44,7 +47,10 @@ export const getHerokuCommand = (
|
||||
const { env, appName } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const args = [
|
||||
"build",
|
||||
@@ -52,7 +58,7 @@ export const getHerokuCommand = (
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"heroku/builder:24",
|
||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { uploadImage } from "../cluster/upload";
|
||||
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -16,6 +17,7 @@ import { buildHeroku, getHerokuCommand } from "./heroku";
|
||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||
import { buildStatic, getStaticCommand } from "./static";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@@ -23,8 +25,16 @@ import { buildStatic, getStaticCommand } from "./static";
|
||||
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
||||
export type ApplicationNested = InferResultType<
|
||||
"applications",
|
||||
{ mounts: true; security: true; redirects: true; ports: true; registry: true }
|
||||
{
|
||||
mounts: true;
|
||||
security: true;
|
||||
redirects: true;
|
||||
ports: true;
|
||||
registry: true;
|
||||
project: true;
|
||||
}
|
||||
>;
|
||||
|
||||
export const buildApplication = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
@@ -69,19 +79,30 @@ export const getBuildCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { buildType } = application;
|
||||
let command = "";
|
||||
const { buildType, registry } = application;
|
||||
switch (buildType) {
|
||||
case "nixpacks":
|
||||
return getNixpacksCommand(application, logPath);
|
||||
command = getNixpacksCommand(application, logPath);
|
||||
break;
|
||||
case "heroku_buildpacks":
|
||||
return getHerokuCommand(application, logPath);
|
||||
command = getHerokuCommand(application, logPath);
|
||||
break;
|
||||
case "paketo_buildpacks":
|
||||
return getPaketoCommand(application, logPath);
|
||||
command = getPaketoCommand(application, logPath);
|
||||
break;
|
||||
case "static":
|
||||
return getStaticCommand(application, logPath);
|
||||
command = getStaticCommand(application, logPath);
|
||||
break;
|
||||
case "dockerfile":
|
||||
return getDockerCommand(application, logPath);
|
||||
command = getDockerCommand(application, logPath);
|
||||
break;
|
||||
}
|
||||
if (registry) {
|
||||
command += uploadImageRemoteCommand(application, logPath);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
export const mechanizeDockerContainer = async (
|
||||
@@ -121,7 +142,10 @@ export const mechanizeDockerContainer = async (
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const image = getImageName(application);
|
||||
const authConfig = getAuthConfig(application);
|
||||
@@ -186,11 +210,11 @@ const getImageName = (application: ApplicationNested) => {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
}
|
||||
|
||||
const registryUrl = registry?.registryUrl || "";
|
||||
const imagePrefix = registry?.imagePrefix ? `${registry.imagePrefix}/` : "";
|
||||
return registry
|
||||
? `${registryUrl}/${imagePrefix}${appName}`
|
||||
: `${appName}:latest`;
|
||||
if (registry) {
|
||||
return join(registry.imagePrefix || "", appName);
|
||||
}
|
||||
|
||||
return `${appName}:latest`;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
|
||||
@@ -14,11 +14,14 @@ export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, publishDirectory, serverId } = application;
|
||||
const { env, appName, publishDirectory } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const writeToStream = (data: string) => {
|
||||
if (writeStream.writable) {
|
||||
@@ -92,7 +95,10 @@ export const getNixpacksCommand = (
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ export const buildPaketo = async (
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
@@ -43,7 +46,10 @@ export const getPaketoCommand = (
|
||||
const { env, appName } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const args = [
|
||||
"build",
|
||||
|
||||
@@ -2,17 +2,29 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||
|
||||
export const createEnvFile = (directory: string, env: string | null) => {
|
||||
export const createEnvFile = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
projectEnv?: string | null,
|
||||
) => {
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
|
||||
"\n",
|
||||
);
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const createEnvFileCommand = (directory: string, env: string | null) => {
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
export const createEnvFileCommand = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
projectEnv?: string | null,
|
||||
) => {
|
||||
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
const encodedContent = encodeBase64(envFileContent || "");
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
@@ -16,23 +17,14 @@ export const uploadImage = async (
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL =
|
||||
registryType === "selfHosted"
|
||||
? process.env.NODE_ENV === "development"
|
||||
? "localhost:5000"
|
||||
: registryUrl
|
||||
: registryUrl;
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = imagePrefix
|
||||
? `${finalURL}/${imagePrefix}/${imageName}`
|
||||
: `${finalURL}/${imageName}`;
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
|
||||
try {
|
||||
console.log(finalURL, registryTag);
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`,
|
||||
);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "-p", registry.password],
|
||||
@@ -59,7 +51,48 @@ export const uploadImage = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// docker:
|
||||
// endpoint: "unix:///var/run/docker.sock"
|
||||
// exposedByDefault: false
|
||||
// swarmMode: true
|
||||
|
||||
export const uploadImageRemoteCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const registry = application.registry;
|
||||
|
||||
if (!registry) {
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
|
||||
try {
|
||||
const command = `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath};
|
||||
docker login ${finalURL} -u ${registry.username} -p ${registry.password} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ DockerHub Failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ DockerHub Login Success" >> ${logPath};
|
||||
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Error tagging image" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
|
||||
docker push ${registryTag} 2>> ${logPath} || {
|
||||
echo "❌ Error pushing image" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Pushed" >> ${logPath};
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MariadbNested = InferResultType<"mariadb", { mounts: true }>;
|
||||
export type MariadbNested = InferResultType<
|
||||
"mariadb",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
const {
|
||||
appName,
|
||||
@@ -37,7 +40,10 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMariadbEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMariadbEnv,
|
||||
mariadb.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mariadb);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MongoNested = InferResultType<"mongo", { mounts: true }>;
|
||||
export type MongoNested = InferResultType<
|
||||
"mongo",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
|
||||
export const buildMongo = async (mongo: MongoNested) => {
|
||||
const {
|
||||
@@ -36,7 +39,10 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMongoEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMongoEnv,
|
||||
mongo.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mongo);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MysqlNested = InferResultType<"mysql", { mounts: true }>;
|
||||
export type MysqlNested = InferResultType<
|
||||
"mysql",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
|
||||
export const buildMysql = async (mysql: MysqlNested) => {
|
||||
const {
|
||||
@@ -43,7 +46,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMysqlEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMysqlEnv,
|
||||
mysql.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mysql);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type PostgresNested = InferResultType<"postgres", { mounts: true }>;
|
||||
export type PostgresNested = InferResultType<
|
||||
"postgres",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
const {
|
||||
appName,
|
||||
@@ -36,7 +39,10 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultPostgresEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultPostgresEnv,
|
||||
postgres.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, postgres);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type RedisNested = InferResultType<"redis", { mounts: true }>;
|
||||
export type RedisNested = InferResultType<
|
||||
"redis",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
export const buildRedis = async (redis: RedisNested) => {
|
||||
const {
|
||||
appName,
|
||||
@@ -34,7 +37,10 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultRedisEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultRedisEnv,
|
||||
redis.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, redis);
|
||||
|
||||
@@ -259,10 +259,10 @@ export const createDomainLabels = async (
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
) => {
|
||||
const { host, port, https, uniqueConfigKey, certificateType } = domain;
|
||||
const { host, port, https, uniqueConfigKey, certificateType, path } = domain;
|
||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||
const labels = [
|
||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)`,
|
||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
|
||||
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
|
||||
`traefik.http.routers.${routerName}.service=${routerName}`,
|
||||
|
||||
@@ -11,12 +11,13 @@ import type { MysqlNested } from "../databases/mysql";
|
||||
import type { PostgresNested } from "../databases/postgres";
|
||||
import type { RedisNested } from "../databases/redis";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
registryUrl: string;
|
||||
}
|
||||
|
||||
export const pullImage = async (
|
||||
@@ -29,29 +30,21 @@ export const pullImage = async (
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.pull(dockerImage, { authconfig: authConfig }, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
docker.modem.followProgress(
|
||||
stream as Readable,
|
||||
(err: Error | null, res) => {
|
||||
if (!err) {
|
||||
resolve(res);
|
||||
}
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
(event) => {
|
||||
onData?.(event);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
if (authConfig?.username && authConfig?.password) {
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[
|
||||
"login",
|
||||
authConfig.registryUrl || "",
|
||||
"-u",
|
||||
authConfig.username,
|
||||
"-p",
|
||||
authConfig.password,
|
||||
],
|
||||
onData,
|
||||
);
|
||||
}
|
||||
await spawnAsync("docker", ["pull", dockerImage], onData);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -258,8 +251,28 @@ export const removeService = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareEnvironmentVariables = (env: string | null) =>
|
||||
Object.entries(parse(env ?? "")).map(([key, value]) => `${key}=${value}`);
|
||||
export const prepareEnvironmentVariables = (
|
||||
serviceEnv: string | null,
|
||||
projectEnv?: string | null,
|
||||
) => {
|
||||
const projectVars = parse(projectEnv ?? "");
|
||||
const serviceVars = parse(serviceEnv ?? "");
|
||||
|
||||
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
|
||||
let resolvedValue = value;
|
||||
if (projectVars) {
|
||||
resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
|
||||
if (projectVars[ref] !== undefined) {
|
||||
return projectVars[ref];
|
||||
}
|
||||
throw new Error(`Invalid project environment variable: project.${ref}`);
|
||||
});
|
||||
}
|
||||
return `${key}=${resolvedValue}`;
|
||||
});
|
||||
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const prepareBuildArgs = (input: string | null) => {
|
||||
const pairs = (input ?? "").split("\n");
|
||||
|
||||
349
packages/server/src/utils/gpu-setup.ts
Normal file
349
packages/server/src/utils/gpu-setup.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { execAsync, sleep } from "../utils/process/execAsync";
|
||||
import { execAsyncRemote } from "../utils/process/execAsync";
|
||||
|
||||
interface GPUInfo {
|
||||
driverInstalled: boolean;
|
||||
driverVersion?: string;
|
||||
gpuModel?: string;
|
||||
runtimeInstalled: boolean;
|
||||
runtimeConfigured: boolean;
|
||||
cudaSupport: boolean;
|
||||
cudaVersion?: string;
|
||||
memoryInfo?: string;
|
||||
availableGPUs: number;
|
||||
swarmEnabled: boolean;
|
||||
gpuResources: number;
|
||||
}
|
||||
|
||||
export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||
try {
|
||||
const [driverInfo, runtimeInfo, swarmInfo, gpuInfo, cudaInfo] =
|
||||
await Promise.all([
|
||||
checkGpuDriver(serverId),
|
||||
checkRuntime(serverId),
|
||||
checkSwarmResources(serverId),
|
||||
checkGpuInfo(serverId),
|
||||
checkCudaSupport(serverId),
|
||||
]);
|
||||
|
||||
return {
|
||||
...driverInfo,
|
||||
...runtimeInfo,
|
||||
...swarmInfo,
|
||||
...gpuInfo,
|
||||
...cudaInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in checkGPUStatus:", error);
|
||||
return {
|
||||
driverInstalled: false,
|
||||
driverVersion: undefined,
|
||||
runtimeInstalled: false,
|
||||
runtimeConfigured: false,
|
||||
cudaSupport: false,
|
||||
cudaVersion: undefined,
|
||||
gpuModel: undefined,
|
||||
memoryInfo: undefined,
|
||||
availableGPUs: 0,
|
||||
swarmEnabled: false,
|
||||
gpuResources: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const checkGpuDriver = async (serverId?: string) => {
|
||||
let driverVersion: string | undefined;
|
||||
let driverInstalled = false;
|
||||
let availableGPUs = 0;
|
||||
|
||||
try {
|
||||
const driverCommand =
|
||||
"nvidia-smi --query-gpu=driver_version --format=csv,noheader";
|
||||
const { stdout: nvidiaSmi } = serverId
|
||||
? await execAsyncRemote(serverId, driverCommand)
|
||||
: await execAsync(driverCommand);
|
||||
|
||||
driverVersion = nvidiaSmi.trim();
|
||||
if (driverVersion) {
|
||||
driverInstalled = true;
|
||||
const countCommand =
|
||||
"nvidia-smi --query-gpu=gpu_name --format=csv,noheader | wc -l";
|
||||
const { stdout: gpuCount } = serverId
|
||||
? await execAsyncRemote(serverId, countCommand)
|
||||
: await execAsync(countCommand);
|
||||
|
||||
availableGPUs = Number.parseInt(gpuCount.trim(), 10);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("GPU driver check:", error);
|
||||
}
|
||||
|
||||
return { driverVersion, driverInstalled, availableGPUs };
|
||||
};
|
||||
|
||||
const checkRuntime = async (serverId?: string) => {
|
||||
let runtimeInstalled = false;
|
||||
let runtimeConfigured = false;
|
||||
|
||||
try {
|
||||
// First check: Is nvidia-container-runtime installed?
|
||||
const checkBinaryCommand = "command -v nvidia-container-runtime";
|
||||
try {
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, checkBinaryCommand)
|
||||
: await execAsync(checkBinaryCommand);
|
||||
runtimeInstalled = !!stdout.trim();
|
||||
} catch (error) {
|
||||
console.debug("Runtime binary check:", error);
|
||||
}
|
||||
|
||||
// Second check: Is it configured in Docker?
|
||||
try {
|
||||
const runtimeCommand = 'docker info --format "{{json .Runtimes}}"';
|
||||
const { stdout: runtimeInfo } = serverId
|
||||
? await execAsyncRemote(serverId, runtimeCommand)
|
||||
: await execAsync(runtimeCommand);
|
||||
|
||||
const defaultCommand = 'docker info --format "{{.DefaultRuntime}}"';
|
||||
const { stdout: defaultRuntime } = serverId
|
||||
? await execAsyncRemote(serverId, defaultCommand)
|
||||
: await execAsync(defaultCommand);
|
||||
|
||||
const runtimes = JSON.parse(runtimeInfo);
|
||||
const hasNvidiaRuntime = "nvidia" in runtimes;
|
||||
const isDefaultRuntime = defaultRuntime.trim() === "nvidia";
|
||||
|
||||
// Only set runtimeConfigured if both conditions are met
|
||||
runtimeConfigured = hasNvidiaRuntime && isDefaultRuntime;
|
||||
} catch (error) {
|
||||
console.debug("Runtime configuration check:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("Runtime check:", error);
|
||||
}
|
||||
|
||||
return { runtimeInstalled, runtimeConfigured };
|
||||
};
|
||||
|
||||
const checkSwarmResources = async (serverId?: string) => {
|
||||
let swarmEnabled = false;
|
||||
let gpuResources = 0;
|
||||
|
||||
try {
|
||||
const nodeCommand =
|
||||
"docker node inspect self --format '{{json .Description.Resources.GenericResources}}'";
|
||||
const { stdout: resources } = serverId
|
||||
? await execAsyncRemote(serverId, nodeCommand)
|
||||
: await execAsync(nodeCommand);
|
||||
|
||||
if (resources && resources !== "null") {
|
||||
const genericResources = JSON.parse(resources);
|
||||
for (const resource of genericResources) {
|
||||
if (
|
||||
resource.DiscreteResourceSpec &&
|
||||
(resource.DiscreteResourceSpec.Kind === "GPU" ||
|
||||
resource.DiscreteResourceSpec.Kind === "gpu")
|
||||
) {
|
||||
gpuResources = resource.DiscreteResourceSpec.Value;
|
||||
swarmEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("Swarm resource check:", error);
|
||||
}
|
||||
|
||||
return { swarmEnabled, gpuResources };
|
||||
};
|
||||
|
||||
const checkGpuInfo = async (serverId?: string) => {
|
||||
let gpuModel: string | undefined;
|
||||
let memoryInfo: string | undefined;
|
||||
|
||||
try {
|
||||
const gpuInfoCommand =
|
||||
"nvidia-smi --query-gpu=gpu_name,memory.total --format=csv,noheader";
|
||||
const { stdout: gpuInfo } = serverId
|
||||
? await execAsyncRemote(serverId, gpuInfoCommand)
|
||||
: await execAsync(gpuInfoCommand);
|
||||
|
||||
[gpuModel, memoryInfo] = gpuInfo.split(",").map((s) => s.trim());
|
||||
} catch (error) {
|
||||
console.debug("GPU info check:", error);
|
||||
}
|
||||
|
||||
return { gpuModel, memoryInfo };
|
||||
};
|
||||
|
||||
const checkCudaSupport = async (serverId?: string) => {
|
||||
let cudaVersion: string | undefined;
|
||||
let cudaSupport = false;
|
||||
|
||||
try {
|
||||
const cudaCommand = 'nvidia-smi -q | grep "CUDA Version"';
|
||||
const { stdout: cudaInfo } = serverId
|
||||
? await execAsyncRemote(serverId, cudaCommand)
|
||||
: await execAsync(cudaCommand);
|
||||
|
||||
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d\.]+)/);
|
||||
cudaVersion = cudaMatch ? cudaMatch[1] : undefined;
|
||||
cudaSupport = !!cudaVersion;
|
||||
} catch (error) {
|
||||
console.debug("CUDA support check:", error);
|
||||
}
|
||||
|
||||
return { cudaVersion, cudaSupport };
|
||||
};
|
||||
|
||||
export async function setupGPUSupport(serverId?: string): Promise<void> {
|
||||
try {
|
||||
// 1. Initial status check and validation
|
||||
const initialStatus = await checkGPUStatus(serverId);
|
||||
const shouldContinue = await validatePrerequisites(initialStatus);
|
||||
if (!shouldContinue) return;
|
||||
|
||||
// 2. Get node ID
|
||||
const nodeId = await getNodeId(serverId);
|
||||
|
||||
// 3. Create daemon configuration
|
||||
const daemonConfig = createDaemonConfig(initialStatus.availableGPUs);
|
||||
|
||||
// 4. Setup server based on environment
|
||||
if (serverId) {
|
||||
await setupRemoteServer(serverId, daemonConfig);
|
||||
} else {
|
||||
await setupLocalServer(daemonConfig);
|
||||
}
|
||||
|
||||
// 5. Wait for Docker restart
|
||||
await sleep(10000);
|
||||
|
||||
// 6. Add GPU label
|
||||
await addGpuLabel(nodeId, serverId);
|
||||
|
||||
// 7. Final verification
|
||||
await sleep(5000);
|
||||
await verifySetup(nodeId, serverId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("password is required")
|
||||
) {
|
||||
throw new Error(
|
||||
"Sudo access required. Please run with appropriate permissions.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const validatePrerequisites = async (initialStatus: GPUInfo) => {
|
||||
if (!initialStatus.driverInstalled) {
|
||||
throw new Error(
|
||||
"NVIDIA drivers not installed. Please install appropriate NVIDIA drivers first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!initialStatus.runtimeInstalled) {
|
||||
throw new Error(
|
||||
"NVIDIA Container Runtime not installed. Please install nvidia-container-runtime first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (initialStatus.swarmEnabled && initialStatus.runtimeConfigured) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getNodeId = async (serverId?: string) => {
|
||||
const nodeIdCommand = 'docker info --format "{{.Swarm.NodeID}}"';
|
||||
const { stdout: nodeId } = serverId
|
||||
? await execAsyncRemote(serverId, nodeIdCommand)
|
||||
: await execAsync(nodeIdCommand);
|
||||
|
||||
const trimmedNodeId = nodeId.trim();
|
||||
if (!trimmedNodeId) {
|
||||
throw new Error("Setup Server before enabling GPU support");
|
||||
}
|
||||
|
||||
return trimmedNodeId;
|
||||
};
|
||||
|
||||
const createDaemonConfig = (availableGPUs: number) => ({
|
||||
runtimes: {
|
||||
nvidia: {
|
||||
path: "nvidia-container-runtime",
|
||||
runtimeArgs: [],
|
||||
},
|
||||
},
|
||||
"default-runtime": "nvidia",
|
||||
"node-generic-resources": [`GPU=${availableGPUs}`],
|
||||
});
|
||||
|
||||
const setupRemoteServer = async (serverId: string, daemonConfig: any) => {
|
||||
const setupCommands = [
|
||||
"sudo -n true",
|
||||
`echo '${JSON.stringify(daemonConfig, null, 2)}' | sudo tee /etc/docker/daemon.json`,
|
||||
"sudo mkdir -p /etc/nvidia-container-runtime",
|
||||
'sudo sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml',
|
||||
'echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" | sudo tee -a /etc/nvidia-container-runtime/config.toml',
|
||||
"sudo systemctl daemon-reload",
|
||||
"sudo systemctl restart docker",
|
||||
].join(" && ");
|
||||
|
||||
await execAsyncRemote(serverId, setupCommands);
|
||||
};
|
||||
|
||||
const setupLocalServer = async (daemonConfig: any) => {
|
||||
const configFile = `/tmp/docker-daemon-${Date.now()}.json`;
|
||||
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||
|
||||
const setupCommands = [
|
||||
`pkexec sh -c '
|
||||
cp ${configFile} /etc/docker/daemon.json &&
|
||||
mkdir -p /etc/nvidia-container-runtime &&
|
||||
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
||||
echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" >> /etc/nvidia-container-runtime/config.toml &&
|
||||
systemctl daemon-reload &&
|
||||
systemctl restart docker
|
||||
'`,
|
||||
`rm ${configFile}`,
|
||||
].join(" && ");
|
||||
|
||||
await execAsync(setupCommands);
|
||||
};
|
||||
|
||||
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
||||
const labelCommand = `docker node update --label-add gpu=true ${nodeId}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, labelCommand);
|
||||
} else {
|
||||
await execAsync(labelCommand);
|
||||
}
|
||||
};
|
||||
|
||||
const verifySetup = async (nodeId: string, serverId?: string) => {
|
||||
const finalStatus = await checkGPUStatus(serverId);
|
||||
|
||||
if (!finalStatus.swarmEnabled) {
|
||||
const diagnosticCommands = [
|
||||
`docker node inspect ${nodeId}`,
|
||||
'nvidia-smi -a | grep "GPU UUID"',
|
||||
"cat /etc/docker/daemon.json",
|
||||
"cat /etc/nvidia-container-runtime/config.toml",
|
||||
].join(" && ");
|
||||
|
||||
const { stdout: diagnostics } = serverId
|
||||
? await execAsyncRemote(serverId, diagnosticCommands)
|
||||
: await execAsync(diagnosticCommands);
|
||||
|
||||
console.error("Diagnostic Information:", diagnostics);
|
||||
throw new Error("GPU support not detected in swarm after setup");
|
||||
}
|
||||
|
||||
return finalStatus;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
applicationType: string;
|
||||
errorMessage: string;
|
||||
buildLink: string;
|
||||
adminId: string;
|
||||
}
|
||||
|
||||
export const sendBuildErrorNotifications = async ({
|
||||
@@ -24,10 +25,14 @@ export const sendBuildErrorNotifications = async ({
|
||||
applicationType,
|
||||
errorMessage,
|
||||
buildLink,
|
||||
adminId,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.appBuildError, true),
|
||||
where: and(
|
||||
eq(notifications.appBuildError, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -54,31 +59,46 @@ export const sendBuildErrorNotifications = async ({
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "⚠️ Build Failed",
|
||||
color: 0xff0000,
|
||||
title: "> `⚠️` - Build Failed",
|
||||
color: 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
name: "`🛠️`・Project",
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Application",
|
||||
name: "`⚙️`・Application",
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
name: "`❔`・Type",
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
value: errorMessage,
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Build Link",
|
||||
value: buildLink,
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Failed",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚠️`・Error Message",
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
applicationName: string;
|
||||
applicationType: string;
|
||||
buildLink: string;
|
||||
adminId: string;
|
||||
}
|
||||
|
||||
export const sendBuildSuccessNotifications = async ({
|
||||
@@ -22,10 +23,14 @@ export const sendBuildSuccessNotifications = async ({
|
||||
applicationName,
|
||||
applicationType,
|
||||
buildLink,
|
||||
adminId,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.appDeploy, true),
|
||||
where: and(
|
||||
eq(notifications.appDeploy, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -52,27 +57,42 @@ export const sendBuildSuccessNotifications = async ({
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "✅ Build Success",
|
||||
color: 0x00ff00,
|
||||
title: "> `✅` - Build Success",
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
name: "`🛠️`・Project",
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Application",
|
||||
name: "`⚙️`・Application",
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
name: "`❔`・Application Type",
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Build Link",
|
||||
value: buildLink,
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -16,16 +16,21 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
databaseType,
|
||||
type,
|
||||
errorMessage,
|
||||
adminId,
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
type: "error" | "success";
|
||||
adminId: string;
|
||||
errorMessage?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.databaseBackup, true),
|
||||
where: and(
|
||||
eq(notifications.databaseBackup, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -59,39 +64,47 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Database Backup Successful"
|
||||
: "❌ Database Backup Failed",
|
||||
color: type === "success" ? 0x00ff00 : 0xff0000,
|
||||
? "> `✅` - Database Backup Successful"
|
||||
: "> `❌` - Database Backup Failed",
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
name: "`🛠️`・Project",
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Application",
|
||||
name: "`⚙️`・Application",
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
name: "`❔`・Database",
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Time",
|
||||
value: date.toLocaleString(),
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
value: type,
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: "Error Message",
|
||||
value: errorMessage,
|
||||
name: "`⚠️`・Error Message",
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -11,11 +11,15 @@ import {
|
||||
} from "./utils";
|
||||
|
||||
export const sendDockerCleanupNotifications = async (
|
||||
adminId: string,
|
||||
message = "Docker cleanup for dokploy",
|
||||
) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.dockerCleanup, true),
|
||||
where: and(
|
||||
eq(notifications.dockerCleanup, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -41,12 +45,27 @@ export const sendDockerCleanupNotifications = async (
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "✅ Docker Cleanup",
|
||||
color: 0x00ff00,
|
||||
title: "> `✅` - Docker Cleanup",
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "Message",
|
||||
value: message,
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📜`・Message",
|
||||
value: `\`\`\`${message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
|
||||
@@ -34,12 +34,22 @@ export const sendDokployRestartNotifications = async () => {
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "✅ Dokploy Server Restarted",
|
||||
color: 0x00ff00,
|
||||
title: "> `✅` - Dokploy Server Restarted",
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "Time",
|
||||
value: date.toLocaleString(),
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { pullImage } from "../docker/utils";
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
registryUrl: string;
|
||||
}
|
||||
|
||||
export const buildDocker = async (
|
||||
@@ -16,6 +16,7 @@ export const buildDocker = async (
|
||||
const authConfig: Partial<RegistryAuth> = {
|
||||
username: username || "",
|
||||
password: password || "",
|
||||
registryUrl: application.registryUrl || "",
|
||||
};
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
@@ -33,7 +34,7 @@ export const buildDocker = async (
|
||||
dockerImage,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(`${data.status}\n`);
|
||||
writeStream.write(`${data}\n`);
|
||||
}
|
||||
},
|
||||
authConfig,
|
||||
@@ -41,7 +42,7 @@ export const buildDocker = async (
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("\nDocker Deployed: ✅\n");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
writeStream.write("❌ Error");
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -74,11 +74,22 @@ export type ApplicationWithGithub = InferResultType<
|
||||
>;
|
||||
|
||||
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
||||
export const cloneGithubRepository = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
|
||||
interface CloneGithubRepository {
|
||||
appName: string;
|
||||
owner: string | null;
|
||||
branch: string | null;
|
||||
githubId: string | null;
|
||||
repository: string | null;
|
||||
logPath: string;
|
||||
type?: "application" | "compose";
|
||||
}
|
||||
export const cloneGithubRepository = async ({
|
||||
logPath,
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository) => {
|
||||
const isCompose = type === "compose";
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
@@ -145,13 +156,13 @@ export const cloneGithubRepository = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubCloneCommand = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
export const getGithubCloneCommand = async ({
|
||||
logPath,
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository & { serverId: string }) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
|
||||
const isCompose = type === "compose";
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
@@ -206,7 +217,7 @@ export const getGithubCloneCommand = async (
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
||||
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
|
||||
Reference in New Issue
Block a user