Compare commits

..

56 Commits

Author SHA1 Message Date
Mauricio Siu
f61fb3aba0 chore: update dokploy version to v0.25.11 2025-11-26 03:13:26 -05:00
Mauricio Siu
d3b7e68da9 Merge pull request #3032 from fir4tozden/canary
chore: naming of redis and postgres volumes has been made understandable
2025-11-26 03:04:21 -05:00
Mauricio Siu
061ca6c95c Merge pull request #3058 from shiqocred/canary
fix: server time is incorrect
2025-11-26 03:03:30 -05:00
Mauricio Siu
e576c1a63f Merge pull request #2698 from Harikrishnan1367709/Users-are-unable-to-see-requests-#2687
Fix: Allow organization members to access requests functionality (#2687)
2025-11-26 03:01:48 -05:00
Mauricio Siu
5d53cf4090 Merge pull request #3113 from Dokploy/Wrong-Railpack-version-in-the-build-logs-#2535
feat: add Railpack installation command to builder script
2025-11-26 03:00:19 -05:00
Mauricio Siu
ff27f0828b feat: add Railpack installation command to builder script
- Introduced a command to set the RAILPACK_VERSION environment variable and execute the Railpack installation script.
- This enhancement ensures that the correct version of Railpack is used during the build process.
2025-11-26 02:59:32 -05:00
Mauricio Siu
33d4f57611 Merge pull request #3112 from Dokploy/Email-test-notification-always-successful.-#2841
refactor: improve error handling in notification components
2025-11-26 02:40:46 -05:00
Mauricio Siu
bacadccaa9 refactor: improve error handling in notification components
- Enhanced error messages in HandleNotifications and notificationRouter to provide more specific feedback.
- Updated email and Discord notification functions to throw detailed errors on failure.
- Ensured consistent error handling across notification utilities for better debugging.
2025-11-26 02:39:01 -05:00
Mauricio Siu
55748749fd Merge pull request #3110 from Dokploy/3096-show-all-users-not-working-in-dashboardsettingsusers
refactor: remove TableCaption from user display in dashboard settings
2025-11-26 02:25:24 -05:00
Mauricio Siu
45b75fdfde refactor: remove TableCaption from user display in dashboard settings 2025-11-26 02:24:52 -05:00
Mauricio Siu
ff822481c5 Merge pull request #3109 from Dokploy/3097-actions-section-not-working-on-users-page
refactor: enhance user management actions in dashboard
2025-11-26 02:23:35 -05:00
Mauricio Siu
783324628f refactor: enhance user management actions in dashboard
- Updated the user action dropdown to conditionally display options based on user role, ensuring that only non-owner users can access certain actions.
- Improved the delete and unlink user functionalities with better error handling and success notifications.
- Streamlined the code for clarity and maintainability.
2025-11-26 02:23:12 -05:00
Mauricio Siu
e70c476c9f Merge pull request #3108 from Dokploy/2690-when-querying-gitlab-projects-using-the-parameter-ownedtrue-prevents-me-from-finding-more-projects
fix: correct GitLab API URL by removing 'owned' parameter from projec…
2025-11-26 02:09:51 -05:00
Mauricio Siu
891260fe41 fix: correct GitLab API URL by removing 'owned' parameter from project fetch request 2025-11-26 02:09:06 -05:00
Mauricio Siu
062037a9e6 Merge pull request #3107 from Dokploy/2883-s3-destinations-test-connection-fails-to-quote-region
fix: update rclone S3 flags to use quotes for improved parsing
2025-11-26 02:04:38 -05:00
Mauricio Siu
7da1be877b fix: update rclone S3 flags to use quotes for improved parsing
- Added quotes around S3 configuration options in rclone flags to ensure proper handling of special characters and spaces.
- This change enhances the reliability of the S3 integration by preventing potential parsing issues.
2025-11-26 02:04:15 -05:00
Mauricio Siu
60e6285e8e Merge pull request #3106 from Dokploy/2884-expired-authentication-not-displayed-on-the-ui
feat: add additional rclone configuration options for S3 integration
2025-11-26 02:00:48 -05:00
Mauricio Siu
cd8c67bb9b feat: add additional rclone configuration options for S3 integration
- Introduced new rclone flags: --retries, --low-level-retries, --timeout, and --contimeout to enhance S3 operations.
- These options aim to improve reliability and performance during file transfers.
2025-11-26 02:00:14 -05:00
Mauricio Siu
4fb3ad3032 Merge pull request #3048 from Bima42/fix/update-pg-data-path
fix: update pg data path for latest docker version
2025-11-26 01:22:12 -05:00
Mauricio Siu
736a7320d4 refactor: remove unused mount-related logic from postgres router
- Removed the findMountsByApplicationId and updateMount functions from the postgres router as they are no longer needed after the recent refactor of the getMountPath function.
- Cleaned up the code to streamline the update process for PostgreSQL instances.
2025-11-26 01:21:56 -05:00
Mauricio Siu
23b235303c refactor: move getMountPath function to services and update logic
- Moved the getMountPath function from the postgres router to the postgres service for better organization.
- Updated the logic to return the correct mount path based on the PostgreSQL version, ensuring compatibility with versions below 18.
2025-11-26 01:20:40 -05:00
Mauricio Siu
eb8c6e4367 Merge pull request #3105 from Dokploy/Failed-to-schedule-a-backup-for-a-non-existent-(deleted)-database
refactor: improve cleanup operation handling in postgres router
2025-11-26 01:14:08 -05:00
Mauricio Siu
965f05c7c8 refactor: improve cleanup operation handling in postgres router
- Changed cleanup operations to use async functions for better error handling.
- Replaced Promise.allSettled with a for loop to individually await each operation, allowing for more granular error management.
2025-11-26 01:12:39 -05:00
Mauricio Siu
e316beaddb Merge pull request #3104 from Dokploy/fix/error-parsing
feat: enhance error handling in deployment processes
2025-11-26 00:53:07 -05:00
Mauricio Siu
8aff1e7614 refactor: simplify execAsync options type for better clarity
- Updated the options parameter in execAsync to a more streamlined type, focusing on cwd, env, and shell properties for improved clarity and usability.
2025-11-26 00:50:51 -05:00
Mauricio Siu
dbe1733dcb refactor: update execAsync options type for improved flexibility
- Enhanced the options parameter in execAsync to accept ObjectEncodingOptions and ExecOptions, allowing for more versatile command execution configurations.
2025-11-26 00:20:39 -05:00
Mauricio Siu
73d87c06e1 feat: enhance error handling in deployment processes
- Introduced a new ExecError class to standardize error handling during command execution.
- Updated deployApplication and deployCompose functions to log detailed error messages, excluding ExecError instances.
- Improved execAsync and execAsyncRemote functions to throw ExecError with additional context for better debugging.
- Added base64 encoding for error messages to ensure sensitive information is handled appropriately.
2025-11-26 00:11:43 -05:00
Mauricio Siu
e136934cbc Fix newline at end of .env.example file 2025-11-25 12:02:52 -05:00
Mauricio Siu
4840abe3a4 Specify Docker version in installation script 2025-11-22 09:55:35 -05:00
Mauricio Siu
f046ba427a Merge pull request #3083 from Dokploy/2992-inconsistent-date-formats-for-environments
feat: add SQL script to standardize date formats in environment table
2025-11-21 15:02:52 -05:00
Mauricio Siu
b12e84c645 feat: add SQL script to standardize date formats in environment table
- Introduced a new SQL script to update the `createdAt` field in the `environment` table, converting PostgreSQL timestamp formats to ISO 8601 format.
- This change addresses issue #2992, ensuring consistency in date formats across environments.
- Updated journal to include the new migration tag for tracking purposes.
2025-11-21 15:02:17 -05:00
Mauricio Siu
d18fe8390b Merge pull request #3082 from Dokploy/3077-cant-put-space-in-services-name
fix: update input handling in application components
2025-11-21 11:58:59 -05:00
Mauricio Siu
e88a9ce96f fix: update input handling in application components
- Modified the onChange event for input fields in AddApplication, AddCompose, and AddDatabase components to ensure proper trimming of whitespace from the input value before slugification.
2025-11-21 11:58:23 -05:00
Mauricio Siu
a5abd46386 Merge pull request #3071 from Dokploy/fix/adjust-export-envs-stack
fix: improve Docker command execution by including environment variab…
2025-11-20 08:49:17 -06:00
Mauricio Siu
ad0e044740 chore: bump version to v0.25.10 in package.json 2025-11-20 08:48:33 -06:00
Mauricio Siu
7a0ff72f51 fix: improve Docker command execution by including environment variable exports
- Updated the Docker command execution to include environment variable exports directly in the command, enhancing the handling of environment variables during deployment.
- Simplified the export command structure for better readability and efficiency.

Fix https://github.com/Dokploy/dokploy/pull/3066#issuecomment-3558022350
2025-11-20 08:43:24 -06:00
Mauricio Siu
2e702dc41f Merge pull request #2952 from spacewaterbear/add_env_in_notifications
feat: display environnement in notification
2025-11-19 23:30:27 -06:00
Mauricio Siu
766f9244da Merge branch 'canary' into add_env_in_notifications 2025-11-19 23:24:39 -06:00
Mauricio Siu
6413fa54e6 chore: add shell-quote dependency and its type definitions
- Added `shell-quote` to dependencies for improved shell argument handling.
- Included `@types/shell-quote` in devDependencies for TypeScript support.
2025-11-19 22:55:53 -06:00
Mauricio Siu
1c9dcc0c9e Merge pull request #3066 from Dokploy/fix/nixpacks-builder
feat: enhance environment variable handling for shell commands
2025-11-19 21:27:04 -06:00
Mauricio Siu
fee802a57b refactor: remove outdated comment in railpack command builder
- Removed a comment regarding the use of shell-quote for escaping export statements, as the functionality is now handled by the `prepareEnvironmentVariablesForShell` function introduced in a previous commit.
2025-11-19 21:18:13 -06:00
Mauricio Siu
af2b053caa feat: enhance environment variable handling for shell commands
- Added `prepareEnvironmentVariablesForShell` function to properly escape environment variables for shell usage.
- Updated various builders (Docker, Heroku, Nixpacks, Paketo, Railpack) to utilize the new function for improved handling of special characters in environment variables.
- Introduced tests to validate the handling of environment variables with various special characters, ensuring robustness in shell command execution.
- Added `shell-quote` dependency to manage quoting of shell arguments effectively.
2025-11-19 21:17:09 -06:00
Mauricio Siu
42a4cc7fff chore: bump version to v0.25.9 in package.json 2025-11-19 10:14:20 -06:00
Mauricio Siu
2a7807c2b3 Merge pull request #3062 from Dokploy/3061-dokploy-instance-env-variables-override-compose-env
fix: update Docker command execution to use a clean environment
2025-11-19 10:00:58 -06:00
фырат ёздэн
153390ff26 Update settings.ts 2025-11-19 18:59:19 +03:00
Mauricio Siu
425b8ec3c2 fix: update Docker command execution to use a clean environment
- Modified Docker command invocations in compose service functions to use `env -i PATH="$PATH"` for improved environment isolation.
- Ensured consistent handling of Docker commands across `removeCompose`, `startCompose`, and `stopCompose` functions in `compose.ts`.
- Updated command execution in the builders to maintain environment integrity during Docker operations.
2025-11-19 09:58:16 -06:00
фырат ёздэн
e86caccfd5 Merge branch 'Dokploy:canary' into canary 2025-11-19 18:49:38 +03:00
фырат ёздэн
8a93116ce0 fix: update docker cleanup commands to include --all options 2025-11-19 18:48:29 +03:00
autofix-ci[bot]
daff2adb02 [autofix.ci] apply automated fixes 2025-11-19 12:35:39 +00:00
MacBook Pro
052fc5ffe1 fix: server time is incorrect 2025-11-19 19:14:48 +07:00
Bima42
d08fdeb939 fix: only upgrade those that use default pg path 2025-11-18 19:47:29 +01:00
Bima42
8ca8839d7e fix: update mount path on editing pg image 2025-11-18 19:40:00 +01:00
фырат ёздэн
b2264a9148 chore: naming of postgres volume has been made understandable 2025-11-16 20:34:55 +03:00
фырат ёздэн
f7ddc715c7 chore: naming of redis volume has been made understandable 2025-11-16 20:34:39 +03:00
spacewaterbear
63568a4887 feat: display environnement in notification 2025-11-03 23:27:18 +01:00
HarikrishnanD
4b44bc86b4 fix: allow all organization members to access requests functionality - Change requests-related procedures from adminProcedure to protectedProcedure - Fixes issue where members with all permissions couldn't see/activate requests - Affects readStatsLogs, haveActivateRequests, toggleRequests, updateLogCleanup, and getLogCleanupStatus - Resolves #2687 2025-09-26 18:30:38 +05:30
43 changed files with 8582 additions and 1198 deletions

View File

@@ -46,7 +46,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install docker
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version 28.5.2 && rm get-docker.sh && curl https://rclone.org/install.sh | bash
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash

View File

@@ -1,3 +1,3 @@
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
PORT=3000
NODE_ENV=development
NODE_ENV=development

View File

@@ -1,4 +1,7 @@
import { prepareEnvironmentVariables } from "@dokploy/server/index";
import {
prepareEnvironmentVariables,
prepareEnvironmentVariablesForShell,
} from "@dokploy/server/index";
import { describe, expect, it } from "vitest";
const projectEnv = `
@@ -332,4 +335,310 @@ IS_DEV=\${{environment.DEVELOPMENT}}
"IS_DEV=0",
]);
});
it("handles environment variables with single quotes in values", () => {
const envWithSingleQuotes = `
ENV_VARIABLE='ENVITONME'NT'
ANOTHER_VAR='value with 'quotes' inside'
SIMPLE_VAR=no-quotes
`;
const serviceWithSingleQuotes = `
TEST_VAR=\${{environment.ENV_VARIABLE}}
ANOTHER_TEST=\${{environment.ANOTHER_VAR}}
SIMPLE=\${{environment.SIMPLE_VAR}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithSingleQuotes,
"",
envWithSingleQuotes,
);
expect(resolved).toEqual([
"TEST_VAR=ENVITONME'NT",
"ANOTHER_TEST=value with 'quotes' inside",
"SIMPLE=no-quotes",
]);
});
});
describe("prepareEnvironmentVariablesForShell (shell escaping)", () => {
it("escapes single quotes in environment variable values", () => {
const serviceEnv = `
ENV_VARIABLE='ENVITONME'NT'
ANOTHER_VAR='value with 'quotes' inside'
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// shell-quote should wrap these in double quotes
expect(resolved).toEqual([
`"ENV_VARIABLE=ENVITONME'NT"`,
`"ANOTHER_VAR=value with 'quotes' inside"`,
]);
});
it("escapes double quotes in environment variable values", () => {
const serviceEnv = `
MESSAGE="Hello "World""
QUOTED_PATH="/path/to/"file""
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// shell-quote wraps in single quotes when there are double quotes inside
expect(resolved).toEqual([
`'MESSAGE=Hello "World"'`,
`'QUOTED_PATH=/path/to/"file"'`,
]);
});
it("escapes dollar signs in environment variable values", () => {
const serviceEnv = `
PRICE=$100
VARIABLE=$HOME/path
TEMPLATE=Hello $USER
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// Dollar signs should be escaped to prevent variable expansion
for (const env of resolved) {
expect(env).toContain("$");
}
});
it("escapes backticks in environment variable values", () => {
const serviceEnv = `
COMMAND=\`echo "test"\`
NESTED=value with \`backticks\` inside
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// Backticks are escaped/removed by dotenv parsing, but values should be safely quoted
expect(resolved.length).toBe(2);
expect(resolved[0]).toContain("COMMAND");
expect(resolved[1]).toContain("NESTED");
});
it("handles environment variables with spaces", () => {
const serviceEnv = `
FULL_NAME="John Doe"
MESSAGE='Hello World'
SENTENCE=This is a test
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// shell-quote uses single quotes for strings with spaces
expect(resolved).toEqual([
`'FULL_NAME=John Doe'`,
`'MESSAGE=Hello World'`,
`'SENTENCE=This is a test'`,
]);
});
it("handles environment variables with backslashes", () => {
const serviceEnv = `
WINDOWS_PATH=C:\\Users\\Documents
ESCAPED=value\\with\\backslashes
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// Backslashes should be properly escaped
expect(resolved.length).toBe(2);
for (const env of resolved) {
expect(env).toContain("\\");
}
});
it("handles simple environment variables without special characters", () => {
const serviceEnv = `
NODE_ENV=production
PORT=3000
DEBUG=true
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// shell-quote escapes the = sign in some cases
expect(resolved).toEqual([
"NODE_ENV\\=production",
"PORT\\=3000",
"DEBUG\\=true",
]);
});
it("handles environment variables with mixed special characters", () => {
const serviceEnv = `
COMPLEX='value with "double" and 'single' quotes'
BASH_COMMAND=echo "$HOME" && echo 'test'
WEIRD=\`echo "$VAR"\` with 'quotes' and "more"
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// All should be escaped, none should throw errors
expect(resolved.length).toBe(3);
// Verify each can be safely used in shell
for (const env of resolved) {
expect(typeof env).toBe("string");
expect(env.length).toBeGreaterThan(0);
}
});
it("handles environment variables with newlines", () => {
const serviceEnv = `
MULTILINE="line1
line2
line3"
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(1);
expect(resolved[0]).toContain("MULTILINE");
});
it("handles empty environment variable values", () => {
const serviceEnv = `
EMPTY=
EMPTY_QUOTED=""
EMPTY_SINGLE=''
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
// shell-quote escapes the = sign for empty values
expect(resolved).toEqual([
"EMPTY\\=",
"EMPTY_QUOTED\\=",
"EMPTY_SINGLE\\=",
]);
});
it("handles environment variables with equals signs in values", () => {
const serviceEnv = `
EQUATION=a=b+c
CONNECTION_STRING=user=admin;password=test
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(2);
expect(resolved[0]).toContain("EQUATION");
expect(resolved[1]).toContain("CONNECTION_STRING");
});
it("resolves and escapes environment variables together", () => {
const projectEnv = `
BASE_URL=https://example.com
API_KEY='secret-key-with-quotes'
`;
const environmentEnv = `
ENV_NAME=production
DB_PASS='pa$$word'
`;
const serviceEnv = `
FULL_URL=\${{project.BASE_URL}}/api
AUTH_KEY=\${{project.API_KEY}}
ENVIRONMENT=\${{environment.ENV_NAME}}
DB_PASSWORD=\${{environment.DB_PASS}}
CUSTOM='value with 'quotes' inside'
`;
const resolved = prepareEnvironmentVariablesForShell(
serviceEnv,
projectEnv,
environmentEnv,
);
expect(resolved.length).toBe(5);
// All resolved values should be properly escaped
for (const env of resolved) {
expect(typeof env).toBe("string");
}
});
it("handles environment variables with semicolons and ampersands", () => {
const serviceEnv = `
COMMAND=echo "test" && echo "test2"
MULTIPLE=cmd1; cmd2; cmd3
URL_WITH_PARAMS=https://example.com?a=1&b=2&c=3
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(3);
// These should be safely escaped to prevent command injection
for (const env of resolved) {
expect(typeof env).toBe("string");
expect(env.length).toBeGreaterThan(0);
}
});
it("handles environment variables with pipes and redirects", () => {
const serviceEnv = `
PIPE_COMMAND=cat file | grep test
REDIRECT=echo "test" > output.txt
BOTH=cat input.txt | grep pattern > output.txt
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(3);
// Pipes and redirects should be safely quoted
expect(resolved[0]).toContain("PIPE_COMMAND");
expect(resolved[1]).toContain("REDIRECT");
expect(resolved[2]).toContain("BOTH");
// At least one should contain a pipe
const hasPipe = resolved.some((env) => env.includes("|"));
expect(hasPipe).toBe(true);
});
it("handles environment variables with parentheses and brackets", () => {
const serviceEnv = `
MATH=(a+b)*c
ARRAY=[1,2,3]
JSON={"key":"value"}
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(3);
expect(resolved[0]).toContain("(");
expect(resolved[1]).toContain("[");
expect(resolved[2]).toContain("{");
});
it("handles very long environment variable values", () => {
const longValue = "a".repeat(10000);
const serviceEnv = `LONG_VAR=${longValue}`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(1);
expect(resolved[0]).toContain("LONG_VAR");
expect(resolved[0]?.length).toBeGreaterThan(10000);
});
it("handles special unicode characters in environment variables", () => {
const serviceEnv = `
EMOJI=Hello 🌍 World 🚀
CHINESE=你好世界
SPECIAL=café résumé naïve
`;
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
expect(resolved.length).toBe(3);
expect(resolved[0]).toContain("🌍");
expect(resolved[1]).toContain("你好");
expect(resolved[2]).toContain("café");
});
});

View File

@@ -150,8 +150,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
placeholder="Frontend"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
const serviceName = slugify(val);
const val = e.target.value || "";
const serviceName = slugify(val.trim());
form.setValue("appName", `${slug}-${serviceName}`);
field.onChange(val);
}}

View File

@@ -161,8 +161,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
placeholder="Frontend"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
const serviceName = slugify(val);
const val = e.target.value || "";
const serviceName = slugify(val.trim());
form.setValue("appName", `${slug}-${serviceName}`);
field.onChange(val);
}}

View File

@@ -395,8 +395,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
placeholder="Name"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
const serviceName = slugify(val);
const val = e.target.value || "";
const serviceName = slugify(val.trim());
form.setValue("appName", `${slug}-${serviceName}`);
field.onChange(val);
}}

View File

@@ -1261,8 +1261,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
});
}
toast.success("Connection Success");
} catch {
toast.error("Error testing the provider");
} catch (error) {
toast.error(
`Error testing the provider ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}}
>

View File

@@ -21,7 +21,6 @@ import {
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
@@ -68,7 +67,6 @@ export const ShowUsers = () => {
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<Table>
<TableCaption>See all users</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Email</TableHead>
@@ -111,35 +109,75 @@ export const ShowUsers = () => {
</TableCell>
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{member.role !== "owner" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
Open menu
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{member.role !== "owner" && (
<AddUserPermissions
userId={member.user.id}
/>
)}
{member.role !== "owner" && (
<>
{!isCloud && (
<DialogAction
title="Delete User"
description="Are you sure you want to delete this user?"
type="destructive"
onClick={async () => {
{!isCloud && (
<DialogAction
title="Delete User"
description="Are you sure you want to delete this user?"
type="destructive"
onClick={async () => {
await mutateAsync({
userId: member.user.id,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting destination",
);
});
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Delete User
</DropdownMenuItem>
</DialogAction>
)}
<DialogAction
title="Unlink User"
description="Are you sure you want to unlink this user?"
type="destructive"
onClick={async () => {
if (!isCloud) {
const orgCount =
await utils.user.checkUserOrganizations.fetch(
{
userId: member.user.id,
},
);
console.log(orgCount);
if (orgCount === 1) {
await mutateAsync({
userId: member.user.id,
})
@@ -151,86 +189,40 @@ export const ShowUsers = () => {
})
.catch(() => {
toast.error(
"Error deleting destination",
"Error deleting user",
);
});
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) =>
e.preventDefault()
}
>
Delete User
</DropdownMenuItem>
</DialogAction>
)}
<DialogAction
title="Unlink User"
description="Are you sure you want to unlink this user?"
type="destructive"
onClick={async () => {
if (!isCloud) {
const orgCount =
await utils.user.checkUserOrganizations.fetch(
{
userId: member.user.id,
},
);
console.log(orgCount);
if (orgCount === 1) {
await mutateAsync({
userId: member.user.id,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting user",
);
});
return;
}
return;
}
}
const { error } =
await authClient.organization.removeMember(
{
memberIdOrEmail: member.id,
},
);
const { error } =
await authClient.organization.removeMember(
{
memberIdOrEmail: member.id,
},
);
if (!error) {
toast.success(
"User unlinked successfully",
);
refetch();
} else {
toast.error(
"Error unlinking user",
);
}
}}
if (!error) {
toast.success(
"User unlinked successfully",
);
refetch();
} else {
toast.error("Error unlinking user");
}
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Unlink User
</DropdownMenuItem>
</DialogAction>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
Unlink User
</DropdownMenuItem>
</DialogAction>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
);

View File

@@ -44,14 +44,20 @@ export function TimeBadge() {
.padStart(2, "0")}`;
};
const formattedTime = new Intl.DateTimeFormat("en-US", {
timeZone: serverTime.timezone,
timeStyle: "medium",
hour12: false,
}).format(time);
return (
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
<span className="hidden sm:inline">Server Time:</span>
<span className="font-medium tabular-nums">
{time.toLocaleTimeString()}
</span>
<span className="hidden sm:inline text-muted-foreground">
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
<div className="inline-flex items-center rounded-full border p-1 text-xs whitespace-nowrap max-w-full overflow-hidden gap-1">
<div className="inline-flex items-center px-1 gap-1">
<span className="hidden sm:inline">Server Time:</span>
<span className="font-medium tabular-nums">{formattedTime}</span>
</div>
<span className="hidden sm:inline text-primary/70 border rounded-full bg-foreground/5 px-1.5 py-0.5">
{serverTime.timezone} | {getUtcOffset(serverTime.timezone)}
</span>
</div>
);

View File

@@ -0,0 +1,9 @@
-- Fix inconsistent date formats in environment.createdAt field
-- Convert PostgreSQL timestamp format to ISO 8601 format
-- This addresses issue #2992 where old environments have PostgreSQL timestamp format
-- while new ones have ISO 8601 format
UPDATE "environment"
SET "createdAt" = to_char("createdAt"::timestamptz, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
WHERE "createdAt" NOT LIKE '%T%';

File diff suppressed because it is too large Load Diff

View File

@@ -848,6 +848,13 @@
"when": 1762632540024,
"tag": "0120_lame_captain_midlands",
"breakpoints": true
},
{
"idx": 121,
"version": "7",
"when": 1763755037033,
"tag": "0121_rainy_cargill",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.25.8",
"version": "v0.25.11",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -98,6 +98,7 @@
"bl": "6.0.11",
"boxen": "^7.1.1",
"bullmq": "5.4.2",
"shell-quote": "^1.8.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^0.2.1",
@@ -157,6 +158,7 @@
"zod-form-data": "^2.0.7"
},
"devDependencies": {
"@types/shell-quote": "^1.7.5",
"@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2",
"@types/js-cookie": "^3.0.6",

View File

@@ -47,15 +47,19 @@ export const destinationRouter = createTRPCRouter({
input;
try {
const rcloneFlags = [
`--s3-access-key-id=${accessKey}`,
`--s3-secret-access-key=${secretAccessKey}`,
`--s3-region=${region}`,
`--s3-endpoint=${endpoint}`,
`--s3-access-key-id="${accessKey}"`,
`--s3-secret-access-key="${secretAccessKey}"`,
`--s3-region="${region}"`,
`--s3-endpoint="${endpoint}"`,
"--s3-no-check-bucket",
"--s3-force-path-style",
"--retries 1",
"--low-level-retries 1",
"--timeout 10s",
"--contimeout 5s",
];
if (provider) {
rcloneFlags.unshift(`--s3-provider=${provider}`);
rcloneFlags.unshift(`--s3-provider="${provider}"`);
}
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;

View File

@@ -111,7 +111,7 @@ export const notificationRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
message: `${error instanceof Error ? error.message : "Unknown error"}`,
cause: error,
});
}
@@ -228,7 +228,7 @@ export const notificationRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
message: `${error instanceof Error ? error.message : "Unknown error"}`,
cause: error,
});
}
@@ -285,7 +285,7 @@ export const notificationRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
message: `${error instanceof Error ? error.message : "Unknown error"}`,
cause: error,
});
}

View File

@@ -8,6 +8,7 @@ import {
findEnvironmentById,
findPostgresById,
findProjectById,
getMountPath,
IS_CLOUD,
rebuildDatabase,
removePostgresById,
@@ -37,6 +38,7 @@ import {
postgres as postgresTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePostgres)
@@ -79,11 +81,13 @@ export const postgresRouter = createTRPCRouter({
);
}
const mountPath = getMountPath(input.dockerImage);
await createMount({
serviceId: newPostgres.postgresId,
serviceType: "postgres",
volumeName: `${newPostgres.appName}-data`,
mountPath: "/var/lib/postgresql/data",
mountPath: mountPath,
type: "volume",
});
@@ -282,12 +286,16 @@ export const postgresRouter = createTRPCRouter({
const backups = await findBackupsByDbId(input.postgresId, "postgres");
const cleanupOperations = [
removeService(postgres.appName, postgres.serverId),
cancelJobs(backups),
removePostgresById(input.postgresId),
async () => await removeService(postgres?.appName, postgres.serverId),
async () => await cancelJobs(backups),
async () => await removePostgresById(input.postgresId),
];
await Promise.allSettled(cleanupOperations);
for (const operation of cleanupOperations) {
try {
await operation();
} catch (_) {}
}
return postgres;
}),
@@ -363,6 +371,7 @@ export const postgresRouter = createTRPCRouter({
message: "You are not authorized to update this Postgres",
});
}
const service = await updatePostgresById(postgresId, {
...rest,
});

View File

@@ -587,7 +587,7 @@ export const settingsRouter = createTRPCRouter({
return ports.some((port) => port.targetPort === 8080);
}),
readStatsLogs: adminProcedure
readStatsLogs: protectedProcedure
.meta({
openapi: {
path: "/read-stats-logs",
@@ -650,7 +650,7 @@ export const settingsRouter = createTRPCRouter({
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
return processedLogs || [];
}),
haveActivateRequests: adminProcedure.query(async () => {
haveActivateRequests: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
@@ -665,7 +665,7 @@ export const settingsRouter = createTRPCRouter({
return !!parsedConfig?.accessLog?.filePath;
}),
toggleRequests: adminProcedure
toggleRequests: protectedProcedure
.input(
z.object({
enable: z.boolean(),
@@ -835,7 +835,7 @@ export const settingsRouter = createTRPCRouter({
const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports;
}),
updateLogCleanup: adminProcedure
updateLogCleanup: protectedProcedure
.input(
z.object({
cronExpression: z.string().nullable(),
@@ -851,7 +851,7 @@ export const settingsRouter = createTRPCRouter({
return stopLogCleanup();
}),
getLogCleanupStatus: adminProcedure.query(async () => {
getLogCleanupStatus: protectedProcedure.query(async () => {
return getLogCleanupStatus();
}),

View File

@@ -75,6 +75,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"shell-quote": "^1.8.1",
"slugify": "^1.6.6",
"ssh2": "1.15.0",
"toml": "3.0.0",
@@ -93,6 +94,7 @@
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/shell-quote": "^1.7.5",
"@types/ssh2": "1.15.1",
"@types/ws": "8.5.10",
"drizzle-kit": "^0.30.6",

View File

@@ -19,6 +19,7 @@ export type TemplateProps = {
applicationType: string;
buildLink: string;
date: string;
environmentName: string;
};
export const BuildSuccessEmail = ({
@@ -27,6 +28,7 @@ export const BuildSuccessEmail = ({
applicationType = "application",
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
date = "2023-05-01T00:00:00.000Z",
environmentName = "production",
}: TemplateProps) => {
const previewText = `Build success for ${applicationName}`;
return (
@@ -74,6 +76,9 @@ export const BuildSuccessEmail = ({
<Text className="!leading-3">
Application Name: <strong>{applicationName}</strong>
</Text>
<Text className="!leading-3">
Environment: <strong>{environmentName}</strong>
</Text>
<Text className="!leading-3">
Application Type: <strong>{applicationType}</strong>
</Text>

View File

@@ -13,6 +13,7 @@ import {
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";
@@ -28,6 +29,7 @@ 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";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import {
createDeployment,
@@ -225,9 +227,19 @@ export const deployApplication = async ({
buildLink,
organizationId: application.environment.project.organizationId,
domains: application.domains,
environmentName: application.environment.name,
});
} catch (error) {
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
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 (application.serverId) {
await execAsyncRemote(application.serverId, command);
} else {
@@ -273,6 +285,7 @@ export const rebuildApplication = async ({
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
@@ -293,7 +306,43 @@ export const rebuildApplication = async ({
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,
applicationType: "application",
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 (application.serverId) {
await execAsyncRemote(application.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;

View File

@@ -18,6 +18,7 @@ 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";
@@ -32,6 +33,7 @@ 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,
@@ -267,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",
@@ -341,6 +359,21 @@ export const rebuildCompose = async ({
composeStatus: "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};`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
@@ -375,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}`;
@@ -402,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(
@@ -437,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

@@ -13,6 +13,18 @@ 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) {
return `/var/lib/postgresql/${version}/data`;
}
}
return "/var/lib/postgresql/data";
}
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {

View File

@@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
try {
currentDigest = await getServiceImageDigest();
} catch (error) {
console.error(error);
// 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
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
return DEFAULT_UPDATE_DATA;
}

View File

@@ -17,7 +17,7 @@ export const initializePostgres = async () => {
Mounts: [
{
Type: "volume",
Source: "dokploy-postgres-database",
Source: "dokploy-postgres",
Target: "/var/lib/postgresql/data",
},
],

View File

@@ -14,7 +14,7 @@ export const initializeRedis = async () => {
Mounts: [
{
Type: "volume",
Source: "redis-data-volume",
Source: "dokploy-redis",
Target: "/data",
},
],

View File

@@ -2,6 +2,7 @@ import { dirname, join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { InferResultType } from "@dokploy/server/types/with";
import boxen from "boxen";
import { quote } from "shell-quote";
import { writeDomainsToCompose } from "../docker/domain";
import {
encodeBase64,
@@ -52,9 +53,8 @@ Compose Type: ${composeType} ✅`;
cd "${projectPath}";
${exportEnvCommand}
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
echo "Docker Compose Deployed: ✅";
@@ -65,7 +65,6 @@ Compose Type: ${composeType} ✅`;
`;
return bashCommand;
// return await execAsyncRemote(compose.serverId, bashCommand);
};
const sanitizeCommand = (command: string) => {
@@ -137,8 +136,8 @@ const getExportEnvCommand = (compose: ComposeNested) => {
compose.environment.project.env,
);
const exports = Object.entries(envVars)
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
.join("\n");
.map(([key, value]) => `${key}=${quote([value])}`)
.join(" ");
return exports ? `\n# Export environment variables\n${exports}\n` : "";
return exports ? `${exports}` : "";
};

View File

@@ -1,7 +1,8 @@
import {
getEnviromentVariablesObject,
prepareEnvironmentVariables,
prepareEnvironmentVariablesForShell,
} from "@dokploy/server/utils/docker/utils";
import { quote } from "shell-quote";
import {
getBuildAppDirectory,
getDockerContextPath,
@@ -40,14 +41,14 @@ export const getDockerCommand = (application: ApplicationNested) => {
commandArgs.push("--no-cache");
}
const args = prepareEnvironmentVariables(
const args = prepareEnvironmentVariablesForShell(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) {
commandArgs.push("--build-arg", `'${arg}'`);
commandArgs.push("--build-arg", arg);
}
const secrets = getEnviromentVariablesObject(
@@ -57,7 +58,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
);
const joinedSecrets = Object.entries(secrets)
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
.map(([key, value]) => `${key}=${quote([value])}`)
.join(" ");
for (const key in secrets) {

View File

@@ -1,4 +1,4 @@
import { prepareEnvironmentVariables } from "../docker/utils";
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
@@ -6,7 +6,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
const envVariables = prepareEnvironmentVariablesForShell(
env,
application.environment.project.env,
application.environment.env,
@@ -26,7 +26,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
}
for (const env of envVariables) {
args.push("--env", `'${env}'`);
args.push("--env", env);
}
const command = `pack ${args.join(" ")}`;

View File

@@ -1,7 +1,7 @@
import path from "node:path";
import { getStaticCommand } from "@dokploy/server/utils/builders/static";
import { nanoid } from "nanoid";
import { prepareEnvironmentVariables } from "../docker/utils";
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
@@ -10,7 +10,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
const envVariables = prepareEnvironmentVariablesForShell(
env,
application.environment.project.env,
application.environment.env,
@@ -23,7 +23,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
}
for (const env of envVariables) {
args.push("--env", `'${env}'`);
args.push("--env", env);
}
if (publishDirectory) {

View File

@@ -1,4 +1,4 @@
import { prepareEnvironmentVariables } from "../docker/utils";
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
@@ -6,7 +6,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
const envVariables = prepareEnvironmentVariablesForShell(
env,
application.environment.project.env,
application.environment.env,
@@ -26,7 +26,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
}
for (const env of envVariables) {
args.push("--env", `'${env}'`);
args.push("--env", env);
}
const command = `pack ${args.join(" ")}`;

View File

@@ -1,8 +1,10 @@
import { createHash } from "node:crypto";
import { nanoid } from "nanoid";
import { quote } from "shell-quote";
import {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
prepareEnvironmentVariablesForShell,
} from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
@@ -18,7 +20,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
export const getRailpackCommand = (application: ApplicationNested) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
const envVariables = prepareEnvironmentVariablesForShell(
env,
application.environment.project.env,
application.environment.env,
@@ -35,7 +37,7 @@ export const getRailpackCommand = (application: ApplicationNested) => {
];
for (const env of envVariables) {
prepareArgs.push("--env", `'${env}'`);
prepareArgs.push("--env", env);
}
// Calculate secrets hash for layer invalidation
@@ -63,12 +65,18 @@ export const getRailpackCommand = (application: ApplicationNested) => {
];
// Add secrets properly formatted
// Use prepareEnvironmentVariables (without ForShell) to get raw values for parsing
const rawEnvVariables = prepareEnvironmentVariables(
env,
application.environment.project.env,
application.environment.env,
);
const exportEnvs = [];
for (const pair of envVariables) {
for (const pair of rawEnvVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}='${value}'`);
exportEnvs.push(`export ${key}=${quote([value])}`);
}
}
@@ -77,6 +85,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
const bashCommand = `
# Ensure we have a builder with containerd
export RAILPACK_VERSION=${application.railpackVersion}
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
docker buildx create --use --name builder-containerd --driver docker-container || true
docker buildx use builder-containerd

View File

@@ -5,6 +5,7 @@ import { docker, paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv";
import { quote } from "shell-quote";
import type { ApplicationNested } from "../builders";
import type { MariadbNested } from "../databases/mariadb";
import type { MongoNested } from "../databases/mongo";
@@ -310,6 +311,21 @@ export const prepareEnvironmentVariables = (
return resolvedVars;
};
export const prepareEnvironmentVariablesForShell = (
serviceEnv: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
): string[] => {
const envVars = prepareEnvironmentVariables(
serviceEnv,
projectEnv,
environmentEnv,
);
// Using shell-quote library to properly escape shell arguments
// This is the standard way to handle special characters in shell commands
return envVars.map((env) => quote([env]));
};
export const parseEnvironmentKeyValuePair = (
pair: string,
): [string, string] => {

View File

@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -52,279 +52,287 @@ export const sendBuildErrorNotifications = async ({
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
projectName,
applicationName,
applicationType,
errorMessage: errorMessage,
buildLink,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(email, "Build failed for dokploy", template);
}
try {
if (email) {
const template = await renderAsync(
BuildFailedEmail({
projectName,
applicationName,
applicationType,
errorMessage: errorMessage,
buildLink,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Build failed for dokploy",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Failed",
inline: true,
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Failed",
inline: true,
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("⚠️", "Build Failed"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("⚠️", "Build Failed"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Failed",
"warning",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` +
`Error:\n${errorMessage}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Failed",
"warning",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` +
`Error:\n${errorMessage}`,
);
}
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
await sendTelegramNotification(
telegram,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton,
);
}
await sendTelegramNotification(
telegram,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":warning: *Build Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Error",
value: `\`\`\`${errorMessage}\`\`\``,
short: false,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":warning: *Build Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Error",
value: `\`\`\`${errorMessage}\`\`\``,
short: false,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "⚠️ Build Failed",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
header: {
title: {
tag: "plain_text",
content: "⚠️ Build Failed",
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
subtitle: {
tag: "plain_text",
content: "",
},
template: "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
type: "danger",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
],
margin: "0px 0px 0px 0px",
},
],
type: "danger",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -8,8 +8,8 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -22,6 +22,7 @@ interface Props {
buildLink: string;
organizationId: string;
domains: Domain[];
environmentName: string;
}
export const sendBuildSuccessNotifications = async ({
@@ -31,6 +32,7 @@ export const sendBuildSuccessNotifications = async ({
buildLink,
organizationId,
domains,
environmentName,
}: Props) => {
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
@@ -53,269 +55,295 @@ export const sendBuildSuccessNotifications = async ({
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
BuildSuccessEmail({
projectName,
applicationName,
applicationType,
buildLink,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(email, "Build success for dokploy", template);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Success"),
color: 0x57f287,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Build Success"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}`,
);
}
if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
array.slice(i * chunkSize, i * chunkSize + chunkSize),
try {
if (email) {
const template = await renderAsync(
BuildSuccessEmail({
projectName,
applicationName,
applicationType,
buildLink,
date: date.toLocaleString(),
environmentName,
}),
).catch();
await sendEmailNotification(
email,
"Build success for dokploy",
template,
);
}
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Successes"),
color: 0x57f287,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`🌍`", "Environment"),
value: environmentName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
],
...chunkArray(domains, 2).map((chunk) =>
chunk.map((data) => ({
text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`,
})),
),
];
});
}
await sendTelegramNotification(
telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton,
);
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Build Success"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("🌍", `Environment: ${environmentName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`🌍Environment: ${environmentName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}`,
);
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
array.slice(i * chunkSize, i * chunkSize + chunkSize),
);
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
...chunkArray(domains, 2).map((chunk) =>
chunk.map((data) => ({
text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`,
})),
),
];
await sendTelegramNotification(
telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Environment",
value: environmentName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Build Success",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
header: {
title: {
tag: "plain_text",
content: "✅ Build Success",
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Environment:**\n${environmentName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
type: "primary",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
],
margin: "0px 0px 0px 0px",
},
],
type: "primary",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -52,309 +52,312 @@ export const sendDatabaseBackupNotifications = async ({
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
try {
if (email) {
const template = await renderAsync(
DatabaseBackupEmail({
projectName,
applicationName,
databaseType,
type,
errorMessage,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Database backup for dokploy",
template,
);
}
if (email) {
const template = await renderAsync(
DatabaseBackupEmail({
projectName,
applicationName,
databaseType,
type,
errorMessage,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Database backup for dokploy",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Database Backup Successful")
: decorate(">", "`❌` Database Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Database"),
value: databaseType,
inline: true,
},
{
name: decorate("`📂`", "Database Name"),
value: databaseName,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && errorMessage
? [
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
},
]
: []),
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Database Backup Notification",
},
});
}
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Database Backup Successful")
: decorate(">", "`❌` Database Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Database"),
value: databaseType,
inline: true,
},
{
name: decorate("`📂`", "Database Name"),
value: databaseName,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && errorMessage
? [
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
},
]
: []),
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Database Backup Notification",
},
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${databaseType}`)}` +
`${decorate("📂", `Database Name: ${databaseName}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${databaseType}`)}` +
`${decorate("📂", `Database Name: ${databaseName}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${databaseType}\n` +
`📂Database Name: ${databaseName}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${databaseType}\n` +
`📂Database Name: ${databaseName}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
);
}
if (telegram) {
const isError = type === "error" && errorMessage;
if (telegram) {
const isError = type === "error" && errorMessage;
const statusEmoji = type === "success" ? "✅" : "❌";
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg = isError
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
: "";
const statusEmoji = type === "success" ? "✅" : "❌";
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg = isError
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
: "";
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
await sendTelegramNotification(telegram, messageText);
}
await sendTelegramNotification(telegram, messageText);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: type === "success" ? "#00FF00" : "#FF0000",
pretext:
type === "success"
? ":white_check_mark: *Database Backup Successful*"
: ":x: *Database Backup Failed*",
fields: [
...(type === "error" && errorMessage
? [
{
title: "Error Message",
value: errorMessage,
short: false,
},
]
: []),
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: databaseType,
short: true,
},
{
title: "Database Name",
value: databaseName,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: type === "success" ? "#00FF00" : "#FF0000",
pretext:
type === "success"
? ":white_check_mark: *Database Backup Successful*"
: ":x: *Database Backup Failed*",
fields: [
...(type === "error" && errorMessage
? [
{
title: "Error Message",
value: errorMessage,
short: false,
},
]
: []),
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: databaseType,
short: true,
},
{
title: "Database Name",
value: databaseName,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
},
],
},
],
});
}
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage =
errorMessage && errorMessage.length > limitCharacter
? errorMessage.substring(0, limitCharacter)
: errorMessage;
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage =
errorMessage && errorMessage.length > limitCharacter
? errorMessage.substring(0, limitCharacter)
: errorMessage;
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
},
header: {
title: {
tag: "plain_text",
content:
type === "success"
? " Database Backup Successful"
: "❌ Database Backup Failed",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: type === "success" ? "green" : "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Type:**\n${databaseType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Name:**\n${databaseName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
header: {
title: {
tag: "plain_text",
content:
type === "success"
? "✅ Database Backup Successful"
: " Database Backup Failed",
},
...(type === "error" && truncatedErrorMessage
? [
subtitle: {
tag: "plain_text",
content: "",
},
template: type === "success" ? "green" : "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Type:**\n${databaseType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
]
: []),
],
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Name:**\n${databaseName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
...(type === "error" && truncatedErrorMessage
? [
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
]
: []),
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -39,182 +39,185 @@ export const sendDockerCleanupNotifications = async (
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
try {
if (email) {
const template = await renderAsync(
DockerCleanupEmail({ message, date: date.toLocaleString() }),
).catch();
if (email) {
const template = await renderAsync(
DockerCleanupEmail({ message, date: date.toLocaleString() }),
).catch();
await sendEmailNotification(
email,
"Docker cleanup for dokploy",
template,
);
}
await sendEmailNotification(
email,
"Docker cleanup for dokploy",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Docker Cleanup"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Docker Cleanup"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`📜`", "Message"),
value: `\`\`\`${message}\`\`\``,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Docker Cleanup Notification",
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`📜`", "Message"),
value: `\`\`\`${message}\`\`\``,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Docker Cleanup Notification",
},
});
}
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Docker Cleanup"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("📜", `Message:\n${message}`)}`,
);
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Docker Cleanup"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("📜", `Message:\n${message}`)}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Docker Cleanup",
"white_check_mark",
"",
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Docker Cleanup",
"white_check_mark",
"",
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Docker Cleanup*",
fields: [
{
title: "Message",
value: message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Docker Cleanup*",
fields: [
{
title: "Message",
value: message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Docker Cleanup",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Cleanup Details:**\n${message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
header: {
title: {
tag: "plain_text",
content: "✅ Docker Cleanup",
},
],
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: "**Status:**\nSuccessful",
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Cleanup Details:**\n${message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -7,8 +7,8 @@ import { eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -34,18 +34,23 @@ export const sendDokployRestartNotifications = async () => {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
DokployRestartEmail({ date: date.toLocaleString() }),
).catch();
await sendEmailNotification(email, "Dokploy Server Restarted", template);
}
try {
if (email) {
const template = await renderAsync(
DokployRestartEmail({ date: date.toLocaleString() }),
).catch();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendEmailNotification(
email,
"Dokploy Server Restarted",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
try {
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287,
@@ -71,27 +76,19 @@ export const sendDokployRestartNotifications = async () => {
text: "Dokploy Restart Notification",
},
});
} catch (error) {
console.log(error);
}
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
try {
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
} catch (error) {
console.log(error);
}
}
if (ntfy) {
try {
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Dokploy Server Restarted",
@@ -99,25 +96,17 @@ export const sendDokployRestartNotifications = async () => {
"",
`🕒Date: ${date.toLocaleString()}`,
);
} catch (error) {
console.log(error);
}
}
if (telegram) {
try {
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
} catch (error) {
console.log(error);
}
}
if (slack) {
const { channel } = slack;
try {
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
@@ -134,13 +123,9 @@ export const sendDokployRestartNotifications = async () => {
},
],
});
} catch (error) {
console.log(error);
}
}
if (lark) {
try {
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
@@ -182,7 +167,7 @@ export const sendDokployRestartNotifications = async () => {
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
content: "**Status:**\nSuccessful",
text_align: "left",
text_size: "normal_v2",
},
@@ -210,9 +195,9 @@ export const sendDokployRestartNotifications = async () => {
},
},
});
} catch (error) {
console.log(error);
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -1,8 +1,8 @@
import type {
discord,
email,
lark,
gotify,
lark,
ntfy,
slack,
telegram,
@@ -38,6 +38,9 @@ export const sendEmailNotification = async (
});
} catch (err) {
console.log(err);
throw new Error(
`Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
@@ -45,15 +48,23 @@ export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert,
embed: any,
) => {
// try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
// } catch (err) {
// console.log(err);
// }
try {
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
if (!response.ok) {
throw new Error(
`Failed to send discord notification ${response.statusText}`,
);
}
} catch (err) {
console.log("error", err);
throw new Error(
`Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendTelegramNotification = async (
@@ -90,13 +101,21 @@ export const sendSlackNotification = async (
message: any,
) => {
try {
await fetch(connection.webhookUrl, {
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
if (!response.ok) {
throw new Error(
`Failed to send slack notification ${response.statusText}`,
);
}
} catch (err) {
console.log(err);
console.log("error", err);
throw new Error(
`Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};

View File

@@ -0,0 +1,55 @@
export interface ExecErrorDetails {
command: string;
stdout?: string;
stderr?: string;
exitCode?: number;
originalError?: Error;
serverId?: string | null;
}
export class ExecError extends Error {
public readonly command: string;
public readonly stdout?: string;
public readonly stderr?: string;
public readonly exitCode?: number;
public readonly originalError?: Error;
public readonly serverId?: string | null;
constructor(message: string, details: ExecErrorDetails) {
super(message);
this.name = "ExecError";
this.command = details.command;
this.stdout = details.stdout;
this.stderr = details.stderr;
this.exitCode = details.exitCode;
this.originalError = details.originalError;
this.serverId = details.serverId;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ExecError);
}
}
/**
* Get a formatted error message with all details
*/
getDetailedMessage(): string {
const parts = [
`Command: ${this.command}`,
this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null,
this.serverId ? `Server ID: ${this.serverId}` : "Location: Local",
this.stderr ? `Stderr: ${this.stderr}` : null,
this.stdout ? `Stdout: ${this.stdout}` : null,
].filter(Boolean);
return `${this.message}\n${parts.join("\n")}`;
}
/**
* Check if this error is from a remote execution
*/
isRemote(): boolean {
return !!this.serverId;
}
}

View File

@@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process";
import util from "node:util";
import { findServerById } from "@dokploy/server/services/server";
import { Client } from "ssh2";
import { ExecError } from "./ExecError";
export const execAsync = util.promisify(exec);
// Re-export ExecError for easier imports
export { ExecError } from "./ExecError";
const execAsyncBase = util.promisify(exec);
export const execAsync = async (
command: string,
options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string },
): Promise<{ stdout: string; stderr: string }> => {
try {
const result = await execAsyncBase(command, options);
return {
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
};
} catch (error) {
if (error instanceof Error) {
// @ts-ignore - exec error has these properties
const exitCode = error.code;
// @ts-ignore
const stdout = error.stdout?.toString() || "";
// @ts-ignore
const stderr = error.stderr?.toString() || "";
throw new ExecError(`Command execution failed: ${error.message}`, {
command,
stdout,
stderr,
exitCode,
originalError: error,
});
}
throw error;
}
};
interface ExecOptions {
cwd?: string;
@@ -21,7 +56,16 @@ export const execAsyncStream = (
const childProcess = exec(command, options, (error) => {
if (error) {
reject(error);
reject(
new ExecError(`Command execution failed: ${error.message}`, {
command,
stdout: stdoutComplete,
stderr: stderrComplete,
// @ts-ignore
exitCode: error.code,
originalError: error,
}),
);
return;
}
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
@@ -45,7 +89,14 @@ export const execAsyncStream = (
childProcess.on("error", (error) => {
console.log(error);
reject(error);
reject(
new ExecError(`Command execution error: ${error.message}`, {
command,
stdout: stdoutComplete,
stderr: stderrComplete,
originalError: error,
}),
);
});
});
};
@@ -108,7 +159,14 @@ export const execAsyncRemote = async (
conn.exec(command, (err, stream) => {
if (err) {
onData?.(err.message);
throw err;
reject(
new ExecError(`Remote command execution failed: ${err.message}`, {
command,
serverId,
originalError: err,
}),
);
return;
}
stream
.on("close", (code: number, _signal: string) => {
@@ -116,7 +174,18 @@ export const execAsyncRemote = async (
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(new Error(`Error occurred ❌: ${stderr}`));
reject(
new ExecError(
`Remote command failed with exit code ${code}`,
{
command,
stdout,
stderr,
exitCode: code,
serverId,
},
),
);
}
})
.on("data", (data: string) => {
@@ -132,17 +201,25 @@ export const execAsyncRemote = async (
.on("error", (err) => {
conn.end();
if (err.level === "client-authentication") {
onData?.(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
);
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
onData?.(errorMsg);
reject(
new Error(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
),
new ExecError(errorMsg, {
command,
serverId,
originalError: err,
}),
);
} else {
onData?.(`SSH connection error: ${err.message}`);
reject(new Error(`SSH connection error: ${err.message}`));
const errorMsg = `SSH connection error: ${err.message}`;
onData?.(errorMsg);
reject(
new ExecError(errorMsg, {
command,
serverId,
originalError: err,
}),
);
}
})
.connect({

View File

@@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
// await recreateDirectory(outputPath);
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;

View File

@@ -290,7 +290,7 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,

17
pnpm-lock.yaml generated
View File

@@ -406,6 +406,9 @@ importers:
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
slugify:
specifier: ^1.6.6
version: 1.6.6
@@ -488,6 +491,9 @@ importers:
'@types/react-dom':
specifier: 18.3.0
version: 18.3.0
'@types/shell-quote':
specifier: ^1.7.5
version: 1.7.5
'@types/ssh2':
specifier: 1.15.1
version: 1.15.1
@@ -726,6 +732,9 @@ importers:
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
slugify:
specifier: ^1.6.6
version: 1.6.6
@@ -778,6 +787,9 @@ importers:
'@types/react-dom':
specifier: 18.3.0
version: 18.3.0
'@types/shell-quote':
specifier: ^1.7.5
version: 1.7.5
'@types/ssh2':
specifier: 1.15.1
version: 1.15.1
@@ -4033,6 +4045,9 @@ packages:
'@types/readable-stream@4.0.20':
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
'@types/shell-quote@1.7.5':
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
'@types/shimmer@1.2.0':
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
@@ -11383,6 +11398,8 @@ snapshots:
dependencies:
'@types/node': 20.17.51
'@types/shell-quote@1.7.5': {}
'@types/shimmer@1.2.0': {}
'@types/ssh2@1.15.1':