Merge branch 'canary' into feature/stop-grace-period-2227-alt

This commit is contained in:
Lucas Manchine
2025-09-05 12:34:17 -03:00
427 changed files with 35912 additions and 4680 deletions

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import alias from "esbuild-plugin-alias";
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
const __dirname = path.dirname(__filename);

View File

@@ -28,13 +28,13 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/azure": "^1.3.23",
"@ai-sdk/cohere": "^1.2.10",
"@ai-sdk/deepinfra": "^0.0.4",
"@ai-sdk/mistral": "^1.2.8",
"@ai-sdk/openai": "^1.3.22",
"@ai-sdk/openai-compatible": "^0.0.13",
"@ai-sdk/anthropic": "^2.0.5",
"@ai-sdk/azure": "^2.0.16",
"@ai-sdk/cohere": "^2.0.4",
"@ai-sdk/deepinfra": "^1.0.10",
"@ai-sdk/mistral": "^2.0.7",
"@ai-sdk/openai": "^2.0.16",
"@ai-sdk/openai-compatible": "^1.0.10",
"@better-auth/utils": "0.2.4",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.1.3",
@@ -44,7 +44,8 @@
"@react-email/components": "^0.0.21",
"@trpc/server": "^10.45.2",
"adm-zip": "^0.5.16",
"ai": "^4.3.16",
"ai": "^5.0.17",
"ai-sdk-ollama": "^0.5.1",
"bcrypt": "5.1.1",
"better-auth": "v1.2.8-beta.7",
"bl": "6.0.11",
@@ -65,7 +66,6 @@
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
"ollama-ai-provider": "^1.2.0",
"otpauth": "^9.4.0",
"pino": "9.4.0",
"pino-pretty": "11.2.2",

View File

@@ -1,14 +0,0 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./server/db/schema/index.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
out: "drizzle",
migrations: {
table: "migrations",
schema: "public",
},
});

View File

@@ -1,6 +1,7 @@
import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
declare global {
var db: PostgresJsDatabase<typeof schema> | undefined;
}

View File

@@ -1,21 +0,0 @@
// import { drizzle } from "drizzle-orm/postgres-js";
// import { migrate } from "drizzle-orm/postgres-js/migrator";
// import postgres from "postgres";
// const connectionString = process.env.DATABASE_URL!;
// const sql = postgres(connectionString, { max: 1 });
// const db = drizzle(sql);
// export const migration = async () =>
// await migrate(db, { migrationsFolder: "drizzle" })
// .then(() => {
// console.log("Migration complete");
// sql.end();
// })
// .catch((error) => {
// console.log("Migration failed", error);
// })
// .finally(() => {
// sql.end();
// });

View File

@@ -1,23 +0,0 @@
import { sql } from "drizzle-orm";
// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL!;
const pg = postgres(connectionString, { max: 1 });
const db = drizzle(pg);
const clearDb = async (): Promise<void> => {
try {
const tablesQuery = sql<string>`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
const tables = await db.execute(tablesQuery);
console.log(tables);
await pg.end();
} catch (error) {
console.error("Error cleaning database", error);
} finally {
}
};
clearDb();

View File

@@ -112,6 +112,10 @@ export const member = pgTable("member", {
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedEnvironments: text("accessedEnvironments")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedServices: text("accesedServices")
.array()
.notNull()

View File

@@ -32,7 +32,7 @@ export const aiRelations = relations(ai, ({ one }) => ({
const createSchema = createInsertSchema(ai, {
name: z.string().min(1, { message: "Name is required" }),
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
apiKey: z.string().min(1, { message: "API Key is required" }),
apiKey: z.string(),
model: z.string().min(1, { message: "Model is required" }),
isEnabled: z.boolean().optional(),
});
@@ -55,7 +55,7 @@ export const apiUpdateAi = createSchema
.omit({ organizationId: true });
export const deploySuggestionSchema = z.object({
projectId: z.string().min(1),
environmentId: z.string().min(1),
id: z.string().min(1),
dockerCompose: z.string().min(1),
envVariables: z.string(),

View File

@@ -14,6 +14,7 @@ import { z } from "zod";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { environments } from "./environment";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -80,6 +81,7 @@ export const applications = pgTable("application", {
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
previewLabels: text("previewLabels").array(),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
previewHttps: boolean("previewHttps").notNull().default(false),
@@ -180,9 +182,9 @@ export const applications = pgTable("application", {
registryId: text("registryId").references(() => registry.registryId, {
onDelete: "set null",
}),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
@@ -203,9 +205,9 @@ export const applications = pgTable("application", {
export const applicationsRelations = relations(
applications,
({ one, many }) => ({
project: one(projects, {
fields: [applications.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [applications.environmentId],
references: [environments.environmentId],
}),
deployments: many(deployments),
customGitSSHKey: one(sshKeys, {
@@ -274,7 +276,7 @@ const createSchema = createInsertSchema(applications, {
customGitBuildPath: z.string().optional(),
customGitUrl: z.string().optional(),
buildPath: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
sourceType: z
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
.optional(),
@@ -310,6 +312,7 @@ const createSchema = createInsertSchema(applications, {
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
@@ -318,7 +321,7 @@ export const apiCreateApplication = createSchema.pick({
name: true,
appName: true,
description: true,
projectId: true,
environmentId: true,
serverId: true,
});

View File

@@ -7,17 +7,17 @@ import { backups } from "./backups";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { environments } from "./environment";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { mounts } from "./mount";
import { projects } from "./project";
import { schedules } from "./schedule";
import { server } from "./server";
import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { schedules } from "./schedule";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git",
"github",
@@ -85,9 +85,9 @@ export const compose = pgTable("compose", {
.default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -110,9 +110,9 @@ export const compose = pgTable("compose", {
});
export const composeRelations = relations(compose, ({ one, many }) => ({
project: one(projects, {
fields: [compose.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [compose.environmentId],
references: [environments.environmentId],
}),
deployments: many(deployments),
mounts: many(mounts),
@@ -150,7 +150,7 @@ const createSchema = createInsertSchema(compose, {
description: z.string(),
env: z.string().optional(),
composeFile: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
customGitSSHKeyId: z.string().optional(),
command: z.string().optional(),
composePath: z.string().min(1),
@@ -161,7 +161,7 @@ const createSchema = createInsertSchema(compose, {
export const apiCreateCompose = createSchema.pick({
name: true,
description: true,
projectId: true,
environmentId: true,
composeType: true,
appName: true,
serverId: true,
@@ -170,7 +170,7 @@ export const apiCreateCompose = createSchema.pick({
export const apiCreateComposeByTemplate = createSchema
.pick({
projectId: true,
environmentId: true,
})
.extend({
id: z.string().min(1),

View File

@@ -0,0 +1,85 @@
import { relations } from "drizzle-orm";
import { pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { projects } from "./project";
import { redis } from "./redis";
export const environments = pgTable("environment", {
environmentId: text("environmentId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
description: text("description"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
env: text("env").notNull().default(""),
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
});
export const environmentRelations = relations(
environments,
({ one, many }) => ({
project: one(projects, {
fields: [environments.projectId],
references: [projects.projectId],
}),
applications: many(applications),
mariadb: many(mariadb),
postgres: many(postgres),
mysql: many(mysql),
redis: many(redis),
mongo: many(mongo),
compose: many(compose),
}),
);
const createSchema = createInsertSchema(environments, {
environmentId: z.string().min(1),
name: z.string().min(1),
description: z.string().optional(),
});
export const apiCreateEnvironment = createSchema.pick({
name: true,
description: true,
projectId: true,
});
export const apiFindOneEnvironment = createSchema
.pick({
environmentId: true,
})
.required();
export const apiRemoveEnvironment = createSchema
.pick({
environmentId: true,
})
.required();
export const apiUpdateEnvironment = createSchema.partial().extend({
environmentId: z.string().min(1),
});
export const apiDuplicateEnvironment = createSchema
.pick({
environmentId: true,
name: true,
description: true,
})
.required({
environmentId: true,
name: true,
});

View File

@@ -1,36 +1,37 @@
export * from "./account";
export * from "./ai";
export * from "./application";
export * from "./postgres";
export * from "./user";
export * from "./project";
export * from "./domain";
export * from "./mariadb";
export * from "./mongo";
export * from "./mysql";
export * from "./backups";
export * from "./destination";
export * from "./deployment";
export * from "./mount";
export * from "./certificate";
export * from "./session";
export * from "./redirects";
export * from "./security";
export * from "./port";
export * from "./redis";
export * from "./shared";
export * from "./compose";
export * from "./registry";
export * from "./notification";
export * from "./ssh-key";
export * from "./git-provider";
export * from "./bitbucket";
export * from "./certificate";
export * from "./compose";
export * from "./deployment";
export * from "./destination";
export * from "./domain";
export * from "./environment";
export * from "./git-provider";
export * from "./gitea";
export * from "./github";
export * from "./gitlab";
export * from "./gitea";
export * from "./server";
export * from "./utils";
export * from "./mariadb";
export * from "./mongo";
export * from "./mount";
export * from "./mysql";
export * from "./notification";
export * from "./port";
export * from "./postgres";
export * from "./preview-deployments";
export * from "./ai";
export * from "./account";
export * from "./schedule";
export * from "./project";
export * from "./redirects";
export * from "./redis";
export * from "./registry";
export * from "./rollbacks";
export * from "./schedule";
export * from "./security";
export * from "./server";
export * from "./session";
export * from "./shared";
export * from "./ssh-key";
export * from "./user";
export * from "./utils";
export * from "./volume-backups";

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -66,18 +66,19 @@ export const mariadb = pgTable("mariadb", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
project: one(projects, {
fields: [mariadb.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [mariadb.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -94,8 +95,19 @@ const createSchema = createInsertSchema(mariadb, {
createdAt: z.string(),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
databasePassword: z.string(),
databaseRootPassword: z.string().optional(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
})
.optional(),
dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(),
env: z.string().optional(),
@@ -103,7 +115,7 @@ const createSchema = createInsertSchema(mariadb, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -124,7 +136,7 @@ export const apiCreateMariaDB = createSchema
appName: true,
dockerImage: true,
databaseRootPassword: true,
projectId: true,
environmentId: true,
description: true,
databaseName: true,
databaseUser: true,

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -62,9 +62,10 @@ export const mongo = pgTable("mongo", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
@@ -72,9 +73,9 @@ export const mongo = pgTable("mongo", {
});
export const mongoRelations = relations(mongo, ({ one, many }) => ({
project: one(projects, {
fields: [mongo.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [mongo.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -89,7 +90,12 @@ const createSchema = createInsertSchema(mongo, {
createdAt: z.string(),
mongoId: z.string(),
name: z.string().min(1),
databasePassword: z.string(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
@@ -98,7 +104,7 @@ const createSchema = createInsertSchema(mongo, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -119,7 +125,7 @@ export const apiCreateMongo = createSchema
name: true,
appName: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
databaseUser: true,
databasePassword: true,

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -64,18 +64,19 @@ export const mysql = pgTable("mysql", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
project: one(projects, {
fields: [mysql.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [mysql.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -92,8 +93,19 @@ const createSchema = createInsertSchema(mysql, {
name: z.string().min(1),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
databasePassword: z.string(),
databaseRootPassword: z.string().optional(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
})
.optional(),
dockerImage: z.string().default("mysql:8"),
command: z.string().optional(),
env: z.string().optional(),
@@ -101,7 +113,6 @@ const createSchema = createInsertSchema(mysql, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -121,7 +132,7 @@ export const apiCreateMySql = createSchema
name: true,
appName: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
databaseName: true,
databaseUser: true,

View File

@@ -4,8 +4,8 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import {
applicationStatus,
@@ -64,18 +64,19 @@ export const postgres = pgTable("postgres", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const postgresRelations = relations(postgres, ({ one, many }) => ({
project: one(projects, {
fields: [postgres.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [postgres.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
@@ -88,7 +89,12 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
const createSchema = createInsertSchema(postgres, {
postgresId: z.string(),
name: z.string().min(1),
databasePassword: z.string(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"),
@@ -98,7 +104,7 @@ const createSchema = createInsertSchema(postgres, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
createdAt: z.string(),
@@ -122,7 +128,7 @@ export const apiCreatePostgres = createSchema
databaseUser: true,
databasePassword: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
serverId: true,
})

View File

@@ -4,13 +4,7 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { applications } from "./application";
import { compose } from "./compose";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
import { environments } from "./environment";
export const projects = pgTable("project", {
projectId: text("projectId")
@@ -30,13 +24,7 @@ export const projects = pgTable("project", {
});
export const projectRelations = relations(projects, ({ many, one }) => ({
mysql: many(mysql),
postgres: many(postgres),
mariadb: many(mariadb),
applications: many(applications),
mongo: many(mongo),
redis: many(redis),
compose: many(compose),
environments: many(environments),
organization: one(organization, {
fields: [projects.organizationId],
references: [organization.id],

View File

@@ -3,6 +3,7 @@ import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { environments } from "./environment";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
@@ -60,18 +61,19 @@ export const redis = pgTable("redis", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
projectId: text("projectId")
environmentId: text("environmentId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const redisRelations = relations(redis, ({ one, many }) => ({
project: one(projects, {
fields: [redis.projectId],
references: [projects.projectId],
environment: one(environments, {
fields: [redis.environmentId],
references: [environments.environmentId],
}),
mounts: many(mounts),
server: one(server, {
@@ -93,7 +95,7 @@ const createSchema = createInsertSchema(redis, {
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
description: z.string().optional(),
@@ -114,7 +116,7 @@ export const apiCreateRedis = createSchema
appName: true,
databasePassword: true,
dockerImage: true,
projectId: true,
environmentId: true,
description: true,
serverId: true,
})

View File

@@ -27,7 +27,9 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type<
Application & {
project: Project;
environment: {
project: Project;
};
mounts: Mount[];
ports: Port[];
registry?: Registry | null;

View File

@@ -175,6 +175,7 @@ export const apiAssignPermissions = createSchema
})
.extend({
accessedProjects: z.array(z.string()).optional(),
accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),

View File

@@ -1,35 +0,0 @@
// import bc from "bcrypt";
// import { drizzle } from "drizzle-orm/postgres-js";
// import postgres from "postgres";
// import { users } from "./schema";
// const connectionString = process.env.DATABASE_URL!;
// const pg = postgres(connectionString, { max: 1 });
// const db = drizzle(pg);
// function password(txt: string) {
// return bc.hashSync(txt, 10);
// }
// async function seed() {
// console.log("> Seed:", process.env.DATABASE_PATH, "\n");
// // const authenticationR = await db
// // .insert(users)
// // .values([
// // {
// // email: "user1@hotmail.com",
// // password: password("12345671"),
// // },
// // ])
// // .onConflictDoNothing()
// // .returning();
// // console.log("\nSemillas Update:", authenticationR.length);
// }
// seed().catch((e) => {
// console.error(e);
// process.exit(1);
// });

View File

@@ -1,51 +1,65 @@
export * from "./auth/random-password";
export * from "./constants/index";
export * from "./db/validations/domain";
export * from "./db/validations/index";
export * from "./lib/auth";
export * from "./lib/logger";
export * from "./monitoring/utils";
export * from "./services/admin";
export * from "./services/user";
export * from "./services/project";
export * from "./services/postgres";
export * from "./services/domain";
export * from "./services/mariadb";
export * from "./services/mongo";
export * from "./services/mysql";
export * from "./services/application";
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";
export * from "./services/mount";
export * from "./services/certificate";
export * from "./services/redirect";
export * from "./services/security";
export * from "./services/preview-deployment";
export * from "./services/port";
export * from "./services/redis";
export * from "./services/compose";
export * from "./services/registry";
export * from "./services/notification";
export * from "./services/ssh-key";
export * from "./services/git-provider";
export * from "./services/bitbucket";
export * from "./services/certificate";
export * from "./services/cluster";
export * from "./services/compose";
export * from "./services/deployment";
export * from "./services/destination";
export * from "./services/docker";
export * from "./services/domain";
export * from "./services/environment";
export * from "./services/git-provider";
export * from "./services/gitea";
export * from "./services/github";
export * from "./services/gitlab";
export * from "./services/gitea";
export * from "./services/server";
export * from "./services/schedule";
export * from "./services/application";
export * from "./services/mariadb";
export * from "./services/mongo";
export * from "./services/mount";
export * from "./services/mysql";
export * from "./services/notification";
export * from "./services/port";
export * from "./services/postgres";
export * from "./services/preview-deployment";
export * from "./services/project";
export * from "./services/redirect";
export * from "./services/redis";
export * from "./services/registry";
export * from "./services/rollbacks";
export * from "./utils/databases/rebuild";
export * from "./services/schedule";
export * from "./services/security";
export * from "./services/server";
export * from "./services/settings";
export * from "./services/ssh-key";
export * from "./services/user";
export * from "./services/volume-backups";
export * from "./setup/config-paths";
export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup";
export * from "./setup/redis-setup";
export * from "./setup/server-audit";
export * from "./setup/server-setup";
export * from "./setup/monitoring-setup";
export * from "./setup/server-validate";
export * from "./setup/setup";
export * from "./setup/traefik-setup";
export * from "./setup/server-validate";
export * from "./setup/server-audit";
export * from "./utils/watch-paths/should-deploy";
export * from "./utils/providers/github";
export * from "./templates/processors";
export * from "./utils/access-log/handler";
export {
getLogCleanupStatus,
startLogCleanup,
stopLogCleanup,
} from "./utils/access-log/handler";
export * from "./utils/access-log/types";
export * from "./utils/access-log/utils";
export * from "./utils/backups/compose";
export * from "./utils/backups/index";
export * from "./utils/backups/mariadb";
export * from "./utils/backups/mongo";
@@ -53,55 +67,51 @@ export * from "./utils/backups/mysql";
export * from "./utils/backups/postgres";
export * from "./utils/backups/utils";
export * from "./utils/backups/web-server";
export * from "./utils/backups/compose";
export * from "./templates/processors";
export * from "./utils/notifications/build-error";
export * from "./utils/notifications/build-success";
export * from "./utils/notifications/database-backup";
export * from "./utils/notifications/dokploy-restart";
export * from "./utils/notifications/utils";
export * from "./utils/notifications/docker-cleanup";
export * from "./utils/notifications/server-threshold";
export * from "./utils/builders/index";
export * from "./utils/builders/compose";
export * from "./utils/builders/docker-file";
export * from "./utils/builders/drop";
export * from "./utils/builders/heroku";
export * from "./utils/builders/index";
export * from "./utils/builders/nixpacks";
export * from "./utils/builders/paketo";
export * from "./utils/builders/static";
export * from "./utils/builders/utils";
export * from "./utils/cluster/upload";
export * from "./utils/docker/compose";
export * from "./utils/databases/rebuild";
export * from "./utils/docker/collision";
export * from "./utils/docker/domain";
export * from "./utils/docker/utils";
export * from "./utils/docker/types";
export * from "./utils/docker/compose";
export * from "./utils/docker/compose/configs";
export * from "./utils/docker/compose/network";
export * from "./utils/docker/compose/secrets";
export * from "./utils/docker/compose/service";
export * from "./utils/docker/compose/volume";
export * from "./utils/docker/domain";
export * from "./utils/docker/types";
export * from "./utils/docker/utils";
export * from "./utils/filesystem/directory";
export * from "./utils/filesystem/ssh";
export * from "./utils/gpu-setup";
export * from "./utils/notifications/build-error";
export * from "./utils/notifications/build-success";
export * from "./utils/notifications/database-backup";
export * from "./utils/notifications/docker-cleanup";
export * from "./utils/notifications/dokploy-restart";
export * from "./utils/notifications/server-threshold";
export * from "./utils/notifications/utils";
export * from "./utils/process/execAsync";
export * from "./utils/process/spawnAsync";
export * from "./utils/providers/bitbucket";
export * from "./utils/providers/docker";
export * from "./utils/providers/git";
export * from "./utils/providers/gitea";
export * from "./utils/providers/github";
export * from "./utils/providers/github";
export * from "./utils/providers/gitlab";
export * from "./utils/providers/gitea";
export * from "./utils/providers/raw";
export * from "./utils/schedules/index";
export * from "./utils/schedules/utils";
export * from "./utils/servers/remote-docker";
export * from "./utils/traefik/application";
export * from "./utils/traefik/domain";
export * from "./utils/traefik/file-types";
@@ -110,30 +120,6 @@ export * from "./utils/traefik/redirect";
export * from "./utils/traefik/security";
export * from "./utils/traefik/types";
export * from "./utils/traefik/web-server";
export * from "./wss/utils";
export * from "./utils/access-log/handler";
export * from "./utils/access-log/types";
export * from "./utils/access-log/utils";
export * from "./constants/index";
export * from "./monitoring/utils";
export * from "./db/validations/domain";
export * from "./db/validations/index";
export * from "./utils/gpu-setup";
export * from "./lib/auth";
export {
startLogCleanup,
stopLogCleanup,
getLogCleanupStatus,
} from "./utils/access-log/handler";
export * from "./utils/schedules/utils";
export * from "./utils/schedules/index";
export * from "./utils/volume-backups/index";
export * from "./lib/logger";
export * from "./utils/watch-paths/should-deploy";
export * from "./wss/utils";

View File

@@ -105,7 +105,11 @@ export const findApplicationById = async (applicationId: string) => {
const application = await db.query.applications.findFirst({
where: eq(applications.applicationId, applicationId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
domains: true,
deployments: true,
mounts: true,
@@ -180,7 +184,7 @@ export const deployApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -227,11 +231,11 @@ export const deployApplication = async ({
}
await sendBuildSuccessNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -239,13 +243,13 @@ export const deployApplication = async ({
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
});
throw error;
@@ -307,7 +311,7 @@ export const deployRemoteApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -363,11 +367,11 @@ export const deployRemoteApplication = async ({
}
await sendBuildSuccessNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -387,12 +391,12 @@ export const deployRemoteApplication = async ({
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.project.organizationId,
organizationId: application.environment.project.organizationId,
});
throw error;

View File

@@ -126,7 +126,11 @@ export const findComposeById = async (composeId: string) => {
const result = await db.query.compose.findFirst({
where: eq(compose.composeId, composeId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
deployments: true,
mounts: true,
domains: true,
@@ -222,7 +226,7 @@ export const deployCompose = async ({
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.projectId
compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -255,11 +259,11 @@ export const deployCompose = async ({
});
await sendBuildSuccessNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -268,13 +272,13 @@ export const deployCompose = async ({
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
});
throw error;
}
@@ -330,7 +334,7 @@ export const deployRemoteCompose = async ({
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.projectId
compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -387,11 +391,11 @@ export const deployRemoteCompose = async ({
});
await sendBuildSuccessNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -410,13 +414,13 @@ export const deployRemoteCompose = async ({
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.project.name,
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.project.organizationId,
organizationId: compose.environment.project.organizationId,
});
throw error;
}

View File

@@ -13,6 +13,7 @@ import {
deployments,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { desc, eq } from "drizzle-orm";
@@ -21,18 +22,16 @@ import {
findApplicationById,
updateApplicationStatus,
} from "./application";
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 Compose, findComposeById, updateCompose } from "./compose";
import {
type PreviewDeployment,
findPreviewDeploymentById,
type PreviewDeployment,
updatePreviewDeployment,
} from "./preview-deployment";
import { removeRollbackById } from "./rollbacks";
import { findScheduleById } from "./schedule";
import { findServerById, type Server } from "./server";
import { findVolumeBackupById } from "./volume-backups";
export type Deployment = typeof deployments.$inferSelect;

View File

@@ -0,0 +1,140 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateEnvironment,
type apiDuplicateEnvironment,
environments,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { asc, eq } from "drizzle-orm";
export type Environment = typeof environments.$inferSelect;
export const createEnvironment = async (
input: typeof apiCreateEnvironment._type,
) => {
const newEnvironment = await db
.insert(environments)
.values({
...input,
})
.returning()
.then((value) => value[0]);
if (!newEnvironment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the environment",
});
}
return newEnvironment;
};
export const findEnvironmentById = async (environmentId: string) => {
const environment = await db.query.environments.findFirst({
where: eq(environments.environmentId, environmentId),
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
project: true,
},
});
if (!environment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Environment not found",
});
}
return environment;
};
export const findEnvironmentsByProjectId = async (projectId: string) => {
const projectEnvironments = await db.query.environments.findMany({
where: eq(environments.projectId, projectId),
orderBy: asc(environments.createdAt),
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
project: true,
},
});
return projectEnvironments;
};
export const deleteEnvironment = async (environmentId: string) => {
const currentEnvironment = await findEnvironmentById(environmentId);
if (currentEnvironment.name === "production") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot delete the production environment",
});
}
const deletedEnvironment = await db
.delete(environments)
.where(eq(environments.environmentId, environmentId))
.returning()
.then((value) => value[0]);
return deletedEnvironment;
};
export const updateEnvironmentById = async (
environmentId: string,
environmentData: Partial<Environment>,
) => {
const result = await db
.update(environments)
.set({
...environmentData,
})
.where(eq(environments.environmentId, environmentId))
.returning()
.then((res) => res[0]);
return result;
};
export const duplicateEnvironment = async (
input: typeof apiDuplicateEnvironment._type,
) => {
// Find the original environment
const originalEnvironment = await findEnvironmentById(input.environmentId);
// Create a new environment with the provided name and description
const newEnvironment = await db
.insert(environments)
.values({
name: input.name,
description: input.description || originalEnvironment.description,
projectId: originalEnvironment.projectId,
})
.returning()
.then((value) => value[0]);
if (!newEnvironment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error duplicating the environment",
});
}
return newEnvironment;
};
export const createProductionEnvironment = async (projectId: string) => {
return createEnvironment({
name: "production",
description: "Production environment",
projectId,
});
};

View File

@@ -1,8 +1,8 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateGitea,
gitProvider,
gitea,
gitProvider,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,8 +1,8 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateGithub,
gitProvider,
github,
gitProvider,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,8 +1,8 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateGitlab,
gitProvider,
gitlab,
gitProvider,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -2,18 +2,17 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateMariaDB,
backups,
buildAppName,
mariadb,
} from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
@@ -57,7 +56,11 @@ export const findMariadbById = async (mariadbId: string) => {
const result = await db.query.mariadb.findFirst({
where: eq(mariadb.mariadbId, mariadbId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {

View File

@@ -2,19 +2,18 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateMongo,
backups,
buildAppName,
compose,
mongo,
} from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => {
@@ -54,7 +53,11 @@ export const findMongoById = async (mongoId: string) => {
const result = await db.query.mongo.findFirst({
where: eq(mongo.mongoId, mongoId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {

View File

@@ -105,13 +105,69 @@ export const findMountById = async (mountId: string) => {
const mount = await db.query.mounts.findFirst({
where: eq(mounts.mountId, mountId),
with: {
application: true,
postgres: true,
mariadb: true,
mongo: true,
mysql: true,
redis: true,
compose: true,
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
postgres: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mariadb: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mongo: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mysql: {
with: {
environment: {
with: {
project: true,
},
},
},
},
redis: {
with: {
environment: {
with: {
project: true,
},
},
},
},
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
});
if (!mount) {
@@ -123,6 +179,34 @@ export const findMountById = async (mountId: string) => {
return mount;
};
export const findMountOrganizationId = async (mountId: string) => {
const mount = await findMountById(mountId);
if (mount.application) {
return mount.application.environment.project.organizationId;
}
if (mount.postgres) {
return mount.postgres.environment.project.organizationId;
}
if (mount.mariadb) {
return mount.mariadb.environment.project.organizationId;
}
if (mount.mongo) {
return mount.mongo.environment.project.organizationId;
}
if (mount.mysql) {
return mount.mysql.environment.project.organizationId;
}
if (mount.redis) {
return mount.redis.environment.project.organizationId;
}
if (mount.compose) {
return mount.compose.environment.project.organizationId;
}
return null;
};
export const updateMount = async (
mountId: string,
mountData: Partial<Mount>,

View File

@@ -1,15 +1,18 @@
import { db } from "@dokploy/server/db";
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import {
type apiCreateMySql,
backups,
buildAppName,
mysql,
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => {
@@ -53,7 +56,11 @@ export const findMySqlById = async (mysqlId: string) => {
const result = await db.query.mysql.findFirst({
where: eq(mysql.mysqlId, mysqlId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {

View File

@@ -27,6 +27,17 @@ export const createPort = async (input: typeof apiCreatePort._type) => {
export const finPortById = async (portId: string) => {
const result = await db.query.ports.findFirst({
where: eq(ports.portId, portId),
with: {
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
});
if (!result) {
throw new TRPCError({

View File

@@ -2,18 +2,17 @@ import { db } from "@dokploy/server/db";
import {
type apiCreatePostgres,
backups,
buildAppName,
postgres,
} from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
@@ -52,7 +51,11 @@ export const findPostgresById = async (postgresId: string) => {
const result = await db.query.postgres.findFirst({
where: eq(postgres.postgresId, postgresId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {

View File

@@ -31,7 +31,11 @@ export const findPreviewDeploymentById = async (
application: {
with: {
server: true,
project: true,
environment: {
with: {
project: true,
},
},
},
},
},
@@ -45,68 +49,43 @@ export const findPreviewDeploymentById = async (
return application;
};
export const findApplicationByPreview = async (applicationId: string) => {
const application = await db.query.applications.findFirst({
with: {
previewDeployments: {
where: eq(previewDeployments.applicationId, applicationId),
},
project: true,
domains: true,
deployments: true,
mounts: true,
redirects: true,
security: true,
ports: true,
registry: true,
gitlab: true,
github: true,
bitbucket: true,
gitea: true,
server: true,
},
});
if (!application) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Applicationnot found",
});
}
return application;
};
export const removePreviewDeployment = async (previewDeploymentId: string) => {
try {
const application = await findApplicationByPreview(previewDeploymentId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const deployment = await db
.delete(previewDeployments)
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
.returning();
const application = await findApplicationById(
previewDeployment.applicationId,
);
application.appName = previewDeployment.appName;
const cleanupOperations = [
async () =>
await removeService(application?.appName, application?.serverId),
async () =>
await removeDeploymentsByPreviewDeploymentId(
previewDeployment,
application.serverId,
application?.serverId,
),
async () =>
await removeDirectoryCode(application.appName, application.serverId),
await removeDirectoryCode(application?.appName, application?.serverId),
async () =>
await removeTraefikConfig(application.appName, application.serverId),
await removeTraefikConfig(application?.appName, application?.serverId),
async () =>
await removeService(application?.appName, application.serverId),
await db
.delete(previewDeployments)
.where(
eq(previewDeployments.previewDeploymentId, previewDeploymentId),
)
.returning(),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch {}
} catch (error) {
console.error(error);
}
}
return deployment[0];
return previewDeployment;
} catch (error) {
const message =
error instanceof Error
@@ -157,7 +136,7 @@ export const createPreviewDeployment = async (
const appName = `preview-${application.appName}-${generatePassword(6)}`;
const org = await db.query.organization.findFirst({
where: eq(organization.id, application.project.organizationId),
where: eq(organization.id, application.environment.project.organizationId),
});
const generateDomain = await generateWildcardDomain(
application.previewWildcard || "*.traefik.me",

View File

@@ -11,6 +11,7 @@ import {
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { createProductionEnvironment } from "./environment";
export type Project = typeof projects.$inferSelect;
@@ -34,20 +35,31 @@ export const createProject = async (
});
}
return newProject;
// Automatically create a production environment
const newEnvironment = await createProductionEnvironment(
newProject.projectId,
);
return {
project: newProject,
environment: newEnvironment,
};
};
export const findProjectById = async (projectId: string) => {
const project = await db.query.projects.findFirst({
where: eq(projects.projectId, projectId),
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
environments: {
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
},
},
},
});
if (!project) {
@@ -86,7 +98,7 @@ export const updateProjectById = async (
};
export const validUniqueServerAppName = async (appName: string) => {
const query = await db.query.projects.findMany({
const query = await db.query.environments.findMany({
with: {
applications: {
where: eq(applications.appName, appName),

View File

@@ -1,15 +1,17 @@
import { db } from "@dokploy/server/db";
import { type apiCreateRedis, redis } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import {
type apiCreateRedis,
buildAppName,
redis,
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildRedis } from "@dokploy/server/utils/databases/redis";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
@@ -50,7 +52,11 @@ export const findRedisById = async (redisId: string) => {
const result = await db.query.redis.findFirst({
where: eq(redis.redisId, redisId),
with: {
project: true,
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
},

View File

@@ -76,9 +76,24 @@ export const createRollback = async (
});
};
const findRollbackById = async (rollbackId: string) => {
export const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId),
with: {
deployment: {
with: {
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
},
},
});
if (!result) {
@@ -179,7 +194,9 @@ const rollbackApplication = async (
image: string,
serverId?: string | null,
fullContext?: Application & {
project: Project;
environment: {
project: Project;
};
mounts: Mount[];
ports: Port[];
},
@@ -225,7 +242,7 @@ const rollbackApplication = async (
const bindsMount = generateBindMounts(mounts);
const envVariables = prepareEnvironmentVariables(
env,
fullContext.project.env,
fullContext.environment.project.env,
);
// For rollback, we use the provided image instead of calculating it

View File

@@ -4,11 +4,11 @@ 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 { type Schedule, schedules } from "../db/schema/schedule";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -35,9 +35,29 @@ export const findScheduleById = async (scheduleId: string) => {
const schedule = await db.query.schedules.findFirst({
where: eq(schedules.scheduleId, scheduleId),
with: {
application: true,
compose: true,
server: true,
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
server: {
with: {
organization: true,
},
},
},
});
@@ -50,6 +70,21 @@ export const findScheduleById = async (scheduleId: string) => {
return schedule;
};
export const findScheduleOrganizationId = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId);
if (schedule?.application) {
return schedule?.application?.environment?.project?.organizationId;
}
if (schedule?.compose) {
return schedule?.compose?.environment?.project?.organizationId;
}
if (schedule?.server) {
return schedule?.server?.organization?.id;
}
return null;
};
export const deleteSchedule = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId);
const serverId =

View File

@@ -253,37 +253,36 @@ export const getDockerResourceType = async (
resourceName: string,
serverId?: string,
) => {
let result = "";
const command = `
RESOURCE_NAME="${resourceName}"
if docker service inspect "$RESOURCE_NAME" &>/dev/null; then
echo "service"
exit 0
fi
try {
let result = "";
const command = `
RESOURCE_NAME="${resourceName}"
if docker service inspect "$RESOURCE_NAME" >/dev/null 2>&1; then
echo "service"
elif docker inspect "$RESOURCE_NAME" >/dev/null 2>&1; then
echo "standalone"
else
echo "unknown"
fi`;
if docker inspect "$RESOURCE_NAME" &>/dev/null; then
echo "standalone"
exit 0
fi
echo "unknown"
exit 0
`;
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "service") {
return "service";
}
if (result === "standalone") {
return "standalone";
}
return "unknown";
} catch (error) {
console.error(error);
return "unknown";
}
if (result === "service") {
return "service";
}
if (result === "standalone") {
return "standalone";
}
return "unknown";
};
export const reloadDockerResource = async (
@@ -294,8 +293,10 @@ export const reloadDockerResource = async (
let command = "";
if (resourceType === "service") {
command = `docker service update --force ${resourceName}`;
} else {
} else if (resourceType === "standalone") {
command = `docker restart ${resourceName}`;
} else {
throw new Error("Resource type not found");
}
if (serverId) {
await execAsyncRemote(serverId, command);
@@ -312,7 +313,7 @@ export const readEnvironmentVariables = async (
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}'`;
} else {
} else if (resourceType === "standalone") {
command = `docker container inspect ${resourceName} --format '{{json .Config.Env}}'`;
}
let result = "";
@@ -339,7 +340,7 @@ export const readPorts = async (
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
} else {
} else if (resourceType === "standalone") {
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
}
let result = "";
@@ -391,22 +392,22 @@ export const readPorts = async (
);
};
export const writeTraefikSetup = async (
input: TraefikOptions,
serverId?: string,
) => {
const resourceType = await getDockerResourceType("dokploy-traefik", serverId);
export const writeTraefikSetup = async (input: TraefikOptions) => {
const resourceType = await getDockerResourceType(
"dokploy-traefik",
input.serverId,
);
if (resourceType === "service") {
await initializeTraefikService({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: serverId,
serverId: input.serverId,
});
} else {
await initializeStandaloneTraefik({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: serverId,
serverId: input.serverId,
});
}
};

View File

@@ -23,6 +23,23 @@ export const addNewProject = async (
);
};
export const addNewEnvironment = async (
userId: string,
environmentId: string,
organizationId: string,
) => {
const userR = await findMemberById(userId, organizationId);
await db
.update(member)
.set({
accessedEnvironments: [...userR.accessedEnvironments, environmentId],
})
.where(
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
);
};
export const addNewService = async (
userId: string,
serviceId: string,
@@ -131,6 +148,21 @@ export const canPerformAccessProject = async (
return false;
};
export const canPerformAccessEnvironment = async (
userId: string,
environmentId: string,
organizationId: string,
) => {
const { accessedEnvironments } = await findMemberById(userId, organizationId);
const haveAccessToEnvironment = accessedEnvironments.includes(environmentId);
if (haveAccessToEnvironment) {
return true;
}
return false;
};
export const canAccessToTraefikFiles = async (
userId: string,
organizationId: string,
@@ -182,6 +214,32 @@ export const checkServiceAccess = async (
}
};
export const checkEnvironmentAccess = async (
userId: string,
environmentId: string,
organizationId: string,
action = "access" as const,
) => {
let hasPermission = false;
switch (action) {
case "access":
hasPermission = await canPerformAccessEnvironment(
userId,
environmentId,
organizationId,
);
break;
default:
hasPermission = false;
}
if (!hasPermission) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Permission denied",
});
}
};
export const checkProjectAccess = async (
authId: string,
action: "create" | "delete" | "access",

View File

@@ -578,8 +578,7 @@ export const createTraefikInstance = () => {
TRAEFIK_VERSION=${TRAEFIK_VERSION}
docker run -d \
--name dokploy-traefik \
--network dokploy-network \
--restart unless-stopped \
--restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
-v /var/run/docker.sock:/var/run/docker.sock \
@@ -587,6 +586,8 @@ export const createTraefikInstance = () => {
-p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
traefik:v$TRAEFIK_VERSION
docker network connect dokploy-network dokploy-traefik;
echo "Traefik version $TRAEFIK_VERSION installed ✅"
fi
`;

View File

@@ -13,7 +13,7 @@ export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0";
export interface TraefikOptions {
env?: string[];
@@ -89,21 +89,14 @@ export const initializeStandaloneTraefik = async ({
const docker = await getRemoteDocker(serverId);
try {
const container = docker.getContainer(containerName);
try {
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik Started ✅");
} catch (error) {
console.error("Error in initializeStandaloneTraefik", error);
}
} catch (error) {
await docker.createContainer(settings);
console.error("Error in initializeStandaloneTraefik", error);
throw error;
}
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch {}
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik Started ✅");
};
export const initializeTraefikService = async ({

View File

@@ -5,6 +5,7 @@ import type {
ExtractTablesWithRelations,
} from "drizzle-orm";
import { z } from "zod";
/*
* This is for testing purposes in the case we need a nested relational types
*

View File

@@ -1,7 +1,7 @@
import { paths } from "@dokploy/server/constants";
import { findAdmin } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync";
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";

View File

@@ -5,17 +5,16 @@ import { createDeepInfra } from "@ai-sdk/deepinfra";
import { createMistral } from "@ai-sdk/mistral";
import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { createOllama } from "ollama-ai-provider";
import { createOllama } from "ai-sdk-ollama";
function getProviderName(apiUrl: string) {
export function getProviderName(apiUrl: string) {
if (apiUrl.includes("api.openai.com")) return "openai";
if (apiUrl.includes("azure.com")) return "azure";
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
if (apiUrl.includes("api.cohere.ai")) return "cohere";
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
if (apiUrl.includes("api.mistral.ai")) return "mistral";
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
return "ollama";
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
return "custom";
}

View File

@@ -4,6 +4,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runComposeBackup = async (
compose: Compose,
backup: BackupSchedule,
) => {
const { projectId, name } = compose;
const project = await findProjectById(projectId);
const { environmentId, name } = compose;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix, databaseType } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,6 +4,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runMariadbBackup = async (
mariadb: Mariadb,
backup: BackupSchedule,
) => {
const { projectId, name } = mariadb;
const project = await findProjectById(projectId);
const { environmentId, name } = mariadb;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,14 +4,16 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findEnvironmentById } from "@dokploy/server/services/environment";
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 runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { projectId, name } = mongo;
const project = await findProjectById(projectId);
const { environmentId, name } = mongo;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,14 +4,16 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findEnvironmentById } from "@dokploy/server/services/environment";
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 runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { projectId, name } = mysql;
const project = await findProjectById(projectId);
const { environmentId, name } = mysql;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,6 +4,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runPostgresBackup = async (
postgres: Postgres,
backup: BackupSchedule,
) => {
const { name, projectId } = postgres;
const project = await findProjectById(projectId);
const { name, environmentId } = postgres;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -1,7 +1,7 @@
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 { scheduledJobs, scheduleJob } from "node-schedule";
import { keepLatestNBackups } from ".";
import { runComposeBackup } from "./compose";
import { runMariadbBackup } from "./mariadb";

View File

@@ -80,7 +80,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
writeStream.write("Zipped database and filesystem\n");
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
writeStream.write(`Running command: ${uploadCommand}\n`);
writeStream.write("Running command to upload backup to S3\n");
await execAsync(uploadCommand);
writeStream.write("Uploaded backup to S3 ✅\n");
writeStream.end();

View File

@@ -22,7 +22,7 @@ import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType<
"compose",
{ project: true; mounts: true; domains: true }
{ environment: { with: { project: true } }; mounts: true; domains: true }
>;
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
@@ -72,7 +72,10 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
NODE_ENV: process.env.NODE_ENV,
PATH: process.env.PATH,
...(composeType === "stack" && {
...getEnviromentVariablesObject(compose.env, compose.project.env),
...getEnviromentVariablesObject(
compose.env,
compose.environment.project.env,
),
}),
},
},
@@ -202,7 +205,8 @@ const createEnvFile = (compose: ComposeNested) => {
const envFileContent = prepareEnvironmentVariables(
envContent,
compose.project.env,
compose.environment.project.env,
compose.environment.env,
).join("\n");
if (!existsSync(dirname(envFilePath))) {
@@ -232,7 +236,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFileContent = prepareEnvironmentVariables(
envContent,
compose.project.env,
compose.environment.project.env,
compose.environment.env,
).join("\n");
const encodedContent = encodeBase64(envFileContent);
@@ -247,7 +252,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
const envVars = getEnviromentVariablesObject(
compose.env,
compose.project.env,
compose.environment.project.env,
);
const exports = Object.entries(envVars)
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)

View File

@@ -1,11 +1,11 @@
import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
import type { ApplicationNested } from ".";
import {
getBuildAppDirectory,
getDockerContextPath,
} from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
import { createEnvFile, createEnvFileCommand } from "./utils";
export const buildCustomDocker = async (
@@ -28,7 +28,8 @@ export const buildCustomDocker = async (
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath = getDockerContextPath(application);
@@ -51,7 +52,12 @@ export const buildCustomDocker = async (
as it could be publicly exposed.
*/
if (!publishDirectory) {
createEnvFile(dockerFilePath, env, application.project.env);
createEnvFile(
dockerFilePath,
env,
application.environment.project.env,
application.environment.env,
);
}
await spawnAsync(
@@ -92,7 +98,8 @@ export const getDockerCommand = (
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath =
@@ -121,7 +128,8 @@ export const getDockerCommand = (
command += createEnvFileCommand(
dockerFilePath,
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
}

View File

@@ -1,8 +1,8 @@
import type { WriteStream } from "node:fs";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildHeroku = async (
@@ -13,7 +13,8 @@ export const buildHeroku = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
try {
const args = [
@@ -53,7 +54,8 @@ export const getHerokuCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const args = [

View File

@@ -30,7 +30,7 @@ export type ApplicationNested = InferResultType<
redirects: true;
ports: true;
registry: true;
project: true;
environment: { with: { project: true } };
}
>;
@@ -149,7 +149,8 @@ export const mechanizeDockerContainer = async (
const filesMount = generateFileMounts(appName, application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const image = getImageName(application);

View File

@@ -1,14 +1,14 @@
import { type WriteStream, existsSync, mkdirSync } from "node:fs";
import { existsSync, mkdirSync, type WriteStream } from "node:fs";
import path from "node:path";
import {
buildStatic,
getStaticCommand,
} from "@dokploy/server/utils/builders/static";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
export const buildNixpacks = async (
application: ApplicationNested,
@@ -20,7 +20,8 @@ export const buildNixpacks = async (
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const writeToStream = (data: string) => {
@@ -101,7 +102,8 @@ export const getNixpacksCommand = (
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const args = ["build", buildAppDirectory, "--name", appName];

View File

@@ -1,8 +1,8 @@
import type { WriteStream } from "node:fs";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
export const buildPaketo = async (
application: ApplicationNested,
@@ -12,7 +12,8 @@ export const buildPaketo = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
try {
const args = [
@@ -52,7 +53,8 @@ export const getPaketoCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const args = [

View File

@@ -26,7 +26,8 @@ export const buildRailpack = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
try {
@@ -123,7 +124,8 @@ export const getRailpackCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
// Prepare command

View File

@@ -3,9 +3,9 @@ import {
buildCustomDocker,
getDockerCommand,
} from "@dokploy/server/utils/builders/docker-file";
import type { ApplicationNested } from ".";
import { createFile, getCreateFileCommand } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
const nginxSpaConfig = `
worker_processes 1;

View File

@@ -6,14 +6,17 @@ export const createEnvFile = (
directory: string,
env: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const envFilePath = join(dirname(directory), ".env");
if (!existsSync(dirname(envFilePath))) {
mkdirSync(dirname(envFilePath), { recursive: true });
}
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
"\n",
);
const envFileContent = prepareEnvironmentVariables(
env,
projectEnv,
environmentEnv,
).join("\n");
writeFileSync(envFilePath, envFileContent);
};
@@ -21,10 +24,13 @@ export const createEnvFileCommand = (
directory: string,
env: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
"\n",
);
const envFileContent = prepareEnvironmentVariables(
env,
projectEnv,
environmentEnv,
).join("\n");
const encodedContent = encodeBase64(envFileContent || "");
const envFilePath = join(dirname(directory), ".env");

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MariadbNested = InferResultType<
"mariadb",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMariadb = async (mariadb: MariadbNested) => {
const {
@@ -54,7 +54,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultMariadbEnv,
mariadb.project.env,
mariadb.environment.project.env,
mariadb.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MongoNested = InferResultType<
"mongo",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMongo = async (mongo: MongoNested) => {
@@ -102,7 +102,8 @@ ${command ?? "wait $MONGOD_PID"}`;
const envVariables = prepareEnvironmentVariables(
defaultMongoEnv,
mongo.project.env,
mongo.environment.project.env,
mongo.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MysqlNested = InferResultType<
"mysql",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMysql = async (mysql: MysqlNested) => {
@@ -60,7 +60,8 @@ export const buildMysql = async (mysql: MysqlNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultMysqlEnv,
mysql.project.env,
mysql.environment.project.env,
mysql.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type PostgresNested = InferResultType<
"postgres",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildPostgres = async (postgres: PostgresNested) => {
const {
@@ -53,7 +53,8 @@ export const buildPostgres = async (postgres: PostgresNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultPostgresEnv,
postgres.project.env,
postgres.environment.project.env,
postgres.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -13,8 +13,7 @@ import { deployPostgres } from "@dokploy/server/services/postgres";
import { deployRedis } from "@dokploy/server/services/redis";
import { eq } from "drizzle-orm";
import { removeService } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { execAsync } from "../process/execAsync";
import { execAsync, execAsyncRemote } from "../process/execAsync";
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type RedisNested = InferResultType<
"redis",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildRedis = async (redis: RedisNested) => {
const {
@@ -51,7 +51,8 @@ export const buildRedis = async (redis: RedisNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultRedisEnv,
redis.project.env,
redis.environment.project.env,
redis.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -1,8 +1,14 @@
import { findComposeById } from "@dokploy/server/services/compose";
import { dump, load } from "js-yaml";
import { dump } from "js-yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "./domain";
import type { ComposeSpecification } from "./types";
export const addAppNameToPreventCollision = (
@@ -24,16 +30,34 @@ export const randomizeIsolatedDeploymentComposeFile = async (
suffix?: string,
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification;
if (compose.serverId) {
await cloneComposeRemote(compose);
} else {
await cloneCompose(compose);
}
let composeData: ComposeSpecification | null;
if (compose.serverId) {
composeData = await loadDockerComposeRemote(compose);
} else {
composeData = await loadDockerCompose(compose);
}
if (!composeData) {
throw new Error("Compose data not found");
}
const randomSuffix = suffix || compose.appName || generateRandomHash();
const newComposeFile = addAppNameToPreventCollision(
composeData,
randomSuffix,
compose.isolatedDeploymentsVolume,
);
const newComposeFile = compose.isolatedDeployment
? addAppNameToPreventCollision(
composeData,
randomSuffix,
compose.isolatedDeploymentsVolume,
)
: composeData;
return dump(newComposeFile);
};

View File

@@ -6,6 +6,7 @@
import _ from "lodash";
import type { ComposeSpecification, DefinitionsService } from "../types";
type DependsOnObject = NonNullable<
Exclude<DefinitionsService["depends_on"], string[]> extends infer T
? { [K in keyof T]: T[K] }

View File

@@ -254,6 +254,9 @@ export const addDomainToCompose = async (
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
labels.unshift("traefik.swarm.network=dokploy-network");
}
}
}

View File

@@ -259,20 +259,51 @@ export const removeService = async (
export const prepareEnvironmentVariables = (
serviceEnv: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const projectVars = parse(projectEnv ?? "");
const environmentVars = parse(environmentEnv ?? "");
const serviceVars = parse(serviceEnv ?? "");
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
let resolvedValue = value;
// Replace project variables
if (projectVars) {
resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
if (projectVars[ref] !== undefined) {
return projectVars[ref];
}
throw new Error(`Invalid project environment variable: project.${ref}`);
});
resolvedValue = resolvedValue.replace(
/\$\{\{project\.(.*?)\}\}/g,
(_, ref) => {
if (projectVars[ref] !== undefined) {
return projectVars[ref];
}
throw new Error(
`Invalid project environment variable: project.${ref}`,
);
},
);
}
// Replace environment variables
if (environmentVars) {
resolvedValue = resolvedValue.replace(
/\$\{\{environment\.(.*?)\}\}/g,
(_, ref) => {
if (environmentVars[ref] !== undefined) {
return environmentVars[ref];
}
throw new Error(`Invalid environment variable: environment.${ref}`);
},
);
}
// Replace self-references (service variables)
resolvedValue = resolvedValue.replace(/\$\{\{(.*?)\}\}/g, (_, ref) => {
if (serviceVars[ref] !== undefined) {
return serviceVars[ref];
}
throw new Error(`Invalid service environment variable: ${ref}`);
});
return `${key}=${resolvedValue}`;
});
@@ -293,8 +324,9 @@ export const parseEnvironmentKeyValuePair = (
export const getEnviromentVariablesObject = (
input: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const envs = prepareEnvironmentVariables(input, projectEnv);
const envs = prepareEnvironmentVariables(input, projectEnv, environmentEnv);
const jsonObject: Record<string, string> = {};

View File

@@ -1,6 +1,5 @@
import * as fs from "node:fs/promises";
import { execAsync, sleep } from "../utils/process/execAsync";
import { execAsyncRemote } from "../utils/process/execAsync";
import { execAsync, execAsyncRemote, sleep } from "../utils/process/execAsync";
interface GPUInfo {
driverInstalled: boolean;
@@ -186,7 +185,7 @@ const checkCudaSupport = async (serverId?: string) => {
? await execAsyncRemote(serverId, cudaCommand)
: await execAsync(cudaCommand);
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d\.]+)/);
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d.]+)/);
cudaVersion = cudaMatch ? cudaMatch[1] : undefined;
cudaSupport = !!cudaVersion;
} catch (error) {

View File

@@ -42,7 +42,9 @@ export const buildDocker = async (
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
writeStream.write("❌ Error");
writeStream.write(
`❌ Error: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
} finally {
writeStream.end();

View File

@@ -237,10 +237,10 @@ const sanitizeRepoPathSSH = (input: string) => {
/^\s*/,
/(?:(?<proto>[a-z]+):\/\/)?/,
/(?:(?<user>[a-z_][a-z0-9_-]+)@)?/,
/(?<domain>[^\s\/\?#:]+)/,
/(?<domain>[^\s/?#:]+)/,
/(?::(?<port>[0-9]{1,5}))?/,
/(?:[\/:](?<owner>[^\s\/\?#:]+))?/,
/(?:[\/:](?<repo>(?:[^\s\?#:.]|\.(?!git\/?\s*$))+))/,
/(?:[/:](?<owner>[^\s/?#:]+))?/,
/(?:[/:](?<repo>(?:[^\s?#:.]|\.(?!git\/?\s*$))+))/,
/(?:.git)?\/?\s*$/,
]
.map((r) => r.source)

View File

@@ -1,17 +1,16 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { findGithubById, type Github } from "@dokploy/server/services/github";
import type { InferResultType } from "@dokploy/server/types/with";
import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit";
import { recreateDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { type Github, findGithubById } from "@dokploy/server/services/github";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const authGithub = (githubProvider: Github): Octokit => {
if (!haveGithubRequirements(githubProvider)) {

View File

@@ -316,7 +316,7 @@ export const getGitlabBranches = async (input: {
while (true) {
const branchesResponse = await fetch(
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,

View File

@@ -1,6 +1,6 @@
export { restorePostgresBackup } from "./postgres";
export { restoreMySqlBackup } from "./mysql";
export { restoreComposeBackup } from "./compose";
export { restoreMariadbBackup } from "./mariadb";
export { restoreMongoBackup } from "./mongo";
export { restoreMySqlBackup } from "./mysql";
export { restorePostgresBackup } from "./postgres";
export { restoreWebServerBackup } from "./web-server";
export { restoreComposeBackup } from "./compose";

View File

@@ -5,10 +5,10 @@ import type { Schedule } from "@dokploy/server/db/schema/schedule";
import {
createDeploymentSchedule,
updateDeployment,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule";
import { getComposeContainer, getServiceContainer } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";

View File

@@ -1,5 +1,4 @@
import fs, { writeFileSync } from "node:fs";
import { createReadStream } from "node:fs";
import fs, { createReadStream, writeFileSync } from "node:fs";
import path from "node:path";
import { createInterface } from "node:readline";
import { paths } from "@dokploy/server/constants";

View File

@@ -1,6 +1,7 @@
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";

View File

@@ -1,12 +1,15 @@
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { scheduledJobs, scheduleJob } from "node-schedule";
import {
createDeploymentVolumeBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import {
execAsync,
execAsyncRemote,
updateDeploymentStatus,
} from "../..";
} from "@dokploy/server/utils/process/execAsync";
import { backupVolume } from "./backup";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
@@ -20,6 +23,33 @@ export const removeVolumeBackupJob = async (volumeBackupId: string) => {
currentJob?.cancel();
};
const cleanupOldVolumeBackups = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
serverId?: string | null,
) => {
const { keepLatestCount, destination, prefix, volumeName } = volumeBackup;
if (!keepLatestCount) return;
try {
const rcloneFlags = getS3Credentials(destination);
const normalizedPrefix = normalizeS3Path(prefix);
const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`;
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
if (serverId) {
await execAsyncRemote(serverId, fullCommand);
} else {
await execAsync(fullCommand);
}
} catch (error) {
console.error("Volume backup retention error", error);
}
};
export const runVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
const serverId =
@@ -40,6 +70,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
await execAsync(commandWithLog);
}
if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) {
await cleanupOldVolumeBackups(volumeBackup, serverId);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");