Merge branch 'canary' into feat/stack-env-support

This commit is contained in:
Mauricio Siu
2024-12-08 18:35:40 -06:00
322 changed files with 39371 additions and 1168 deletions

View File

@@ -53,6 +53,8 @@ const createSchema = createInsertSchema(admins, {
letsEncryptEmail: z.string().optional(),
});
export const apiUpdateAdmin = createSchema.partial();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,

View File

@@ -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();

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -29,3 +29,4 @@ export * from "./github";
export * from "./gitlab";
export * from "./server";
export * from "./utils";
export * from "./preview-deployments";

View 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),
});

View File

@@ -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 });

View File

@@ -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(),
});

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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";

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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}`;
};

View File

@@ -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]);
};

View File

@@ -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();

View 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);
};

View File

@@ -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 = () => {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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 `

View File

@@ -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 += `

View File

@@ -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) {

View File

@@ -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) => {

View File

@@ -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];

View File

@@ -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",

View File

@@ -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");

View File

@@ -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;
}
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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}`,

View File

@@ -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");

View 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;
};

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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}\`\`\``,
},
]
: []),

View File

@@ -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(),

View File

@@ -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,
},
],

View File

@@ -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();

View File

@@ -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};