Merge branch 'canary' into feat/add-mattermost-notification-provider

Resolves merge conflicts between mattermost notification provider (this PR)
and pushover/custom/lark notification providers (from canary).

All notification providers are now included:
- slack, telegram, discord, email, gotify, ntfy
- mattermost (this PR)
- pushover, custom, lark (from canary)
This commit is contained in:
Hootan
2026-01-30 23:50:41 +01:00
343 changed files with 164082 additions and 8184 deletions

View File

@@ -3,26 +3,27 @@ import {
invitation,
member,
organization,
users_temp,
user,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { getWebServerSettings } from "./web-server-settings";
export const findUserById = async (userId: string) => {
const user = await db.query.users_temp.findFirst({
where: eq(users_temp.id, userId),
const userResult = await db.query.user.findFirst({
where: eq(user.id, userId),
// with: {
// account: true,
// },
});
if (!user) {
if (!userResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return user;
return userResult;
};
export const findOrganizationById = async (organizationId: string) => {
@@ -46,7 +47,7 @@ export const isAdminPresent = async () => {
return true;
};
export const findAdmin = async () => {
export const findOwner = async () => {
const admin = await db.query.member.findFirst({
where: eq(member.role, "owner"),
with: {
@@ -64,7 +65,7 @@ export const findAdmin = async () => {
};
export const getUserByToken = async (token: string) => {
const user = await db.query.invitation.findFirst({
const userResult = await db.query.invitation.findFirst({
where: eq(invitation.id, token),
columns: {
id: true,
@@ -76,29 +77,29 @@ export const getUserByToken = async (token: string) => {
},
});
if (!user) {
if (!userResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Invitation not found",
});
}
const userAlreadyExists = await db.query.users_temp.findFirst({
where: eq(users_temp.email, user?.email || ""),
const userAlreadyExists = await db.query.user.findFirst({
where: eq(user.email, userResult?.email || ""),
});
const { expiresAt, ...rest } = user;
const { expiresAt, ...rest } = userResult;
return {
...rest,
isExpired: user.expiresAt < new Date(),
isExpired: userResult.expiresAt < new Date(),
userAlreadyExists: !!userAlreadyExists,
};
};
export const removeUserById = async (userId: string) => {
await db
.delete(users_temp)
.where(eq(users_temp.id, userId))
.delete(user)
.where(eq(user.id, userId))
.returning()
.then((res) => res[0]);
};
@@ -107,10 +108,11 @@ export const getDokployUrl = async () => {
if (IS_CLOUD) {
return "https://app.dokploy.com";
}
const admin = await findAdmin();
const settings = await getWebServerSettings();
if (admin.user.host) {
return `https://${admin.user.host}`;
if (settings?.host) {
const protocol = settings?.https ? "https" : "http";
return `${protocol}://${settings?.host}`;
}
return `http://${admin.user.serverIp}:${process.env.PORT}`;
return `http://${settings?.serverIp}:${process.env.PORT}`;
};

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server";
import { getWebServerSettings } from "./web-server-settings";
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
let ip = "";
if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId);
ip = organization?.owner.serverIp || "";
const settings = await getWebServerSettings();
ip = settings?.serverIp || "";
}
if (serverId) {
@@ -104,14 +104,28 @@ export const suggestVariants = async ({
),
}),
prompt: `
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
Act as advanced DevOps engineer and analyze the user's request to determine the appropriate suggestions (up to 3 items).
CRITICAL - Read the user's request carefully and follow the appropriate strategy:
Strategy A - If the user specifies a PARTICULAR APPLICATION/SERVICE (e.g., "deploy Chatwoot", "install sendingtk/chatwoot:develop", "setup Bitwarden"):
- Generate different deployment VARIANTS of that SAME application
- Each variant should be a different configuration (minimal, full stack, with different databases, development vs production, etc.)
- Example: For "Chatwoot" → "Chatwoot with PostgreSQL", "Chatwoot Development", "Chatwoot Full Stack"
- The name MUST include the specific application name the user mentioned
Strategy B - If the user describes a GENERAL NEED or USE CASE (e.g., "personal blog", "project management tool", "chat application"):
- Suggest different open source projects that fulfill that need
- Each suggestion should be a different tool/platform that solves the same problem
- Example: For "personal blog" → "WordPress", "Ghost", "Hugo with Nginx"
- The name should be the actual project name
Return your response as a JSON object with the following structure:
{
"suggestions": [
{
"id": "project-slug",
"name": "Project Name",
"id": "project-or-variant-slug",
"name": "Project Name or Variant Name",
"shortDescription": "Brief one-line description",
"description": "Detailed description"
}
@@ -120,10 +134,14 @@ export const suggestVariants = async ({
Important rules for the response:
1. Use slug format for the id field (lowercase, hyphenated)
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
4. The shortDescription should be a single-line summary focusing on the main technologies
5. All projects should be installable in docker and have docker compose support
2. Determine which strategy to use based on whether the user specified a particular application or described a general need
3. For Strategy A (specific app): The name must include the app name and describe the variant configuration
4. For Strategy B (general need): The name should be the actual project/tool name that fulfills the need
5. The description field should ONLY contain a plain text description of the project or variant, its features, and use cases
6. Do NOT include any code snippets, configuration examples, or installation instructions in the description
7. The shortDescription should be a single-line summary focusing on key technologies or differentiators
8. All suggestions should be installable in docker and have docker compose support
9. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches
User wants to create a new project with the following details:
@@ -186,6 +204,24 @@ export const suggestVariants = async ({
6. If a service depends on a database or other service, INCLUDE that service in the docker-compose
7. Make sure all required services are defined in the docker-compose
Docker Image Rules (CRITICAL):
1. ALWAYS use 'image:' field, NEVER use 'build:' field
2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles
3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io)
4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.)
5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible
6. Examples of correct image usage:
- image: sendingtk/chatwoot:develop
- image: postgres:16-alpine
- image: redis:7-alpine
- image: chatwoot/chatwoot:latest
7. Examples of INCORRECT usage (DO NOT USE):
- build: .
- build: ./app
- build:
context: .
dockerfile: Dockerfile
Volume Mounting and Configuration Rules:
1. DO NOT create configuration files unless the service CANNOT work without them
2. Most services can work with just environment variables - USE THEM FIRST
@@ -214,6 +250,8 @@ export const suggestVariants = async ({
- serviceName: the name of the service in the docker-compose
2. Make sure the service is properly configured to work with the specified port
User's original request: ${input}
Project details:
${suggestion?.description}
`,

View File

@@ -7,37 +7,25 @@ import {
} from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
import {
buildApplication,
getBuildCommand,
mechanizeDockerContainer,
} from "@dokploy/server/utils/builders";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@dokploy/server/utils/providers/bitbucket";
import {
buildDocker,
buildRemoteDocker,
} from "@dokploy/server/utils/providers/docker";
ExecError,
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker";
import {
cloneGitRepository,
getCustomGitCloneCommand,
getGitCommitInfo,
} from "@dokploy/server/utils/providers/git";
import {
cloneGiteaRepository,
getGiteaCloneCommand,
} from "@dokploy/server/utils/providers/gitea";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@dokploy/server/utils/providers/gitlab";
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
@@ -46,6 +34,7 @@ import { getDokployUrl } from "./admin";
import {
createDeployment,
createDeploymentPreview,
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
import { type Domain, getDomainHost } from "./domain";
@@ -60,7 +49,6 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
import { createRollback } from "./rollbacks";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -123,6 +111,8 @@ export const findApplicationById = async (applicationId: string) => {
gitea: true,
server: true,
previewDeployments: true,
buildRegistry: true,
rollbackRegistry: true,
},
});
if (!application) {
@@ -183,6 +173,7 @@ export const deployApplication = async ({
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const serverId = application.buildServerId || application.serverId;
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
@@ -192,44 +183,34 @@ export const deployApplication = async ({
});
try {
let command = "set -e;";
if (application.sourceType === "github") {
await cloneGithubRepository({
...application,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
command += await cloneGithubRepository(application);
} else if (application.sourceType === "gitlab") {
await cloneGitlabRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
command += await cloneGitlabRepository(application);
} else if (application.sourceType === "gitea") {
await cloneGiteaRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
command += await cloneGiteaRepository(application);
} else if (application.sourceType === "bitbucket") {
await cloneBitbucketRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") {
await buildDocker(application, deployment.logPath);
command += await cloneBitbucketRepository(application);
} else if (application.sourceType === "git") {
await cloneGitRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "drop") {
await buildApplication(application, deployment.logPath);
command += await cloneGitRepository(application);
} else if (application.sourceType === "docker") {
command += await buildRemoteDocker(application);
}
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
@@ -237,8 +218,24 @@ export const deployApplication = async ({
buildLink,
organizationId: application.environment.project.organizationId,
domains: application.domains,
environmentName: application.environment.name,
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
@@ -253,8 +250,23 @@ export const deployApplication = async ({
});
throw error;
}
} finally {
// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
const commitInfo = await getGitCommitInfo({
appName: application.appName,
type: "application",
serverId: serverId,
});
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
}
return true;
};
@@ -268,50 +280,9 @@ export const rebuildApplication = async ({
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
description: descriptionLog,
});
try {
if (application.sourceType === "github") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") {
await buildDocker(application, deployment.logPath);
} else if (application.sourceType === "git") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "drop") {
await buildApplication(application, deployment.logPath);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
}
return true;
};
export const deployRemoteApplication = async ({
applicationId,
titleLog = "Manual deployment",
descriptionLog = "",
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const serverId = application.buildServerId || application.serverId;
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,
@@ -319,53 +290,19 @@ export const deployRemoteApplication = async ({
});
try {
if (application.serverId) {
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
...application,
serverId: application.serverId,
logPath: deployment.logPath,
});
} else if (application.sourceType === "gitlab") {
command += await getGitlabCloneCommand(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") {
command += await getBitbucketCloneCommand(
application,
deployment.logPath,
);
} else if (application.sourceType === "gitea") {
command += await getGiteaCloneCommand(application, deployment.logPath);
} else if (application.sourceType === "git") {
command += await getCustomGitCloneCommand(
application,
deployment.logPath,
);
} else if (application.sourceType === "docker") {
command += await buildRemoteDocker(application, deployment.logPath);
}
if (application.sourceType !== "docker") {
command += getBuildCommand(application, deployment.logPath);
}
await execAsyncRemote(application.serverId, command);
await mechanizeDockerContainer(application);
let command = "set -e;";
// Check case for docker only
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
@@ -373,32 +310,26 @@ export const deployRemoteApplication = async ({
buildLink,
organizationId: application.environment.project.organizationId,
domains: application.domains,
environmentName: application.environment.name,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
let command = "";
const encodedContent = encodeBase64(errorMessage);
await execAsyncRemote(
application.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.environment.project.organizationId,
});
throw error;
}
@@ -474,15 +405,27 @@ export const deployPreviewApplication = async ({
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
let command = "set -e;";
if (application.sourceType === "github") {
await cloneGithubRepository({
command += await cloneGithubRepository({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
}
const successComment = getIssueComment(
application.name,
@@ -513,9 +456,9 @@ export const deployPreviewApplication = async ({
return true;
};
export const deployRemotePreviewApplication = async ({
export const rebuildPreviewApplication = async ({
applicationId,
titleLog = "Preview Deployment",
titleLog = "Rebuild Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
@@ -525,6 +468,8 @@ export const deployRemotePreviewApplication = async ({
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const deployment = await createDeploymentPreview({
title: titleLog,
@@ -532,13 +477,6 @@ export const deployRemotePreviewApplication = async ({
previewDeploymentId: previewDeploymentId,
});
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
await updatePreviewDeployment(previewDeploymentId, {
createdAt: new Date().toISOString(),
});
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
const issueParams = {
owner: application?.owner || "",
@@ -547,6 +485,7 @@ export const deployRemotePreviewApplication = async ({
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
@@ -569,6 +508,7 @@ export const deployRemotePreviewApplication = async ({
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
@@ -578,27 +518,28 @@ export const deployRemotePreviewApplication = async ({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
// Set application properties for preview deployment
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
if (application.serverId) {
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
serverId: application.serverId,
logPath: deployment.logPath,
});
}
command += getBuildCommand(application, deployment.logPath);
await execAsyncRemote(application.serverId, command);
await mechanizeDockerContainer(application);
const serverId = application.serverId;
let command = "set -e;";
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
const successComment = getIssueComment(
application.name,
@@ -614,6 +555,23 @@ export const deployRemotePreviewApplication = async ({
previewStatus: "done",
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
const serverId = application.buildServerId || application.serverId;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
@@ -629,54 +587,10 @@ export const deployRemotePreviewApplication = async ({
return true;
};
export const rebuildRemoteApplication = async ({
applicationId,
titleLog = "Rebuild deployment",
descriptionLog = "",
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
description: descriptionLog,
});
try {
if (application.serverId) {
if (application.sourceType !== "docker") {
let command = "set -e;";
command += getBuildCommand(application, deployment.logPath);
await execAsyncRemote(application.serverId, command);
}
await mechanizeDockerContainer(application);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
await execAsyncRemote(
application.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
}
return true;
};
export const getApplicationStats = async (appName: string) => {
if (appName === "dokploy") {
return await getAdvancedStats(appName);
}
const filter = {
status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`],

View File

@@ -78,7 +78,6 @@ const BUNNY_CDN_IPS = new Set([
"89.187.188.227",
"89.187.188.228",
"139.180.134.196",
"89.38.96.158",
"89.187.162.249",
"89.187.162.242",
"185.102.217.65",
@@ -106,12 +105,9 @@ const BUNNY_CDN_IPS = new Set([
"200.25.38.69",
"200.25.42.70",
"200.25.36.166",
"195.206.229.106",
"194.242.11.186",
"185.164.35.8",
"94.20.154.22",
"185.93.1.244",
"156.59.145.154",
"143.244.49.177",
"138.199.46.66",
"138.199.37.227",
@@ -136,7 +132,6 @@ const BUNNY_CDN_IPS = new Set([
"84.17.59.115",
"89.187.165.194",
"138.199.15.193",
"89.35.237.170",
"37.19.216.130",
"185.93.1.247",
"185.93.3.244",
@@ -150,6 +145,7 @@ const BUNNY_CDN_IPS = new Set([
"84.17.63.178",
"200.25.32.131",
"37.19.207.34",
"37.19.207.38",
"192.189.65.146",
"143.244.45.177",
"185.93.1.249",
@@ -168,9 +164,7 @@ const BUNNY_CDN_IPS = new Set([
"129.227.217.178",
"129.227.217.179",
"200.25.69.94",
"128.1.52.179",
"200.25.16.103",
"15.235.54.226",
"102.67.138.155",
"156.146.43.65",
"195.181.163.203",
@@ -278,13 +272,11 @@ const BUNNY_CDN_IPS = new Set([
"107.155.47.146",
"193.201.190.174",
"156.59.95.218",
"213.170.143.139",
"129.227.186.154",
"195.238.127.98",
"200.25.22.6",
"204.16.244.92",
"200.25.70.101",
"200.25.66.100",
"139.180.209.182",
"103.108.231.41",
"103.108.229.5",
@@ -387,46 +379,13 @@ const BUNNY_CDN_IPS = new Set([
"38.54.5.37",
"38.54.3.92",
"185.165.170.74",
"207.121.80.118",
"207.121.46.228",
"207.121.46.236",
"207.121.46.244",
"207.121.46.252",
"216.202.235.164",
"207.121.46.220",
"207.121.75.132",
"207.121.80.12",
"207.121.80.172",
"207.121.90.60",
"207.121.90.68",
"207.121.97.204",
"207.121.90.252",
"207.121.97.236",
"207.121.99.12",
"138.199.24.219",
"185.93.2.251",
"138.199.46.65",
"207.121.41.196",
"207.121.99.20",
"207.121.99.36",
"207.121.99.44",
"207.121.99.52",
"207.121.99.60",
"207.121.23.68",
"207.121.23.124",
"207.121.23.244",
"207.121.23.180",
"207.121.23.188",
"207.121.23.196",
"207.121.23.204",
"207.121.24.52",
"207.121.24.60",
"207.121.24.68",
"207.121.24.76",
"207.121.24.92",
"207.121.24.100",
"207.121.24.108",
"207.121.24.116",
"154.95.86.76",
"5.9.99.73",
"78.46.92.118",
@@ -434,14 +393,52 @@ const BUNNY_CDN_IPS = new Set([
"78.46.156.89",
"88.198.9.155",
"144.76.79.22",
"103.1.215.93",
"103.137.12.33",
"103.107.196.31",
"116.90.72.155",
"103.137.14.5",
"116.90.75.65",
"37.19.207.37",
"208.83.234.224",
"79.127.237.104",
"79.127.243.187",
"45.156.248.73",
"79.127.134.225",
"79.127.134.226",
"79.127.134.227",
"79.127.134.228",
"79.127.134.229",
"79.127.134.230",
"79.127.134.231",
"79.127.134.130",
"79.127.134.131",
"79.127.134.132",
"79.127.134.234",
"79.127.134.235",
"185.111.111.154",
"185.111.111.155",
"185.111.111.156",
"185.111.111.157",
"185.111.111.158",
"185.111.111.159",
"185.111.111.160",
"141.227.142.242",
"94.128.254.166",
"195.206.229.69",
"200.25.86.90",
"148.113.190.161",
"46.151.194.242",
"46.151.194.243",
"212.102.40.120",
"213.170.143.100",
"154.93.86.71",
"143.244.60.196",
"143.244.60.197",
"143.244.60.195",
"79.127.134.129",
"79.127.134.133",
"152.233.22.97",
"152.233.22.98",
"152.233.22.100",
"152.233.22.99",
"152.233.22.101",
"152.233.22.102",
"152.233.22.103",
"116.202.155.146",
"116.202.193.178",
"116.202.224.168",
@@ -502,6 +499,12 @@ const BUNNY_CDN_IPS = new Set([
"103.60.15.166",
"103.60.15.167",
"103.60.15.168",
"176.9.139.94",
"148.251.129.132",
"148.251.131.73",
"148.251.131.74",
"136.243.70.170",
"148.251.131.238",
"109.248.43.116",
"109.248.43.117",
"109.248.43.162",
@@ -527,7 +530,9 @@ const BUNNY_CDN_IPS = new Set([
"139.180.129.216",
"139.99.174.7",
"89.187.169.18",
"143.244.38.133",
"89.187.179.7",
"169.150.213.50",
"143.244.62.213",
"185.93.3.246",
"195.181.163.198",
@@ -535,7 +540,6 @@ const BUNNY_CDN_IPS = new Set([
"84.17.37.211",
"212.102.50.54",
"212.102.46.115",
"143.244.38.135",
"169.150.238.21",
"169.150.207.51",
"169.150.207.49",
@@ -546,7 +550,6 @@ const BUNNY_CDN_IPS = new Set([
"169.150.247.139",
"169.150.247.177",
"169.150.247.178",
"169.150.213.49",
"212.102.46.119",
"84.17.38.234",
"84.17.38.233",
@@ -558,7 +561,6 @@ const BUNNY_CDN_IPS = new Set([
"169.150.247.138",
"169.150.247.184",
"169.150.247.185",
"156.146.58.83",
"212.102.43.88",
"89.187.169.26",
"109.61.89.57",
@@ -587,6 +589,17 @@ const BUNNY_CDN_IPS = new Set([
"138.199.4.177",
"37.19.222.34",
"46.151.193.85",
"79.127.237.99",
"212.104.158.30",
"212.104.158.31",
"212.104.158.32",
"212.104.158.33",
"212.104.158.34",
"212.104.158.28",
"212.104.158.29",
"212.104.158.35",
"212.104.158.36",
"212.104.158.37",
"212.104.158.17",
"212.104.158.18",
"212.104.158.19",
@@ -595,12 +608,20 @@ const BUNNY_CDN_IPS = new Set([
"212.104.158.22",
"212.104.158.24",
"212.104.158.26",
"79.127.237.134",
"89.187.184.177",
"89.187.184.179",
"89.187.184.173",
"89.187.184.178",
"89.187.184.176",
"212.104.158.25",
"212.104.158.27",
"212.104.158.67",
"212.104.158.10",
"212.104.158.12",
"212.104.158.64",
"212.104.158.16",
"212.104.158.23",
"212.104.158.54",
]);
// Arvancloud IP ranges
@@ -616,6 +637,7 @@ const ARVANCLOUD_IP_RANGES = [
"37.32.18.0/27",
"37.32.19.0/27",
"185.215.232.0/22",
"178.131.120.48/28",
];
const CDN_PROVIDERS: CDNProvider[] = [

View File

@@ -7,14 +7,10 @@ import {
cleanAppName,
compose,
} from "@dokploy/server/db/schema";
import {
buildCompose,
getBuildComposeCommand,
} from "@dokploy/server/utils/builders/compose";
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "@dokploy/server/utils/docker/domain";
@@ -22,38 +18,28 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import {
ExecError,
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@dokploy/server/utils/providers/bitbucket";
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
import {
cloneGitRepository,
getCustomGitCloneCommand,
getGitCommitInfo,
} from "@dokploy/server/utils/providers/git";
import {
cloneGiteaRepository,
getGiteaCloneCommand,
} from "@dokploy/server/utils/providers/gitea";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@dokploy/server/utils/providers/gitlab";
import {
createComposeFile,
getCreateComposeFileCommand,
} from "@dokploy/server/utils/providers/raw";
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import {
createDeploymentCompose,
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect;
@@ -163,10 +149,11 @@ export const loadServices = async (
const compose = await findComposeById(composeId);
if (type === "fetch") {
const command = await cloneCompose(compose);
if (compose.serverId) {
await cloneComposeRemote(compose);
await execAsyncRemote(compose.serverId, command);
} else {
await cloneCompose(compose);
await execAsync(command);
}
}
@@ -235,24 +222,41 @@ export const deployCompose = async ({
});
try {
const entity = {
...compose,
type: "compose" as const,
};
let command = "set -e;";
if (compose.sourceType === "github") {
await cloneGithubRepository({
...compose,
logPath: deployment.logPath,
type: "compose",
});
command += await cloneGithubRepository(entity);
} else if (compose.sourceType === "gitlab") {
await cloneGitlabRepository(compose, deployment.logPath, true);
command += await cloneGitlabRepository(entity);
} else if (compose.sourceType === "bitbucket") {
await cloneBitbucketRepository(compose, deployment.logPath, true);
command += await cloneBitbucketRepository(entity);
} else if (compose.sourceType === "git") {
await cloneGitRepository(compose, deployment.logPath, true);
command += await cloneGitRepository(entity);
} else if (compose.sourceType === "gitea") {
await cloneGiteaRepository(compose, deployment.logPath, true);
command += await cloneGiteaRepository(entity);
} else if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath);
command += getCreateComposeFileCommand(entity);
}
await buildCompose(compose, deployment.logPath);
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
command = "set -e;";
command += await getBuildComposeCommand(entity);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
@@ -265,8 +269,24 @@ export const deployCompose = async ({
buildLink,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
environmentName: compose.environment.name,
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
@@ -281,6 +301,19 @@ export const deployCompose = async ({
organizationId: compose.environment.project.organizationId,
});
throw error;
} finally {
if (compose.sourceType !== "raw") {
const commitInfo = await getGitCommitInfo({
...compose,
type: "compose",
});
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
}
};
@@ -302,154 +335,23 @@ export const rebuildCompose = async ({
});
try {
let command = "set -e;";
if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath);
command += getCreateComposeFileCommand(compose);
}
await buildCompose(compose, deployment.logPath);
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
});
throw error;
}
return true;
};
export const deployRemoteCompose = async ({
composeId,
titleLog = "Manual deployment",
descriptionLog = "",
}: {
composeId: string;
titleLog: string;
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
title: titleLog,
description: descriptionLog,
});
try {
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
let command = "set -e;";
if (compose.sourceType === "github") {
command += await getGithubCloneCommand({
...compose,
logPath: deployment.logPath,
type: "compose",
serverId: compose.serverId,
});
} else if (compose.sourceType === "gitlab") {
command += await getGitlabCloneCommand(
compose,
deployment.logPath,
true,
);
} else if (compose.sourceType === "bitbucket") {
command += await getBitbucketCloneCommand(
compose,
deployment.logPath,
true,
);
} else if (compose.sourceType === "git") {
command += await getCustomGitCloneCommand(
compose,
deployment.logPath,
true,
);
console.log(command);
} else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") {
command += await getGiteaCloneCommand(
compose,
deployment.logPath,
true,
);
}
await execAsyncRemote(compose.serverId, command);
await getBuildComposeCommand(compose, deployment.logPath);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
});
await sendBuildSuccessNotifications({
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
await execAsyncRemote(
compose.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.environment.project.organizationId,
});
throw error;
}
};
export const rebuildRemoteCompose = async ({
composeId,
titleLog = "Rebuild deployment",
descriptionLog = "",
}: {
composeId: string;
titleLog: string;
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const deployment = await createDeploymentCompose({
composeId: composeId,
title: titleLog,
description: descriptionLog,
});
try {
if (compose.sourceType === "raw") {
const command = getCreateComposeFileCommand(compose, deployment.logPath);
await execAsyncRemote(compose.serverId, command);
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
command += await getBuildComposeCommand(compose);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath);
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
@@ -457,16 +359,21 @@ export const rebuildRemoteCompose = async ({
composeStatus: "done",
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
let command = "";
await execAsyncRemote(
compose.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
@@ -501,7 +408,7 @@ export const removeCompose = async (
} else {
const command = `
docker network disconnect ${compose.appName} dokploy-traefik;
cd ${projectPath} && docker compose -p ${compose.appName} down ${
cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
deleteVolumes ? "--volumes" : ""
} && rm -rf ${projectPath}`;
@@ -528,7 +435,7 @@ export const startCompose = async (composeId: string) => {
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const path =
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
const baseCommand = `env -i PATH="$PATH" docker compose -p ${compose.appName} -f ${path} up -d`;
if (compose.composeType === "docker-compose") {
if (compose.serverId) {
await execAsyncRemote(
@@ -563,14 +470,17 @@ export const stopCompose = async (composeId: string) => {
if (compose.serverId) {
await execAsyncRemote(
compose.serverId,
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
`cd ${join(COMPOSE_PATH, compose.appName)} && env -i PATH="$PATH" docker compose -p ${
compose.appName
} stop`,
);
} else {
await execAsync(`docker compose -p ${compose.appName} stop`, {
cwd: join(COMPOSE_PATH, compose.appName),
});
await execAsync(
`env -i PATH="$PATH" docker compose -p ${compose.appName} stop`,
{
cwd: join(COMPOSE_PATH, compose.appName),
},
);
}
}

View File

@@ -74,24 +74,26 @@ export const createDeployment = async (
>,
) => {
const application = await findApplicationById(deployment.applicationId);
try {
await removeLastTenDeployments(
deployment.applicationId,
"application",
application.serverId,
);
const { LOGS_PATH } = paths(!!application.serverId);
const serverId = application.buildServerId || application.serverId;
const { LOGS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${application.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
if (application.serverId) {
const server = await findServerById(application.serverId);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${LOGS_PATH}/${application.appName};
echo "Initializing deployment" >> ${logFilePath};
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
@@ -99,7 +101,7 @@ export const createDeployment = async (
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment");
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
}
const deploymentCreate = await db
@@ -111,6 +113,9 @@ export const createDeployment = async (
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
...(application.buildServerId && {
buildServerId: application.buildServerId,
}),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@@ -249,7 +254,7 @@ export const createDeploymentCompose = async (
const command = `
mkdir -p ${LOGS_PATH}/${compose.appName};
echo "Initializing deployment" >> ${logFilePath};
echo "Initializing deployment\n" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
@@ -257,7 +262,7 @@ echo "Initializing deployment" >> ${logFilePath};
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment");
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
}
const deploymentCreate = await db

View File

@@ -1,12 +1,12 @@
import dns from "node:dns";
import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server";
@@ -19,6 +19,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
.insert(domains)
.values({
...input,
host: input.host?.trim(),
})
.returning()
.then((response) => response[0]);
@@ -60,9 +61,9 @@ export const generateTraefikMeDomain = async (
projectName: appName,
});
}
const admin = await findUserById(userId);
const settings = await getWebServerSettings();
return generateRandomDomain({
serverIp: admin?.serverIp || "",
serverIp: settings?.serverIp || "",
projectName: appName,
});
};
@@ -120,6 +121,7 @@ export const updateDomainById = async (
.update(domains)
.set({
...domainData,
...(domainData.host && { host: domainData.host.trim() }),
})
.where(eq(domains.domainId, domainId))
.returning();

View File

@@ -34,13 +34,43 @@ 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,
applications: {
with: {
deployments: true,
server: true,
},
},
mariadb: {
with: {
server: true,
},
},
mongo: {
with: {
server: true,
},
},
mysql: {
with: {
server: true,
},
},
postgres: {
with: {
server: true,
},
},
redis: {
with: {
server: true,
},
},
compose: {
with: {
deployments: true,
server: true,
},
},
project: true,
},
});
@@ -73,10 +103,10 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
export const deleteEnvironment = async (environmentId: string) => {
const currentEnvironment = await findEnvironmentById(environmentId);
if (currentEnvironment.name === "production") {
if (currentEnvironment.isDefault) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot delete the production environment",
message: "You cannot delete the default environment",
});
}
const deletedEnvironment = await db
@@ -132,9 +162,23 @@ export const duplicateEnvironment = async (
};
export const createProductionEnvironment = async (projectId: string) => {
return createEnvironment({
name: "production",
description: "Production environment",
projectId,
});
const newEnvironment = await db
.insert(environments)
.values({
name: "production",
description: "Production environment",
projectId,
isDefault: true,
})
.returning()
.then((value) => value[0]);
if (!newEnvironment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the production environment",
});
}
return newEnvironment;
};

View File

@@ -1,28 +1,34 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateCustom,
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateLark,
type apiCreateGotify,
type apiCreateLark,
type apiCreateMattermost,
type apiCreateNtfy,
type apiCreatePushover,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateCustom,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateLark,
type apiUpdateGotify,
type apiUpdateLark,
type apiUpdateMattermost,
type apiUpdateNtfy,
type apiUpdatePushover,
type apiUpdateSlack,
type apiUpdateTelegram,
custom,
discord,
email,
lark,
gotify,
lark,
mattermost,
notifications,
ntfy,
pushover,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -60,6 +66,7 @@ export const createSlackNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "slack",
@@ -91,6 +98,7 @@ export const updateSlackNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -151,6 +159,7 @@ export const createTelegramNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "telegram",
@@ -182,6 +191,7 @@ export const updateTelegramNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -242,6 +252,7 @@ export const createDiscordNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "discord",
@@ -273,6 +284,7 @@ export const updateDiscordNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -336,6 +348,7 @@ export const createEmailNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "email",
@@ -367,6 +380,7 @@ export const updateEmailNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -432,6 +446,7 @@ export const createGotifyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "gotify",
@@ -462,6 +477,7 @@ export const updateGotifyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -501,7 +517,7 @@ export const createNtfyNotification = async (
.values({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
accessToken: input.accessToken ?? null,
priority: input.priority,
})
.returning()
@@ -522,6 +538,7 @@ export const createNtfyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "ntfy",
@@ -552,6 +569,7 @@ export const updateNtfyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -572,7 +590,7 @@ export const updateNtfyNotification = async (
.set({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
accessToken: input.accessToken ?? null,
priority: input.priority,
})
.where(eq(ntfy.ntfyId, input.ntfyId));
@@ -581,6 +599,95 @@ export const updateNtfyNotification = async (
});
};
export const createCustomNotification = async (
input: typeof apiCreateCustom._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newCustom = await tx
.insert(custom)
.values({
endpoint: input.endpoint,
headers: input.headers,
})
.returning()
.then((value) => value[0]);
if (!newCustom) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting custom",
});
}
const newDestination = await tx
.insert(notifications)
.values({
customId: newCustom.customId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "custom",
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateCustomNotification = async (
input: typeof apiUpdateCustom._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(custom)
.set({
endpoint: input.endpoint,
headers: input.headers,
})
.where(eq(custom.customId, input.customId));
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -592,7 +699,9 @@ export const findNotificationById = async (notificationId: string) => {
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
},
});
if (!notification) {
@@ -672,6 +781,7 @@ export const updateLarkNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
@@ -807,3 +917,99 @@ export const updateMattermostNotification = async (
return newDestination;
});
};
export const createPushoverNotification = async (
input: typeof apiCreatePushover._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newPushover = await tx
.insert(pushover)
.values({
userKey: input.userKey,
apiToken: input.apiToken,
priority: input.priority,
retry: input.retry,
expire: input.expire,
})
.returning()
.then((value) => value[0]);
if (!newPushover) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting pushover",
});
}
const newDestination = await tx
.insert(notifications)
.values({
pushoverId: newPushover.pushoverId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
serverThreshold: input.serverThreshold,
notificationType: "pushover",
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updatePushoverNotification = async (
input: typeof apiUpdatePushover._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(pushover)
.set({
userKey: input.userKey,
apiToken: input.apiToken,
priority: input.priority,
retry: input.retry,
expire: input.expire,
})
.where(eq(pushover.pushoverId, input.pushoverId));
return newDestination;
});
};

View File

@@ -13,6 +13,19 @@ import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export function getMountPath(dockerImage: string): string {
const versionMatch = dockerImage.match(/postgres:(\d+)/);
if (versionMatch?.[1]) {
const version = Number.parseInt(versionMatch[1], 10);
if (version >= 18) {
// PostgreSQL 18+ uses /var/lib/postgresql/{version}/docker as the default PGDATA
return `/var/lib/postgresql/${version}/docker`;
}
}
return "/var/lib/postgresql/data";
}
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {

View File

@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
}
if (!ip) {
const admin = await findUserById(userId);
ip = admin?.serverIp || "";
const settings = await getWebServerSettings();
ip = settings?.serverIp || "";
}
const slugIp = ip.replaceAll(".", "-");

View File

@@ -7,7 +7,8 @@ import {
deployments as deploymentsSchema,
rollbacks,
} from "../db/schema";
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
import type { ApplicationNested } from "../utils/builders";
import { getRegistryTag } from "../utils/cluster/upload";
import {
calculateResources,
generateBindMounts,
@@ -22,11 +23,12 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import type { Registry } from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
) => {
await db.transaction(async (tx) => {
return await db.transaction(async (tx) => {
const { fullContext, ...other } = input;
const rollback = await tx
.insert(rollbacks)
@@ -70,9 +72,11 @@ export const createRollback = async (
})
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
await createRollbackImage(rest, tagImage);
const updatedRollback = await tx.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollback.rollbackId),
});
return rollback;
return updatedRollback;
});
};
@@ -103,27 +107,6 @@ export const findRollbackById = async (rollbackId: string) => {
return result;
};
const createRollbackImage = async (
application: ApplicationNested,
tagImage: string,
) => {
const docker = await getRemoteDocker(application.serverId);
const appTagName =
application.sourceType === "docker"
? application.dockerImage
: `${application.appName}:latest`;
const result = docker.getImage(appTagName || "");
const [repo, version] = tagImage.split(":");
await result.tag({
repo,
tag: version,
});
};
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
@@ -179,7 +162,6 @@ export const rollback = async (rollbackId: string) => {
if (!result.fullContext) {
throw new Error("Rollback context not found");
}
// Use the full context for rollback
await rollbackApplication(
application.appName,
@@ -199,6 +181,7 @@ const rollbackApplication = async (
};
mounts: Mount[];
ports: Port[];
rollbackRegistry?: Registry;
},
) => {
if (!fullContext) {
@@ -245,16 +228,24 @@ const rollbackApplication = async (
fullContext.environment.project.env,
);
// For rollback, we use the provided image instead of calculating it
const authConfig = getAuthConfig(fullContext as ApplicationNested);
// Build the full registry image path if rollbackRegistry is available
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
let rollbackImage = image;
if (fullContext.rollbackRegistry) {
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
}
const settings: CreateServiceOptions = {
authconfig: authConfig,
authconfig: {
password: fullContext.rollbackRegistry?.password || "",
username: fullContext.rollbackRegistry?.username || "",
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
},
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: image,
Image: rollbackImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount],
...(command
@@ -297,7 +288,8 @@ const rollbackApplication = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
} catch (error) {
console.error(error);
await docker.createService(settings);
}
};

View File

@@ -5,12 +5,12 @@ import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import semver from "semver";
import {
initializeStandaloneTraefik,
initializeTraefikService,
type TraefikOptions,
} from "../setup/traefik-setup";
export interface IUpdateData {
latestVersion: string | null;
updateAvailable: boolean;
@@ -55,58 +55,95 @@ export const getServiceImageDigest = async () => {
};
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
export const getUpdateData = async (): Promise<IUpdateData> => {
let currentDigest: string;
export const getUpdateData = async (
currentVersion: string,
): Promise<IUpdateData> => {
try {
currentDigest = await getServiceImageDigest();
} catch {
// Docker service might not exist locally
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
// https://docs.dokploy.com/docs/core/manual-installation
return DEFAULT_UPDATE_DATA;
}
const baseUrl =
"https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
let url: string | null = `${baseUrl}?page_size=100`;
let allResults: { digest: string; name: string }[] = [];
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
let url: string | null = `${baseUrl}?page_size=100`;
let allResults: { digest: string; name: string }[] = [];
while (url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
// Fetch all tags from Docker Hub
while (url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const data = (await response.json()) as {
next: string | null;
results: { digest: string; name: string }[];
};
const data = (await response.json()) as {
next: string | null;
results: { digest: string; name: string }[];
};
allResults = allResults.concat(data.results);
url = data?.next;
}
allResults = allResults.concat(data.results);
url = data?.next;
}
const imageTag = getDokployImageTag();
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
const currentImageTag = getDokployImageTag();
if (!searchedDigest) {
return DEFAULT_UPDATE_DATA;
}
// Special handling for canary and feature branches
// For development versions (canary/feature), don't perform update checks
// These are unstable versions that change frequently, and users on these
// branches are expected to manually manage updates
if (currentImageTag === "canary" || currentImageTag === "feature") {
const currentDigest = await getServiceImageDigest();
const latestDigest = allResults.find(
(t) => t.name === currentImageTag,
)?.digest;
if (!latestDigest) {
return DEFAULT_UPDATE_DATA;
}
if (currentDigest !== latestDigest) {
return {
latestVersion: currentImageTag,
updateAvailable: true,
};
}
return {
latestVersion: currentImageTag,
updateAvailable: false,
};
}
if (imageTag === "latest") {
const versionedTag = allResults.find(
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
);
// For stable versions, use semver comparison
// Find the "latest" tag and get its digest
const latestTag = allResults.find((t) => t.name === "latest");
if (!versionedTag) {
if (!latestTag) {
return DEFAULT_UPDATE_DATA;
}
const { name: latestVersion, digest } = versionedTag;
const updateAvailable = digest !== currentDigest;
// Find the versioned tag (v0.x.x) that has the same digest as "latest"
const latestVersionTag = allResults.find(
(t) => t.digest === latestTag.digest && t.name.startsWith("v"),
);
return { latestVersion, updateAvailable };
if (!latestVersionTag) {
return DEFAULT_UPDATE_DATA;
}
const latestVersion = latestVersionTag.name;
// Use semver to compare versions for stable releases
const cleanedCurrent = semver.clean(currentVersion);
const cleanedLatest = semver.clean(latestVersion);
if (!cleanedCurrent || !cleanedLatest) {
return DEFAULT_UPDATE_DATA;
}
// Check if the latest version is greater than the current version
const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
return {
latestVersion,
updateAvailable,
};
} catch (error) {
console.error("Error fetching update data:", error);
return DEFAULT_UPDATE_DATA;
}
const updateAvailable = searchedDigest !== currentDigest;
return { latestVersion: imageTag, updateAvailable };
};
interface TreeDataItem {
@@ -217,38 +254,6 @@ echo "$json_output"
return result;
};
export const cleanupFullDocker = async (serverId?: string | null) => {
const cleanupImages = "docker image prune --force";
const cleanupVolumes = "docker volume prune --force";
const cleanupContainers = "docker container prune --force";
const cleanupSystem = "docker system prune --force --volumes";
const cleanupBuilder = "docker builder prune --force";
try {
if (serverId) {
await execAsyncRemote(
serverId,
`
${cleanupImages}
${cleanupVolumes}
${cleanupContainers}
${cleanupSystem}
${cleanupBuilder}
`,
);
}
await execAsync(`
${cleanupImages}
${cleanupVolumes}
${cleanupContainers}
${cleanupSystem}
${cleanupBuilder}
`);
} catch (error) {
console.log(error);
}
};
export const getDockerResourceType = async (
resourceName: string,
serverId?: string,
@@ -288,11 +293,22 @@ fi`;
export const reloadDockerResource = async (
resourceName: string,
serverId?: string,
version?: string,
) => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service update --force ${resourceName}`;
if (resourceName === "dokploy") {
const currentImageTag = getDokployImageTag();
let imageTag = version;
if (currentImageTag === "canary" || currentImageTag === "feature") {
imageTag = currentImageTag;
}
command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
} else {
command = `docker service update --force ${resourceName}`;
}
} else if (resourceType === "standalone") {
command = `docker restart ${resourceName}`;
} else {
@@ -374,19 +390,27 @@ export const readPorts = async (
publishedPort: number;
protocol?: string;
}[] = [];
const seenPorts = new Set<string>();
for (const key in parsedResult) {
if (Object.hasOwn(parsedResult, key)) {
const containerPortMapppings = parsedResult[key];
const protocol = key.split("/")[1];
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
containerPortMapppings.forEach((mapping: any) => {
ports.push({
targetPort: targetPort,
publishedPort: Number.parseInt(mapping.HostPort, 10),
protocol: protocol,
});
});
// Take only the first mapping to avoid duplicates (IPv4 and IPv6)
const firstMapping = containerPortMapppings[0];
if (firstMapping) {
const publishedPort = Number.parseInt(firstMapping.HostPort, 10);
const portKey = `${targetPort}-${publishedPort}-${protocol}`;
if (!seenPorts.has(portKey)) {
seenPorts.add(portKey);
ports.push({
targetPort: targetPort,
publishedPort: publishedPort,
protocol: protocol,
});
}
}
}
}
return ports.filter(
@@ -394,6 +418,28 @@ export const readPorts = async (
);
};
export const checkPortInUse = async (
port: number,
serverId?: string,
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
try {
const command = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
const container = stdout.trim();
return {
isInUse: !!container,
conflictingContainer: container || undefined,
};
} catch (error) {
console.error("Error checking port availability:", error);
return { isInUse: false };
}
};
export const writeTraefikSetup = async (input: TraefikOptions) => {
const resourceType = await getDockerResourceType(
"dokploy-traefik",

View File

@@ -1,10 +1,10 @@
import { db } from "@dokploy/server/db";
import { apikey, member, users_temp } from "@dokploy/server/db/schema";
import { apikey, member, user } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import { auth } from "../lib/auth";
export type User = typeof users_temp.$inferSelect;
export type User = typeof user.$inferSelect;
export const addNewProject = async (
userId: string,
@@ -403,16 +403,16 @@ export const updateUser = async (userId: string, userData: Partial<User>) => {
}
}
const user = await db
.update(users_temp)
const userResult = await db
.update(user)
.set({
...userData,
})
.where(eq(users_temp.id, userId))
.where(eq(user.id, userId))
.returning()
.then((res) => res[0]);
return user;
return userResult;
};
export const createApiKey = async (

View File

@@ -12,13 +12,69 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
const volumeBackup = await db.query.volumeBackups.findFirst({
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
with: {
application: true,
postgres: true,
mysql: true,
mariadb: true,
mongo: true,
redis: true,
compose: true,
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
postgres: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mysql: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mariadb: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mongo: {
with: {
environment: {
with: {
project: true,
},
},
},
},
redis: {
with: {
environment: {
with: {
project: true,
},
},
},
},
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
destination: true,
},
});

View File

@@ -0,0 +1,44 @@
import { db } from "@dokploy/server/db";
import { webServerSettings } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
/**
* Get the web server settings (singleton - only one row should exist)
*/
export const getWebServerSettings = async () => {
const settings = await db.query.webServerSettings.findFirst({
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
});
if (!settings) {
// Create default settings if none exist
const [newSettings] = await db
.insert(webServerSettings)
.values({})
.returning();
return newSettings;
}
return settings;
};
/**
* Update web server settings
*/
export const updateWebServerSettings = async (
updates: Partial<typeof webServerSettings.$inferInsert>,
) => {
const current = await getWebServerSettings();
const [updated] = await db
.update(webServerSettings)
.set({
...updates,
updatedAt: new Date(),
})
.where(eq(webServerSettings.id, current?.id ?? ""))
.returning();
return updated;
};