mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-17 20:05:24 +02:00
Merge branch 'canary' into 1365-create-preview-deployment-using-api
This commit is contained in:
@@ -28,78 +28,82 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
"@ai-sdk/cohere": "^1.0.6",
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/azure": "^1.3.23",
|
||||
"@ai-sdk/cohere": "^1.2.10",
|
||||
"@ai-sdk/deepinfra": "^0.0.4",
|
||||
"@ai-sdk/mistral": "^1.0.6",
|
||||
"@ai-sdk/openai": "^1.0.12",
|
||||
"@ai-sdk/mistral": "^1.2.8",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||
"@better-auth/utils":"0.2.3",
|
||||
"@oslojs/encoding":"1.1.0",
|
||||
"@oslojs/crypto":"1.0.1",
|
||||
"drizzle-dbml-generator":"0.10.0",
|
||||
"better-auth":"1.2.0",
|
||||
"@better-auth/utils": "0.2.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@trpc/server": "^10.43.6",
|
||||
"adm-zip": "^0.5.14",
|
||||
"ai": "^4.0.23",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"ai": "^4.3.16",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"lucia": "^3.0.1",
|
||||
"nanoid": "3",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"node-os-utils": "1.3.7",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"ollama-ai-provider": "^1.1.0",
|
||||
"otpauth": "^9.2.3",
|
||||
"ollama-ai-provider": "^1.2.0",
|
||||
"otpauth": "^9.4.0",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
"public-ip": "6.0.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"slugify": "^1.6.6",
|
||||
"ssh2": "1.15.0",
|
||||
"toml": "3.0.0",
|
||||
"ws": "8.16.0",
|
||||
"zod": "^3.23.4"
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/dockerode": "3.3.23",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/node": "^18.17.0",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.19.104",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/ssh2": "1.15.1",
|
||||
"@types/ws": "8.5.10",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"esbuild": "0.20.2",
|
||||
"esbuild-plugin-alias": "0.2.1",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.2"
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,7 @@ export const paths = (isServer = false) => {
|
||||
CERTIFICATES_PATH: `${DYNAMIC_TRAEFIK_PATH}/certificates`,
|
||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
SCHEDULES_PATH: `${BASE_PATH}/schedules`,
|
||||
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { z } from "zod";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
@@ -23,16 +24,16 @@ import { redirects } from "./redirects";
|
||||
import { registry } from "./registry";
|
||||
import { security } from "./security";
|
||||
import { server } from "./server";
|
||||
import { applicationStatus, certificateType } from "./shared";
|
||||
import { applicationStatus, certificateType, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"drop",
|
||||
]);
|
||||
|
||||
@@ -116,6 +117,7 @@ export const applications = pgTable("application", {
|
||||
description: text("description"),
|
||||
env: text("env"),
|
||||
previewEnv: text("previewEnv"),
|
||||
watchPaths: text("watchPaths").array(),
|
||||
previewBuildArgs: text("previewBuildArgs"),
|
||||
previewWildcard: text("previewWildcard"),
|
||||
previewPort: integer("previewPort").default(3000),
|
||||
@@ -124,10 +126,12 @@ export const applications = pgTable("application", {
|
||||
previewCertificateType: certificateType("certificateType")
|
||||
.notNull()
|
||||
.default("none"),
|
||||
previewCustomCertResolver: text("previewCustomCertResolver"),
|
||||
previewLimit: integer("previewLimit").default(3),
|
||||
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||
false,
|
||||
),
|
||||
rollbackActive: boolean("rollbackActive").default(false),
|
||||
buildArgs: text("buildArgs"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -139,11 +143,13 @@ export const applications = pgTable("application", {
|
||||
command: text("command"),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||
cleanCache: boolean("cleanCache").default(false),
|
||||
// Github
|
||||
repository: text("repository"),
|
||||
owner: text("owner"),
|
||||
branch: text("branch"),
|
||||
buildPath: text("buildPath").default("/"),
|
||||
triggerType: triggerType("triggerType").default("push"),
|
||||
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
|
||||
// Gitlab
|
||||
gitlabProjectId: integer("gitlabProjectId"),
|
||||
@@ -152,6 +158,11 @@ export const applications = pgTable("application", {
|
||||
gitlabBranch: text("gitlabBranch"),
|
||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
@@ -172,6 +183,7 @@ export const applications = pgTable("application", {
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
|
||||
dockerfile: text("dockerfile"),
|
||||
dockerContextPath: text("dockerContextPath"),
|
||||
dockerBuildStage: text("dockerBuildStage"),
|
||||
@@ -194,6 +206,7 @@ export const applications = pgTable("application", {
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -209,6 +222,9 @@ export const applications = pgTable("application", {
|
||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -246,6 +262,10 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.gitlabId],
|
||||
references: [gitlab.gitlabId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [applications.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
bitbucket: one(bitbucket, {
|
||||
fields: [applications.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
@@ -376,7 +396,9 @@ const createSchema = createInsertSchema(applications, {
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.enum(["github", "docker", "git"]).optional(),
|
||||
sourceType: z
|
||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||
.optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
@@ -388,6 +410,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
]),
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
isStaticSpa: z.boolean().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@@ -404,7 +427,9 @@ const createSchema = createInsertSchema(applications, {
|
||||
previewLimit: z.number().optional(),
|
||||
previewHttps: z.boolean().optional(),
|
||||
previewPath: z.string().optional(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
cleanCache: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
@@ -438,7 +463,7 @@ export const apiSaveBuildType = createSchema
|
||||
herokuVersion: true,
|
||||
})
|
||||
.required()
|
||||
.merge(createSchema.pick({ publishDirectory: true }));
|
||||
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
|
||||
|
||||
export const apiSaveGithubProvider = createSchema
|
||||
.pick({
|
||||
@@ -448,8 +473,13 @@ export const apiSaveGithubProvider = createSchema
|
||||
owner: true,
|
||||
buildPath: true,
|
||||
githubId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||
});
|
||||
|
||||
export const apiSaveGitlabProvider = createSchema
|
||||
.pick({
|
||||
@@ -461,6 +491,8 @@ export const apiSaveGitlabProvider = createSchema
|
||||
gitlabId: true,
|
||||
gitlabProjectId: true,
|
||||
gitlabPathNamespace: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -472,6 +504,21 @@ export const apiSaveBitbucketProvider = createSchema
|
||||
bitbucketRepository: true,
|
||||
bitbucketId: true,
|
||||
applicationId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGiteaProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
giteaBranch: true,
|
||||
giteaBuildPath: true,
|
||||
giteaOwner: true,
|
||||
giteaRepository: true,
|
||||
giteaId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -491,6 +538,8 @@ export const apiSaveGitProvider = createSchema
|
||||
applicationId: true,
|
||||
customGitBuildPath: true,
|
||||
customGitUrl: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required()
|
||||
.merge(
|
||||
|
||||
@@ -2,6 +2,8 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
@@ -9,34 +11,51 @@ import {
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { generateAppName } from ".";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
import { users_temp } from "./user";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
]);
|
||||
|
||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||
|
||||
export const backups = pgTable("backup", {
|
||||
backupId: text("backupId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("backup"))
|
||||
.unique(),
|
||||
schedule: text("schedule").notNull(),
|
||||
enabled: boolean("enabled"),
|
||||
database: text("database").notNull(),
|
||||
prefix: text("prefix").notNull(),
|
||||
|
||||
serviceName: text("serviceName"),
|
||||
destinationId: text("destinationId")
|
||||
.notNull()
|
||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||
|
||||
keepLatestCount: integer("keepLatestCount"),
|
||||
backupType: backupType("backupType").notNull().default("database"),
|
||||
databaseType: databaseType("databaseType").notNull(),
|
||||
composeId: text("composeId").references(
|
||||
(): AnyPgColumn => compose.composeId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
postgresId: text("postgresId").references(
|
||||
(): AnyPgColumn => postgres.postgresId,
|
||||
{
|
||||
@@ -55,9 +74,30 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id),
|
||||
// Only for compose backups
|
||||
metadata: jsonb("metadata").$type<
|
||||
| {
|
||||
postgres?: {
|
||||
databaseUser: string;
|
||||
};
|
||||
mariadb?: {
|
||||
databaseUser: string;
|
||||
databasePassword: string;
|
||||
};
|
||||
mongo?: {
|
||||
databaseUser: string;
|
||||
databasePassword: string;
|
||||
};
|
||||
mysql?: {
|
||||
databaseRootPassword: string;
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
>(),
|
||||
});
|
||||
|
||||
export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
export const backupsRelations = relations(backups, ({ one, many }) => ({
|
||||
destination: one(destinations, {
|
||||
fields: [backups.destinationId],
|
||||
references: [destinations.destinationId],
|
||||
@@ -78,6 +118,15 @@ export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [backups.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [backups.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(backups, {
|
||||
@@ -87,11 +136,14 @@ const createSchema = createInsertSchema(backups, {
|
||||
prefix: z.string().min(1),
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
|
||||
keepLatestCount: z.number().optional(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateBackup = createSchema.pick({
|
||||
@@ -99,12 +151,18 @@ export const apiCreateBackup = createSchema.pick({
|
||||
enabled: true,
|
||||
prefix: true,
|
||||
destinationId: true,
|
||||
keepLatestCount: true,
|
||||
database: true,
|
||||
mariadbId: true,
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
backupType: true,
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export const apiFindOneBackup = createSchema
|
||||
@@ -127,5 +185,45 @@ export const apiUpdateBackup = createSchema
|
||||
backupId: true,
|
||||
destinationId: true,
|
||||
database: true,
|
||||
keepLatestCount: true,
|
||||
serviceName: true,
|
||||
metadata: true,
|
||||
databaseType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: z.string().min(1),
|
||||
destinationId: z.string().min(1),
|
||||
metadata: z
|
||||
.object({
|
||||
serviceName: z.string().optional(),
|
||||
postgres: z
|
||||
.object({
|
||||
databaseUser: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
mariadb: z
|
||||
.object({
|
||||
databaseUser: z.string(),
|
||||
databasePassword: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
mongo: z
|
||||
.object({
|
||||
databaseUser: z.string(),
|
||||
databasePassword: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
mysql: z
|
||||
.object({
|
||||
databaseRootPassword: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -3,23 +3,27 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { applicationStatus, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
import { schedules } from "./schedule";
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
@@ -55,6 +59,10 @@ export const compose = pgTable("compose", {
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
@@ -66,10 +74,16 @@ export const compose = pgTable("compose", {
|
||||
),
|
||||
command: text("command").notNull().default(""),
|
||||
//
|
||||
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
|
||||
composePath: text("composePath").notNull().default("./docker-compose.yml"),
|
||||
suffix: text("suffix").notNull().default(""),
|
||||
randomize: boolean("randomize").notNull().default(false),
|
||||
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
|
||||
// Keep this for backward compatibility since we will not add the prefix anymore to volumes
|
||||
isolatedDeploymentsVolume: boolean("isolatedDeploymentsVolume")
|
||||
.notNull()
|
||||
.default(false),
|
||||
triggerType: triggerType("triggerType").default("push"),
|
||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
@@ -77,7 +91,7 @@ export const compose = pgTable("compose", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
watchPaths: text("watchPaths").array(),
|
||||
githubId: text("githubId").references(() => github.githubId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -87,6 +101,9 @@ export const compose = pgTable("compose", {
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -116,22 +133,29 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
fields: [compose.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [compose.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [compose.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(compose, {
|
||||
name: z.string().min(1),
|
||||
description: z.string(),
|
||||
env: z.string().optional(),
|
||||
composeFile: z.string().min(1),
|
||||
composeFile: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
composePath: z.string().min(1),
|
||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateCompose = createSchema.pick({
|
||||
@@ -141,6 +165,7 @@ export const apiCreateCompose = createSchema.pick({
|
||||
composeType: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
composeFile: true,
|
||||
});
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
|
||||
@@ -10,10 +10,13 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { backups } from "./backups";
|
||||
import { compose } from "./compose";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { schedules } from "./schedule";
|
||||
import { server } from "./server";
|
||||
|
||||
import { rollbacks } from "./rollbacks";
|
||||
import { volumeBackups } from "./volume-backups";
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
@@ -29,6 +32,7 @@ export const deployments = pgTable("deployment", {
|
||||
description: text("description"),
|
||||
status: deploymentStatus("status").default("running"),
|
||||
logPath: text("logPath").notNull(),
|
||||
pid: text("pid"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
@@ -47,7 +51,24 @@ export const deployments = pgTable("deployment", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
startedAt: text("startedAt"),
|
||||
finishedAt: text("finishedAt"),
|
||||
errorMessage: text("errorMessage"),
|
||||
scheduleId: text("scheduleId").references(
|
||||
(): AnyPgColumn => schedules.scheduleId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
backupId: text("backupId").references((): AnyPgColumn => backups.backupId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
rollbackId: text("rollbackId").references(
|
||||
(): AnyPgColumn => rollbacks.rollbackId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
volumeBackupId: text("volumeBackupId").references(
|
||||
(): AnyPgColumn => volumeBackups.volumeBackupId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
@@ -67,6 +88,22 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
fields: [deployments.previewDeploymentId],
|
||||
references: [previewDeployments.previewDeploymentId],
|
||||
}),
|
||||
schedule: one(schedules, {
|
||||
fields: [deployments.scheduleId],
|
||||
references: [schedules.scheduleId],
|
||||
}),
|
||||
backup: one(backups, {
|
||||
fields: [deployments.backupId],
|
||||
references: [backups.backupId],
|
||||
}),
|
||||
rollback: one(rollbacks, {
|
||||
fields: [deployments.deploymentId],
|
||||
references: [rollbacks.deploymentId],
|
||||
}),
|
||||
volumeBackup: one(volumeBackups, {
|
||||
fields: [deployments.volumeBackupId],
|
||||
references: [volumeBackups.volumeBackupId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
@@ -116,6 +153,18 @@ export const apiCreateDeploymentCompose = schema
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentBackup = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
backupId: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
backupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentServer = schema
|
||||
.pick({
|
||||
title: true,
|
||||
@@ -128,6 +177,28 @@ export const apiCreateDeploymentServer = schema
|
||||
serverId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentSchedule = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
scheduleId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentVolumeBackup = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByApplication = schema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
@@ -154,3 +225,18 @@ export const apiFindAllByServer = schema
|
||||
serverId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindAllByType = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"compose",
|
||||
"server",
|
||||
"schedule",
|
||||
"previewDeployment",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
]),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -41,6 +41,7 @@ export const domains = pgTable("domain", {
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
customCertResolver: text("customCertResolver"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
@@ -50,6 +51,8 @@ export const domains = pgTable("domain", {
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
internalPath: text("internalPath").default("/"),
|
||||
stripPath: boolean("stripPath").notNull().default(false),
|
||||
});
|
||||
|
||||
export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
@@ -76,10 +79,13 @@ export const apiCreateDomain = createSchema.pick({
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
previewDeploymentId: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
@@ -107,7 +113,10 @@ export const apiUpdateDomain = createSchema
|
||||
port: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
})
|
||||
.merge(createSchema.pick({ domainId: true }).required());
|
||||
|
||||
@@ -5,13 +5,16 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { users_temp } from "./user";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
]);
|
||||
|
||||
export const gitProvider = pgTable("git_provider", {
|
||||
@@ -27,6 +30,9 @@ export const gitProvider = pgTable("git_provider", {
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -42,10 +48,18 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [bitbucket.gitProviderId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [gitea.gitProviderId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitProvider);
|
||||
|
||||
86
packages/server/src/db/schema/gitea.ts
Normal file
86
packages/server/src/db/schema/gitea.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
|
||||
export const gitea = pgTable("gitea", {
|
||||
giteaId: text("giteaId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
|
||||
redirectUri: text("redirect_uri"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at"),
|
||||
scopes: text("scopes").default("repo,repo:status,read:user,read:org"),
|
||||
lastAuthenticatedAt: integer("last_authenticated_at"),
|
||||
});
|
||||
|
||||
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [gitea.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitea);
|
||||
|
||||
export const apiCreateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneGitea = createSchema
|
||||
.extend({
|
||||
giteaId: z.string().min(1),
|
||||
})
|
||||
.pick({ giteaId: true });
|
||||
|
||||
export const apiGiteaTestConnection = createSchema
|
||||
.extend({
|
||||
organizationName: z.string().optional(),
|
||||
})
|
||||
.pick({ giteaId: true, organizationName: true });
|
||||
|
||||
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
|
||||
|
||||
export const apiFindGiteaBranches = z.object({
|
||||
owner: z.string().min(1),
|
||||
repositoryName: z.string().min(1),
|
||||
giteaId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaId: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
@@ -25,8 +25,12 @@ export * from "./git-provider";
|
||||
export * from "./bitbucket";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./gitea";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
export * from "./ai";
|
||||
export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
export * from "./volume-backups";
|
||||
|
||||
@@ -146,3 +146,9 @@ export const apiUpdateMariaDB = createSchema
|
||||
mariadbId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildMariadb = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -141,3 +141,9 @@ export const apiResetMongo = createSchema
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRebuildMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -144,3 +144,9 @@ export const apiUpdateMySql = createSchema
|
||||
mysqlId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildMysql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
|
||||
export const protocolType = pgEnum("protocolType", ["tcp", "udp"]);
|
||||
export const publishModeType = pgEnum("publishModeType", ["ingress", "host"]);
|
||||
|
||||
export const ports = pgTable("port", {
|
||||
portId: text("portId")
|
||||
@@ -13,6 +14,7 @@ export const ports = pgTable("port", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
publishedPort: integer("publishedPort").notNull(),
|
||||
publishMode: publishModeType("publishMode").notNull().default("host"),
|
||||
targetPort: integer("targetPort").notNull(),
|
||||
protocol: protocolType("protocol").notNull(),
|
||||
|
||||
@@ -32,6 +34,7 @@ const createSchema = createInsertSchema(ports, {
|
||||
portId: z.string().min(1),
|
||||
applicationId: z.string().min(1),
|
||||
publishedPort: z.number(),
|
||||
publishMode: z.enum(["ingress", "host"]).default("ingress"),
|
||||
targetPort: z.number(),
|
||||
protocol: z.enum(["tcp", "udp"]).default("tcp"),
|
||||
});
|
||||
@@ -39,6 +42,7 @@ const createSchema = createInsertSchema(ports, {
|
||||
export const apiCreatePort = createSchema
|
||||
.pick({
|
||||
publishedPort: true,
|
||||
publishMode: true,
|
||||
targetPort: true,
|
||||
protocol: true,
|
||||
applicationId: true,
|
||||
@@ -55,6 +59,7 @@ export const apiUpdatePort = createSchema
|
||||
.pick({
|
||||
portId: true,
|
||||
publishedPort: true,
|
||||
publishMode: true,
|
||||
targetPort: true,
|
||||
protocol: true,
|
||||
})
|
||||
|
||||
@@ -140,3 +140,9 @@ export const apiUpdatePostgres = createSchema
|
||||
postgresId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildPostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -52,6 +52,7 @@ const createSchema = createInsertSchema(projects, {
|
||||
export const apiCreateProject = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
env: true,
|
||||
});
|
||||
|
||||
export const apiFindOneProject = createSchema
|
||||
|
||||
@@ -133,3 +133,9 @@ export const apiUpdateRedis = createSchema
|
||||
redisId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
57
packages/server/src/db/schema/rollbacks.ts
Normal file
57
packages/server/src/db/schema/rollbacks.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { deployments } from "./deployment";
|
||||
import type { Application } from "@dokploy/server/services/application";
|
||||
import type { Project } from "@dokploy/server/services/project";
|
||||
import type { Mount } from "@dokploy/server/services/mount";
|
||||
import type { Port } from "@dokploy/server/services/port";
|
||||
import type { Registry } from "@dokploy/server/services/registry";
|
||||
|
||||
export const rollbacks = pgTable("rollback", {
|
||||
rollbackId: text("rollbackId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
deploymentId: text("deploymentId")
|
||||
.notNull()
|
||||
.references(() => deployments.deploymentId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
version: serial(),
|
||||
image: text("image"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
fullContext: jsonb("fullContext").$type<
|
||||
Application & {
|
||||
project: Project;
|
||||
mounts: Mount[];
|
||||
ports: Port[];
|
||||
registry?: Registry | null;
|
||||
}
|
||||
>(),
|
||||
});
|
||||
|
||||
export type Rollback = typeof rollbacks.$inferSelect;
|
||||
|
||||
export const rollbacksRelations = relations(rollbacks, ({ one }) => ({
|
||||
deployment: one(deployments, {
|
||||
fields: [rollbacks.deploymentId],
|
||||
references: [deployments.deploymentId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const createRollbackSchema = createInsertSchema(rollbacks).extend({
|
||||
appName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const updateRollbackSchema = createRollbackSchema.extend({
|
||||
rollbackId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneRollback = z.object({
|
||||
rollbackId: z.string().min(1),
|
||||
});
|
||||
83
packages/server/src/db/schema/schedule.ts
Normal file
83
packages/server/src/db/schema/schedule.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { 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 { deployments } from "./deployment";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { generateAppName } from "./utils";
|
||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||
|
||||
export const scheduleType = pgEnum("scheduleType", [
|
||||
"application",
|
||||
"compose",
|
||||
"server",
|
||||
"dokploy-server",
|
||||
]);
|
||||
|
||||
export const schedules = pgTable("schedule", {
|
||||
scheduleId: text("scheduleId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
cronExpression: text("cronExpression").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("schedule")),
|
||||
serviceName: text("serviceName"),
|
||||
shellType: shellTypes("shellType").notNull().default("bash"),
|
||||
scheduleType: scheduleType("scheduleType").notNull().default("application"),
|
||||
command: text("command").notNull(),
|
||||
script: text("script"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
export type Schedule = typeof schedules.$inferSelect;
|
||||
|
||||
export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
||||
application: one(applications, {
|
||||
fields: [schedules.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [schedules.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [schedules.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [schedules.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
export const createScheduleSchema = createInsertSchema(schedules);
|
||||
|
||||
export const updateScheduleSchema = createScheduleSchema.extend({
|
||||
scheduleId: z.string().min(1),
|
||||
});
|
||||
@@ -46,6 +46,7 @@ enum gitProviderType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
}
|
||||
|
||||
enum mountType {
|
||||
@@ -98,6 +99,7 @@ enum sourceType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
drop
|
||||
}
|
||||
|
||||
@@ -106,6 +108,7 @@ enum sourceTypeCompose {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
raw
|
||||
}
|
||||
|
||||
@@ -159,6 +162,7 @@ table application {
|
||||
command text
|
||||
refreshToken text
|
||||
sourceType sourceType [not null, default: 'github']
|
||||
cleanCache boolean [default: false]
|
||||
repository text
|
||||
owner text
|
||||
branch text
|
||||
@@ -205,6 +209,7 @@ table application {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -279,6 +284,9 @@ table compose {
|
||||
bitbucketRepository text
|
||||
bitbucketOwner text
|
||||
bitbucketBranch text
|
||||
giteaRepository text
|
||||
giteaOwner text
|
||||
giteaBranch text
|
||||
customGitUrl text
|
||||
customGitBranch text
|
||||
customGitSSHKeyId text
|
||||
@@ -293,6 +301,7 @@ table compose {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -388,6 +397,20 @@ table gitlab {
|
||||
gitProviderId text [not null]
|
||||
}
|
||||
|
||||
table gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
redirect_uri text
|
||||
client_id text [not null]
|
||||
client_secret text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
gitProviderId text [not null]
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
table gotify {
|
||||
gotifyId text [pk, not null]
|
||||
serverUrl text [not null]
|
||||
@@ -819,6 +842,8 @@ ref: github.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: git_provider.userId - user.id
|
||||
|
||||
ref: mariadb.projectId > project.projectId
|
||||
|
||||
@@ -20,9 +20,9 @@ import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
import { schedules } from "./schedule";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||
|
||||
export const server = pgTable("server", {
|
||||
@@ -114,6 +114,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
|
||||
fields: [server.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(server, {
|
||||
|
||||
@@ -10,4 +10,7 @@ export const applicationStatus = pgEnum("applicationStatus", [
|
||||
export const certificateType = pgEnum("certificateType", [
|
||||
"letsencrypt",
|
||||
"none",
|
||||
"custom",
|
||||
]);
|
||||
|
||||
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
|
||||
|
||||
@@ -10,9 +10,12 @@ import {
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { account, organization, apikey } from "./account";
|
||||
import { account, apikey, organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
import { certificateType } from "./shared";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
@@ -49,13 +52,16 @@ export const users_temp = pgTable("user_temp", {
|
||||
// Admin
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron"),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
@@ -124,11 +130,15 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
||||
organizations: many(organization),
|
||||
projects: many(projects),
|
||||
apiKeys: many(apikey),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(users_temp, {
|
||||
id: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
}).omit({
|
||||
role: true,
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
@@ -200,10 +210,12 @@ export const apiAssignDomain = createSchema
|
||||
host: true,
|
||||
certificateType: true,
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
});
|
||||
|
||||
export const apiUpdateDockerCleanup = createSchema
|
||||
@@ -225,7 +237,31 @@ export const apiModifyTraefikConfig = z.object({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
export const apiReadTraefikConfig = z.object({
|
||||
path: z.string().min(1),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
(path) => {
|
||||
// Prevent directory traversal attacks
|
||||
if (path.includes("../") || path.includes("..\\")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { MAIN_TRAEFIK_PATH } = paths();
|
||||
if (path.startsWith("/") && !path.startsWith(MAIN_TRAEFIK_PATH)) {
|
||||
return false;
|
||||
}
|
||||
// Prevent null bytes and other dangerous characters
|
||||
if (path.includes("\0") || path.includes("\x00")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid path: path traversal or unauthorized directory access detected",
|
||||
},
|
||||
),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
|
||||
118
packages/server/src/db/schema/volume-backups.ts
Normal file
118
packages/server/src/db/schema/volume-backups.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { serviceType } from "./mount";
|
||||
import { applications } from "./application";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { redis } from "./redis";
|
||||
import { compose } from "./compose";
|
||||
import { postgres } from "./postgres";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { destinations } from "./destination";
|
||||
import { deployments } from "./deployment";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const volumeBackups = pgTable("volume_backup", {
|
||||
volumeBackupId: text("volumeBackupId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
volumeName: text("volumeName").notNull(),
|
||||
prefix: text("prefix").notNull(),
|
||||
serviceType: serviceType("serviceType").notNull().default("application"),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("volumeBackup")),
|
||||
serviceName: text("serviceName"),
|
||||
turnOff: boolean("turnOff").notNull().default(false),
|
||||
cronExpression: text("cronExpression").notNull(),
|
||||
keepLatestCount: integer("keepLatestCount"),
|
||||
enabled: boolean("enabled"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mongoId: text("mongoId").references(() => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
destinationId: text("destinationId")
|
||||
.notNull()
|
||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export type VolumeBackup = typeof volumeBackups.$inferSelect;
|
||||
|
||||
export const volumeBackupsRelations = relations(
|
||||
volumeBackups,
|
||||
({ one, many }) => ({
|
||||
application: one(applications, {
|
||||
fields: [volumeBackups.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [volumeBackups.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [volumeBackups.mariadbId],
|
||||
references: [mariadb.mariadbId],
|
||||
}),
|
||||
mongo: one(mongo, {
|
||||
fields: [volumeBackups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
mysql: one(mysql, {
|
||||
fields: [volumeBackups.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
redis: one(redis, {
|
||||
fields: [volumeBackups.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [volumeBackups.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
destination: one(destinations, {
|
||||
fields: [volumeBackups.destinationId],
|
||||
references: [destinations.destinationId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}),
|
||||
);
|
||||
|
||||
export const createVolumeBackupSchema = createInsertSchema(volumeBackups).omit({
|
||||
volumeBackupId: true,
|
||||
});
|
||||
|
||||
export const updateVolumeBackupSchema = createVolumeBackupSchema.extend({
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneVolumeBackup = z.object({
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
@@ -4,13 +4,16 @@ export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
customCertResolver: z.string(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (input.https && !input.certificateType) {
|
||||
@@ -20,19 +23,53 @@ export const domain = z
|
||||
message: "Required",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.certificateType === "custom" && !input.customCertResolver) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["customCertResolver"],
|
||||
message: "Required when certificate type is custom",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate stripPath requires a valid path
|
||||
if (input.stripPath && (!input.path || input.path === "/")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["stripPath"],
|
||||
message:
|
||||
"Strip path can only be enabled when a path other than '/' is specified",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate internalPath starts with /
|
||||
if (
|
||||
input.internalPath &&
|
||||
input.internalPath !== "/" &&
|
||||
!input.internalPath.startsWith("/")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["internalPath"],
|
||||
message: "Internal path must start with '/'",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const domainCompose = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Host is required" }),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
customCertResolver: z.string(),
|
||||
serviceName: z.string().min(1, { message: "Service name is required" }),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
@@ -43,4 +80,35 @@ export const domainCompose = z
|
||||
message: "Required",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.certificateType === "custom" && !input.customCertResolver) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["customCertResolver"],
|
||||
message: "Required when certificate type is custom",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate stripPath requires a valid path
|
||||
if (input.stripPath && (!input.path || input.path === "/")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["stripPath"],
|
||||
message:
|
||||
"Strip path can only be enabled when a path other than '/' is specified",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate internalPath starts with /
|
||||
if (
|
||||
input.internalPath &&
|
||||
input.internalPath !== "/" &&
|
||||
!input.internalPath.startsWith("/")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["internalPath"],
|
||||
message: "Internal path must start with '/'",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./services/mysql";
|
||||
export * from "./services/backup";
|
||||
export * from "./services/cluster";
|
||||
export * from "./services/settings";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/docker";
|
||||
export * from "./services/destination";
|
||||
export * from "./services/deployment";
|
||||
@@ -28,9 +29,12 @@ export * from "./services/git-provider";
|
||||
export * from "./services/bitbucket";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/server";
|
||||
export * from "./services/schedule";
|
||||
export * from "./services/application";
|
||||
|
||||
export * from "./services/rollbacks";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./setup/config-paths";
|
||||
export * from "./setup/postgres-setup";
|
||||
export * from "./setup/redis-setup";
|
||||
@@ -40,13 +44,16 @@ export * from "./setup/setup";
|
||||
export * from "./setup/traefik-setup";
|
||||
export * from "./setup/server-validate";
|
||||
export * from "./setup/server-audit";
|
||||
|
||||
export * from "./utils/watch-paths/should-deploy";
|
||||
export * from "./utils/backups/index";
|
||||
export * from "./utils/backups/mariadb";
|
||||
export * from "./utils/backups/mongo";
|
||||
export * from "./utils/backups/mysql";
|
||||
export * from "./utils/backups/postgres";
|
||||
export * from "./utils/backups/utils";
|
||||
export * from "./utils/backups/web-server";
|
||||
export * from "./utils/backups/compose";
|
||||
export * from "./templates/processors";
|
||||
|
||||
export * from "./utils/notifications/build-error";
|
||||
export * from "./utils/notifications/build-success";
|
||||
@@ -89,6 +96,7 @@ export * from "./utils/providers/docker";
|
||||
export * from "./utils/providers/git";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/gitlab";
|
||||
export * from "./utils/providers/gitea";
|
||||
export * from "./utils/providers/raw";
|
||||
|
||||
export * from "./utils/servers/remote-docker";
|
||||
@@ -122,3 +130,9 @@ export {
|
||||
stopLogCleanup,
|
||||
getLogCleanupStatus,
|
||||
} from "./utils/access-log/handler";
|
||||
|
||||
export * from "./utils/schedules/utils";
|
||||
export * from "./utils/schedules/index";
|
||||
export * from "./utils/volume-backups/index";
|
||||
|
||||
export * from "./lib/logger";
|
||||
|
||||
@@ -2,21 +2,22 @@ import type { IncomingMessage } from "node:http";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { organization, twoFactor, apiKey } from "better-auth/plugins";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import { getUserByToken } from "../services/admin";
|
||||
import { updateUser } from "../services/user";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
|
||||
const { handler, api } = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
logger: {
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
appName: "Dokploy",
|
||||
socialProviders: {
|
||||
github: {
|
||||
@@ -28,6 +29,26 @@ const { handler, api } = betterAuth({
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
},
|
||||
},
|
||||
...(!IS_CLOUD && {
|
||||
async trustedOrigins() {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (admin) {
|
||||
return [
|
||||
...(admin.user.serverIp
|
||||
? [`http://${admin.user.serverIp}:3000`]
|
||||
: []),
|
||||
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
@@ -68,11 +89,40 @@ const { handler, api } = betterAuth({
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (_user, context) => {
|
||||
if (!IS_CLOUD) {
|
||||
const xDokployToken =
|
||||
context?.request?.headers?.get("x-dokploy-token");
|
||||
if (xDokployToken) {
|
||||
const user = await getUserByToken(xDokployToken);
|
||||
if (!user) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
if (isAdminPresent) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Admin is already created",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
after: async (user) => {
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
await updateUser(user.id, {
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD || !isAdminPresent) {
|
||||
await db.transaction(async (tx) => {
|
||||
const organization = await tx
|
||||
@@ -117,6 +167,10 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 3,
|
||||
updateAge: 60 * 60 * 24,
|
||||
},
|
||||
user: {
|
||||
modelName: "users_temp",
|
||||
additionalFields: {
|
||||
@@ -130,9 +184,13 @@ const { handler, api } = betterAuth({
|
||||
// required: true,
|
||||
input: false,
|
||||
},
|
||||
allowImpersonation: {
|
||||
fieldName: "allowImpersonation",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableMetadata: true,
|
||||
@@ -144,7 +202,7 @@ const { handler, api } = betterAuth({
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: "https://dokploy.com";
|
||||
: "https://app.dokploy.com";
|
||||
const inviteLink = `${host}/invitation?token=${data.id}`;
|
||||
|
||||
await sendEmail({
|
||||
@@ -157,6 +215,13 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
},
|
||||
}),
|
||||
...(IS_CLOUD
|
||||
? [
|
||||
admin({
|
||||
adminUserIds: [process.env.USER_ADMIN_ID as string],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
11
packages/server/src/lib/logger.ts
Normal file
11
packages/server/src/lib/logger.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -73,7 +73,7 @@ export const readStatsFile = async (
|
||||
const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
|
||||
const data = await promises.readFile(filePath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -108,7 +108,7 @@ export const readLastValueStatsFile = async (
|
||||
const data = await promises.readFile(filePath, "utf-8");
|
||||
const stats = JSON.parse(data);
|
||||
return stats[stats.length - 1] || null;
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { generateObject } from "ai";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findServerById } from "./server";
|
||||
import { findOrganizationById } from "./admin";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
const aiSettings = await db.query.ai.findMany({
|
||||
|
||||
@@ -26,6 +26,10 @@ import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
@@ -56,6 +60,7 @@ import {
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { createRollback } from "./rollbacks";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -111,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
},
|
||||
@@ -182,12 +188,6 @@ export const deployApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
// const admin = await findUserById(application.project.userId);
|
||||
|
||||
// if (admin.cleanupCacheApplications) {
|
||||
// await cleanupFullDocker(application?.serverId);
|
||||
// }
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
@@ -197,6 +197,9 @@ export const deployApplication = async ({
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
@@ -212,6 +215,17 @@ export const deployApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -257,11 +271,6 @@ export const rebuildApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
// const admin = await findUserById(application.project.userId);
|
||||
|
||||
// if (admin.cleanupCacheApplications) {
|
||||
// await cleanupFullDocker(application?.serverId);
|
||||
// }
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
@@ -306,11 +315,6 @@ export const deployRemoteApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
// const admin = await findUserById(application.project.userId);
|
||||
|
||||
// if (admin.cleanupCacheApplications) {
|
||||
// await cleanupFullDocker(application?.serverId);
|
||||
// }
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
@@ -325,6 +329,8 @@ export const deployRemoteApplication = async ({
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
application,
|
||||
@@ -344,6 +350,17 @@ export const deployRemoteApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -453,7 +470,7 @@ export const deployPreviewApplication = async ({
|
||||
}
|
||||
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
@@ -571,15 +588,10 @@ export const deployRemotePreviewApplication = async ({
|
||||
}
|
||||
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.serverId) {
|
||||
// const admin = await findUserById(application.project.userId);
|
||||
|
||||
// if (admin.cleanupCacheOnPreviews) {
|
||||
// await cleanupFullDocker(application?.serverId);
|
||||
// }
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
@@ -648,11 +660,6 @@ export const rebuildRemoteApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
// const admin = await findUserById(application.project.userId);
|
||||
|
||||
// if (admin.cleanupCacheApplications) {
|
||||
// await cleanupFullDocker(application?.serverId);
|
||||
// }
|
||||
if (application.sourceType !== "docker") {
|
||||
let command = "set -e;";
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
|
||||
@@ -35,6 +35,7 @@ export const findBackupById = async (backupId: string) => {
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
destination: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
if (!backup) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Bitbucket = typeof bitbucket.$inferSelect;
|
||||
export const createBitbucket = async (
|
||||
input: typeof apiCreateBitbucket._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -21,6 +22,7 @@ export const createBitbucket = async (
|
||||
providerType: "bitbucket",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
634
packages/server/src/services/cdn.ts
Normal file
634
packages/server/src/services/cdn.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
// CDN Provider Interface
|
||||
export interface CDNProvider {
|
||||
name: string;
|
||||
displayName: string;
|
||||
checkIp: (ip: string) => boolean;
|
||||
warningMessage: string;
|
||||
}
|
||||
|
||||
const isIPInCIDR = (ip: string, cidr: string): boolean => {
|
||||
const [network, prefixLength] = cidr.split("/");
|
||||
if (!network || !prefixLength) return false;
|
||||
const prefix = Number.parseInt(prefixLength, 10);
|
||||
|
||||
// Convert IP addresses to 32-bit integers
|
||||
const ipToInt = (ipStr: string): number => {
|
||||
return (
|
||||
ipStr
|
||||
.split(".")
|
||||
.reduce((acc, octet) => (acc << 8) + Number.parseInt(octet, 10), 0) >>>
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const ipInt = ipToInt(ip);
|
||||
const networkInt = ipToInt(network);
|
||||
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (networkInt & mask);
|
||||
};
|
||||
|
||||
// Cloudflare IP ranges
|
||||
// https://www.cloudflare.com/ips-v4
|
||||
const CLOUDFLARE_IP_RANGES = [
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.158.0.0/15",
|
||||
"104.16.0.0/13",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"131.0.72.0/22",
|
||||
];
|
||||
|
||||
// Fastly IP ranges
|
||||
// https://api.fastly.com/public-ip-list
|
||||
const FASTLY_IP_RANGES = [
|
||||
"23.235.32.0/20",
|
||||
"43.249.72.0/22",
|
||||
"103.244.50.0/24",
|
||||
"103.245.222.0/23",
|
||||
"103.245.224.0/24",
|
||||
"104.156.80.0/20",
|
||||
"140.248.64.0/18",
|
||||
"140.248.128.0/17",
|
||||
"146.75.0.0/17",
|
||||
"151.101.0.0/16",
|
||||
"157.52.64.0/18",
|
||||
"167.82.0.0/17",
|
||||
"167.82.128.0/20",
|
||||
"167.82.160.0/20",
|
||||
"167.82.224.0/20",
|
||||
"172.111.64.0/18",
|
||||
"185.31.16.0/22",
|
||||
"199.27.72.0/21",
|
||||
"199.232.0.0/16",
|
||||
];
|
||||
|
||||
// Bunny CDN IP addresses
|
||||
// https://bunnycdn.com/api/system/edgeserverlist
|
||||
const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.188.227",
|
||||
"89.187.188.228",
|
||||
"139.180.134.196",
|
||||
"89.38.96.158",
|
||||
"89.187.162.249",
|
||||
"89.187.162.242",
|
||||
"185.102.217.65",
|
||||
"185.93.1.243",
|
||||
"156.146.40.49",
|
||||
"185.59.220.199",
|
||||
"185.59.220.198",
|
||||
"195.181.166.158",
|
||||
"185.180.12.68",
|
||||
"138.199.24.209",
|
||||
"138.199.24.211",
|
||||
"79.127.216.111",
|
||||
"79.127.216.112",
|
||||
"89.187.169.47",
|
||||
"138.199.24.218",
|
||||
"185.40.106.117",
|
||||
"200.25.45.4",
|
||||
"200.25.57.5",
|
||||
"193.162.131.1",
|
||||
"200.25.11.8",
|
||||
"200.25.53.5",
|
||||
"200.25.13.98",
|
||||
"41.242.2.18",
|
||||
"200.25.62.5",
|
||||
"200.25.38.69",
|
||||
"200.25.42.70",
|
||||
"200.25.36.166",
|
||||
"195.206.229.106",
|
||||
"194.242.11.186",
|
||||
"185.164.35.8",
|
||||
"94.20.154.22",
|
||||
"185.93.1.244",
|
||||
"156.59.145.154",
|
||||
"143.244.49.177",
|
||||
"138.199.46.66",
|
||||
"138.199.37.227",
|
||||
"138.199.37.231",
|
||||
"138.199.37.230",
|
||||
"138.199.37.229",
|
||||
"138.199.46.69",
|
||||
"138.199.46.68",
|
||||
"138.199.46.67",
|
||||
"185.93.1.246",
|
||||
"138.199.37.232",
|
||||
"195.181.163.196",
|
||||
"107.182.163.162",
|
||||
"195.181.163.195",
|
||||
"84.17.46.53",
|
||||
"212.102.40.114",
|
||||
"84.17.46.54",
|
||||
"138.199.40.58",
|
||||
"143.244.38.134",
|
||||
"143.244.38.136",
|
||||
"185.152.64.17",
|
||||
"84.17.59.115",
|
||||
"89.187.165.194",
|
||||
"138.199.15.193",
|
||||
"89.35.237.170",
|
||||
"37.19.216.130",
|
||||
"185.93.1.247",
|
||||
"185.93.3.244",
|
||||
"143.244.49.179",
|
||||
"143.244.49.180",
|
||||
"138.199.9.104",
|
||||
"185.152.66.243",
|
||||
"143.244.49.178",
|
||||
"169.150.221.147",
|
||||
"200.25.18.73",
|
||||
"84.17.63.178",
|
||||
"200.25.32.131",
|
||||
"37.19.207.34",
|
||||
"192.189.65.146",
|
||||
"143.244.45.177",
|
||||
"185.93.1.249",
|
||||
"185.93.1.250",
|
||||
"169.150.215.115",
|
||||
"209.177.87.197",
|
||||
"156.146.56.162",
|
||||
"156.146.56.161",
|
||||
"185.93.2.246",
|
||||
"185.93.2.245",
|
||||
"212.102.40.113",
|
||||
"185.93.2.244",
|
||||
"143.244.50.82",
|
||||
"143.244.50.83",
|
||||
"156.146.56.163",
|
||||
"129.227.217.178",
|
||||
"129.227.217.179",
|
||||
"200.25.69.94",
|
||||
"128.1.52.179",
|
||||
"200.25.16.103",
|
||||
"15.235.54.226",
|
||||
"102.67.138.155",
|
||||
"156.146.43.65",
|
||||
"195.181.163.203",
|
||||
"195.181.163.202",
|
||||
"156.146.56.169",
|
||||
"156.146.56.170",
|
||||
"156.146.56.166",
|
||||
"156.146.56.171",
|
||||
"169.150.207.210",
|
||||
"156.146.56.167",
|
||||
"143.244.50.84",
|
||||
"143.244.50.85",
|
||||
"143.244.50.86",
|
||||
"143.244.50.87",
|
||||
"156.146.56.168",
|
||||
"169.150.207.211",
|
||||
"212.102.50.59",
|
||||
"146.185.248.15",
|
||||
"143.244.50.90",
|
||||
"143.244.50.91",
|
||||
"143.244.50.88",
|
||||
"143.244.50.209",
|
||||
"143.244.50.213",
|
||||
"143.244.50.214",
|
||||
"143.244.49.183",
|
||||
"143.244.50.89",
|
||||
"143.244.50.210",
|
||||
"143.244.50.211",
|
||||
"143.244.50.212",
|
||||
"5.42.206.66",
|
||||
"94.46.27.186",
|
||||
"169.150.207.213",
|
||||
"169.150.207.214",
|
||||
"169.150.207.215",
|
||||
"169.150.207.212",
|
||||
"169.150.219.114",
|
||||
"169.150.202.210",
|
||||
"169.150.242.193",
|
||||
"185.93.1.251",
|
||||
"169.150.207.216",
|
||||
"169.150.207.217",
|
||||
"169.150.238.19",
|
||||
"102.219.126.20",
|
||||
"156.59.66.182",
|
||||
"122.10.251.130",
|
||||
"185.24.11.18",
|
||||
"138.199.36.7",
|
||||
"138.199.36.8",
|
||||
"138.199.36.9",
|
||||
"138.199.36.10",
|
||||
"138.199.36.11",
|
||||
"138.199.37.225",
|
||||
"84.17.46.49",
|
||||
"84.17.37.217",
|
||||
"169.150.225.35",
|
||||
"169.150.225.36",
|
||||
"169.150.225.37",
|
||||
"169.150.225.38",
|
||||
"169.150.225.39",
|
||||
"169.150.225.34",
|
||||
"169.150.236.97",
|
||||
"169.150.236.98",
|
||||
"169.150.236.99",
|
||||
"169.150.236.100",
|
||||
"93.189.63.149",
|
||||
"143.244.56.49",
|
||||
"143.244.56.50",
|
||||
"143.244.56.51",
|
||||
"169.150.247.40",
|
||||
"169.150.247.33",
|
||||
"169.150.247.34",
|
||||
"169.150.247.35",
|
||||
"169.150.247.36",
|
||||
"169.150.247.37",
|
||||
"169.150.247.38",
|
||||
"169.150.247.39",
|
||||
"38.142.94.218",
|
||||
"87.249.137.52",
|
||||
"38.104.169.186",
|
||||
"66.181.163.74",
|
||||
"84.17.38.227",
|
||||
"84.17.38.228",
|
||||
"84.17.38.229",
|
||||
"84.17.38.230",
|
||||
"84.17.38.231",
|
||||
"84.17.38.232",
|
||||
"169.150.225.41",
|
||||
"169.150.225.42",
|
||||
"169.150.249.162",
|
||||
"169.150.249.163",
|
||||
"169.150.249.164",
|
||||
"169.150.249.165",
|
||||
"169.150.249.166",
|
||||
"169.150.249.167",
|
||||
"169.150.249.168",
|
||||
"169.150.249.169",
|
||||
"185.131.64.124",
|
||||
"103.112.0.22",
|
||||
"37.236.234.2",
|
||||
"169.150.252.209",
|
||||
"212.102.46.118",
|
||||
"192.169.120.162",
|
||||
"93.180.217.214",
|
||||
"37.19.203.178",
|
||||
"107.155.47.146",
|
||||
"193.201.190.174",
|
||||
"156.59.95.218",
|
||||
"213.170.143.139",
|
||||
"129.227.186.154",
|
||||
"195.238.127.98",
|
||||
"200.25.22.6",
|
||||
"204.16.244.92",
|
||||
"200.25.70.101",
|
||||
"200.25.66.100",
|
||||
"139.180.209.182",
|
||||
"103.108.231.41",
|
||||
"103.108.229.5",
|
||||
"103.216.220.9",
|
||||
"169.150.225.40",
|
||||
"212.102.50.49",
|
||||
"212.102.50.52",
|
||||
"109.61.83.242",
|
||||
"109.61.83.243",
|
||||
"212.102.50.50",
|
||||
"169.150.225.43",
|
||||
"45.125.247.57",
|
||||
"103.235.199.170",
|
||||
"128.1.35.170",
|
||||
"38.32.110.58",
|
||||
"169.150.220.228",
|
||||
"169.150.220.229",
|
||||
"169.150.220.230",
|
||||
"169.150.220.231",
|
||||
"138.199.4.179",
|
||||
"207.211.214.145",
|
||||
"109.61.86.193",
|
||||
"103.214.20.95",
|
||||
"178.175.134.51",
|
||||
"138.199.4.178",
|
||||
"172.255.253.140",
|
||||
"185.24.11.19",
|
||||
"109.61.83.244",
|
||||
"109.61.83.245",
|
||||
"84.17.38.250",
|
||||
"84.17.38.251",
|
||||
"146.59.69.202",
|
||||
"146.70.80.218",
|
||||
"200.25.80.74",
|
||||
"79.127.213.214",
|
||||
"79.127.213.215",
|
||||
"79.127.213.216",
|
||||
"79.127.213.217",
|
||||
"195.69.140.112",
|
||||
"109.61.83.247",
|
||||
"109.61.83.246",
|
||||
"185.93.2.248",
|
||||
"109.61.83.249",
|
||||
"109.61.83.250",
|
||||
"109.61.83.251",
|
||||
"46.199.75.115",
|
||||
"141.164.35.160",
|
||||
"109.61.83.97",
|
||||
"109.61.83.98",
|
||||
"109.61.83.99",
|
||||
"129.227.179.18",
|
||||
"185.180.14.250",
|
||||
"152.89.160.26",
|
||||
"5.189.202.62",
|
||||
"98.98.242.142",
|
||||
"156.59.92.126",
|
||||
"84.17.59.117",
|
||||
"79.127.216.66",
|
||||
"79.127.204.113",
|
||||
"79.127.237.132",
|
||||
"169.150.236.104",
|
||||
"169.150.236.105",
|
||||
"37.27.135.61",
|
||||
"158.51.123.205",
|
||||
"156.146.43.70",
|
||||
"156.146.43.71",
|
||||
"156.146.43.72",
|
||||
"180.149.231.175",
|
||||
"185.93.2.243",
|
||||
"143.244.56.52",
|
||||
"143.244.56.53",
|
||||
"143.244.56.54",
|
||||
"143.244.56.55",
|
||||
"143.244.56.56",
|
||||
"143.244.56.57",
|
||||
"143.244.56.58",
|
||||
"144.76.236.44",
|
||||
"88.198.57.50",
|
||||
"78.46.69.199",
|
||||
"136.243.16.49",
|
||||
"138.201.86.122",
|
||||
"136.243.42.90",
|
||||
"88.99.95.221",
|
||||
"178.63.2.112",
|
||||
"5.9.98.45",
|
||||
"136.243.42.10",
|
||||
"169.150.236.106",
|
||||
"169.150.236.107",
|
||||
"185.93.1.242",
|
||||
"185.93.1.245",
|
||||
"143.244.60.193",
|
||||
"195.181.163.194",
|
||||
"79.127.188.193",
|
||||
"79.127.188.196",
|
||||
"79.127.188.194",
|
||||
"79.127.188.195",
|
||||
"104.166.144.106",
|
||||
"156.59.126.78",
|
||||
"185.135.85.154",
|
||||
"38.54.5.37",
|
||||
"38.54.3.92",
|
||||
"185.165.170.74",
|
||||
"207.121.80.118",
|
||||
"207.121.46.228",
|
||||
"207.121.46.236",
|
||||
"207.121.46.244",
|
||||
"207.121.46.252",
|
||||
"216.202.235.164",
|
||||
"207.121.46.220",
|
||||
"207.121.75.132",
|
||||
"207.121.80.12",
|
||||
"207.121.80.172",
|
||||
"207.121.90.60",
|
||||
"207.121.90.68",
|
||||
"207.121.97.204",
|
||||
"207.121.90.252",
|
||||
"207.121.97.236",
|
||||
"207.121.99.12",
|
||||
"138.199.24.219",
|
||||
"185.93.2.251",
|
||||
"138.199.46.65",
|
||||
"207.121.41.196",
|
||||
"207.121.99.20",
|
||||
"207.121.99.36",
|
||||
"207.121.99.44",
|
||||
"207.121.99.52",
|
||||
"207.121.99.60",
|
||||
"207.121.23.68",
|
||||
"207.121.23.124",
|
||||
"207.121.23.244",
|
||||
"207.121.23.180",
|
||||
"207.121.23.188",
|
||||
"207.121.23.196",
|
||||
"207.121.23.204",
|
||||
"207.121.24.52",
|
||||
"207.121.24.60",
|
||||
"207.121.24.68",
|
||||
"207.121.24.76",
|
||||
"207.121.24.92",
|
||||
"207.121.24.100",
|
||||
"207.121.24.108",
|
||||
"207.121.24.116",
|
||||
"154.95.86.76",
|
||||
"5.9.99.73",
|
||||
"78.46.92.118",
|
||||
"144.76.65.213",
|
||||
"78.46.156.89",
|
||||
"88.198.9.155",
|
||||
"144.76.79.22",
|
||||
"103.1.215.93",
|
||||
"103.137.12.33",
|
||||
"103.107.196.31",
|
||||
"116.90.72.155",
|
||||
"103.137.14.5",
|
||||
"116.90.75.65",
|
||||
"37.19.207.37",
|
||||
"208.83.234.224",
|
||||
"116.202.155.146",
|
||||
"116.202.193.178",
|
||||
"116.202.224.168",
|
||||
"188.40.126.227",
|
||||
"88.99.26.189",
|
||||
"168.119.39.238",
|
||||
"88.99.26.97",
|
||||
"168.119.12.188",
|
||||
"176.9.139.55",
|
||||
"142.132.223.79",
|
||||
"142.132.223.80",
|
||||
"142.132.223.81",
|
||||
"46.4.116.17",
|
||||
"46.4.119.81",
|
||||
"167.235.114.167",
|
||||
"159.69.68.171",
|
||||
"178.63.21.52",
|
||||
"46.4.120.152",
|
||||
"116.202.80.247",
|
||||
"5.9.71.119",
|
||||
"195.201.11.156",
|
||||
"78.46.123.17",
|
||||
"46.4.113.143",
|
||||
"136.243.2.236",
|
||||
"195.201.81.217",
|
||||
"148.251.42.123",
|
||||
"94.130.68.122",
|
||||
"88.198.22.103",
|
||||
"46.4.102.90",
|
||||
"157.90.180.205",
|
||||
"162.55.135.11",
|
||||
"195.201.109.59",
|
||||
"148.251.41.244",
|
||||
"116.202.235.16",
|
||||
"128.140.70.141",
|
||||
"78.46.74.86",
|
||||
"78.46.74.85",
|
||||
"178.63.41.242",
|
||||
"178.63.41.247",
|
||||
"178.63.41.234",
|
||||
"104.237.53.74",
|
||||
"104.237.54.154",
|
||||
"104.237.51.58",
|
||||
"64.185.235.90",
|
||||
"64.185.234.114",
|
||||
"64.185.232.194",
|
||||
"64.185.232.178",
|
||||
"64.185.232.82",
|
||||
"103.60.15.169",
|
||||
"103.60.15.170",
|
||||
"103.60.15.171",
|
||||
"103.60.15.172",
|
||||
"103.60.15.173",
|
||||
"103.60.15.162",
|
||||
"103.60.15.163",
|
||||
"103.60.15.164",
|
||||
"103.60.15.165",
|
||||
"103.60.15.166",
|
||||
"103.60.15.167",
|
||||
"103.60.15.168",
|
||||
"109.248.43.116",
|
||||
"109.248.43.117",
|
||||
"109.248.43.162",
|
||||
"109.248.43.163",
|
||||
"109.248.43.164",
|
||||
"109.248.43.165",
|
||||
"49.12.71.27",
|
||||
"49.12.0.158",
|
||||
"78.47.94.156",
|
||||
"109.248.43.159",
|
||||
"109.248.43.160",
|
||||
"109.248.43.208",
|
||||
"109.248.43.179",
|
||||
"109.248.43.232",
|
||||
"109.248.43.231",
|
||||
"109.248.43.241",
|
||||
"109.248.43.236",
|
||||
"109.248.43.240",
|
||||
"109.248.43.103",
|
||||
"116.202.118.194",
|
||||
"116.202.80.29",
|
||||
"159.69.57.80",
|
||||
"139.180.129.216",
|
||||
"139.99.174.7",
|
||||
"89.187.169.18",
|
||||
"89.187.179.7",
|
||||
"143.244.62.213",
|
||||
"185.93.3.246",
|
||||
"195.181.163.198",
|
||||
"185.152.64.19",
|
||||
"84.17.37.211",
|
||||
"212.102.50.54",
|
||||
"212.102.46.115",
|
||||
"143.244.38.135",
|
||||
"169.150.238.21",
|
||||
"169.150.207.51",
|
||||
"169.150.207.49",
|
||||
"84.17.38.226",
|
||||
"84.17.38.225",
|
||||
"37.19.222.248",
|
||||
"37.19.222.249",
|
||||
"169.150.247.139",
|
||||
"169.150.247.177",
|
||||
"169.150.247.178",
|
||||
"169.150.213.49",
|
||||
"212.102.46.119",
|
||||
"84.17.38.234",
|
||||
"84.17.38.233",
|
||||
"169.150.247.179",
|
||||
"169.150.247.180",
|
||||
"169.150.247.181",
|
||||
"169.150.247.182",
|
||||
"169.150.247.183",
|
||||
"169.150.247.138",
|
||||
"169.150.247.184",
|
||||
"169.150.247.185",
|
||||
"156.146.58.83",
|
||||
"212.102.43.88",
|
||||
"89.187.169.26",
|
||||
"109.61.89.57",
|
||||
"109.61.89.58",
|
||||
"109.61.83.241",
|
||||
"84.17.38.243",
|
||||
"84.17.38.244",
|
||||
"84.17.38.246",
|
||||
"84.17.38.247",
|
||||
"84.17.38.245",
|
||||
"143.244.38.129",
|
||||
"84.17.38.248",
|
||||
"89.187.176.34",
|
||||
"185.152.64.23",
|
||||
"79.127.213.209",
|
||||
"79.127.213.210",
|
||||
"84.17.37.209",
|
||||
"156.146.43.68",
|
||||
"185.93.3.243",
|
||||
"79.127.219.198",
|
||||
"138.199.33.57",
|
||||
"79.127.242.89",
|
||||
"138.199.4.136",
|
||||
"169.150.220.235",
|
||||
"138.199.4.129",
|
||||
"138.199.4.177",
|
||||
"37.19.222.34",
|
||||
"46.151.193.85",
|
||||
"212.104.158.17",
|
||||
"212.104.158.18",
|
||||
"212.104.158.19",
|
||||
"212.104.158.20",
|
||||
"212.104.158.21",
|
||||
"212.104.158.22",
|
||||
"212.104.158.24",
|
||||
"212.104.158.26",
|
||||
"79.127.237.134",
|
||||
"89.187.184.177",
|
||||
"89.187.184.179",
|
||||
"89.187.184.173",
|
||||
"89.187.184.178",
|
||||
"89.187.184.176",
|
||||
]);
|
||||
|
||||
const CDN_PROVIDERS: CDNProvider[] = [
|
||||
{
|
||||
name: "cloudflare",
|
||||
displayName: "Cloudflare",
|
||||
checkIp: (ip: string) =>
|
||||
CLOUDFLARE_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
},
|
||||
{
|
||||
name: "bunnycdn",
|
||||
displayName: "Bunny CDN",
|
||||
checkIp: (ip: string) => BUNNY_CDN_IPS.has(ip),
|
||||
warningMessage:
|
||||
"Domain is behind Bunny CDN - actual IP is masked by CDN proxy",
|
||||
},
|
||||
{
|
||||
name: "fastly",
|
||||
displayName: "Fastly",
|
||||
checkIp: (ip: string) =>
|
||||
FASTLY_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Fastly - actual IP is masked by CDN proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export const detectCDNProvider = (ip: string): CDNProvider | null => {
|
||||
return CDN_PROVIDERS.find((provider) => provider.checkIp(ip)) || null;
|
||||
};
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
@@ -65,7 +69,7 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
composeFile: "",
|
||||
composeFile: input.composeFile || "",
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
@@ -125,7 +129,14 @@ export const findComposeById = async (composeId: string) => {
|
||||
github: true,
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
@@ -216,10 +227,6 @@ export const deployCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
// const admin = await findUserById(compose.project.userId);
|
||||
// if (admin.cleanupCacheOnCompose) {
|
||||
// await cleanupFullDocker(compose?.serverId);
|
||||
// }
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...compose,
|
||||
@@ -232,6 +239,8 @@ export const deployCompose = async ({
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
@@ -285,15 +294,10 @@ export const rebuildCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
// const admin = await findUserById(compose.project.userId);
|
||||
// if (admin.cleanupCacheOnCompose) {
|
||||
// await cleanupFullDocker(compose?.serverId);
|
||||
// }
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
} else {
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
@@ -331,10 +335,6 @@ export const deployRemoteCompose = async ({
|
||||
});
|
||||
try {
|
||||
if (compose.serverId) {
|
||||
// const admin = await findUserById(compose.project.userId);
|
||||
// if (admin.cleanupCacheOnCompose) {
|
||||
// await cleanupFullDocker(compose?.serverId);
|
||||
// }
|
||||
let command = "set -e;";
|
||||
|
||||
if (compose.sourceType === "github") {
|
||||
@@ -362,8 +362,15 @@ export const deployRemoteCompose = async ({
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
console.log(command);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
@@ -429,10 +436,10 @@ export const rebuildRemoteCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
// const admin = await findUserById(compose.project.userId);
|
||||
// if (admin.cleanupCacheOnCompose) {
|
||||
// await cleanupFullDocker(compose?.serverId);
|
||||
// }
|
||||
if (compose.sourceType === "raw") {
|
||||
const command = getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
}
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
}
|
||||
@@ -471,7 +478,9 @@ export const removeCompose = async (
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
@@ -482,12 +491,11 @@ export const removeCompose = async (
|
||||
cwd: projectPath,
|
||||
});
|
||||
} else {
|
||||
let command: string;
|
||||
if (deleteVolumes) {
|
||||
command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`;
|
||||
} else {
|
||||
command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
||||
}
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker compose -p ${compose.appName} down ${
|
||||
deleteVolumes ? "--volumes" : ""
|
||||
} && rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
|
||||
@@ -4,9 +4,12 @@ import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateDeployment,
|
||||
type apiCreateDeploymentBackup,
|
||||
type apiCreateDeploymentCompose,
|
||||
type apiCreateDeploymentPreview,
|
||||
type apiCreateDeploymentSchedule,
|
||||
type apiCreateDeploymentServer,
|
||||
type apiCreateDeploymentVolumeBackup,
|
||||
deployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||
@@ -22,28 +25,47 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
|
||||
import { type Server, findServerById } from "./server";
|
||||
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { findBackupById } from "./backup";
|
||||
import {
|
||||
type PreviewDeployment,
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { findScheduleById } from "./schedule";
|
||||
import { removeRollbackById } from "./rollbacks";
|
||||
import { findVolumeBackupById } from "./volume-backups";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
export const findDeploymentById = async (applicationId: string) => {
|
||||
const application = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
export const findDeploymentById = async (deploymentId: string) => {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.deploymentId, deploymentId),
|
||||
with: {
|
||||
application: true,
|
||||
schedule: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
if (!deployment) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const findDeploymentByApplicationId = async (applicationId: string) => {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const createDeployment = async (
|
||||
@@ -57,6 +79,7 @@ export const createDeployment = async (
|
||||
try {
|
||||
await removeLastTenDeployments(
|
||||
deployment.applicationId,
|
||||
"application",
|
||||
application.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!application.serverId);
|
||||
@@ -88,6 +111,7 @@ export const createDeployment = async (
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@@ -107,6 +131,8 @@ export const createDeployment = async (
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
await updateApplicationStatus(application.applicationId, "error");
|
||||
@@ -128,8 +154,9 @@ export const createDeploymentPreview = async (
|
||||
deployment.previewDeploymentId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenPreviewDeploymenById(
|
||||
await removeLastTenDeployments(
|
||||
deployment.previewDeploymentId,
|
||||
"previewDeployment",
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
@@ -165,6 +192,7 @@ export const createDeploymentPreview = async (
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@@ -184,6 +212,8 @@ export const createDeploymentPreview = async (
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
await updatePreviewDeployment(deployment.previewDeploymentId, {
|
||||
@@ -205,8 +235,9 @@ export const createDeploymentCompose = async (
|
||||
) => {
|
||||
const compose = await findComposeById(deployment.composeId);
|
||||
try {
|
||||
await removeLastTenComposeDeployments(
|
||||
await removeLastTenDeployments(
|
||||
deployment.composeId,
|
||||
"compose",
|
||||
compose.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||
@@ -238,6 +269,7 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
description: deployment.description || "",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@@ -257,6 +289,8 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
await updateCompose(compose.composeId, {
|
||||
@@ -270,6 +304,247 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentBackup._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const backup = await findBackupById(deployment.backupId);
|
||||
|
||||
let serverId: string | null | undefined;
|
||||
if (backup.backupType === "database") {
|
||||
serverId =
|
||||
backup.postgres?.serverId ||
|
||||
backup.mariadb?.serverId ||
|
||||
backup.mysql?.serverId ||
|
||||
backup.mongo?.serverId;
|
||||
} else if (backup.backupType === "compose") {
|
||||
serverId = backup.compose?.serverId;
|
||||
}
|
||||
try {
|
||||
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${backup.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, backup.appName, fileName);
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${backup.appName};
|
||||
echo "Initializing backup\n" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, backup.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing backup\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
backupId: deployment.backupId,
|
||||
title: deployment.title || "Backup",
|
||||
description: deployment.description || "",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the backup",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
backupId: deployment.backupId,
|
||||
title: deployment.title || "Backup",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the backup",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentSchedule = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentSchedule._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const schedule = await findScheduleById(deployment.scheduleId);
|
||||
|
||||
try {
|
||||
const serverId =
|
||||
schedule.application?.serverId ||
|
||||
schedule.compose?.serverId ||
|
||||
schedule.server?.serverId;
|
||||
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
|
||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${SCHEDULES_PATH}/${schedule.appName};
|
||||
echo "Initializing schedule" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(SCHEDULES_PATH, schedule.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing schedule\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
scheduleId: deployment.scheduleId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
scheduleId: deployment.scheduleId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentVolumeBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentVolumeBackup._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
|
||||
|
||||
try {
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
await removeLastTenDeployments(
|
||||
deployment.volumeBackupId,
|
||||
"volumeBackup",
|
||||
serverId,
|
||||
);
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(
|
||||
VOLUME_BACKUPS_PATH,
|
||||
volumeBackup.appName,
|
||||
fileName,
|
||||
);
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${VOLUME_BACKUPS_PATH}/${volumeBackup.appName};
|
||||
echo "Initializing volume backup" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(
|
||||
path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName),
|
||||
{
|
||||
recursive: true,
|
||||
},
|
||||
);
|
||||
await fsPromises.writeFile(logFilePath, "Initializing volume backup\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
volumeBackupId: deployment.volumeBackupId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
volumeBackupId: deployment.volumeBackupId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployment = async (deploymentId: string) => {
|
||||
try {
|
||||
const deployment = await db
|
||||
@@ -296,109 +571,25 @@ export const removeDeploymentsByApplicationId = async (
|
||||
.returning();
|
||||
};
|
||||
|
||||
const removeLastTenDeployments = async (
|
||||
applicationId: string,
|
||||
serverId: string | null,
|
||||
const getDeploymentsByType = async (
|
||||
id: string,
|
||||
type:
|
||||
| "application"
|
||||
| "compose"
|
||||
| "server"
|
||||
| "schedule"
|
||||
| "previewDeployment"
|
||||
| "backup"
|
||||
| "volumeBackup",
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
where: eq(deployments[`${type}Id`], id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
with: {
|
||||
rollback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(9);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeLastTenComposeDeployments = async (
|
||||
composeId: string,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.composeId, composeId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
if (deploymentList.length > 10) {
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
const deploymentsToDelete = deploymentList.slice(9);
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
const deploymentsToDelete = deploymentList.slice(9);
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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(9);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return deploymentList;
|
||||
};
|
||||
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
@@ -413,6 +604,57 @@ export const removeDeployments = async (application: Application) => {
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
const removeLastTenDeployments = async (
|
||||
id: string,
|
||||
type:
|
||||
| "application"
|
||||
| "compose"
|
||||
| "server"
|
||||
| "schedule"
|
||||
| "previewDeployment"
|
||||
| "backup"
|
||||
| "volumeBackup",
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const deploymentList = await getDeploymentsByType(id, type);
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
|
||||
if (logPath !== ".") {
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (
|
||||
existsSync(logPath) &&
|
||||
!oldDeployment.errorMessage &&
|
||||
logPath !== "."
|
||||
) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeploymentsByPreviewDeploymentId = async (
|
||||
previewDeployment: PreviewDeployment,
|
||||
serverId: string | null,
|
||||
@@ -494,6 +736,10 @@ export const updateDeploymentStatus = async (
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: deploymentStatus,
|
||||
finishedAt:
|
||||
deploymentStatus === "done" || deploymentStatus === "error"
|
||||
? new Date().toISOString()
|
||||
: null,
|
||||
})
|
||||
.where(eq(deployments.deploymentId, deploymentId))
|
||||
.returning();
|
||||
|
||||
@@ -98,7 +98,7 @@ export const getConfig = async (
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getContainersByAppNameMatch = async (
|
||||
@@ -156,7 +156,7 @@ export const getContainersByAppNameMatch = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -214,7 +214,7 @@ export const getStackContainersByAppName = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -274,20 +274,26 @@ export const getServiceContainersByAppName = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getContainersByAppLabel = async (
|
||||
appName: string,
|
||||
type: "standalone" | "swarm",
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
|
||||
const command =
|
||||
type === "swarm"
|
||||
? `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`
|
||||
: type === "standalone"
|
||||
? `docker ps --filter "name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`
|
||||
: `docker ps --filter "label=com.docker.compose.project=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
@@ -325,7 +331,7 @@ export const getContainersByAppLabel = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -344,7 +350,7 @@ export const containerRestart = async (containerId: string) => {
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getSwarmNodes = async (serverId?: string) => {
|
||||
@@ -373,7 +379,7 @@ export const getSwarmNodes = async (serverId?: string) => {
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
return nodesArray;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||
@@ -399,7 +405,7 @@ export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||
const nodeInfo = JSON.parse(stdout);
|
||||
|
||||
return nodeInfo;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getNodeApplications = async (serverId?: string) => {
|
||||
@@ -431,7 +437,7 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
|
||||
return appArray;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getApplicationInfo = async (
|
||||
@@ -464,5 +470,5 @@ export const getApplicationInfo = async (
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates/utils";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { type apiCreateDomain, domains } from "../db/schema";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
@@ -137,3 +140,64 @@ export const removeDomainById = async (domainId: string) => {
|
||||
export const getDomainHost = (domain: Domain) => {
|
||||
return `${domain.https ? "https" : "http"}://${domain.host}`;
|
||||
};
|
||||
|
||||
const resolveDns = promisify(dns.resolve4);
|
||||
|
||||
export const validateDomain = async (
|
||||
domain: string,
|
||||
expectedIp?: string,
|
||||
): Promise<{
|
||||
isValid: boolean;
|
||||
resolvedIp?: string;
|
||||
error?: string;
|
||||
isCloudflare?: boolean;
|
||||
cdnProvider?: string;
|
||||
}> => {
|
||||
try {
|
||||
// Remove protocol and path if present
|
||||
const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0];
|
||||
|
||||
// Resolve the domain to get its IP
|
||||
const ips = await resolveDns(cleanDomain || "");
|
||||
|
||||
const resolvedIps = ips.map((ip) => ip.toString());
|
||||
|
||||
// Check if any IP belongs to a CDN provider
|
||||
const cdnProvider = ips
|
||||
.map((ip) => detectCDNProvider(ip))
|
||||
.find((provider) => provider !== null);
|
||||
|
||||
// If behind a CDN, we consider it valid but inform the user
|
||||
if (cdnProvider) {
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
cdnProvider: cdnProvider.displayName,
|
||||
error: cdnProvider.warningMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// If we have an expected IP, validate against it
|
||||
if (expectedIp) {
|
||||
return {
|
||||
isValid: resolvedIps.includes(expectedIp),
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
error: !resolvedIps.includes(expectedIp)
|
||||
? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIp}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// If no expected IP, just return the resolved IP
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Failed to resolve domain",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
100
packages/server/src/services/gitea.ts
Normal file
100
packages/server/src/services/gitea.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateGitea,
|
||||
gitProvider,
|
||||
gitea,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type Gitea = typeof gitea.$inferSelect;
|
||||
|
||||
export const createGitea = async (
|
||||
input: typeof apiCreateGitea._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "gitea",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
const giteaProvider = await tx
|
||||
.insert(gitea)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response: (typeof gitea.$inferSelect)[]) => response[0]);
|
||||
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
giteaId: giteaProvider.giteaId,
|
||||
clientId: giteaProvider.clientId,
|
||||
giteaUrl: giteaProvider.giteaUrl,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const findGiteaById = async (giteaId: string) => {
|
||||
try {
|
||||
const giteaProviderResult = await db.query.gitea.findFirst({
|
||||
where: eq(gitea.giteaId, giteaId),
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!giteaProviderResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return giteaProviderResult;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateGitea = async (giteaId: string, input: Partial<Gitea>) => {
|
||||
try {
|
||||
const updateResult = await db
|
||||
.update(gitea)
|
||||
.set(input)
|
||||
.where(eq(gitea.giteaId, giteaId))
|
||||
.returning();
|
||||
|
||||
const result = updateResult[0] as Gitea | undefined;
|
||||
|
||||
if (!result) {
|
||||
throw new Error(`Failed to update Gitea provider with ID ${giteaId}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export type Github = typeof github.$inferSelect;
|
||||
export const createGithub = async (
|
||||
input: typeof apiCreateGithub._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -21,6 +22,7 @@ export const createGithub = async (
|
||||
providerType: "github",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
@@ -119,7 +121,7 @@ export const issueCommentExists = async ({
|
||||
comment_id: comment_id,
|
||||
});
|
||||
return true;
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Gitlab = typeof gitlab.$inferSelect;
|
||||
export const createGitlab = async (
|
||||
input: typeof apiCreateGitlab._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -20,6 +21,7 @@ export const createGitlab = async (
|
||||
providerType: "gitlab",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
mariadb,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -63,6 +63,7 @@ export const findMariadbById = async (mariadbId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
type apiCreateMongo,
|
||||
backups,
|
||||
compose,
|
||||
mongo,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -55,6 +60,7 @@ export const findMongoById = async (mongoId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -103,6 +109,25 @@ export const findMongoByBackupId = async (backupId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findComposeByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(compose),
|
||||
})
|
||||
.from(compose)
|
||||
.innerJoin(backups, eq(compose.composeId, backups.composeId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Compose not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeMongoById = async (mongoId: string) => {
|
||||
const result = await db
|
||||
.delete(mongo)
|
||||
|
||||
@@ -144,7 +144,8 @@ export const updateMount = async (
|
||||
await deleteFileMount(mountId);
|
||||
await createFileMount(mountId);
|
||||
}
|
||||
return mount;
|
||||
|
||||
return await findMountById(mountId);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -211,7 +212,7 @@ export const deleteFileMount = async (mountId: string) => {
|
||||
} else {
|
||||
await removeFileOrDirectory(fullPath);
|
||||
}
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getBaseFilesPath = async (mountId: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -59,6 +59,7 @@ export const findMySqlById = async (mysqlId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
postgres,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -58,6 +58,7 @@ export const findPostgresById = async (postgresId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { generatePassword } from "../templates/utils";
|
||||
import { generatePassword } from "../templates";
|
||||
import { removeService } from "../utils/docker/utils";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
@@ -62,6 +62,7 @@ export const findApplicationByPreview = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
@@ -103,7 +104,7 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
}
|
||||
return deployment[0];
|
||||
} catch (error) {
|
||||
@@ -173,7 +174,7 @@ export const createPreviewDeployment = async (
|
||||
const runningComment = getIssueComment(
|
||||
application.name,
|
||||
"initializing",
|
||||
generateDomain,
|
||||
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
@@ -209,6 +210,7 @@ export const createPreviewDeployment = async (
|
||||
port: application.previewPort,
|
||||
https: application.previewHttps,
|
||||
certificateType: application.previewCertificateType,
|
||||
customCertResolver: application.previewCustomCertResolver,
|
||||
domainType: "preview",
|
||||
previewDeploymentId: previewDeployment.previewDeploymentId,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateRedis, redis } from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildRedis } from "@dokploy/server/utils/databases/redis";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
286
packages/server/src/services/rollbacks.ts
Normal file
286
packages/server/src/services/rollbacks.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
type createRollbackSchema,
|
||||
rollbacks,
|
||||
deployments as deploymentsSchema,
|
||||
} from "../db/schema";
|
||||
import type { z } from "zod";
|
||||
import { type Application, findApplicationById } from "./application";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import { getAuthConfig, type ApplicationNested } from "../utils/builders";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { findDeploymentById } from "./deployment";
|
||||
import {
|
||||
prepareEnvironmentVariables,
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateVolumeMounts,
|
||||
} from "../utils/docker/utils";
|
||||
import type { Project } from "./project";
|
||||
import type { Mount } from "./mount";
|
||||
import type { Port } from "./port";
|
||||
|
||||
export const createRollback = async (
|
||||
input: z.infer<typeof createRollbackSchema>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const { fullContext, ...other } = input;
|
||||
const rollback = await tx
|
||||
.insert(rollbacks)
|
||||
.values(other)
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!rollback) {
|
||||
throw new Error("Failed to create rollback");
|
||||
}
|
||||
|
||||
const tagImage = `${input.appName}:v${rollback.version}`;
|
||||
const deployment = await findDeploymentById(rollback.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const {
|
||||
deployments: _,
|
||||
bitbucket,
|
||||
github,
|
||||
gitlab,
|
||||
gitea,
|
||||
...rest
|
||||
} = await findApplicationById(deployment.applicationId);
|
||||
|
||||
await tx
|
||||
.update(rollbacks)
|
||||
.set({
|
||||
image: tagImage,
|
||||
fullContext: rest,
|
||||
})
|
||||
.where(eq(rollbacks.rollbackId, rollback.rollbackId));
|
||||
|
||||
// Update the deployment to reference this rollback
|
||||
await tx
|
||||
.update(deploymentsSchema)
|
||||
.set({
|
||||
rollbackId: rollback.rollbackId,
|
||||
})
|
||||
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
||||
|
||||
await createRollbackImage(rest, tagImage);
|
||||
|
||||
return rollback;
|
||||
});
|
||||
};
|
||||
|
||||
const findRollbackById = async (rollbackId: string) => {
|
||||
const result = await db.query.rollbacks.findFirst({
|
||||
where: eq(rollbacks.rollbackId, rollbackId),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Rollback not found");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const createRollbackImage = async (
|
||||
application: ApplicationNested,
|
||||
tagImage: string,
|
||||
) => {
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const appTagName =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: `${application.appName}:latest`;
|
||||
|
||||
const result = docker.getImage(appTagName || "");
|
||||
|
||||
const [repo, version] = tagImage.split(":");
|
||||
|
||||
await result.tag({
|
||||
repo,
|
||||
tag: version,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
||||
const command = `docker image rm ${image} --force`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(command, serverId);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeRollbackById = async (rollbackId: string) => {
|
||||
const rollback = await findRollbackById(rollbackId);
|
||||
|
||||
if (!rollback) {
|
||||
throw new Error("Rollback not found");
|
||||
}
|
||||
|
||||
if (rollback?.image) {
|
||||
try {
|
||||
const deployment = await findDeploymentById(rollback.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
await deleteRollbackImage(rollback.image, application.serverId);
|
||||
|
||||
await db
|
||||
.delete(rollbacks)
|
||||
.where(eq(rollbacks.rollbackId, rollbackId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return rollback;
|
||||
};
|
||||
|
||||
export const rollback = async (rollbackId: string) => {
|
||||
const result = await findRollbackById(rollbackId);
|
||||
|
||||
const deployment = await findDeploymentById(result.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
if (!result.fullContext) {
|
||||
throw new Error("Rollback context not found");
|
||||
}
|
||||
|
||||
// Use the full context for rollback
|
||||
await rollbackApplication(
|
||||
application.appName,
|
||||
result.image || "",
|
||||
application.serverId,
|
||||
result.fullContext,
|
||||
);
|
||||
};
|
||||
|
||||
const rollbackApplication = async (
|
||||
appName: string,
|
||||
image: string,
|
||||
serverId?: string | null,
|
||||
fullContext?: Application & {
|
||||
project: Project;
|
||||
mounts: Mount[];
|
||||
ports: Port[];
|
||||
},
|
||||
) => {
|
||||
if (!fullContext) {
|
||||
throw new Error("Full context is required for rollback");
|
||||
}
|
||||
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
|
||||
// Use the same configuration as mechanizeDockerContainer
|
||||
const {
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
} = fullContext;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(fullContext as ApplicationNested);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
fullContext.project.env,
|
||||
);
|
||||
|
||||
// For rollback, we use the provided image instead of calculating it
|
||||
const authConfig = getAuthConfig(fullContext as ApplicationNested);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
PublishMode: port.publishMode,
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
131
packages/server/src/services/schedule.ts
Normal file
131
packages/server/src/services/schedule.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import path from "node:path";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { paths } from "../constants";
|
||||
import { db } from "../db";
|
||||
import { type Schedule, schedules } from "../db/schema/schedule";
|
||||
import type {
|
||||
createScheduleSchema,
|
||||
updateScheduleSchema,
|
||||
} from "../db/schema/schedule";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
|
||||
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
|
||||
|
||||
export const createSchedule = async (
|
||||
input: z.infer<typeof createScheduleSchema>,
|
||||
) => {
|
||||
const { scheduleId, ...rest } = input;
|
||||
const [newSchedule] = await db.insert(schedules).values(rest).returning();
|
||||
|
||||
if (
|
||||
newSchedule &&
|
||||
(newSchedule.scheduleType === "dokploy-server" ||
|
||||
newSchedule.scheduleType === "server")
|
||||
) {
|
||||
await handleScript(newSchedule);
|
||||
}
|
||||
|
||||
return newSchedule;
|
||||
};
|
||||
|
||||
export const findScheduleById = async (scheduleId: string) => {
|
||||
const schedule = await db.query.schedules.findFirst({
|
||||
where: eq(schedules.scheduleId, scheduleId),
|
||||
with: {
|
||||
application: true,
|
||||
compose: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Schedule not found",
|
||||
});
|
||||
}
|
||||
return schedule;
|
||||
};
|
||||
|
||||
export const deleteSchedule = async (scheduleId: string) => {
|
||||
const schedule = await findScheduleById(scheduleId);
|
||||
const serverId =
|
||||
schedule?.serverId ||
|
||||
schedule?.application?.serverId ||
|
||||
schedule?.compose?.serverId;
|
||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||
|
||||
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||
const command = `rm -rf ${fullPath}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
const scheduleResult = await db
|
||||
.delete(schedules)
|
||||
.where(eq(schedules.scheduleId, scheduleId));
|
||||
if (!scheduleResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Schedule not found",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const updateSchedule = async (
|
||||
input: z.infer<typeof updateScheduleSchema>,
|
||||
) => {
|
||||
const { scheduleId, ...rest } = input;
|
||||
const [updatedSchedule] = await db
|
||||
.update(schedules)
|
||||
.set(rest)
|
||||
.where(eq(schedules.scheduleId, scheduleId))
|
||||
.returning();
|
||||
|
||||
if (!updatedSchedule) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Schedule not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
updatedSchedule?.scheduleType === "dokploy-server" ||
|
||||
updatedSchedule?.scheduleType === "server"
|
||||
) {
|
||||
await handleScript(updatedSchedule);
|
||||
}
|
||||
|
||||
return updatedSchedule;
|
||||
};
|
||||
|
||||
const handleScript = async (schedule: Schedule) => {
|
||||
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
|
||||
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||
|
||||
// Add PID and Schedule ID echo by default to all scripts
|
||||
const scriptWithPid = `echo "PID: $$ | Schedule ID: ${schedule.scheduleId}"
|
||||
${schedule?.script || ""}`;
|
||||
|
||||
const encodedContent = encodeBase64(scriptWithPid);
|
||||
const script = `
|
||||
mkdir -p ${fullPath}
|
||||
rm -f ${fullPath}/script.sh
|
||||
touch ${fullPath}/script.sh
|
||||
chmod +x ${fullPath}/script.sh
|
||||
echo "${encodedContent}" | base64 -d > ${fullPath}/script.sh
|
||||
`;
|
||||
|
||||
if (schedule?.scheduleType === "dokploy-server") {
|
||||
await execAsync(script);
|
||||
} else if (schedule?.scheduleType === "server") {
|
||||
await execAsyncRemote(schedule?.serverId || "", script);
|
||||
}
|
||||
};
|
||||
64
packages/server/src/services/volume-backups.ts
Normal file
64
packages/server/src/services/volume-backups.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
type createVolumeBackupSchema,
|
||||
type updateVolumeBackupSchema,
|
||||
volumeBackups,
|
||||
} from "../db/schema";
|
||||
import { db } from "../db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await db.query.volumeBackups.findFirst({
|
||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||
with: {
|
||||
application: true,
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!volumeBackup) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Volume backup not found",
|
||||
});
|
||||
}
|
||||
|
||||
return volumeBackup;
|
||||
};
|
||||
|
||||
export const createVolumeBackup = async (
|
||||
volumeBackup: z.infer<typeof createVolumeBackupSchema>,
|
||||
) => {
|
||||
const newVolumeBackup = await db
|
||||
.insert(volumeBackups)
|
||||
.values(volumeBackup)
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
|
||||
return newVolumeBackup;
|
||||
};
|
||||
|
||||
export const removeVolumeBackup = async (volumeBackupId: string) => {
|
||||
await db
|
||||
.delete(volumeBackups)
|
||||
.where(eq(volumeBackups.volumeBackupId, volumeBackupId));
|
||||
};
|
||||
|
||||
export const updateVolumeBackup = async (
|
||||
volumeBackupId: string,
|
||||
volumeBackup: z.infer<typeof updateVolumeBackupSchema>,
|
||||
) => {
|
||||
return await db
|
||||
.update(volumeBackups)
|
||||
.set(volumeBackup)
|
||||
.where(eq(volumeBackups.volumeBackupId, volumeBackupId))
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
};
|
||||
@@ -18,6 +18,8 @@ export const setupDirectories = () => {
|
||||
MAIN_TRAEFIK_PATH,
|
||||
MONITORING_PATH,
|
||||
SSH_PATH,
|
||||
SCHEDULES_PATH,
|
||||
VOLUME_BACKUPS_PATH,
|
||||
} = paths();
|
||||
const directories = [
|
||||
BASE_PATH,
|
||||
@@ -28,6 +30,8 @@ export const setupDirectories = () => {
|
||||
SSH_PATH,
|
||||
CERTIFICATES_PATH,
|
||||
MONITORING_PATH,
|
||||
SCHEDULES_PATH,
|
||||
VOLUME_BACKUPS_PATH,
|
||||
];
|
||||
|
||||
for (const dir of directories) {
|
||||
|
||||
@@ -30,6 +30,9 @@ export const setupMonitoring = async (serverId: string) => {
|
||||
// PidMode: "host",
|
||||
// CapAdd: ["NET_ADMIN", "SYS_ADMIN"],
|
||||
// Privileged: true,
|
||||
RestartPolicy: {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${server.metricsConfig.server.port}/tcp`]: [
|
||||
{
|
||||
@@ -66,7 +69,7 @@ export const setupMonitoring = async (serverId: string) => {
|
||||
await container.inspect();
|
||||
await container.remove({ force: true });
|
||||
console.log("Removed existing container");
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
// Container doesn't exist, continue
|
||||
}
|
||||
|
||||
@@ -103,6 +106,9 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
// PidMode: "host",
|
||||
// CapAdd: ["NET_ADMIN", "SYS_ADMIN"],
|
||||
// Privileged: true,
|
||||
RestartPolicy: {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: [
|
||||
{
|
||||
@@ -135,7 +141,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
await container.inspect();
|
||||
await container.remove({ force: true });
|
||||
console.log("Removed existing container");
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
|
||||
@@ -14,13 +14,50 @@ const validateUfw = () => `
|
||||
`;
|
||||
|
||||
const validateSsh = () => `
|
||||
if systemctl is-active --quiet sshd; then
|
||||
if systemctl is-active --quiet sshd || systemctl is-active --quiet ssh; then
|
||||
isEnabled=true
|
||||
hasKeyAuth=$(find "$HOME/.ssh" -type f -name "authorized_keys" 2>/dev/null | grep -q . && echo true || echo false)
|
||||
permitRootLogin=$(sudo sshd -T | grep -i "^PermitRootLogin" | awk '{print $2}')
|
||||
passwordAuth=$(sudo sshd -T | grep -i "^PasswordAuthentication" | awk '{print $2}')
|
||||
usePam=$(sudo sshd -T | grep -i "^UsePAM" | awk '{print $2}')
|
||||
echo "{\\"enabled\\": $isEnabled, \\"keyAuth\\": $hasKeyAuth, \\"permitRootLogin\\": \\"$permitRootLogin\\", \\"passwordAuth\\": \\"$passwordAuth\\", \\"usePam\\": \\"$usePam\\"}"
|
||||
|
||||
# Get the sshd config file path
|
||||
sshd_config=$(sudo sshd -T 2>/dev/null | grep -i "^configfile" | awk '{print $2}')
|
||||
|
||||
# If we couldn't get the path, use the default
|
||||
if [ -z "$sshd_config" ]; then
|
||||
sshd_config="/etc/ssh/sshd_config"
|
||||
fi
|
||||
|
||||
# Check for key authentication
|
||||
# SSH key auth is enabled by default unless explicitly disabled
|
||||
pubkey_line=$(sudo grep -i "^PubkeyAuthentication" "$sshd_config" 2>/dev/null | grep -v "#")
|
||||
if [ -z "$pubkey_line" ] || echo "$pubkey_line" | grep -q -i "yes"; then
|
||||
keyAuth=true
|
||||
else
|
||||
keyAuth=false
|
||||
fi
|
||||
|
||||
# Get the exact PermitRootLogin value from config
|
||||
# This preserves values like "prohibit-password" without normalization
|
||||
permitRootLogin=$(sudo grep -i "^PermitRootLogin" "$sshd_config" 2>/dev/null | grep -v "#" | awk '{print $2}')
|
||||
if [ -z "$permitRootLogin" ]; then
|
||||
# Default is prohibit-password in newer versions
|
||||
permitRootLogin="prohibit-password"
|
||||
fi
|
||||
|
||||
# Get the exact PasswordAuthentication value from config
|
||||
passwordAuth=$(sudo grep -i "^PasswordAuthentication" "$sshd_config" 2>/dev/null | grep -v "#" | awk '{print $2}')
|
||||
if [ -z "$passwordAuth" ]; then
|
||||
# Default is yes
|
||||
passwordAuth="yes"
|
||||
fi
|
||||
|
||||
# Get the exact UsePAM value from config
|
||||
usePam=$(sudo grep -i "^UsePAM" "$sshd_config" 2>/dev/null | grep -v "#" | awk '{print $2}')
|
||||
if [ -z "$usePam" ]; then
|
||||
# Default is yes in most distros
|
||||
usePam="yes"
|
||||
fi
|
||||
|
||||
# Return the results with exact values from config file
|
||||
echo "{\\"enabled\\": $isEnabled, \\"keyAuth\\": $keyAuth, \\"permitRootLogin\\": \\"$permitRootLogin\\", \\"passwordAuth\\": \\"$passwordAuth\\", \\"usePam\\": \\"$usePam\\"}"
|
||||
else
|
||||
echo "{\\"enabled\\": false, \\"keyAuth\\": false, \\"permitRootLogin\\": \\"unknown\\", \\"passwordAuth\\": \\"unknown\\", \\"usePam\\": \\"unknown\\"}"
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
TRAEFIK_HTTP3_PORT,
|
||||
TRAEFIK_PORT,
|
||||
TRAEFIK_SSL_PORT,
|
||||
TRAEFIK_VERSION,
|
||||
@@ -75,7 +76,7 @@ CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
fi
|
||||
|
||||
@@ -115,7 +116,7 @@ if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | opencloudos | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
@@ -262,7 +263,7 @@ const setupMainDirectory = () => `
|
||||
# Create the /etc/dokploy directory
|
||||
mkdir -p /etc/dokploy
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
|
||||
echo "Directory /etc/dokploy created ✅"
|
||||
fi
|
||||
`;
|
||||
@@ -275,16 +276,16 @@ export const setupSwarm = () => `
|
||||
# Get IP address
|
||||
get_ip() {
|
||||
local ip=""
|
||||
|
||||
|
||||
# Try IPv4 with multiple services
|
||||
# First attempt: ifconfig.io
|
||||
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
|
||||
|
||||
|
||||
# Second attempt: icanhazip.com
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
|
||||
fi
|
||||
|
||||
|
||||
# Third attempt: ipecho.net
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
|
||||
@@ -294,12 +295,12 @@ export const setupSwarm = () => `
|
||||
if [ -z "\$ip" ]; then
|
||||
# Try IPv6 with ifconfig.io
|
||||
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
|
||||
|
||||
|
||||
# Try IPv6 with icanhazip.com
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
|
||||
fi
|
||||
|
||||
|
||||
# Try IPv6 with ipecho.net
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
|
||||
@@ -355,20 +356,20 @@ const installUtilities = () => `
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
pacman -Sy --noconfirm --needed curl wget git git-lfs 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
|
||||
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
@@ -376,12 +377,12 @@ const installUtilities = () => `
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl unzip >/dev/null
|
||||
dnf install -y wget git git-lfs 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
|
||||
zypper install -y curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
@@ -417,6 +418,28 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"opencloudos")
|
||||
# Special handling for OpenCloud OS
|
||||
echo " - Installing Docker for OpenCloud OS..."
|
||||
dnf install -y docker >/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
|
||||
|
||||
# Remove --live-restore parameter from Docker configuration if it exists
|
||||
if [ -f "/etc/sysconfig/docker" ]; then
|
||||
echo " - Removing --live-restore parameter from Docker configuration..."
|
||||
sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
# Clean up any double spaces that might be left
|
||||
sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
echo " - Docker configured for OpenCloud OS"
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
@@ -542,22 +565,28 @@ export const installRClone = () => `
|
||||
export const createTraefikInstance = () => {
|
||||
const command = `
|
||||
# Check if dokpyloy-traefik exists
|
||||
if docker service ls | grep -q 'dokploy-traefik'; then
|
||||
if docker service inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
echo "Migrating Traefik to Standalone..."
|
||||
docker service rm dokploy-traefik
|
||||
sleep 8
|
||||
echo "Traefik migrated to Standalone ✅"
|
||||
fi
|
||||
|
||||
if docker inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
echo "Traefik already exists ✅"
|
||||
else
|
||||
# Create the dokploy-traefik service
|
||||
# Create the dokploy-traefik container
|
||||
TRAEFIK_VERSION=${TRAEFIK_VERSION}
|
||||
docker service create \
|
||||
docker run -d \
|
||||
--name dokploy-traefik \
|
||||
--replicas 1 \
|
||||
--constraint 'node.role==manager' \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \
|
||||
--mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \
|
||||
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
--label traefik.enable=true \
|
||||
--publish mode=host,target=${TRAEFIK_SSL_PORT},published=${TRAEFIK_SSL_PORT} \
|
||||
--publish mode=host,target=${TRAEFIK_PORT},published=${TRAEFIK_PORT} \
|
||||
--restart unless-stopped \
|
||||
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
|
||||
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
|
||||
-p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
|
||||
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
|
||||
traefik:v$TRAEFIK_VERSION
|
||||
echo "Traefik version $TRAEFIK_VERSION installed ✅"
|
||||
fi
|
||||
@@ -570,7 +599,7 @@ const installNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
export NIXPACKS_VERSION=1.29.1
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
@@ -580,7 +609,7 @@ const installRailpack = () => `
|
||||
if command_exists railpack; then
|
||||
echo "Railpack already installed ✅"
|
||||
else
|
||||
export RAILPACK_VERSION=0.0.37
|
||||
export RAILPACK_VERSION=0.0.64
|
||||
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
echo "Railpack version $RAILPACK_VERSION installed ✅"
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,7 @@ export const dockerSwarmInitialized = async () => {
|
||||
await docker.swarmInspect();
|
||||
|
||||
return true;
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export const dockerNetworkInitialized = async () => {
|
||||
try {
|
||||
await docker.getNetwork("dokploy-network").inspect();
|
||||
return true;
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ContainerTaskSpec, CreateServiceOptions } from "dockerode";
|
||||
import type { ContainerCreateOptions } from "dockerode";
|
||||
import { dump } from "js-yaml";
|
||||
import { paths } from "../constants";
|
||||
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import type { FileConfig } from "../utils/traefik/file-types";
|
||||
import type { MainTraefikConfig } from "../utils/traefik/types";
|
||||
@@ -12,6 +11,8 @@ export const TRAEFIK_SSL_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
|
||||
export const TRAEFIK_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
export const TRAEFIK_HTTP3_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
|
||||
|
||||
interface TraefikOptions {
|
||||
@@ -21,8 +22,8 @@ interface TraefikOptions {
|
||||
additionalPorts?: {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
publishMode?: "ingress" | "host";
|
||||
}[];
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export const initializeTraefik = async ({
|
||||
@@ -30,113 +31,121 @@ export const initializeTraefik = async ({
|
||||
env,
|
||||
serverId,
|
||||
additionalPorts = [],
|
||||
force = false,
|
||||
}: TraefikOptions = {}) => {
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
||||
const containerName = "dokploy-traefik";
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Env: env,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
|
||||
Target: "/etc/traefik/traefik.yml",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: DYNAMIC_TRAEFIK_PATH,
|
||||
Target: "/etc/dokploy/traefik/dynamic",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 443,
|
||||
PublishedPort: TRAEFIK_SSL_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: TRAEFIK_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
...(enableDashboard
|
||||
? [
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 8080,
|
||||
PublishMode: "host" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...additionalPorts.map((port) => ({
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
PublishMode: port.publishMode || ("host" as const),
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
const exposedPorts: Record<string, {}> = {
|
||||
[`${TRAEFIK_PORT}/tcp`]: {},
|
||||
[`${TRAEFIK_SSL_PORT}/tcp`]: {},
|
||||
[`${TRAEFIK_HTTP3_PORT}/udp`]: {},
|
||||
};
|
||||
|
||||
const portBindings: Record<string, Array<{ HostPort: string }>> = {
|
||||
[`${TRAEFIK_PORT}/tcp`]: [{ HostPort: TRAEFIK_PORT.toString() }],
|
||||
[`${TRAEFIK_SSL_PORT}/tcp`]: [{ HostPort: TRAEFIK_SSL_PORT.toString() }],
|
||||
[`${TRAEFIK_HTTP3_PORT}/udp`]: [
|
||||
{ HostPort: TRAEFIK_HTTP3_PORT.toString() },
|
||||
],
|
||||
};
|
||||
|
||||
if (enableDashboard) {
|
||||
exposedPorts["8080/tcp"] = {};
|
||||
portBindings["8080/tcp"] = [{ HostPort: "8080" }];
|
||||
}
|
||||
|
||||
for (const port of additionalPorts) {
|
||||
const portKey = `${port.targetPort}/tcp`;
|
||||
exposedPorts[portKey] = {};
|
||||
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
|
||||
}
|
||||
|
||||
const settings: ContainerCreateOptions = {
|
||||
name: containerName,
|
||||
Image: imageName,
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
"dokploy-network": {},
|
||||
},
|
||||
},
|
||||
ExposedPorts: exposedPorts,
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
Name: "always",
|
||||
},
|
||||
Binds: [
|
||||
`${MAIN_TRAEFIK_PATH}/traefik.yml:/etc/traefik/traefik.yml`,
|
||||
`${DYNAMIC_TRAEFIK_PATH}:/etc/dokploy/traefik/dynamic`,
|
||||
"/var/run/docker.sock:/var/run/docker.sock",
|
||||
],
|
||||
PortBindings: portBindings,
|
||||
},
|
||||
Env: env,
|
||||
};
|
||||
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
if (serverId) {
|
||||
await pullRemoteImage(imageName, serverId);
|
||||
} else {
|
||||
await pullImage(imageName);
|
||||
try {
|
||||
const service = docker.getService("dokploy-traefik");
|
||||
await service?.remove({ force: true });
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 5;
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
await docker.listServices({
|
||||
filters: { name: ["dokploy-traefik"] },
|
||||
});
|
||||
console.log("Waiting for service cleanup...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
attempts++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.log("No existing service to remove");
|
||||
}
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || [];
|
||||
const updatedEnv = !env ? existingEnv : env;
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ContainerSpec: {
|
||||
...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec,
|
||||
Env: updatedEnv,
|
||||
},
|
||||
},
|
||||
};
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...updatedSettings,
|
||||
});
|
||||
|
||||
console.log("Traefik Started ✅");
|
||||
} catch (_) {
|
||||
// Then try to remove any existing container
|
||||
const container = docker.getContainer(containerName);
|
||||
try {
|
||||
await docker.createService(settings);
|
||||
const inspect = await container.inspect();
|
||||
if (inspect.State.Status === "running" && !force) {
|
||||
console.log("Traefik already running");
|
||||
return;
|
||||
}
|
||||
|
||||
await container.remove({ force: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
} catch {
|
||||
console.log("No existing container to remove");
|
||||
}
|
||||
|
||||
// Create and start the new container
|
||||
try {
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully");
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 409) {
|
||||
if (error?.json?.message?.includes("port is already allocated")) {
|
||||
console.log("Ports still in use, waiting longer for cleanup...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
// Try one more time
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully after retry");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
console.log("Traefik service already exists, continuing...");
|
||||
}
|
||||
console.log("Traefik Not Found: Starting ✅");
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize Traefik:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,6 +221,9 @@ export const getDefaultTraefikConfig = () => {
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
http3: {
|
||||
advertisedPort: TRAEFIK_HTTP3_PORT,
|
||||
},
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
@@ -267,6 +279,9 @@ export const getDefaultServerTraefikConfig = () => {
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
http3: {
|
||||
advertisedPort: TRAEFIK_HTTP3_PORT,
|
||||
},
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
|
||||
109
packages/server/src/templates/github.ts
Normal file
109
packages/server/src/templates/github.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { parse } from "toml";
|
||||
|
||||
/**
|
||||
* Complete template interface that includes both metadata and configuration
|
||||
*/
|
||||
export interface CompleteTemplate {
|
||||
metadata: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
version: string;
|
||||
logo: string;
|
||||
links: {
|
||||
github: string;
|
||||
website?: string;
|
||||
docs?: string;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
[key: string]: string;
|
||||
};
|
||||
config: {
|
||||
domains: Array<{
|
||||
serviceName: string;
|
||||
port: number;
|
||||
path?: string;
|
||||
host?: string;
|
||||
}>;
|
||||
env: Record<string, string>;
|
||||
mounts?: Array<{
|
||||
filePath: string;
|
||||
content: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface TemplateMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
logo: string;
|
||||
links: {
|
||||
github: string;
|
||||
website?: string;
|
||||
docs?: string;
|
||||
};
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of available templates from meta.json
|
||||
*/
|
||||
export async function fetchTemplatesList(
|
||||
baseUrl = "https://templates.dokploy.com",
|
||||
): Promise<TemplateMetadata[]> {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/meta.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch templates: ${response.statusText}`);
|
||||
}
|
||||
const templates = (await response.json()) as TemplateMetadata[];
|
||||
return templates.map((template) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
version: template.version,
|
||||
logo: template.logo,
|
||||
links: template.links,
|
||||
tags: template.tags,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates list:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a specific template's files
|
||||
*/
|
||||
export async function fetchTemplateFiles(
|
||||
templateId: string,
|
||||
baseUrl = "https://templates.dokploy.com",
|
||||
): Promise<{ config: CompleteTemplate; dockerCompose: string }> {
|
||||
try {
|
||||
// Fetch both files in parallel
|
||||
const [templateYmlResponse, dockerComposeResponse] = await Promise.all([
|
||||
fetch(`${baseUrl}/blueprints/${templateId}/template.toml`),
|
||||
fetch(`${baseUrl}/blueprints/${templateId}/docker-compose.yml`),
|
||||
]);
|
||||
|
||||
if (!templateYmlResponse.ok || !dockerComposeResponse.ok) {
|
||||
throw new Error("Template files not found");
|
||||
}
|
||||
|
||||
const [templateYml, dockerCompose] = await Promise.all([
|
||||
templateYmlResponse.text(),
|
||||
dockerComposeResponse.text(),
|
||||
]);
|
||||
|
||||
const config = parse(templateYml) as CompleteTemplate;
|
||||
|
||||
return { config, dockerCompose };
|
||||
} catch (error) {
|
||||
console.error(`Error fetching template ${templateId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
166
packages/server/src/templates/index.ts
Normal file
166
packages/server/src/templates/index.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { createHmac, randomBytes } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { fetchTemplateFiles } from "./github";
|
||||
|
||||
export interface Schema {
|
||||
serverIp: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export type DomainSchema = Pick<Domain, "host" | "port" | "serviceName"> & {
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export interface Template {
|
||||
envs: string[];
|
||||
mounts: Array<{
|
||||
filePath: string;
|
||||
content: string;
|
||||
}>;
|
||||
domains: DomainSchema[];
|
||||
}
|
||||
|
||||
export interface GenerateJWTOptions {
|
||||
length?: number;
|
||||
secret?: string;
|
||||
payload?: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
export const generateRandomDomain = ({
|
||||
serverIp,
|
||||
projectName,
|
||||
}: Schema): string => {
|
||||
const hash = randomBytes(3).toString("hex");
|
||||
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
|
||||
|
||||
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
|
||||
};
|
||||
|
||||
export const generateHash = (length = 8): string => {
|
||||
return randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex")
|
||||
.substring(0, length);
|
||||
};
|
||||
|
||||
export const generatePassword = (quantity = 16): string => {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let password = "";
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
password += characters.charAt(
|
||||
Math.floor(Math.random() * characters.length),
|
||||
);
|
||||
}
|
||||
return password.toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random base64 string from N random bytes
|
||||
* @param bytes Number of random bytes to generate before base64 encoding (default: 32)
|
||||
* @returns base64 encoded string of the random bytes
|
||||
*/
|
||||
export function generateBase64(bytes = 32): string {
|
||||
return randomBytes(bytes).toString("base64");
|
||||
}
|
||||
|
||||
function safeBase64(str: string): string {
|
||||
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
function objToJWTBase64(obj: any): string {
|
||||
return safeBase64(
|
||||
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
|
||||
);
|
||||
}
|
||||
|
||||
export function generateJwt(options: GenerateJWTOptions = {}): string {
|
||||
let { length, secret, payload = {} } = options;
|
||||
if (length) {
|
||||
return randomBytes(length).toString("hex");
|
||||
}
|
||||
const encodedHeader = objToJWTBase64({
|
||||
alg: "HS256",
|
||||
typ: "JWT",
|
||||
});
|
||||
if (!payload.iss) {
|
||||
payload.iss = "dokploy";
|
||||
}
|
||||
if (!payload.iat) {
|
||||
payload.iat = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
if (!payload.exp) {
|
||||
payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000);
|
||||
}
|
||||
const encodedPayload = objToJWTBase64({
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000),
|
||||
...payload,
|
||||
});
|
||||
if (!secret) {
|
||||
secret = randomBytes(32).toString("hex");
|
||||
}
|
||||
const signature = safeBase64(
|
||||
createHmac("SHA256", secret)
|
||||
.update(`${encodedHeader}.${encodedPayload}`)
|
||||
.digest("base64"),
|
||||
);
|
||||
|
||||
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a template's docker-compose.yml file
|
||||
* First tries to fetch from GitHub, falls back to local cache if fetch fails
|
||||
*/
|
||||
export const readTemplateComposeFile = async (id: string) => {
|
||||
// First try to fetch from GitHub
|
||||
try {
|
||||
const { dockerCompose } = await fetchTemplateFiles(id);
|
||||
|
||||
// Cache the file for future use
|
||||
const cwd = process.cwd();
|
||||
const templatePath = join(cwd, ".next", "templates", id);
|
||||
const composeFilePath = join(templatePath, "docker-compose.yml");
|
||||
|
||||
// Ensure the template directory exists
|
||||
if (!existsSync(templatePath)) {
|
||||
await mkdir(templatePath, { recursive: true });
|
||||
}
|
||||
|
||||
// Cache the file for future use
|
||||
await writeFile(composeFilePath, dockerCompose, "utf8");
|
||||
|
||||
return dockerCompose;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch template ${id} from GitHub:`, error);
|
||||
|
||||
// Try to use cached version as fallback
|
||||
const cwd = process.cwd();
|
||||
const composeFilePath = join(
|
||||
cwd,
|
||||
".next",
|
||||
"templates",
|
||||
id,
|
||||
"docker-compose.yml",
|
||||
);
|
||||
|
||||
if (existsSync(composeFilePath)) {
|
||||
console.warn(`Using cached version of template ${id}`);
|
||||
return await readFile(composeFilePath, "utf8");
|
||||
}
|
||||
|
||||
console.error(`Error: Template ${id} not found in GitHub or cache`);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Template ${id} not found or could not be fetched`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a template module from GitHub or local cache
|
||||
* First tries to fetch from GitHub, falls back to local cache if fetch fails
|
||||
*/
|
||||
328
packages/server/src/templates/processors.ts
Normal file
328
packages/server/src/templates/processors.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import type { Schema } from "./index";
|
||||
import {
|
||||
generateBase64,
|
||||
generateHash,
|
||||
generateJwt,
|
||||
generatePassword,
|
||||
generateRandomDomain,
|
||||
} from "./index";
|
||||
|
||||
/**
|
||||
* Domain configuration
|
||||
*/
|
||||
interface DomainConfig {
|
||||
serviceName: string;
|
||||
port: number;
|
||||
path?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount configuration
|
||||
*/
|
||||
interface MountConfig {
|
||||
filePath: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete template interface that includes both metadata and configuration
|
||||
*/
|
||||
export interface CompleteTemplate {
|
||||
metadata: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
version: string;
|
||||
logo: string;
|
||||
links: {
|
||||
github: string;
|
||||
website?: string;
|
||||
docs?: string;
|
||||
};
|
||||
};
|
||||
variables: Record<string, string>;
|
||||
config: {
|
||||
domains: DomainConfig[];
|
||||
env:
|
||||
| Record<string, string | boolean | number>
|
||||
| (string | Record<string, string | boolean | number>)[];
|
||||
mounts?: MountConfig[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processed template output
|
||||
*/
|
||||
export interface Template {
|
||||
domains: Array<DomainConfig>;
|
||||
envs: string[];
|
||||
mounts: MountConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a string value and replace variables
|
||||
*/
|
||||
export function processValue(
|
||||
value: string,
|
||||
variables: Record<string, string>,
|
||||
schema: Schema,
|
||||
): string {
|
||||
// First replace utility functions
|
||||
let processedValue = value?.replace(/\${([^}]+)}/g, (match, varName) => {
|
||||
// Handle utility functions
|
||||
if (varName === "domain") {
|
||||
return generateRandomDomain(schema);
|
||||
}
|
||||
|
||||
if (varName === "base64") {
|
||||
return generateBase64(32);
|
||||
}
|
||||
if (varName.startsWith("base64:")) {
|
||||
const length = Number.parseInt(varName.split(":")[1], 10) || 32;
|
||||
return generateBase64(length);
|
||||
}
|
||||
|
||||
if (varName.startsWith("password:")) {
|
||||
const length = Number.parseInt(varName.split(":")[1], 10) || 16;
|
||||
return generatePassword(length);
|
||||
}
|
||||
if (varName === "password") {
|
||||
return generatePassword(16);
|
||||
}
|
||||
|
||||
if (varName.startsWith("hash:")) {
|
||||
const length = Number.parseInt(varName.split(":")[1], 10) || 8;
|
||||
return generateHash(length);
|
||||
}
|
||||
if (varName === "hash") {
|
||||
return generateHash();
|
||||
}
|
||||
|
||||
if (varName === "uuid") {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
if (varName === "timestamp" || varName === "timestampms") {
|
||||
return Date.now().toString();
|
||||
}
|
||||
|
||||
if (varName === "timestamps") {
|
||||
return Math.round(Date.now() / 1000).toString();
|
||||
}
|
||||
|
||||
if (varName.startsWith("timestampms:")) {
|
||||
return new Date(varName.slice(12)).getTime().toString();
|
||||
}
|
||||
if (varName.startsWith("timestamps:")) {
|
||||
return Math.round(
|
||||
new Date(varName.slice(11)).getTime() / 1000,
|
||||
).toString();
|
||||
}
|
||||
|
||||
if (varName === "randomPort") {
|
||||
return Math.floor(Math.random() * 65535).toString();
|
||||
}
|
||||
|
||||
if (varName === "jwt") {
|
||||
return generateJwt();
|
||||
}
|
||||
|
||||
if (varName.startsWith("jwt:")) {
|
||||
const params: string[] = varName.split(":").slice(1);
|
||||
if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
|
||||
return generateJwt({ length: Number.parseInt(params[0], 10) });
|
||||
}
|
||||
let [secret, payload] = params;
|
||||
if (typeof payload === "string" && variables[payload]) {
|
||||
payload = variables[payload];
|
||||
}
|
||||
if (
|
||||
typeof payload === "string" &&
|
||||
payload.startsWith("{") &&
|
||||
payload.endsWith("}")
|
||||
) {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (e) {
|
||||
// If payload is not a valid JSON, invalid it
|
||||
payload = undefined;
|
||||
console.error("Invalid JWT payload", e);
|
||||
}
|
||||
}
|
||||
if (typeof payload !== "object") {
|
||||
payload = undefined;
|
||||
}
|
||||
return generateJwt({
|
||||
secret: secret ? variables[secret] || secret : undefined,
|
||||
payload: payload as any,
|
||||
});
|
||||
}
|
||||
|
||||
if (varName === "username") {
|
||||
return faker.internet.userName().toLowerCase();
|
||||
}
|
||||
|
||||
if (varName === "email") {
|
||||
return faker.internet.email().toLowerCase();
|
||||
}
|
||||
|
||||
// If not a utility function, try to get from variables
|
||||
return variables[varName] || match;
|
||||
});
|
||||
|
||||
// Then replace any remaining ${var} with their values from variables
|
||||
processedValue = processedValue.replace(/\${([^}]+)}/g, (match, varName) => {
|
||||
return variables[varName] || match;
|
||||
});
|
||||
|
||||
return processedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process variables in a template
|
||||
*/
|
||||
export function processVariables(
|
||||
template: CompleteTemplate,
|
||||
schema: Schema,
|
||||
): Record<string, string> {
|
||||
const variables: Record<string, string> = {};
|
||||
|
||||
// First pass: Process some variables that don't depend on other variables
|
||||
for (const [key, value] of Object.entries(template.variables)) {
|
||||
if (typeof value !== "string") continue;
|
||||
|
||||
if (value === "${domain}") {
|
||||
variables[key] = generateRandomDomain(schema);
|
||||
} else if (value.startsWith("${base64:")) {
|
||||
const match = value.match(/\${base64:(\d+)}/);
|
||||
const length = match?.[1] ? Number.parseInt(match[1], 10) : 32;
|
||||
variables[key] = generateBase64(length);
|
||||
} else if (value.startsWith("${password:")) {
|
||||
const match = value.match(/\${password:(\d+)}/);
|
||||
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
|
||||
variables[key] = generatePassword(length);
|
||||
} else if (value === "${hash}") {
|
||||
variables[key] = generateHash();
|
||||
} else if (value.startsWith("${hash:")) {
|
||||
const match = value.match(/\${hash:(\d+)}/);
|
||||
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;
|
||||
variables[key] = generateHash(length);
|
||||
} else {
|
||||
variables[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Process variables that reference other variables
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
variables[key] = processValue(value, variables, schema);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process domains in a template
|
||||
*/
|
||||
export function processDomains(
|
||||
template: CompleteTemplate,
|
||||
variables: Record<string, string>,
|
||||
schema: Schema,
|
||||
): Template["domains"] {
|
||||
if (
|
||||
!template?.config?.domains ||
|
||||
template.config.domains.length === 0 ||
|
||||
template.config.domains.every((domain) => !domain.serviceName)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return template?.config?.domains?.map((domain: DomainConfig) => ({
|
||||
...domain,
|
||||
host: domain.host
|
||||
? processValue(domain.host, variables, schema)
|
||||
: generateRandomDomain(schema),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process environment variables in a template
|
||||
*/
|
||||
export function processEnvVars(
|
||||
template: CompleteTemplate,
|
||||
variables: Record<string, string>,
|
||||
schema: Schema,
|
||||
): Template["envs"] {
|
||||
if (!template?.config?.env || Object.keys(template.config.env).length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle array of env vars
|
||||
if (Array.isArray(template.config.env)) {
|
||||
return template.config.env.map((env) => {
|
||||
if (typeof env === "string") {
|
||||
return processValue(env, variables, schema);
|
||||
}
|
||||
// Si es un objeto, asumimos que es un par clave-valor
|
||||
if (typeof env === "object" && env !== null) {
|
||||
const keys = Object.keys(env);
|
||||
if (keys.length > 0) {
|
||||
const key = keys[0];
|
||||
return `${key}=${env[key as keyof typeof env]}`;
|
||||
}
|
||||
}
|
||||
// Para valores primitivos (boolean, number)
|
||||
return String(env);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle object of env vars
|
||||
return Object.entries(template.config.env).map(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
const processedValue = processValue(value, variables, schema);
|
||||
return `${key}=${processedValue}`;
|
||||
}
|
||||
return `${key}=${value}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process mounts in a template
|
||||
*/
|
||||
export function processMounts(
|
||||
template: CompleteTemplate,
|
||||
variables: Record<string, string>,
|
||||
schema: Schema,
|
||||
): Template["mounts"] {
|
||||
if (
|
||||
!template?.config?.mounts ||
|
||||
template.config.mounts.length === 0 ||
|
||||
template.config.mounts.every((mount) => !mount.filePath && !mount.content)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return template?.config?.mounts?.map((mount: MountConfig) => ({
|
||||
filePath: processValue(mount.filePath, variables, schema),
|
||||
content: processValue(mount.content, variables, schema),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a complete template
|
||||
*/
|
||||
export function processTemplate(
|
||||
template: CompleteTemplate,
|
||||
schema: Schema,
|
||||
): Template {
|
||||
// First process variables as they might be referenced by other sections
|
||||
const variables = processVariables(template, schema);
|
||||
|
||||
return {
|
||||
domains: processDomains(template, variables, schema),
|
||||
envs: processEnvVars(template, variables, schema),
|
||||
mounts: processMounts(template, variables, schema),
|
||||
};
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
|
||||
export interface Schema {
|
||||
serverIp: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export type DomainSchema = Pick<Domain, "host" | "port" | "serviceName">;
|
||||
|
||||
export interface Template {
|
||||
envs?: string[];
|
||||
mounts?: {
|
||||
filePath: string;
|
||||
content?: string;
|
||||
}[];
|
||||
domains?: DomainSchema[];
|
||||
}
|
||||
|
||||
export const generateRandomDomain = ({
|
||||
serverIp,
|
||||
projectName,
|
||||
}: Schema): string => {
|
||||
const hash = randomBytes(3).toString("hex");
|
||||
const slugIp = serverIp.replaceAll(".", "-");
|
||||
|
||||
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
|
||||
};
|
||||
|
||||
export const generateHash = (projectName: string, quantity = 3): string => {
|
||||
const hash = randomBytes(quantity).toString("hex");
|
||||
return `${projectName}-${hash}`;
|
||||
};
|
||||
|
||||
export const generatePassword = (quantity = 16): string => {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let password = "";
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
password += characters.charAt(
|
||||
Math.floor(Math.random() * characters.length),
|
||||
);
|
||||
}
|
||||
return password.toLowerCase();
|
||||
};
|
||||
|
||||
export const generateBase64 = (bytes = 32): string => {
|
||||
return randomBytes(bytes).toString("base64");
|
||||
};
|
||||
|
||||
export const readTemplateComposeFile = async (id: string) => {
|
||||
const cwd = process.cwd();
|
||||
const composeFile = await readFile(
|
||||
join(cwd, ".next", "templates", id, "docker-compose.yml"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
return composeFile;
|
||||
};
|
||||
44
packages/server/src/types/template.ts
Normal file
44
packages/server/src/types/template.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface TemplateConfig {
|
||||
variables: Record<string, string>;
|
||||
domains: Array<{
|
||||
serviceName: string;
|
||||
port: number;
|
||||
path?: string;
|
||||
host?: string;
|
||||
}>;
|
||||
env: Record<string, string>;
|
||||
mounts: Array<{
|
||||
filePath: string;
|
||||
content: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
metadata: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
logo: string;
|
||||
links: {
|
||||
github: string;
|
||||
website?: string;
|
||||
docs?: string;
|
||||
};
|
||||
tags: string[];
|
||||
};
|
||||
variables: Record<string, string>;
|
||||
config: {
|
||||
domains: Array<{
|
||||
serviceName: string;
|
||||
port: number;
|
||||
path?: string;
|
||||
host?: string;
|
||||
}>;
|
||||
env: Record<string, string>;
|
||||
mounts?: Array<{
|
||||
filePath: string;
|
||||
content: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { findAdmin } from "@dokploy/server/services/admin";
|
||||
import { updateUser } from "@dokploy/server/services/user";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
|
||||
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
|
||||
|
||||
@@ -37,7 +37,8 @@ export const startLogCleanup = async (
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
console.error("Error starting log cleanup:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
88
packages/server/src/utils/backups/compose.ts
Normal file
88
packages/server/src/utils/backups/compose.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runComposeBackup = async (
|
||||
compose: Compose,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { projectId, name } = compose;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, databaseType } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Compose Backup",
|
||||
description: "Compose Backup",
|
||||
});
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, backupCommand);
|
||||
} else {
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: getDatabaseType(databaseType),
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: getDatabaseType(databaseType),
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseType = (databaseType: BackupSchedule["databaseType"]) => {
|
||||
if (databaseType === "mongo") {
|
||||
return "mongodb";
|
||||
}
|
||||
if (databaseType === "postgres") {
|
||||
return "postgres";
|
||||
}
|
||||
if (databaseType === "mariadb") {
|
||||
return "mariadb";
|
||||
}
|
||||
if (databaseType === "mysql") {
|
||||
return "mysql";
|
||||
}
|
||||
return "mongodb";
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from "node:path";
|
||||
import { getAllServers } from "@dokploy/server/services/server";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
@@ -7,19 +8,29 @@ import {
|
||||
cleanUpUnusedImages,
|
||||
} from "../docker/utils";
|
||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
import { findAdmin } from "../../services/admin";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
console.log("Setting up cron jobs....");
|
||||
|
||||
const admin = await findAdmin();
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (admin?.user.enableDockerCleanup) {
|
||||
if (!admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.user.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
@@ -51,126 +62,68 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const pgs = await db.query.postgres.findMany({
|
||||
const backups = await db.query.backups.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
user: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
for (const pg of pgs) {
|
||||
for (const backup of pg.backups) {
|
||||
const { schedule, backupId, enabled, database } = backup;
|
||||
if (enabled) {
|
||||
|
||||
for (const backup of backups) {
|
||||
try {
|
||||
if (backup.enabled) {
|
||||
scheduleBackup(backup);
|
||||
console.log(
|
||||
`[Backup] Postgres DB ${pg.name} for ${database} Activated`,
|
||||
`[Backup] ${backup.databaseType} Enabled with cron: [${backup.schedule}]`,
|
||||
);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
runPostgresBackup(pg, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mariadbs = await db.query.mariadb.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const maria of mariadbs) {
|
||||
for (const backup of maria.backups) {
|
||||
const { schedule, backupId, enabled, database } = backup;
|
||||
if (enabled) {
|
||||
console.log(
|
||||
`[Backup] MariaDB DB ${maria.name} for ${database} Activated`,
|
||||
);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
await runMariadbBackup(maria, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbs = await db.query.mongo.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const mongo of mongodbs) {
|
||||
for (const backup of mongo.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
console.log(`[Backup] MongoDB DB ${mongo.name} Activated`);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
await runMongoBackup(mongo, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mysqls = await db.query.mysql.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const mysql of mysqls) {
|
||||
for (const backup of mysql.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
console.log(`[Backup] MySQL DB ${mysql.name} Activated`);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
await runMySqlBackup(mysql, backup);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Backup] ${backup.databaseType} Error`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
export const keepLatestNBackups = async (
|
||||
backup: BackupSchedule,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
// 0 also immediately returns which is good as the empty "keep latest" field in the UI
|
||||
// is saved as 0 in the database
|
||||
if (!backup.keepLatestCount) return;
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(backup.destination);
|
||||
const backupFilesPath = path.join(
|
||||
`:s3:${backup.destination.bucket}`,
|
||||
backup.prefix,
|
||||
);
|
||||
|
||||
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
|
||||
// when we pipe the above command with this one, we only get the list of files we want to delete
|
||||
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
|
||||
// this command deletes the files
|
||||
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}/{}
|
||||
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}/{}`;
|
||||
|
||||
const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, rcloneCommand);
|
||||
} else {
|
||||
await execAsync(rcloneCommand);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databasePassword, databaseUser, projectId, name } = mariadb;
|
||||
const { projectId, name } = mariadb;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "MariaDB Backup",
|
||||
description: "MariaDB Backup",
|
||||
});
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
if (mariadb.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
mariadb.serverId,
|
||||
appName,
|
||||
);
|
||||
const mariadbDumpCommand = `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
mariadb.serverId,
|
||||
`${mariadbDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
await execAsyncRemote(mariadb.serverId, backupCommand);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
const mariadbDumpCommand = `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
||||
|
||||
await execAsync(`${mariadbDumpCommand} | ${rcloneCommand}`);
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -50,7 +48,9 @@ export const runMariadbBackup = async (
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -61,7 +61,9 @@ export const runMariadbBackup = async (
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { appName, databasePassword, databaseUser, projectId, name } = mongo;
|
||||
const { projectId, name } = mongo;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "MongoDB Backup",
|
||||
description: "MongoDB Backup",
|
||||
});
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
if (mongo.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
mongo.serverId,
|
||||
appName,
|
||||
);
|
||||
const mongoDumpCommand = `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase=admin --gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
mongo.serverId,
|
||||
`${mongoDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
|
||||
if (mongo.serverId) {
|
||||
await execAsyncRemote(mongo.serverId, backupCommand);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
const mongoDumpCommand = `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase=admin --gzip"`;
|
||||
await execAsync(`${mongoDumpCommand} | ${rcloneCommand}`);
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -47,7 +46,9 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
databaseType: "mongodb",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -58,8 +59,9 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// mongorestore -d monguito -u mongo -p Bqh7AQl-PRbnBu --authenticationDatabase admin --gzip --archive=2024-04-13T05:03:58.937Z.dump.gz
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { MySql } from "@dokploy/server/services/mysql";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { appName, databaseRootPassword, projectId, name } = mysql;
|
||||
const { projectId, name } = mysql;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "MySQL Backup",
|
||||
description: "MySQL Backup",
|
||||
});
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
|
||||
if (mysql.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
mysql.serverId,
|
||||
appName,
|
||||
);
|
||||
const mysqlDumpCommand = `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
mysql.serverId,
|
||||
`${mysqlDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
await execAsyncRemote(mysql.serverId, backupCommand);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
const mysqlDumpCommand = `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
||||
|
||||
await execAsync(`${mysqlDumpCommand} | ${rcloneCommand}`);
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
@@ -46,7 +47,9 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
databaseType: "mysql",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -57,7 +60,9 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databaseUser, name, projectId } = postgres;
|
||||
const { name, projectId } = postgres;
|
||||
const project = await findProjectById(projectId);
|
||||
|
||||
const { prefix, database } = backup;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Initializing Backup",
|
||||
description: "Initializing Backup",
|
||||
});
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
if (postgres.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
postgres.serverId,
|
||||
appName,
|
||||
);
|
||||
const pgDumpCommand = `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
postgres.serverId,
|
||||
`${pgDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
await execAsyncRemote(postgres.serverId, backupCommand);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
|
||||
const pgDumpCommand = `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
||||
await execAsync(`${pgDumpCommand} | ${rcloneCommand}`);
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -50,7 +50,10 @@ export const runPostgresBackup = async (
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
@@ -60,12 +63,12 @@ export const runPostgresBackup = async (
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
// Restore
|
||||
// /Applications/pgAdmin 4.app/Contents/SharedSupport/pg_restore --host "localhost" --port "5432" --username "mauricio" --no-password --dbname "postgres" --verbose "/Users/mauricio/Downloads/_databases_2024-04-12T07_02_05.234Z.sql"
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
import { logger } from "@dokploy/server/lib/logger";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { keepLatestNBackups } from ".";
|
||||
import { runComposeBackup } from "./compose";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
import { runWebServerBackup } from "./web-server";
|
||||
|
||||
export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
const { schedule, backupId, databaseType, postgres, mysql, mongo, mariadb } =
|
||||
backup;
|
||||
const {
|
||||
schedule,
|
||||
backupId,
|
||||
databaseType,
|
||||
postgres,
|
||||
mysql,
|
||||
mongo,
|
||||
mariadb,
|
||||
compose,
|
||||
} = backup;
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
if (databaseType === "postgres" && postgres) {
|
||||
await runPostgresBackup(postgres, backup);
|
||||
} else if (databaseType === "mysql" && mysql) {
|
||||
await runMySqlBackup(mysql, backup);
|
||||
} else if (databaseType === "mongo" && mongo) {
|
||||
await runMongoBackup(mongo, backup);
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
if (backup.backupType === "database") {
|
||||
if (databaseType === "postgres" && postgres) {
|
||||
await runPostgresBackup(postgres, backup);
|
||||
await keepLatestNBackups(backup, postgres.serverId);
|
||||
} else if (databaseType === "mysql" && mysql) {
|
||||
await runMySqlBackup(mysql, backup);
|
||||
await keepLatestNBackups(backup, mysql.serverId);
|
||||
} else if (databaseType === "mongo" && mongo) {
|
||||
await runMongoBackup(mongo, backup);
|
||||
await keepLatestNBackups(backup, mongo.serverId);
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb.serverId);
|
||||
} else if (databaseType === "web-server") {
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
}
|
||||
} else if (backup.backupType === "compose" && compose) {
|
||||
await runComposeBackup(compose, backup);
|
||||
await keepLatestNBackups(backup, compose.serverId);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -27,6 +51,13 @@ export const removeScheduleBackup = (backupId: string) => {
|
||||
currentJob?.cancel();
|
||||
};
|
||||
|
||||
export const normalizeS3Path = (prefix: string) => {
|
||||
// Trim whitespace and remove leading/trailing slashes
|
||||
const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, "");
|
||||
// Return empty string if prefix is empty, otherwise append trailing slash
|
||||
return normalizedPrefix ? `${normalizedPrefix}/` : "";
|
||||
};
|
||||
|
||||
export const getS3Credentials = (destination: Destination) => {
|
||||
const { accessKey, secretAccessKey, region, endpoint, provider } =
|
||||
destination;
|
||||
@@ -45,3 +76,195 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
|
||||
export const getPostgresBackupCommand = (
|
||||
database: string,
|
||||
databaseUser: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
||||
};
|
||||
|
||||
export const getMariadbBackupCommand = (
|
||||
database: string,
|
||||
databaseUser: string,
|
||||
databasePassword: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
||||
};
|
||||
|
||||
export const getMysqlBackupCommand = (
|
||||
database: string,
|
||||
databasePassword: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databasePassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
||||
};
|
||||
|
||||
export const getMongoBackupCommand = (
|
||||
database: string,
|
||||
databaseUser: string,
|
||||
databasePassword: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
||||
};
|
||||
|
||||
export const getServiceContainerCommand = (appName: string) => {
|
||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
||||
};
|
||||
|
||||
export const getComposeContainerCommand = (
|
||||
appName: string,
|
||||
serviceName: string,
|
||||
composeType: "stack" | "docker-compose" | undefined,
|
||||
) => {
|
||||
if (composeType === "stack") {
|
||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.stack.namespace=${appName}" --filter "label=com.docker.swarm.service.name=${appName}_${serviceName}" | head -n 1`;
|
||||
}
|
||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.compose.project=${appName}" --filter "label=com.docker.compose.service=${serviceName}" | head -n 1`;
|
||||
};
|
||||
|
||||
const getContainerSearchCommand = (backup: BackupSchedule) => {
|
||||
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
|
||||
backup;
|
||||
|
||||
if (backupType === "database") {
|
||||
const appName =
|
||||
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
|
||||
return getServiceContainerCommand(appName || "");
|
||||
}
|
||||
if (backupType === "compose") {
|
||||
const { appName, composeType } = compose || {};
|
||||
return getComposeContainerCommand(
|
||||
appName || "",
|
||||
serviceName || "",
|
||||
composeType,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const generateBackupCommand = (backup: BackupSchedule) => {
|
||||
const { backupType, databaseType } = backup;
|
||||
switch (databaseType) {
|
||||
case "postgres": {
|
||||
const postgres = backup.postgres;
|
||||
if (backupType === "database" && postgres) {
|
||||
return getPostgresBackupCommand(backup.database, postgres.databaseUser);
|
||||
}
|
||||
if (backupType === "compose" && backup.metadata?.postgres) {
|
||||
return getPostgresBackupCommand(
|
||||
backup.database,
|
||||
backup.metadata.postgres.databaseUser,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mysql": {
|
||||
const mysql = backup.mysql;
|
||||
if (backupType === "database" && mysql) {
|
||||
return getMysqlBackupCommand(
|
||||
backup.database,
|
||||
mysql.databaseRootPassword,
|
||||
);
|
||||
}
|
||||
if (backupType === "compose" && backup.metadata?.mysql) {
|
||||
return getMysqlBackupCommand(
|
||||
backup.database,
|
||||
backup.metadata?.mysql?.databaseRootPassword || "",
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mariadb": {
|
||||
const mariadb = backup.mariadb;
|
||||
if (backupType === "database" && mariadb) {
|
||||
return getMariadbBackupCommand(
|
||||
backup.database,
|
||||
mariadb.databaseUser,
|
||||
mariadb.databasePassword,
|
||||
);
|
||||
}
|
||||
if (backupType === "compose" && backup.metadata?.mariadb) {
|
||||
return getMariadbBackupCommand(
|
||||
backup.database,
|
||||
backup.metadata.mariadb.databaseUser,
|
||||
backup.metadata.mariadb.databasePassword,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mongo": {
|
||||
const mongo = backup.mongo;
|
||||
if (backupType === "database" && mongo) {
|
||||
return getMongoBackupCommand(
|
||||
backup.database,
|
||||
mongo.databaseUser,
|
||||
mongo.databasePassword,
|
||||
);
|
||||
}
|
||||
if (backupType === "compose" && backup.metadata?.mongo) {
|
||||
return getMongoBackupCommand(
|
||||
backup.database,
|
||||
backup.metadata.mongo.databaseUser,
|
||||
backup.metadata.mongo.databasePassword,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Database type not supported: ${databaseType}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getBackupCommand = (
|
||||
backup: BackupSchedule,
|
||||
rcloneCommand: string,
|
||||
logPath: string,
|
||||
) => {
|
||||
const containerSearch = getContainerSearchCommand(backup);
|
||||
const backupCommand = generateBackupCommand(backup);
|
||||
|
||||
logger.info(
|
||||
{
|
||||
containerSearch,
|
||||
backupCommand,
|
||||
rcloneCommand,
|
||||
logPath,
|
||||
},
|
||||
`Executing backup command: ${backup.databaseType} ${backup.backupType}`,
|
||||
);
|
||||
|
||||
return `
|
||||
set -eo pipefail;
|
||||
echo "[$(date)] Starting backup process..." >> ${logPath};
|
||||
echo "[$(date)] Executing backup command..." >> ${logPath};
|
||||
CONTAINER_ID=$(${containerSearch})
|
||||
|
||||
if [ -z "$CONTAINER_ID" ]; then
|
||||
echo "[$(date)] ❌ Error: Container not found" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "[$(date)] Container Up: $CONTAINER_ID" >> ${logPath};
|
||||
|
||||
# Run the backup command and capture the exit status
|
||||
BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || {
|
||||
echo "[$(date)] ❌ Error: Backup failed" >> ${logPath};
|
||||
echo "Error: $BACKUP_OUTPUT" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
|
||||
echo "[$(date)] ✅ backup completed successfully" >> ${logPath};
|
||||
echo "[$(date)] Starting upload to S3..." >> ${logPath};
|
||||
|
||||
# Run the upload command and capture the exit status
|
||||
UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || {
|
||||
echo "[$(date)] ❌ Error: Upload to S3 failed" >> ${logPath};
|
||||
echo "Error: $UPLOAD_OUTPUT" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
|
||||
echo "[$(date)] ✅ Upload to S3 completed successfully" >> ${logPath};
|
||||
echo "Backup done ✅" >> ${logPath};
|
||||
`;
|
||||
};
|
||||
|
||||
106
packages/server/src/utils/backups/web-server.ts
Normal file
106
packages/server/src/utils/backups/web-server.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
if (IS_CLOUD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Web Server Backup",
|
||||
description: "Web Server Backup",
|
||||
});
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
|
||||
try {
|
||||
const destination = await findDestinationById(backup.destinationId);
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const { BASE_PATH } = paths();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
||||
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
||||
const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
|
||||
|
||||
try {
|
||||
await execAsync(`mkdir -p ${tempDir}/filesystem`);
|
||||
|
||||
// First get the container ID
|
||||
const { stdout: containerId } = await execAsync(
|
||||
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
|
||||
);
|
||||
|
||||
if (!containerId) {
|
||||
writeStream.write("Dokploy postgres container not found❌\n");
|
||||
writeStream.end();
|
||||
throw new Error("Dokploy postgres container not found");
|
||||
}
|
||||
|
||||
writeStream.write(`Dokploy postgres container ID: ${containerId}\n`);
|
||||
|
||||
const postgresContainerId = containerId.trim();
|
||||
|
||||
// First dump the database inside the container
|
||||
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
|
||||
writeStream.write(`Running dump command: ${dumpCommand}\n`);
|
||||
await execAsync(dumpCommand);
|
||||
|
||||
// Then copy the file from the container to host
|
||||
const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
|
||||
writeStream.write(`Copying database dump: ${copyCommand}\n`);
|
||||
await execAsync(copyCommand);
|
||||
|
||||
// Clean up the temp file in the container
|
||||
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
|
||||
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
|
||||
await execAsync(cleanupCommand);
|
||||
|
||||
await execAsync(
|
||||
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
);
|
||||
|
||||
writeStream.write("Copied filesystem to temp directory\n");
|
||||
|
||||
await execAsync(
|
||||
// Zip all .sql files since we created more than one
|
||||
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
|
||||
);
|
||||
|
||||
writeStream.write("Zipped database and filesystem\n");
|
||||
|
||||
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
|
||||
writeStream.write(`Running command: ${uploadCommand}\n`);
|
||||
await execAsync(uploadCommand);
|
||||
writeStream.write("Uploaded backup to S3 ✅\n");
|
||||
writeStream.end();
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
return true;
|
||||
} finally {
|
||||
try {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
} catch (cleanupError) {
|
||||
console.error("Cleanup error:", cleanupError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Backup error:", error);
|
||||
writeStream.write("Backup error❌\n");
|
||||
writeStream.write(
|
||||
error instanceof Error ? error.message : "Unknown error\n",
|
||||
);
|
||||
writeStream.end();
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -35,7 +35,7 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
|
||||
if (compose.isolatedDeployment) {
|
||||
await execAsync(
|
||||
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}`,
|
||||
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,9 +190,10 @@ const createEnvFile = (compose: ComposeNested) => {
|
||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
let envContent = env || "";
|
||||
let envContent = `APP_NAME=${appName}\n`;
|
||||
envContent += env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
@@ -219,9 +220,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
|
||||
let envContent = env || "";
|
||||
let envContent = `APP_NAME=${appName}\n`;
|
||||
envContent += env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
|
||||
@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
||||
application;
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
publishDirectory,
|
||||
buildArgs,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
} = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
try {
|
||||
const image = `${appName}`;
|
||||
@@ -29,6 +35,10 @@ export const buildCustomDocker = async (
|
||||
|
||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||
|
||||
if (cleanCache) {
|
||||
commandArgs.push("--no-cache");
|
||||
}
|
||||
|
||||
if (dockerBuildStage) {
|
||||
commandArgs.push("--target", dockerBuildStage);
|
||||
}
|
||||
@@ -65,8 +75,14 @@ export const getDockerCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
||||
application;
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
publishDirectory,
|
||||
buildArgs,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
} = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
@@ -88,8 +104,12 @@ export const getDockerCommand = (
|
||||
commandArgs.push("--target", dockerBuildStage);
|
||||
}
|
||||
|
||||
if (cleanCache) {
|
||||
commandArgs.push("--no-cache");
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
commandArgs.push("--build-arg", `'${arg}'`);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -9,7 +9,7 @@ export const buildHeroku = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
@@ -29,6 +29,10 @@ export const buildHeroku = async (
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--clear-cache");
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
@@ -44,7 +48,7 @@ export const getHerokuCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const { env, appName, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
@@ -61,6 +65,10 @@ export const getHerokuCommand = (
|
||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||
];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--clear-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
@@ -184,6 +183,7 @@ export const mechanizeDockerContainer = async (
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
PublishMode: port.publishMode,
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
@@ -195,6 +195,7 @@ export const mechanizeDockerContainer = async (
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
@@ -203,26 +204,30 @@ export const mechanizeDockerContainer = async (
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageName = (application: ApplicationNested) => {
|
||||
const { appName, sourceType, dockerImage, registry } = application;
|
||||
|
||||
const imageName = `${appName}:latest`;
|
||||
if (sourceType === "docker") {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
}
|
||||
|
||||
if (registry) {
|
||||
return join(registry.registryUrl, registry.imagePrefix || "", appName);
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
return registryTag;
|
||||
}
|
||||
|
||||
return `${appName}:latest`;
|
||||
return imageName;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
export const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType, registryUrl } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, publishDirectory } = application;
|
||||
const { env, appName, publishDirectory, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
@@ -32,6 +32,10 @@ export const buildNixpacks = async (
|
||||
try {
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--no-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
@@ -91,7 +95,7 @@ export const getNixpacksCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName, publishDirectory } = application;
|
||||
const { env, appName, publishDirectory, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
@@ -102,6 +106,10 @@ export const getNixpacksCommand = (
|
||||
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--no-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const buildPaketo = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
@@ -24,6 +24,10 @@ export const buildPaketo = async (
|
||||
"paketobuildpacks/builder-jammy-full",
|
||||
];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--clear-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
@@ -43,7 +47,7 @@ export const getPaketoCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const { env, appName, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
@@ -60,6 +64,10 @@ export const getPaketoCommand = (
|
||||
"paketobuildpacks/builder-jammy-full",
|
||||
];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--clear-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
const calculateSecretsHash = (envVariables: string[]): string => {
|
||||
const hash = createHash("sha256");
|
||||
for (const env of envVariables.sort()) {
|
||||
hash.update(env);
|
||||
}
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const buildRailpack = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
@@ -17,32 +30,80 @@ export const buildRailpack = async (
|
||||
);
|
||||
|
||||
try {
|
||||
// Ensure buildkit container is running, create if it doesn't exist
|
||||
await execAsync(
|
||||
"docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit",
|
||||
"docker buildx create --use --name builder-containerd --driver docker-container || true",
|
||||
);
|
||||
|
||||
// Build the application using railpack
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
await execAsync("docker buildx use builder-containerd");
|
||||
|
||||
// Add environment variables
|
||||
// First prepare the build plan and info
|
||||
const prepareArgs = [
|
||||
"prepare",
|
||||
buildAppDirectory,
|
||||
"--plan-out",
|
||||
`${buildAppDirectory}/railpack-plan.json`,
|
||||
"--info-out",
|
||||
`${buildAppDirectory}/railpack-info.json`,
|
||||
];
|
||||
|
||||
// Add environment variables to prepare command
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
prepareArgs.push("--env", env);
|
||||
}
|
||||
|
||||
// Run prepare command
|
||||
await spawnAsync("railpack", prepareArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate secrets hash for layer invalidation
|
||||
const secretsHash = calculateSecretsHash(envVariables);
|
||||
|
||||
// Build with BuildKit using the Railpack frontend
|
||||
const cacheKey = cleanCache ? nanoid(10) : undefined;
|
||||
const buildArgs = [
|
||||
"buildx",
|
||||
"build",
|
||||
...(cacheKey
|
||||
? [
|
||||
"--build-arg",
|
||||
`secrets-hash=${secretsHash}`,
|
||||
"--build-arg",
|
||||
`cache-key=${cacheKey}`,
|
||||
]
|
||||
: []),
|
||||
"--build-arg",
|
||||
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
|
||||
"-f",
|
||||
`${buildAppDirectory}/railpack-plan.json`,
|
||||
"--output",
|
||||
`type=docker,name=${appName}`,
|
||||
];
|
||||
|
||||
// Add secrets properly formatted
|
||||
const env: { [key: string]: string } = {};
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
buildArgs.push(buildAppDirectory);
|
||||
|
||||
await spawnAsync(
|
||||
"railpack",
|
||||
args,
|
||||
"docker",
|
||||
buildArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
BUILDKIT_HOST: "docker-container://buildkit",
|
||||
},
|
||||
env: { ...process.env, ...env },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -56,32 +117,84 @@ export const getRailpackCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
// Build the application using railpack
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
// Prepare command
|
||||
const prepareArgs = [
|
||||
"prepare",
|
||||
buildAppDirectory,
|
||||
"--plan-out",
|
||||
`${buildAppDirectory}/railpack-plan.json`,
|
||||
"--info-out",
|
||||
`${buildAppDirectory}/railpack-info.json`,
|
||||
];
|
||||
|
||||
// Add environment variables
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
prepareArgs.push("--env", `'${env}'`);
|
||||
}
|
||||
|
||||
const command = `railpack ${args.join(" ")}`;
|
||||
// Calculate secrets hash for layer invalidation
|
||||
const secretsHash = calculateSecretsHash(envVariables);
|
||||
|
||||
const cacheKey = cleanCache ? nanoid(10) : undefined;
|
||||
// Build command
|
||||
const buildArgs = [
|
||||
"buildx",
|
||||
"build",
|
||||
...(cacheKey
|
||||
? [
|
||||
"--build-arg",
|
||||
`secrets-hash=${secretsHash}`,
|
||||
"--build-arg",
|
||||
`cache-key=${cacheKey}`,
|
||||
]
|
||||
: []),
|
||||
"--build-arg",
|
||||
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
|
||||
"-f",
|
||||
`${buildAppDirectory}/railpack-plan.json`,
|
||||
"--output",
|
||||
`type=docker,name=${appName}`,
|
||||
];
|
||||
|
||||
// Add secrets properly formatted
|
||||
const exportEnvs = [];
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
exportEnvs.push(`export ${key}='${value}'`);
|
||||
}
|
||||
}
|
||||
|
||||
buildArgs.push(buildAppDirectory);
|
||||
|
||||
const bashCommand = `
|
||||
echo "Building with Railpack..." >> "${logPath}";
|
||||
docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit;
|
||||
export BUILDKIT_HOST=docker-container://buildkit;
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Railpack build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Railpack build completed." >> ${logPath};
|
||||
`;
|
||||
# Ensure we have a builder with containerd
|
||||
docker buildx create --use --name builder-containerd --driver docker-container || true
|
||||
docker buildx use builder-containerd
|
||||
|
||||
echo "Preparing Railpack build plan..." >> "${logPath}";
|
||||
railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Railpack prepare failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Railpack prepare completed." >> ${logPath};
|
||||
|
||||
echo "Building with Railpack frontend..." >> "${logPath}";
|
||||
# Export environment variables for secrets
|
||||
${exportEnvs.join("\n")}
|
||||
docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Railpack build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Railpack build completed." >> ${logPath};
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
@@ -7,24 +7,67 @@ import type { ApplicationNested } from ".";
|
||||
import { createFile, getCreateFileCommand } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
|
||||
const nginxSpaConfig = `
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stderr;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const buildStatic = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { publishDirectory } = application;
|
||||
const { publishDirectory, isStaticSpa } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
if (isStaticSpa) {
|
||||
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
|
||||
}
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
".dockerignore",
|
||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||
);
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
'CMD ["nginx", "-g", "daemon off;"]',
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
".dockerignore",
|
||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||
);
|
||||
|
||||
await buildCustomDocker(
|
||||
{
|
||||
...application,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import path, { join } from "node:path";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
@@ -13,19 +12,22 @@ export const uploadImage = async (
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
||||
// For ghcr.io: ghcr.io/username/image:tag
|
||||
// For docker.io: docker.io/username/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`,
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL} | ${registryTag}\n`,
|
||||
);
|
||||
const loginCommand = spawnAsync(
|
||||
"docker",
|
||||
@@ -67,15 +69,16 @@ export const uploadImageRemoteCommand = (
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
const command = `
|
||||
|
||||
@@ -31,7 +31,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
mounts,
|
||||
} = mariadb;
|
||||
|
||||
const defaultMariadbEnv = `MARIADB_DATABASE=${databaseName}\nMARIADB_USER=${databaseUser}\nMARIADB_PASSWORD=${databasePassword}\nMARIADB_ROOT_PASSWORD=${databaseRootPassword}${
|
||||
const defaultMariadbEnv = `MARIADB_DATABASE="${databaseName}"\nMARIADB_USER="${databaseUser}"\nMARIADB_PASSWORD="${databasePassword}"\nMARIADB_ROOT_PASSWORD="${databaseRootPassword}"${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
@@ -98,7 +98,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ fi
|
||||
|
||||
${command ?? "wait $MONGOD_PID"}`;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME="${databaseUser}"\nMONGO_INITDB_ROOT_PASSWORD="${databasePassword}"${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
|
||||
@@ -152,7 +152,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,10 +34,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
|
||||
const defaultMysqlEnv =
|
||||
databaseUser !== "root"
|
||||
? `MYSQL_USER=${databaseUser}\nMYSQL_DATABASE=${databaseName}\nMYSQL_PASSWORD=${databasePassword}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
|
||||
? `MYSQL_USER="${databaseUser}"\nMYSQL_DATABASE="${databaseName}"\nMYSQL_PASSWORD="${databasePassword}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${
|
||||
env ? `\n${env}` : ""
|
||||
}`
|
||||
: `MYSQL_DATABASE=${databaseName}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
|
||||
: `MYSQL_DATABASE="${databaseName}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
@@ -104,7 +104,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
mounts,
|
||||
} = postgres;
|
||||
|
||||
const defaultPostgresEnv = `POSTGRES_DB=${databaseName}\nPOSTGRES_USER=${databaseUser}\nPOSTGRES_PASSWORD=${databasePassword}${
|
||||
const defaultPostgresEnv = `POSTGRES_DB="${databaseName}"\nPOSTGRES_USER="${databaseUser}"\nPOSTGRES_PASSWORD="${databasePassword}"${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
|
||||
99
packages/server/src/utils/databases/rebuild.ts
Normal file
99
packages/server/src/utils/databases/rebuild.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
redis,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { deployMariadb } from "@dokploy/server/services/mariadb";
|
||||
import { deployMongo } from "@dokploy/server/services/mongo";
|
||||
import { deployMySql } from "@dokploy/server/services/mysql";
|
||||
import { deployPostgres } from "@dokploy/server/services/postgres";
|
||||
import { deployRedis } from "@dokploy/server/services/redis";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { removeService } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
|
||||
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
|
||||
export const rebuildDatabase = async (
|
||||
databaseId: string,
|
||||
type: DatabaseType,
|
||||
) => {
|
||||
const database = await findDatabaseById(databaseId, type);
|
||||
|
||||
if (!database) {
|
||||
throw new Error("Database not found");
|
||||
}
|
||||
|
||||
await removeService(database.appName, database.serverId);
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
|
||||
for (const mount of database.mounts) {
|
||||
if (mount.type === "volume") {
|
||||
const command = `docker volume rm ${mount?.volumeName} --force`;
|
||||
if (database.serverId) {
|
||||
await execAsyncRemote(database.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
} else if (type === "mariadb") {
|
||||
await deployMariadb(databaseId);
|
||||
} else if (type === "mongo") {
|
||||
await deployMongo(databaseId);
|
||||
} else if (type === "redis") {
|
||||
await deployRedis(databaseId);
|
||||
}
|
||||
};
|
||||
|
||||
const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mariadb") {
|
||||
return await db.query.mariadb.findFirst({
|
||||
where: eq(mariadb.mariadbId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mongo") {
|
||||
return await db.query.mongo.findFirst({
|
||||
where: eq(mongo.mongoId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "redis") {
|
||||
return await db.query.redis.findFirst({
|
||||
where: eq(redis.redisId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -28,7 +28,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
mounts,
|
||||
} = redis;
|
||||
|
||||
const defaultRedisEnv = `REDIS_PASSWORD=${databasePassword}${
|
||||
const defaultRedisEnv = `REDIS_PASSWORD="${databasePassword}"${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
@@ -95,7 +95,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
||||
import { generateRandomHash } from "./compose";
|
||||
import { addSuffixToAllVolumes } from "./compose/volume";
|
||||
import { generateRandomHash } from "./compose";
|
||||
import type { ComposeSpecification } from "./types";
|
||||
|
||||
export const addAppNameToPreventCollision = (
|
||||
composeData: ComposeSpecification,
|
||||
appName: string,
|
||||
isolatedDeploymentsVolume: boolean,
|
||||
): ComposeSpecification => {
|
||||
let updatedComposeData = { ...composeData };
|
||||
|
||||
updatedComposeData = addAppNameToAllServiceNames(updatedComposeData, appName);
|
||||
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName);
|
||||
if (isolatedDeploymentsVolume) {
|
||||
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName);
|
||||
}
|
||||
return updatedComposeData;
|
||||
};
|
||||
|
||||
@@ -29,6 +32,7 @@ export const randomizeIsolatedDeploymentComposeFile = async (
|
||||
const newComposeFile = addAppNameToPreventCollision(
|
||||
composeData,
|
||||
randomSuffix,
|
||||
compose.isolatedDeploymentsVolume,
|
||||
);
|
||||
|
||||
return dump(newComposeFile);
|
||||
@@ -36,11 +40,16 @@ export const randomizeIsolatedDeploymentComposeFile = async (
|
||||
|
||||
export const randomizeDeployableSpecificationFile = (
|
||||
composeSpec: ComposeSpecification,
|
||||
isolatedDeploymentsVolume: boolean,
|
||||
suffix?: string,
|
||||
) => {
|
||||
if (!suffix) {
|
||||
return composeSpec;
|
||||
}
|
||||
const newComposeFile = addAppNameToPreventCollision(composeSpec, suffix);
|
||||
const newComposeFile = addAppNameToPreventCollision(
|
||||
composeSpec,
|
||||
suffix,
|
||||
isolatedDeploymentsVolume,
|
||||
);
|
||||
return newComposeFile;
|
||||
};
|
||||
|
||||
@@ -30,12 +30,22 @@ export const addSuffixToVolumesInServices = (
|
||||
|
||||
// skip bind mounts and variables (e.g. $PWD)
|
||||
if (
|
||||
volumeName?.startsWith(".") ||
|
||||
volumeName?.startsWith("/") ||
|
||||
volumeName?.startsWith("$")
|
||||
!volumeName ||
|
||||
volumeName.startsWith(".") ||
|
||||
volumeName.startsWith("/") ||
|
||||
volumeName.startsWith("$")
|
||||
) {
|
||||
return volume;
|
||||
}
|
||||
|
||||
// Handle volume paths with subdirectories
|
||||
const parts = volumeName.split("/");
|
||||
if (parts.length > 1) {
|
||||
const baseName = parts[0];
|
||||
const rest = parts.slice(1).join("/");
|
||||
return `${baseName}-${suffix}/${rest}:${path}`;
|
||||
}
|
||||
|
||||
return `${volumeName}-${suffix}:${path}`;
|
||||
}
|
||||
if (_.isObject(volume) && volume.type === "volume" && volume.source) {
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
cloneGitRawRepository,
|
||||
cloneRawGitRepositoryRemote,
|
||||
} from "../providers/git";
|
||||
import {
|
||||
cloneRawGiteaRepository,
|
||||
cloneRawGiteaRepositoryRemote,
|
||||
} from "../providers/gitea";
|
||||
import {
|
||||
cloneRawGithubRepository,
|
||||
cloneRawGithubRepositoryRemote,
|
||||
@@ -44,6 +48,8 @@ export const cloneCompose = async (compose: Compose) => {
|
||||
await cloneRawBitbucketRepository(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRawRepository(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepository(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRaw(compose);
|
||||
}
|
||||
@@ -58,6 +64,8 @@ export const cloneComposeRemote = async (compose: Compose) => {
|
||||
await cloneRawBitbucketRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneRawGitRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRawRemote(compose);
|
||||
}
|
||||
@@ -109,7 +117,7 @@ export const loadDockerComposeRemote = async (
|
||||
if (!stdout) return null;
|
||||
const parsedConfig = load(stdout) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -194,6 +202,7 @@ export const addDomainToCompose = async (
|
||||
if (compose.isolatedDeployment) {
|
||||
const randomized = randomizeDeployableSpecificationFile(
|
||||
result,
|
||||
compose.isolatedDeploymentsVolume,
|
||||
compose.suffix || compose.appName,
|
||||
);
|
||||
result = randomized;
|
||||
@@ -211,13 +220,9 @@ export const addDomainToCompose = async (
|
||||
throw new Error(`The service ${serviceName} not found in the compose`);
|
||||
}
|
||||
|
||||
const httpLabels = await createDomainLabels(appName, domain, "web");
|
||||
const httpLabels = createDomainLabels(appName, domain, "web");
|
||||
if (https) {
|
||||
const httpsLabels = await createDomainLabels(
|
||||
appName,
|
||||
domain,
|
||||
"websecure",
|
||||
);
|
||||
const httpsLabels = createDomainLabels(appName, domain, "websecure");
|
||||
httpLabels.push(...httpsLabels);
|
||||
}
|
||||
|
||||
@@ -242,9 +247,14 @@ export const addDomainToCompose = async (
|
||||
|
||||
if (Array.isArray(labels)) {
|
||||
if (!labels.includes("traefik.enable=true")) {
|
||||
labels.push("traefik.enable=true");
|
||||
labels.unshift("traefik.enable=true");
|
||||
}
|
||||
labels.unshift(...httpLabels);
|
||||
if (!compose.isolatedDeployment) {
|
||||
if (!labels.includes("traefik.docker.network=dokploy-network")) {
|
||||
labels.unshift("traefik.docker.network=dokploy-network");
|
||||
}
|
||||
}
|
||||
labels.push(...httpLabels);
|
||||
}
|
||||
|
||||
if (!compose.isolatedDeployment) {
|
||||
@@ -279,12 +289,22 @@ export const writeComposeFile = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const createDomainLabels = async (
|
||||
export const createDomainLabels = (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
) => {
|
||||
const { host, port, https, uniqueConfigKey, certificateType, path } = domain;
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
certificateType,
|
||||
path,
|
||||
customCertResolver,
|
||||
stripPath,
|
||||
internalPath,
|
||||
} = domain;
|
||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||
const labels = [
|
||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
@@ -293,6 +313,34 @@ export const createDomainLabels = async (
|
||||
`traefik.http.routers.${routerName}.service=${routerName}`,
|
||||
];
|
||||
|
||||
// Validate stripPath - it should only be used when path is defined and not "/"
|
||||
if (stripPath) {
|
||||
if (!path || path === "/") {
|
||||
console.warn(
|
||||
`stripPath is enabled but path is not defined or is "/" for domain ${host}`,
|
||||
);
|
||||
} else {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate internalPath - ensure it's a valid path format
|
||||
if (internalPath && internalPath !== "/") {
|
||||
if (!internalPath.startsWith("/")) {
|
||||
console.warn(
|
||||
`internalPath "${internalPath}" should start with "/" and not be empty for domain ${host}`,
|
||||
);
|
||||
} else {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entrypoint === "web" && https) {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
|
||||
@@ -304,6 +352,10 @@ export const createDomainLabels = async (
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
|
||||
);
|
||||
} else if (certificateType === "custom" && customCertResolver) {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.tls.certresolver=${customCertResolver}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Readable } from "node:stream";
|
||||
import { docker, paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import { parse } from "dotenv";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
@@ -100,7 +101,7 @@ export const containerExists = async (containerName: string) => {
|
||||
try {
|
||||
await container.inspect();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -211,7 +212,7 @@ export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||
};
|
||||
|
||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||
const command = "docker system prune --all --force --volumes";
|
||||
const command = "docker system prune --force --volumes";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
@@ -278,6 +279,17 @@ export const prepareEnvironmentVariables = (
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const parseEnvironmentKeyValuePair = (
|
||||
pair: string,
|
||||
): [string, string] => {
|
||||
const [key, ...valueParts] = pair.split("=");
|
||||
if (!key || !valueParts.length) {
|
||||
throw new Error(`Invalid environment variable pair: ${pair}`);
|
||||
}
|
||||
|
||||
return [key, valueParts.join("=")];
|
||||
};
|
||||
|
||||
export const getEnviromentVariablesObject = (
|
||||
input: string | null,
|
||||
projectEnv?: string | null,
|
||||
@@ -287,7 +299,7 @@ export const getEnviromentVariablesObject = (
|
||||
const jsonObject: Record<string, string> = {};
|
||||
|
||||
for (const pair of envs) {
|
||||
const [key, value] = pair.split("=");
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
jsonObject[key] = value;
|
||||
}
|
||||
@@ -493,32 +505,9 @@ export const getCreateFileCommand = (
|
||||
`;
|
||||
};
|
||||
|
||||
export const getServiceContainer = async (appName: string) => {
|
||||
try {
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
};
|
||||
|
||||
const containers = await docker.listContainers({
|
||||
filters: JSON.stringify(filter),
|
||||
});
|
||||
|
||||
if (containers.length === 0 || !containers[0]) {
|
||||
throw new Error(`No container found with name: ${appName}`);
|
||||
}
|
||||
|
||||
const container = containers[0];
|
||||
|
||||
return container;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRemoteServiceContainer = async (
|
||||
serverId: string,
|
||||
export const getServiceContainer = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
try {
|
||||
const filter = {
|
||||
@@ -531,7 +520,7 @@ export const getRemoteServiceContainer = async (
|
||||
});
|
||||
|
||||
if (containers.length === 0 || !containers[0]) {
|
||||
throw new Error(`No container found with name: ${appName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = containers[0];
|
||||
@@ -541,3 +530,42 @@ export const getRemoteServiceContainer = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getComposeContainer = async (
|
||||
compose: Compose,
|
||||
serviceName: string,
|
||||
) => {
|
||||
try {
|
||||
const { appName, composeType, serverId } = compose;
|
||||
// 1. Determine the correct labels based on composeType
|
||||
const labels: string[] = [];
|
||||
if (composeType === "stack") {
|
||||
// Labels for Docker Swarm stack services
|
||||
labels.push(`com.docker.stack.namespace=${appName}`);
|
||||
labels.push(`com.docker.swarm.service.name=${appName}_${serviceName}`);
|
||||
} else {
|
||||
// Labels for Docker Compose projects (default)
|
||||
labels.push(`com.docker.compose.project=${appName}`);
|
||||
labels.push(`com.docker.compose.service=${serviceName}`);
|
||||
}
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
label: labels,
|
||||
};
|
||||
|
||||
const remoteDocker = await getRemoteDocker(serverId);
|
||||
const containers = await remoteDocker.listContainers({
|
||||
filters: JSON.stringify(filter),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (containers.length === 0 || !containers[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = containers[0];
|
||||
return container;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,6 +113,8 @@ export const getBuildAppDirectory = (application: Application) => {
|
||||
buildPath = application?.gitlabBuildPath || "";
|
||||
} else if (sourceType === "bitbucket") {
|
||||
buildPath = application?.bitbucketBuildPath || "";
|
||||
} else if (sourceType === "gitea") {
|
||||
buildPath = application?.giteaBuildPath || "";
|
||||
} else if (sourceType === "drop") {
|
||||
buildPath = application?.dropBuildPath || "";
|
||||
} else if (sourceType === "git") {
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||
...gpuInfo,
|
||||
...cudaInfo,
|
||||
};
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return {
|
||||
driverInstalled: false,
|
||||
driverVersion: undefined,
|
||||
@@ -315,7 +315,7 @@ const setupLocalServer = async (daemonConfig: any) => {
|
||||
|
||||
try {
|
||||
await execAsync(setupCommands);
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
type,
|
||||
errorMessage,
|
||||
organizationId,
|
||||
databaseName,
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
@@ -26,6 +27,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
databaseName: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
@@ -90,6 +92,11 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📂`", "Database Name"),
|
||||
value: databaseName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
@@ -136,6 +143,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("❔", `Type: ${databaseType}`)}` +
|
||||
`${decorate("📂", `Database Name: ${databaseName}`)}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
@@ -150,7 +158,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
@@ -191,6 +199,10 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
value: databaseType,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Database Name",
|
||||
value: databaseName,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
|
||||
@@ -40,68 +40,84 @@ export const sendDokployRestartNotifications = async () => {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
try {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Restart Notification",
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Restart Notification",
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Dokploy Server Restarted"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||
);
|
||||
try {
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Dokploy Server Restarted"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
try {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#00FF00",
|
||||
pretext: ":white_check_mark: *Dokploy Server Restarted*",
|
||||
fields: [
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
try {
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#00FF00",
|
||||
pretext: ":white_check_mark: *Dokploy Server Restarted*",
|
||||
fields: [
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,93 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { exec, execFile } from "node:child_process";
|
||||
import util from "node:util";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { Client } from "ssh2";
|
||||
|
||||
export const execAsync = util.promisify(exec);
|
||||
|
||||
interface ExecOptions {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
export const execAsyncStream = (
|
||||
command: string,
|
||||
onData?: (data: string) => void,
|
||||
options: ExecOptions = {},
|
||||
): Promise<{ stdout: string; stderr: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdoutComplete = "";
|
||||
let stderrComplete = "";
|
||||
|
||||
const childProcess = exec(command, options, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
|
||||
});
|
||||
|
||||
childProcess.stdout?.on("data", (data: Buffer | string) => {
|
||||
const stringData = data.toString();
|
||||
stdoutComplete += stringData;
|
||||
if (onData) {
|
||||
onData(stringData);
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.stderr?.on("data", (data: Buffer | string) => {
|
||||
const stringData = data.toString();
|
||||
stderrComplete += stringData;
|
||||
if (onData) {
|
||||
onData(stringData);
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on("error", (error) => {
|
||||
console.log(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const execFileAsync = async (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { input?: string } = {},
|
||||
): Promise<{ stdout: string; stderr: string }> => {
|
||||
const child = execFile(command, args);
|
||||
|
||||
if (options.input && child.stdin) {
|
||||
child.stdin.write(options.input);
|
||||
child.stdin.end();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Command failed with code ${code}. Stderr: ${stderr}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const execAsyncRemote = async (
|
||||
serverId: string | null,
|
||||
command: string,
|
||||
|
||||
@@ -37,6 +37,7 @@ export const cloneBitbucketRepository = async (
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
bitbucket,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
@@ -53,28 +54,26 @@ export const cloneBitbucketRepository = async (
|
||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -89,6 +88,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
@@ -106,17 +106,19 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -131,6 +133,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -153,11 +156,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
const command = `
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
|
||||
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
await execAsyncRemote(serverId, cloneCommand);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -176,6 +179,7 @@ export const getBitbucketCloneCommand = async (
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -207,7 +211,7 @@ export const getBitbucketCloneCommand = async (
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
@@ -17,12 +17,19 @@ export const cloneGitRepository = async (
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
@@ -70,19 +77,21 @@ export const cloneGitRepository = async (
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
cloneArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
@@ -114,6 +123,7 @@ export const getCustomGitCloneCommand = async (
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
serverId: string | null;
|
||||
enableSubmodules: boolean;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
@@ -125,6 +135,7 @@ export const getCustomGitCloneCommand = async (
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
@@ -181,7 +192,7 @@ export const getCustomGitCloneCommand = async (
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
@@ -261,8 +272,15 @@ export const cloneGitRawRepository = async (entity: {
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
}) => {
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
@@ -307,29 +325,26 @@ export const cloneGitRawRepository = async (entity: {
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(_data) => {},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (_data) => {}, {
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -342,6 +357,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
customGitUrl,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -396,7 +412,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
|
||||
echo "[ERROR] Fail to clone the repository ";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
516
packages/server/src/utils/providers/gitea.ts
Normal file
516
packages/server/src/utils/providers/gitea.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
type Gitea,
|
||||
findGiteaById,
|
||||
updateGitea,
|
||||
} from "@dokploy/server/services/gitea";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const getErrorCloneRequirements = (entity: {
|
||||
giteaRepository?: string | null;
|
||||
giteaOwner?: string | null;
|
||||
giteaBranch?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { giteaBranch, giteaOwner, giteaRepository } = entity;
|
||||
|
||||
if (!giteaRepository) reasons.push("1. Repository not assigned.");
|
||||
if (!giteaOwner) reasons.push("2. Owner not specified.");
|
||||
if (!giteaBranch) reasons.push("3. Branch not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export const refreshGiteaToken = async (giteaProviderId: string) => {
|
||||
try {
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
|
||||
if (
|
||||
!giteaProvider?.clientId ||
|
||||
!giteaProvider?.clientSecret ||
|
||||
!giteaProvider?.refreshToken
|
||||
) {
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
// Check if token is still valid (add some buffer time, e.g., 5 minutes)
|
||||
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
||||
const bufferTimeSeconds = 300; // 5 minutes
|
||||
|
||||
if (
|
||||
giteaProvider.expiresAt &&
|
||||
giteaProvider.expiresAt > currentTimeSeconds + bufferTimeSeconds &&
|
||||
giteaProvider.accessToken
|
||||
) {
|
||||
// Token is still valid, no need to refresh
|
||||
return giteaProvider.accessToken;
|
||||
}
|
||||
|
||||
// Token is expired or about to expire, refresh it
|
||||
const tokenEndpoint = `${giteaProvider.giteaUrl}/login/oauth/access_token`;
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: giteaProvider.refreshToken,
|
||||
client_id: giteaProvider.clientId,
|
||||
client_secret: giteaProvider.clientSecret,
|
||||
});
|
||||
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { access_token, refresh_token, expires_in } = data;
|
||||
|
||||
if (!access_token) {
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + (expires_in || 3600) * 1000;
|
||||
const expiresAtSeconds = Math.floor(expiresAt / 1000);
|
||||
|
||||
await updateGitea(giteaProviderId, {
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token || giteaProvider.refreshToken,
|
||||
expiresAt: expiresAtSeconds,
|
||||
});
|
||||
|
||||
return access_token;
|
||||
} catch (error) {
|
||||
console.error("Error refreshing Gitea token:", error);
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
};
|
||||
|
||||
export type ApplicationWithGitea = InferResultType<
|
||||
"applications",
|
||||
{ gitea: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
|
||||
|
||||
export const getGiteaCloneCommand = async (
|
||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
giteaOwner,
|
||||
giteaRepository,
|
||||
serverId,
|
||||
gitea,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!giteaId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Use paths(true) for remote operations
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
await refreshGiteaToken(giteaId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const cloneGiteaRepository = async (
|
||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const {
|
||||
appName,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
giteaOwner,
|
||||
giteaRepository,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGiteaRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
giteaRepository,
|
||||
giteaOwner,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
const { COMPOSE_PATH } = paths();
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
giteaRepository,
|
||||
giteaOwner,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
|
||||
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
|
||||
};
|
||||
|
||||
export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
try {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new Error("Gitea provider not found");
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
const provider = await findGiteaById(giteaId);
|
||||
if (!provider || !provider.accessToken) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "No access token available. Please authorize with Gitea.",
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
||||
const limit = 30;
|
||||
let allRepos = 0;
|
||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
||||
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${provider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to connect to Gitea API: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
allRepos += repos.data.length;
|
||||
|
||||
const linkHeader = response.headers.get("link");
|
||||
nextUrl = "";
|
||||
|
||||
if (linkHeader) {
|
||||
const nextLink = linkHeader
|
||||
.split(",")
|
||||
.find((link) => link.includes('rel="next"'));
|
||||
if (nextLink) {
|
||||
const matches = nextLink.match(/<([^>]+)>/);
|
||||
if (matches?.[1]) {
|
||||
nextUrl = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateGitea(giteaId, {
|
||||
lastAuthenticatedAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
return allRepos;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
if (!giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const limit = 30;
|
||||
let allRepositories: any[] = [];
|
||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
||||
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
allRepositories = [...allRepositories, ...result.data];
|
||||
|
||||
const linkHeader = response.headers.get("link");
|
||||
nextUrl = "";
|
||||
|
||||
if (linkHeader) {
|
||||
const nextLink = linkHeader
|
||||
.split(",")
|
||||
.find((link) => link.includes('rel="next"'));
|
||||
if (nextLink) {
|
||||
const matches = nextLink.match(/<([^>]+)>/);
|
||||
if (matches?.[1]) {
|
||||
nextUrl = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
allRepositories?.map((repo: any) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
url: repo.full_name,
|
||||
owner: {
|
||||
username: repo.owner.login,
|
||||
},
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
|
||||
export const getGiteaBranches = async (input: {
|
||||
giteaId?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
if (!input.giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await refreshGiteaToken(input.giteaId);
|
||||
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await response.json();
|
||||
|
||||
if (!branches) {
|
||||
return [];
|
||||
}
|
||||
return branches?.map((branch: any) => ({
|
||||
id: branch.name,
|
||||
name: branch.name,
|
||||
commit: {
|
||||
id: branch.commit.id,
|
||||
},
|
||||
})) as {
|
||||
id: string;
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
@@ -83,6 +83,7 @@ interface CloneGithubRepository {
|
||||
repository: string | null;
|
||||
logPath: string;
|
||||
type?: "application" | "compose";
|
||||
enableSubmodules: boolean;
|
||||
}
|
||||
export const cloneGithubRepository = async ({
|
||||
logPath,
|
||||
@@ -92,7 +93,8 @@ export const cloneGithubRepository = async ({
|
||||
const isCompose = type === "compose";
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
||||
entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
@@ -128,28 +130,26 @@ export const cloneGithubRepository = async ({
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -161,7 +161,15 @@ export const getGithubCloneCommand = async ({
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository & { serverId: string }) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
const isCompose = type === "compose";
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -216,7 +224,7 @@ export const getGithubCloneCommand = async ({
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
@@ -227,7 +235,8 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
||||
entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
@@ -245,24 +254,33 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
];
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = compose;
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -288,7 +306,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user