mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-01 09:42:16 +00:00
Merge branch 'canary' into 1853-external-volume-should-not-be-isolated
This commit is contained in:
@@ -24,5 +24,6 @@ export const paths = (isServer = false) => {
|
||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
SCHEDULES_PATH: `${BASE_PATH}/schedules`,
|
||||
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,6 @@ import { server } from "./server";
|
||||
import { applicationStatus, certificateType, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
"git",
|
||||
@@ -132,6 +131,7 @@ export const applications = pgTable("application", {
|
||||
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||
false,
|
||||
),
|
||||
rollbackActive: boolean("rollbackActive").default(false),
|
||||
buildArgs: text("buildArgs"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -206,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()),
|
||||
@@ -409,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(),
|
||||
@@ -461,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({
|
||||
|
||||
@@ -11,15 +11,15 @@ 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";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { generateAppName } from ".";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
@@ -15,7 +16,6 @@ import { server } from "./server";
|
||||
import { applicationStatus, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { backups } from "./backups";
|
||||
|
||||
import { schedules } from "./schedule";
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
|
||||
@@ -10,11 +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 { server } from "./server";
|
||||
import { schedules } from "./schedule";
|
||||
import { backups } from "./backups";
|
||||
import { server } from "./server";
|
||||
import { rollbacks } from "./rollbacks";
|
||||
import { volumeBackups } from "./volume-backups";
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
@@ -30,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" },
|
||||
@@ -58,6 +61,14 @@ export const deployments = pgTable("deployment", {
|
||||
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 }) => ({
|
||||
@@ -85,6 +96,14 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
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, {
|
||||
@@ -169,6 +188,17 @@ export const apiCreateDeploymentSchedule = schema
|
||||
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,
|
||||
@@ -206,6 +236,7 @@ export const apiFindAllByType = z
|
||||
"schedule",
|
||||
"previewDeployment",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
]),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -51,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 }) => ({
|
||||
@@ -82,6 +84,8 @@ export const apiCreateDomain = createSchema.pick({
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
previewDeploymentId: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
@@ -112,5 +116,7 @@ export const apiUpdateDomain = createSchema
|
||||
customCertResolver: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
})
|
||||
.merge(createSchema.pick({ domainId: true }).required());
|
||||
|
||||
@@ -8,6 +8,7 @@ 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",
|
||||
@@ -29,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 }) => ({
|
||||
@@ -52,6 +56,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitProvider);
|
||||
|
||||
@@ -32,3 +32,5 @@ export * from "./preview-deployments";
|
||||
export * from "./ai";
|
||||
export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
export * from "./volume-backups";
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
@@ -4,11 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { deployments } from "./deployment";
|
||||
import { generateAppName } from "./utils";
|
||||
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", [
|
||||
|
||||
@@ -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";
|
||||
import { schedules } from "./schedule";
|
||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||
|
||||
export const server = pgTable("server", {
|
||||
|
||||
@@ -11,10 +11,11 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { account, apikey, organization } from "./account";
|
||||
import { projects } from "./project";
|
||||
import { certificateType } from "./shared";
|
||||
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.
|
||||
@@ -56,7 +57,7 @@ export const users_temp = pgTable("user_temp", {
|
||||
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),
|
||||
@@ -236,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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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,6 +4,8 @@ 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" })
|
||||
@@ -29,12 +31,37 @@ export const domain = z
|
||||
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" })
|
||||
@@ -61,4 +88,27 @@ export const domainCompose = z
|
||||
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";
|
||||
@@ -32,6 +33,7 @@ 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";
|
||||
@@ -131,3 +133,6 @@ export {
|
||||
|
||||
export * from "./utils/schedules/utils";
|
||||
export * from "./utils/schedules/index";
|
||||
export * from "./utils/volume-backups/index";
|
||||
|
||||
export * from "./lib/logger";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { apiKey, organization, twoFactor, admin } from "better-auth/plugins";
|
||||
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";
|
||||
@@ -18,9 +18,6 @@ const { handler, api } = betterAuth({
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
logger: {
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
appName: "Dokploy",
|
||||
socialProviders: {
|
||||
github: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,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 (
|
||||
@@ -214,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,
|
||||
@@ -338,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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type apiCreateDeploymentPreview,
|
||||
type apiCreateDeploymentSchedule,
|
||||
type apiCreateDeploymentServer,
|
||||
type apiCreateDeploymentVolumeBackup,
|
||||
deployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||
@@ -24,30 +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 { findBackupById } from "./backup";
|
||||
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 (
|
||||
@@ -442,6 +460,91 @@ export const createDeploymentSchedule = async (
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
@@ -476,11 +579,15 @@ const getDeploymentsByType = async (
|
||||
| "server"
|
||||
| "schedule"
|
||||
| "previewDeployment"
|
||||
| "backup",
|
||||
| "backup"
|
||||
| "volumeBackup",
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments[`${type}Id`], id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
with: {
|
||||
rollback: true,
|
||||
},
|
||||
});
|
||||
return deploymentList;
|
||||
};
|
||||
@@ -505,7 +612,8 @@ const removeLastTenDeployments = async (
|
||||
| "server"
|
||||
| "schedule"
|
||||
| "previewDeployment"
|
||||
| "backup",
|
||||
| "backup"
|
||||
| "volumeBackup",
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const deploymentList = await getDeploymentsByType(id, type);
|
||||
@@ -515,18 +623,30 @@ const removeLastTenDeployments = async (
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
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)) {
|
||||
if (
|
||||
existsSync(logPath) &&
|
||||
!oldDeployment.errorMessage &&
|
||||
logPath !== "."
|
||||
) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
|
||||
@@ -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,7 +274,7 @@ export const getServiceContainersByAppName = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -331,7 +331,7 @@ export const getContainersByAppLabel = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -350,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) => {
|
||||
@@ -379,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) => {
|
||||
@@ -405,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) => {
|
||||
@@ -437,7 +437,7 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
|
||||
return appArray;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getApplicationInfo = async (
|
||||
@@ -470,5 +470,5 @@ export const getApplicationInfo = async (
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||
@@ -6,9 +8,8 @@ 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";
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
@@ -142,28 +143,6 @@ export const getDomainHost = (domain: Domain) => {
|
||||
|
||||
const resolveDns = promisify(dns.resolve4);
|
||||
|
||||
// Cloudflare IP ranges (simplified - these are some common ones)
|
||||
const CLOUDFLARE_IPS = [
|
||||
"172.67.",
|
||||
"104.21.",
|
||||
"104.16.",
|
||||
"104.17.",
|
||||
"104.18.",
|
||||
"104.19.",
|
||||
"104.20.",
|
||||
"104.22.",
|
||||
"104.23.",
|
||||
"104.24.",
|
||||
"104.25.",
|
||||
"104.26.",
|
||||
"104.27.",
|
||||
"104.28.",
|
||||
];
|
||||
|
||||
const isCloudflareIp = (ip: string) => {
|
||||
return CLOUDFLARE_IPS.some((range) => ip.startsWith(range));
|
||||
};
|
||||
|
||||
export const validateDomain = async (
|
||||
domain: string,
|
||||
expectedIp?: string,
|
||||
@@ -172,6 +151,7 @@ export const validateDomain = async (
|
||||
resolvedIp?: string;
|
||||
error?: string;
|
||||
isCloudflare?: boolean;
|
||||
cdnProvider?: string;
|
||||
}> => {
|
||||
try {
|
||||
// Remove protocol and path if present
|
||||
@@ -182,17 +162,18 @@ export const validateDomain = async (
|
||||
|
||||
const resolvedIps = ips.map((ip) => ip.toString());
|
||||
|
||||
// Check if it's a Cloudflare IP
|
||||
const behindCloudflare = ips.some((ip) => isCloudflareIp(ip));
|
||||
// Check if any IP belongs to a CDN provider
|
||||
const cdnProvider = ips
|
||||
.map((ip) => detectCDNProvider(ip))
|
||||
.find((provider) => provider !== null);
|
||||
|
||||
// If behind Cloudflare, we consider it valid but inform the user
|
||||
if (behindCloudflare) {
|
||||
// If behind a CDN, we consider it valid but inform the user
|
||||
if (cdnProvider) {
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
isCloudflare: true,
|
||||
error:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
cdnProvider: cdnProvider.displayName,
|
||||
error: cdnProvider.warningMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -20,6 +21,7 @@ export const createGitea = async (
|
||||
providerType: "gitea",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -212,7 +212,7 @@ export const deleteFileMount = async (mountId: string) => {
|
||||
} else {
|
||||
await removeFileOrDirectory(fullPath);
|
||||
}
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getBaseFilesPath = async (mountId: string) => {
|
||||
|
||||
@@ -104,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) {
|
||||
@@ -171,7 +171,7 @@ export const createPreviewDeployment = async (
|
||||
const runningComment = getIssueComment(
|
||||
application.name,
|
||||
"initializing",
|
||||
generateDomain,
|
||||
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
import { type Schedule, schedules } from "../db/schema/schedule";
|
||||
import { db } from "../db";
|
||||
import { eq } from "drizzle-orm";
|
||||
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 { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { paths } from "../constants";
|
||||
import path from "node:path";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
|
||||
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
|
||||
|
||||
@@ -109,7 +109,12 @@ export const updateSchedule = async (
|
||||
const handleScript = async (schedule: Schedule) => {
|
||||
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
|
||||
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||
const encodedContent = encodeBase64(schedule?.script || "");
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -19,6 +19,7 @@ export const setupDirectories = () => {
|
||||
MONITORING_PATH,
|
||||
SSH_PATH,
|
||||
SCHEDULES_PATH,
|
||||
VOLUME_BACKUPS_PATH,
|
||||
} = paths();
|
||||
const directories = [
|
||||
BASE_PATH,
|
||||
@@ -30,6 +31,7 @@ export const setupDirectories = () => {
|
||||
CERTIFICATES_PATH,
|
||||
MONITORING_PATH,
|
||||
SCHEDULES_PATH,
|
||||
VOLUME_BACKUPS_PATH,
|
||||
];
|
||||
|
||||
for (const dir of directories) {
|
||||
|
||||
@@ -66,7 +66,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
|
||||
}
|
||||
|
||||
@@ -135,7 +135,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);
|
||||
|
||||
@@ -116,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
|
||||
@@ -356,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 sudo unzip tar >/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
|
||||
@@ -377,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."
|
||||
@@ -418,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
|
||||
@@ -577,7 +599,7 @@ const installNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
export NIXPACKS_VERSION=1.35.0
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,11 +101,11 @@ export const initializeTraefik = async ({
|
||||
console.log("Waiting for service cleanup...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
attempts++;
|
||||
} catch (_e) {
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
console.log("No existing service to remove");
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export const initializeTraefik = async ({
|
||||
|
||||
await container.remove({ force: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
console.log("No existing container to remove");
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@ export const initializeTraefik = async ({
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully after retry");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { randomBytes, createHmac } from "node:crypto";
|
||||
import { createHmac, randomBytes } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -35,7 +35,7 @@ export const generateRandomDomain = ({
|
||||
projectName,
|
||||
}: Schema): string => {
|
||||
const hash = randomBytes(3).toString("hex");
|
||||
const slugIp = serverIp.replaceAll(".", "-");
|
||||
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
|
||||
|
||||
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
|
||||
};
|
||||
|
||||
@@ -37,7 +37,8 @@ export const startLogCleanup = async (
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
console.error("Error starting log cleanup:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
|
||||
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,
|
||||
@@ -15,7 +15,7 @@ export const runComposeBackup = async (
|
||||
) => {
|
||||
const { projectId, name } = compose;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix } = backup;
|
||||
const { prefix, databaseType } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
@@ -46,9 +46,10 @@ export const runComposeBackup = async (
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
databaseType: getDatabaseType(databaseType),
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -57,14 +58,31 @@ export const runComposeBackup = async (
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
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";
|
||||
};
|
||||
|
||||
@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
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....");
|
||||
@@ -88,6 +88,7 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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 { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
@@ -48,6 +48,7 @@ export const runMariadbBackup = async (
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
@@ -60,6 +61,7 @@ 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,13 +1,13 @@
|
||||
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 { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { projectId, name } = mongo;
|
||||
@@ -46,6 +46,7 @@ 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) {
|
||||
@@ -58,6 +59,7 @@ 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;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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 { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { projectId, name } = mysql;
|
||||
@@ -47,6 +47,7 @@ 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) {
|
||||
@@ -59,6 +60,7 @@ 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,13 +1,13 @@
|
||||
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 { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
@@ -50,6 +50,7 @@ export const runPostgresBackup = async (
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -62,6 +63,7 @@ export const runPostgresBackup = async (
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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";
|
||||
import { runComposeBackup } from "./compose";
|
||||
|
||||
export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
const {
|
||||
@@ -222,6 +223,17 @@ export const getBackupCommand = (
|
||||
) => {
|
||||
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};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
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 { createWriteStream } from "node:fs";
|
||||
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) {
|
||||
@@ -51,13 +51,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
|
||||
const postgresContainerId = containerId.trim();
|
||||
|
||||
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
|
||||
// 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);
|
||||
|
||||
writeStream.write(`Running command: ${postgresCommand}\n`);
|
||||
await execAsync(postgresCommand);
|
||||
// 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 -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
);
|
||||
|
||||
writeStream.write("Copied filesystem to temp directory\n");
|
||||
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
return true;
|
||||
} finally {
|
||||
await execAsync(`rm -rf ${tempDir}`);
|
||||
try {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
} catch (cleanupError) {
|
||||
console.error("Cleanup error:", cleanupError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Backup error:", error);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
@@ -204,26 +204,30 @@ export const mechanizeDockerContainer = async (
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (_error: unknown) {
|
||||
} 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") {
|
||||
|
||||
@@ -2,7 +2,10 @@ 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 { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
@@ -81,10 +84,10 @@ export const buildRailpack = async (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const env: { [key: string]: string } = {};
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const exportEnvs = [];
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
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("--secret", `id=${key},env=${key}`);
|
||||
exportEnvs.push(`export ${key}='${value}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,21 +7,58 @@ 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"),
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -98,7 +98,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,7 +152,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export const loadDockerComposeRemote = async (
|
||||
if (!stdout) return null;
|
||||
const parsedConfig = load(stdout) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -301,6 +301,8 @@ export const createDomainLabels = (
|
||||
certificateType,
|
||||
path,
|
||||
customCertResolver,
|
||||
stripPath,
|
||||
internalPath,
|
||||
} = domain;
|
||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||
const labels = [
|
||||
@@ -310,6 +312,34 @@ export const createDomainLabels = (
|
||||
`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`,
|
||||
|
||||
@@ -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";
|
||||
@@ -13,7 +14,6 @@ import type { RedisNested } from "../databases/redis";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
@@ -101,7 +101,7 @@ export const containerExists = async (containerName: string) => {
|
||||
try {
|
||||
await container.inspect();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -212,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 {
|
||||
@@ -279,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,
|
||||
@@ -288,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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -73,7 +73,7 @@ export const cloneBitbucketRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -155,7 +155,7 @@ export const getGiteaCloneCommand = async (
|
||||
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;
|
||||
@@ -232,7 +232,7 @@ export const cloneGiteaRepository = async (
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -149,7 +149,7 @@ export const cloneGithubRepository = async ({
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -132,7 +132,7 @@ export const cloneGitlabRepository = async (
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
@@ -152,7 +152,7 @@ export const cloneGitlabRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -246,34 +246,18 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const allProjects = await validateGitlabProvider(gitlabProvider);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const filteredRepos = allProjects.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
||||
|
||||
if (groupName) {
|
||||
const isIncluded = groupName
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) => full_path.toLowerCase().includes(name));
|
||||
|
||||
return isIncluded && kind === "group";
|
||||
.some((name) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
@@ -432,23 +416,7 @@ export const testGitlabConnection = async (
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
const repositories = await validateGitlabProvider(gitlabProvider);
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
@@ -456,10 +424,56 @@ export const testGitlabConnection = async (
|
||||
if (groupName) {
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) => full_path.toLowerCase().includes(name));
|
||||
.some((name) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
|
||||
return filteredRepos.length;
|
||||
};
|
||||
|
||||
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
||||
try {
|
||||
const allProjects = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // GitLab's max per page is 100
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const projects = await response.json();
|
||||
|
||||
if (projects.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allProjects.push(...projects);
|
||||
page++;
|
||||
|
||||
const total = response.headers.get("x-total");
|
||||
if (total && allProjects.length >= Number.parseInt(total)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allProjects;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
interface DatabaseCredentials {
|
||||
databaseUser?: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restoreMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restoreMongoBackup = async (
|
||||
mongo: Mongo,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { MySql } from "@dokploy/server/services/mysql";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restoreMySqlBackup = async (
|
||||
mysql: MySql,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restorePostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
|
||||
database: string,
|
||||
databaseUser: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} --clean --if-exists"`;
|
||||
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
|
||||
};
|
||||
|
||||
export const getMariadbRestoreCommand = (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { paths, IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
export const restoreWebServerBackup = async (
|
||||
destination: Destination,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "../../db/index";
|
||||
import { schedules } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db/index";
|
||||
import { scheduleJob } from "./utils";
|
||||
|
||||
export const initSchedules = async () => {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Schedule } from "@dokploy/server/db/schema/schedule";
|
||||
import {
|
||||
createDeploymentSchedule,
|
||||
updateDeployment,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
|
||||
import { findScheduleById } from "@dokploy/server/services/schedule";
|
||||
import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule";
|
||||
import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
|
||||
import { getComposeContainer, getServiceContainer } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import path from "node:path";
|
||||
|
||||
export const scheduleJob = (schedule: Schedule) => {
|
||||
const { cronExpression, scheduleId } = schedule;
|
||||
@@ -113,8 +116,16 @@ export const runCommand = async (scheduleId: string) => {
|
||||
await spawnAsync(
|
||||
"bash",
|
||||
["-c", "./script.sh"],
|
||||
(data) => {
|
||||
async (data) => {
|
||||
if (writeStream.writable) {
|
||||
// we need to extract the PID and Schedule ID from the data
|
||||
const pid = data?.match(/PID: (\d+)/)?.[1];
|
||||
|
||||
if (pid) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
pid,
|
||||
});
|
||||
}
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
@@ -133,13 +144,21 @@ export const runCommand = async (scheduleId: string) => {
|
||||
const command = `
|
||||
set -e
|
||||
echo "Running script" >> ${deployment.logPath};
|
||||
bash -c ${fullPath}/script.sh >> ${deployment.logPath} 2>> ${deployment.logPath} || {
|
||||
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
||||
echo "❌ Command failed" >> ${deployment.logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Command executed successfully" >> ${deployment.logPath};
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
await execAsyncRemote(serverId, command, async (data) => {
|
||||
// we need to extract the PID and Schedule ID from the data
|
||||
const pid = data?.match(/PID: (\d+)/)?.[1];
|
||||
if (pid) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
pid,
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs, { writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { dump, load } from "js-yaml";
|
||||
@@ -67,7 +69,7 @@ export const removeTraefikConfig = async (
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const removeTraefikConfigRemote = async (
|
||||
@@ -78,7 +80,7 @@ export const removeTraefikConfigRemote = async (
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const loadOrCreateConfig = (appName: string): FileConfig => {
|
||||
@@ -110,7 +112,7 @@ export const loadOrCreateConfigRemote = async (
|
||||
http: { routers: {}, services: {} },
|
||||
};
|
||||
return parsedConfig;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return fileConfig;
|
||||
}
|
||||
};
|
||||
@@ -132,44 +134,45 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
|
||||
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
|
||||
if (!stdout) return null;
|
||||
return stdout;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const readMonitoringConfig = (readAll = false) => {
|
||||
export const readMonitoringConfig = async (readAll = false) => {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||
if (fs.existsSync(configPath)) {
|
||||
if (!readAll) {
|
||||
// Read first 500 lines
|
||||
// Read first 500 lines using streams
|
||||
let content = "";
|
||||
let chunk = "";
|
||||
let validCount = 0;
|
||||
|
||||
for (const char of fs.readFileSync(configPath, "utf8")) {
|
||||
chunk += char;
|
||||
if (char === "\n") {
|
||||
try {
|
||||
const trimmed = chunk.trim();
|
||||
if (
|
||||
trimmed !== "" &&
|
||||
trimmed.startsWith("{") &&
|
||||
trimmed.endsWith("}")
|
||||
) {
|
||||
const log = JSON.parse(trimmed);
|
||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||
content += chunk;
|
||||
validCount++;
|
||||
if (validCount >= 500) {
|
||||
break;
|
||||
}
|
||||
const fileStream = createReadStream(configPath, { encoding: "utf8" });
|
||||
const readline = createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
|
||||
for await (const line of readline) {
|
||||
try {
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
trimmed !== "" &&
|
||||
trimmed.startsWith("{") &&
|
||||
trimmed.endsWith("}")
|
||||
) {
|
||||
const log = JSON.parse(trimmed);
|
||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||
content += `${line}\n`;
|
||||
validCount++;
|
||||
if (validCount >= 500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid JSON
|
||||
}
|
||||
chunk = "";
|
||||
} catch {
|
||||
// Ignore invalid JSON
|
||||
}
|
||||
}
|
||||
return content;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type { FileConfig, HttpRouter } from "./file-types";
|
||||
import { createPathMiddlewares, removePathMiddlewares } from "./middleware";
|
||||
|
||||
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
const { appName } = app;
|
||||
@@ -46,6 +47,8 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
|
||||
config.http.services[serviceName] = createServiceConfig(appName, domain);
|
||||
|
||||
await createPathMiddlewares(app, domain);
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, appName, app.serverId);
|
||||
} else {
|
||||
@@ -80,6 +83,8 @@ export const removeDomain = async (
|
||||
delete config.http.services[serviceKey];
|
||||
}
|
||||
|
||||
await removePathMiddlewares(application, uniqueKey);
|
||||
|
||||
// verify if is the last router if so we delete the router
|
||||
if (
|
||||
config?.http?.routers &&
|
||||
@@ -107,7 +112,8 @@ export const createRouterConfig = async (
|
||||
const { appName, redirects, security } = app;
|
||||
const { certificateType } = domain;
|
||||
|
||||
const { host, path, https, uniqueConfigKey } = domain;
|
||||
const { host, path, https, uniqueConfigKey, internalPath, stripPath } =
|
||||
domain;
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${host}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
service: `${appName}-service-${uniqueConfigKey}`,
|
||||
@@ -115,6 +121,17 @@ export const createRouterConfig = async (
|
||||
entryPoints: [entryPoint],
|
||||
};
|
||||
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
|
||||
if (entryPoint === "web" && https) {
|
||||
routerConfig.middlewares = ["redirect-to-https"];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ApplicationNested } from "../builders";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { writeTraefikConfigRemote } from "./application";
|
||||
import type { FileConfig } from "./file-types";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
|
||||
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
|
||||
if (config.http?.routers) {
|
||||
@@ -105,3 +106,97 @@ export const writeMiddleware = <T>(config: T) => {
|
||||
const newYamlContent = dump(config);
|
||||
writeFileSync(configPath, newYamlContent, "utf8");
|
||||
};
|
||||
|
||||
export const createPathMiddlewares = async (
|
||||
app: ApplicationNested,
|
||||
domain: Domain,
|
||||
) => {
|
||||
let config: FileConfig;
|
||||
|
||||
if (app.serverId) {
|
||||
try {
|
||||
config = await loadRemoteMiddlewares(app.serverId);
|
||||
} catch {
|
||||
config = { http: { middlewares: {} } };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
config = loadMiddlewares<FileConfig>();
|
||||
} catch {
|
||||
config = { http: { middlewares: {} } };
|
||||
}
|
||||
}
|
||||
|
||||
const { appName } = app;
|
||||
const { uniqueConfigKey, internalPath, stripPath, path } = domain;
|
||||
|
||||
if (!config.http) {
|
||||
config.http = { middlewares: {} };
|
||||
}
|
||||
if (!config.http.middlewares) {
|
||||
config.http.middlewares = {};
|
||||
}
|
||||
|
||||
// Add internal path prefix middleware
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
config.http.middlewares[middlewareName] = {
|
||||
addPrefix: {
|
||||
prefix: internalPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Strip external path middleware if needed
|
||||
if (stripPath && path && path !== "/") {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
config.http.middlewares[middlewareName] = {
|
||||
stripPrefix: {
|
||||
prefixes: [path],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", app.serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
};
|
||||
|
||||
export const removePathMiddlewares = async (
|
||||
app: ApplicationNested,
|
||||
uniqueConfigKey: number,
|
||||
) => {
|
||||
let config: FileConfig;
|
||||
|
||||
if (app.serverId) {
|
||||
try {
|
||||
config = await loadRemoteMiddlewares(app.serverId);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
config = loadMiddlewares<FileConfig>();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { appName } = app;
|
||||
|
||||
if (config.http?.middlewares) {
|
||||
const addPrefixMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
const stripPrefixMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
|
||||
delete config.http.middlewares[addPrefixMiddleware];
|
||||
delete config.http.middlewares[stripPrefixMiddleware];
|
||||
}
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", app.serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import { normalizeS3Path } from "../backups/utils";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
|
||||
export const backupVolume = async (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
) => {
|
||||
const { serviceType, volumeName, turnOff, prefix } = volumeBackup;
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const destination = volumeBackup.destination;
|
||||
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const rcloneFlags = getS3Credentials(volumeBackup.destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);
|
||||
|
||||
const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`;
|
||||
|
||||
const baseCommand = `
|
||||
set -e
|
||||
echo "Volume name: ${volumeName}"
|
||||
echo "Backup file name: ${backupFileName}"
|
||||
echo "Turning off volume backup: ${turnOff ? "Yes" : "No"}"
|
||||
echo "Starting volume backup"
|
||||
echo "Dir: ${volumeBackupPath}"
|
||||
docker run --rm \
|
||||
-v ${volumeName}:/volume_data \
|
||||
-v ${volumeBackupPath}:/backup \
|
||||
ubuntu \
|
||||
bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ."
|
||||
echo "Volume backup done ✅"
|
||||
echo "Starting upload to S3..."
|
||||
${rcloneCommand}
|
||||
echo "Upload to S3 done ✅"
|
||||
`;
|
||||
|
||||
if (!turnOff) {
|
||||
return baseCommand;
|
||||
}
|
||||
|
||||
if (serviceType === "application") {
|
||||
return `
|
||||
echo "Stopping application to 0 replicas"
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service scale ${volumeBackup.application?.appName}=0
|
||||
${baseCommand}
|
||||
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
||||
docker service scale ${volumeBackup.application?.appName}=$ACTUAL_REPLICAS
|
||||
`;
|
||||
}
|
||||
if (serviceType === "compose") {
|
||||
const compose = await findComposeById(
|
||||
volumeBackup.compose?.composeId || "",
|
||||
);
|
||||
let stopCommand = "";
|
||||
let startCommand = "";
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
stopCommand = `
|
||||
echo "Stopping compose to 0 replicas"
|
||||
echo "Service name: ${compose.appName}_${volumeBackup.serviceName}"
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service scale ${compose.appName}_${volumeBackup.serviceName}=0`;
|
||||
startCommand = `
|
||||
echo "Starting compose to $ACTUAL_REPLICAS replicas"
|
||||
docker service scale ${compose.appName}_${volumeBackup.serviceName}=$ACTUAL_REPLICAS`;
|
||||
} else {
|
||||
stopCommand = `
|
||||
echo "Stopping compose container"
|
||||
ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}")
|
||||
docker stop $ID`;
|
||||
startCommand = `
|
||||
echo "Starting compose container"
|
||||
docker start $ID
|
||||
echo "Compose container started"
|
||||
`;
|
||||
}
|
||||
return `
|
||||
${stopCommand}
|
||||
${baseCommand}
|
||||
${startCommand}
|
||||
`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export * from "./backup";
|
||||
export * from "./restore";
|
||||
export * from "./utils";
|
||||
import { volumeBackups } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db/index";
|
||||
import { scheduleVolumeBackup } from "./utils";
|
||||
|
||||
export const initVolumeBackupsCronJobs = async () => {
|
||||
console.log("Setting up volume backups cron jobs....");
|
||||
try {
|
||||
const volumeBackupsResult = await db.query.volumeBackups.findMany({
|
||||
where: eq(volumeBackups.enabled, true),
|
||||
with: {
|
||||
application: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Initializing ${volumeBackupsResult.length} volume backups`);
|
||||
for (const volumeBackup of volumeBackupsResult) {
|
||||
scheduleVolumeBackup(volumeBackup.volumeBackupId);
|
||||
console.log(
|
||||
`Initialized volume backup: ${volumeBackup.name} ${volumeBackup.serviceType} ✅`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error initializing volume backups: ${error}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findDestinationById,
|
||||
getS3Credentials,
|
||||
paths,
|
||||
} from "../..";
|
||||
import path from "node:path";
|
||||
|
||||
export const restoreVolume = async (
|
||||
id: string,
|
||||
destinationId: string,
|
||||
volumeName: string,
|
||||
backupFileName: string,
|
||||
serverId: string,
|
||||
serviceType: "application" | "compose",
|
||||
) => {
|
||||
const destination = await findDestinationById(destinationId);
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeName);
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
const backupPath = `${bucketPath}/${backupFileName}`;
|
||||
|
||||
// Command to download backup file from S3
|
||||
const downloadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${backupPath}" "${volumeBackupPath}/${backupFileName}"`;
|
||||
|
||||
// Base restore command that creates the volume and restores data
|
||||
const baseRestoreCommand = `
|
||||
set -e
|
||||
echo "Volume name: ${volumeName}"
|
||||
echo "Backup file name: ${backupFileName}"
|
||||
echo "Volume backup path: ${volumeBackupPath}"
|
||||
echo "Downloading backup from S3..."
|
||||
mkdir -p ${volumeBackupPath}
|
||||
${downloadCommand}
|
||||
echo "Download completed ✅"
|
||||
echo "Creating new volume and restoring data..."
|
||||
docker run --rm \
|
||||
-v ${volumeName}:/volume_data \
|
||||
-v ${volumeBackupPath}:/backup \
|
||||
ubuntu \
|
||||
bash -c "cd /volume_data && tar xvf /backup/${backupFileName} ."
|
||||
echo "Volume restore completed ✅"
|
||||
`;
|
||||
|
||||
// Function to check if volume exists and get containers using it
|
||||
const checkVolumeCommand = `
|
||||
# Check if volume exists
|
||||
VOLUME_EXISTS=$(docker volume ls -q --filter name="^${volumeName}$" | wc -l)
|
||||
echo "Volume exists: $VOLUME_EXISTS"
|
||||
|
||||
if [ "$VOLUME_EXISTS" = "0" ]; then
|
||||
echo "Volume doesn't exist, proceeding with direct restore"
|
||||
${baseRestoreCommand}
|
||||
else
|
||||
echo "Volume exists, checking for containers using it (including stopped ones)..."
|
||||
|
||||
# Get ALL containers (running and stopped) using this volume - much simpler with native filter!
|
||||
CONTAINERS_USING_VOLUME=$(docker ps -a --filter "volume=${volumeName}" --format "{{.ID}}|{{.Names}}|{{.State}}|{{.Labels}}")
|
||||
|
||||
if [ -z "$CONTAINERS_USING_VOLUME" ]; then
|
||||
echo "Volume exists but no containers are using it"
|
||||
echo "Removing existing volume and proceeding with restore"
|
||||
docker volume rm ${volumeName} --force
|
||||
${baseRestoreCommand}
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ WARNING: Cannot restore volume as it is currently in use!"
|
||||
echo ""
|
||||
echo "📋 The following containers are using volume '${volumeName}':"
|
||||
echo ""
|
||||
|
||||
echo "$CONTAINERS_USING_VOLUME" | while IFS='|' read container_id container_name container_state labels; do
|
||||
echo " 🐳 Container: $container_name ($container_id)"
|
||||
echo " Status: $container_state"
|
||||
|
||||
# Determine container type
|
||||
if echo "$labels" | grep -q "com.docker.swarm.service.name="; then
|
||||
SERVICE_NAME=$(echo "$labels" | grep -o "com.docker.swarm.service.name=[^,]*" | cut -d'=' -f2)
|
||||
echo " Type: Docker Swarm Service ($SERVICE_NAME)"
|
||||
elif echo "$labels" | grep -q "com.docker.compose.project="; then
|
||||
PROJECT_NAME=$(echo "$labels" | grep -o "com.docker.compose.project=[^,]*" | cut -d'=' -f2)
|
||||
echo " Type: Docker Compose ($PROJECT_NAME)"
|
||||
else
|
||||
echo " Type: Regular Container"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🔧 To restore this volume, please:"
|
||||
echo " 1. Stop all containers/services using this volume"
|
||||
echo " 2. Remove the existing volume: docker volume rm ${volumeName}"
|
||||
echo " 3. Run the restore operation again"
|
||||
echo ""
|
||||
echo "❌ Volume restore aborted - volume is in use"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
`;
|
||||
|
||||
if (serviceType === "application") {
|
||||
const application = await findApplicationById(id);
|
||||
return `
|
||||
echo "=== VOLUME RESTORE FOR APPLICATION ==="
|
||||
echo "Application: ${application.appName}"
|
||||
${checkVolumeCommand}
|
||||
`;
|
||||
}
|
||||
|
||||
if (serviceType === "compose") {
|
||||
const compose = await findComposeById(id);
|
||||
|
||||
return `
|
||||
echo "=== VOLUME RESTORE FOR COMPOSE ==="
|
||||
echo "Compose: ${compose.appName}"
|
||||
echo "Compose Type: ${compose.composeType}"
|
||||
${checkVolumeCommand}
|
||||
`;
|
||||
}
|
||||
|
||||
// Fallback for unknown service types
|
||||
return checkVolumeCommand;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import {
|
||||
createDeploymentVolumeBackup,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
updateDeploymentStatus,
|
||||
} from "../..";
|
||||
import { backupVolume } from "./backup";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
|
||||
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
|
||||
await runVolumeBackup(volumeBackupId);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeVolumeBackupJob = async (volumeBackupId: string) => {
|
||||
const currentJob = scheduledJobs[volumeBackupId];
|
||||
currentJob?.cancel();
|
||||
};
|
||||
|
||||
export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
const deployment = await createDeploymentVolumeBackup({
|
||||
volumeBackupId: volumeBackup.volumeBackupId,
|
||||
title: "Volume Backup",
|
||||
description: "Volume Backup",
|
||||
});
|
||||
|
||||
try {
|
||||
const command = await backupVolume(volumeBackup);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user