Compare commits

..

1 Commits

Author SHA1 Message Date
dosubot[bot]
2a790a4ba4 docs: expand contributing guidelines and API documentation 2026-03-24 05:53:51 +00:00
33 changed files with 970 additions and 9668 deletions

View File

@@ -16,6 +16,7 @@ jobs:
steps:
- uses: peakoss/anti-slop@v0
with:
max-failures: 4
blocked-commit-authors: "claude,copilot"
require-description: true
min-account-age: 5

View File

@@ -62,6 +62,22 @@ pnpm install
cp apps/dokploy/.env.example apps/dokploy/.env
```
### Optional Docker Configuration
Docker socket detection is automatic for local development. The system automatically detects and uses Docker sockets in the following order:
- DOCKER_HOST environment variable (if set)
- Rancher Desktop socket (~/.rd/docker.sock)
- Standard Docker socket (/var/run/docker.sock)
Contributors using Docker Desktop, Rancher Desktop, Colima, or other Docker alternatives can run `pnpm run dokploy:setup` without any additional configuration.
The following environment variables are only needed for remote Docker host configurations:
- **DOKPLOY_DOCKER_HOST**: Specify a remote Docker daemon host
- **DOKPLOY_DOCKER_PORT**: Specify a remote Docker daemon port
- **DOKPLOY_DOCKER_API_VERSION**: Specify which Docker API version to use (optional)
## Requirements
- [Docker](/GUIDES.md#docker)
@@ -99,14 +115,7 @@ pnpm run dokploy:build
## Docker
To build the docker image first run commands to copy .env files
```bash
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
```
then run build command
To build the docker image
```bash
pnpm run docker:build
@@ -132,21 +141,6 @@ If you want to test the webhooks on development mode using localtunnel, make sur
pnpm dlx localtunnel --port 3000
```
### Testing GitLab Webhooks
To test GitLab webhook functionality locally:
1. Configure a GitLab provider in Dokploy with a webhook secret
2. Set up the webhook in your GitLab project (Settings → Webhooks):
- **Webhook URL:** Your localtunnel URL + `/api/deploy/gitlab`
- **Secret token:** Paste the webhook secret from your Dokploy GitLab provider
- **Enable events:** Push events and Merge request events
The GitLab webhook endpoint (`/api/deploy/gitlab`) authenticates requests via the `X-Gitlab-Token` header matched against the provider's `webhookSecret`. The endpoint handles:
- **Push Hooks** — deploys matching applications and compose stacks; respects `watchPaths` filtering using commit file lists (added/modified/removed)
- **Merge Request Hooks** — creates or rebuilds preview deployments on `open/update/reopen/labeled` events; tears down preview deployments on `close/merge` events
If you run into permission issues of docker run the following command
```bash
@@ -193,6 +187,11 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
### Pull Request Guidelines
- **Keep PRs small and focused.** Avoid very large PRs; prefer several smaller PRs (e.g., one template or one logical change per PR). This speeds up review and keeps the history clear.
- **Test before submitting.** Any PR that has not been tested by the contributor will be closed. This keeps the PR queue tidy and ensures that only contributions that have been verified by their authors are considered.
Thank you for your contribution!
## Templates

View File

@@ -6,3 +6,401 @@ npm run dev
```
open http://localhost:3000
```
## Environment Variables
The API server requires the following environment variables for configuration:
### Inngest Configuration
Required for the GET /jobs endpoint to list deployment jobs:
- **INNGEST_BASE_URL** - The base URL for the Inngest instance
- Self-hosted: `http://localhost:8288`
- Production: `https://dev-inngest.dokploy.com`
- **INNGEST_SIGNING_KEY** - The signing key for authenticating with Inngest
Optional configuration for filtering and pagination:
- **INNGEST_EVENTS_RECEIVED_AFTER** (optional) - An RFC3339 timestamp to filter events received after a specific date (e.g., `2024-01-01T00:00:00Z`). If unset, no date filter is applied.
- **INNGEST_JOBS_MAX_EVENTS** (optional) - Maximum number of events to fetch when listing jobs. Default is 100, maximum is 10000. Used for pagination with cursor.
### Lemon Squeezy Integration
- **LEMON_SQUEEZY_API_KEY** - API key for Lemon Squeezy integration
- **LEMON_SQUEEZY_STORE_ID** - Store ID for Lemon Squeezy integration
### Docker Configuration
Dokploy automatically detects Docker sockets in the following priority order:
1. **DOCKER_HOST** environment variable (if set)
2. Rancher Desktop socket (`~/.rd/docker.sock`)
3. Standard Docker socket (`/var/run/docker.sock`)
This automatic detection means that Docker Desktop, Rancher Desktop, Colima, and other Docker alternatives work out-of-the-box without manual configuration.
**Optional Environment Variables:**
- **DOCKER_HOST** (optional) - Specifies a custom Docker socket path (e.g., `unix:///path/to/docker.sock`). When set, this takes priority over automatic socket detection.
- **DOCKER_API_VERSION** (optional) - Specifies which Docker API version to use when connecting to the Docker daemon. If not set, the Docker client uses the default API version.
**Remote Docker Host Configuration:**
For connecting to remote Docker daemons, use the following variables:
- **DOKPLOY_DOCKER_HOST** (optional) - Specifies the remote Docker daemon host to connect to (e.g., `tcp://remote-host`).
- **DOKPLOY_DOCKER_PORT** (optional) - Specifies the port for connecting to the remote Docker daemon.
Note: `DOKPLOY_DOCKER_HOST` and `DOKPLOY_DOCKER_PORT` are intended for remote Docker host configurations. For local Docker installations, the automatic socket detection handles connection setup without requiring these variables.
## API Endpoints
### GET /jobs
Lists deployment jobs (Inngest runs) for a specified server.
**Query Parameters:**
- `serverId` (required) - The ID of the server to list deployment jobs for
**Response:**
Returns an array of deployment job objects with the same shape as BullMQ queue jobs:
```json
[
{
"id": "string",
"name": "string",
"data": {},
"timestamp": 0,
"processedOn": 0,
"finishedOn": 0,
"failedReason": "string",
"state": "string"
}
]
```
**Error Responses:**
- `400` - serverId is not provided
- `503` - INNGEST_BASE_URL is not configured
- `200` - Empty array on other errors
This endpoint is used by the UI to display deployment queue information in the dashboard.
### POST /drop-deployment
Upload and deploy application code via ZIP file.
**Content-Type:** `multipart/form-data`
**Form Fields:**
- `applicationId` (required) - The ID of the application to deploy
- `zip` (required) - A ZIP file containing the application code
- `dropBuildPath` (optional) - Custom build path within the ZIP file
**Response:**
Initiates a deployment using the uploaded ZIP file.
**Example:**
```bash
curl -X POST https://your-dokploy-instance.com/api/drop-deployment \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "applicationId=YOUR_APP_ID" \
-F "zip=@/path/to/your/app.zip" \
-F "dropBuildPath=optional/build/path"
```
## Search Endpoints
The following search endpoints provide flexible querying capabilities with pagination support. All search endpoints respect member permissions, returning only resources the user has access to.
### application.search
Search applications across name, appName, description, repository, owner, and dockerImage fields.
**Query Parameters:**
- `q` (optional string) - General search term that searches across name, appName, description, repository, owner, and dockerImage
- `name` (optional string) - Filter by application name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `repository` (optional string) - Filter by repository
- `owner` (optional string) - Filter by owner
- `dockerImage` (optional string) - Filter by Docker image
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum number of results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"applicationId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### compose.search
Search compose services with filtering by name, appName, and description.
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"composeId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"composeStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### environment.search
Search environments by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"environmentId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"env": "string",
"projectId": "string",
"isDefault": true
}
],
"total": 0
}
```
### project.search
Search projects by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"projectId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"organizationId": "string",
"env": "string"
}
],
"total": 0
}
```
### Database Service Search Endpoints
The following database services all share the same search interface:
- **postgres.search**
- **mysql.search**
- **mariadb.search**
- **mongo.search**
- **redis.search**
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"postgresId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"createdAt": "string"
}
],
"total": 0
}
```
*Note: The response shape is similar across all database services, with the ID field varying (e.g., `mysqlId`, `mariadbId`, `mongoId`, `redisId`).*
**Search Behavior:**
- All searches use case-insensitive pattern matching with wildcards
- Results are ordered by creation date (descending)
- Members only see services they have access to
- Returns total count for pagination UI
## Database Service Update Endpoints
All database services support update operations with flexible configuration options. The following database services share a common update interface:
- **postgres.update** (apiUpdatePostgres)
- **mysql.update** (apiUpdateMySql)
- **mariadb.update** (apiUpdateMariaDB)
- **mongo.update** (apiUpdateMongo)
- **redis.update** (apiUpdateRedis)
**Common Parameters:**
All database update endpoints accept their respective ID field (e.g., `postgresId`, `mysqlId`, `mariadbId`, `mongoId`, `redisId`) as a required parameter, along with optional configuration fields.
**Optional Configuration:**
- `dockerImage` (optional string) - Specifies a custom Docker image for the database service. This allows users to use specific versions or custom-built images instead of the default image for the database type. Available for all five database services (PostgreSQL, MySQL, MariaDB, MongoDB, and Redis).
Additional service-specific parameters are available depending on the database type. The `dockerImage` field provides enhanced configuration flexibility for advanced use cases such as version pinning or using specialized database distributions.
## Whitelabeling Endpoints
The whitelabeling endpoints allow enterprise/self-hosted Dokploy instances to customize branding, logos, colors, and UI appearance. These endpoints are only available in self-hosted mode (not cloud).
### whitelabeling.get
Get the current whitelabeling configuration.
**Requirements:**
- Enterprise license required
- Only available for self-hosted (not cloud)
**Response:**
Returns the whitelabeling configuration object or null if not configured.
```json
{
"appName": "string | null",
"appDescription": "string | null",
"logoUrl": "string | null",
"faviconUrl": "string | null",
"primaryColor": "string | null",
"customCss": "string | null",
"loginLogoUrl": "string | null",
"supportUrl": "string | null",
"docsUrl": "string | null",
"errorPageTitle": "string | null",
"errorPageDescription": "string | null",
"metaTitle": "string | null",
"footerText": "string | null"
}
```
### whitelabeling.update
Update the whitelabeling configuration.
**Requirements:**
- Enterprise license required
- Owner role required
- Only available for self-hosted (not cloud)
**Input:**
```json
{
"whitelabelingConfig": {
"appName": "string | null",
"appDescription": "string | null",
"logoUrl": "string | null",
"faviconUrl": "string | null",
"primaryColor": "string | null",
"customCss": "string | null",
"loginLogoUrl": "string | null",
"supportUrl": "string | null",
"docsUrl": "string | null",
"errorPageTitle": "string | null",
"errorPageDescription": "string | null",
"metaTitle": "string | null",
"footerText": "string | null"
}
}
```
**Response:**
```json
{
"success": true
}
```
### whitelabeling.reset
Reset whitelabeling configuration to default values (all fields set to null).
**Requirements:**
- Enterprise license required
- Owner role required
- Only available for self-hosted (not cloud)
**Response:**
```json
{
"success": true
}
```
### whitelabeling.getPublic
Public endpoint to fetch whitelabeling configuration. This endpoint can be accessed without authentication, allowing the whitelabeling settings to be applied globally (including on the login page before auth).
**Requirements:**
- No authentication required
- Only available for self-hosted (not cloud)
**Response:**
Returns the whitelabeling configuration object or null if not configured. Response shape is identical to `whitelabeling.get`.

View File

@@ -1,66 +0,0 @@
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isPending } = api.docker.removeContainer.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Remove Container
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently remove the container{" "}
<span className="font-semibold">{containerId}</span>. If the
container is running, it will be forcefully stopped and removed.
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isPending}
onClick={async () => {
await mutateAsync({ containerId, serverId })
.then(async () => {
toast.success("Container removed successfully");
await utils.docker.getContainers.invalidate();
})
.catch((err) => {
toast.error(err.message);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dropdown-menu";
import { ShowContainerConfig } from "../config/show-container-config";
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
import { RemoveContainerDialog } from "../remove/remove-container";
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
import type { Container } from "./show-containers";
@@ -128,10 +127,6 @@ export const columns: ColumnDef<Container>[] = [
>
Terminal
</DockerTerminalModal>
<RemoveContainerDialog
containerId={container.containerId}
serverId={container.serverId}
/>
</DropdownMenuContent>
</DropdownMenu>
);

View File

@@ -12,7 +12,6 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
MattermostIcon,
GotifyIcon,
LarkIcon,
NtfyIcon,
@@ -135,14 +134,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
priority: z.number().min(1).max(5).default(3),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("mattermost"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
channel: z.string().optional(),
username: z.string().optional(),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("pushover"),
@@ -219,10 +210,6 @@ export const notificationsMap = {
icon: <NtfyIcon />,
label: "ntfy",
},
mattermost: {
icon: <MattermostIcon />,
label: "Mattermost",
},
pushover: {
icon: <PushoverIcon />,
label: "Pushover",
@@ -266,16 +253,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
api.notification.testGotifyConnection.useMutation();
const { mutateAsync: testNtfyConnection, isPending: isLoadingNtfy } =
api.notification.testNtfyConnection.useMutation();
const {
mutateAsync: testMattermostConnection,
isPending: isLoadingMattermost,
} = api.notification.testMattermostConnection.useMutation();
const { mutateAsync: testLarkConnection, isPending: isLoadingLark } =
api.notification.testLarkConnection.useMutation();
const { mutateAsync: testTeamsConnection, isPending: isLoadingTeams } =
api.notification.testTeamsConnection.useMutation();
const { mutateAsync: testCustomConnection, isPending: isLoadingCustom } =
api.notification.testCustomConnection.useMutation();
const { mutateAsync: testPushoverConnection, isPending: isLoadingPushover } =
api.notification.testPushoverConnection.useMutation();
@@ -303,9 +288,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const ntfyMutation = notificationId
? api.notification.updateNtfy.useMutation()
: api.notification.createNtfy.useMutation();
const mattermostMutation = notificationId
? api.notification.updateMattermost.useMutation()
: api.notification.createMattermost.useMutation();
const larkMutation = notificationId
? api.notification.updateLark.useMutation()
: api.notification.createLark.useMutation();
@@ -456,21 +438,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "mattermost") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.mattermost?.webhookUrl,
channel: notification.mattermost?.channel || "",
username: notification.mattermost?.username || "",
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "lark") {
form.reset({
appBuildError: notification.appBuildError,
@@ -549,7 +516,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
resend: resendMutation,
gotify: gotifyMutation,
ntfy: ntfyMutation,
mattermost: mattermostMutation,
lark: larkMutation,
teams: teamsMutation,
custom: customMutation,
@@ -680,22 +646,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
ntfyId: notification?.ntfyId || "",
});
} else if (data.type === "mattermost") {
promise = mattermostMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
username: data.username || undefined,
name: data.name,
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
mattermostId: notification?.mattermostId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "lark") {
promise = larkMutation.mutateAsync({
appBuildError: appBuildError,
@@ -1456,62 +1406,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
/>
</>
)}
{type === "mattermost" && (
<>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://your-mattermost.com/hooks/xxx-generatedkey-xxx"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="channel"
render={({ field }) => (
<FormItem>
<FormLabel>Channel</FormLabel>
<FormControl>
<Input placeholder="deployments" {...field} />
</FormControl>
<FormDescription>
Optional. Channel to post to (without #).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Dokploy" {...field} />
</FormControl>
<FormDescription>
Optional. Display name for the webhook.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{type === "custom" && (
<div className="space-y-4">
<FormField
@@ -1598,7 +1492,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
</div>
</div>
)}
{type === "lark" && (
<>
<FormField
@@ -1959,7 +1852,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingResend ||
isLoadingGotify ||
isLoadingNtfy ||
isLoadingMattermost ||
isLoadingLark ||
isLoadingTeams ||
isLoadingCustom ||
@@ -2019,12 +1911,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
accessToken: data.accessToken || "",
priority: data.priority ?? 0,
});
} else if (data.type === "mattermost") {
await testMattermostConnection({
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
username: data.username || undefined,
});
} else if (data.type === "lark") {
await testLarkConnection({
webhookUrl: data.webhookUrl,

View File

@@ -4,7 +4,6 @@ import {
DiscordIcon,
GotifyIcon,
LarkIcon,
MattermostIcon,
NtfyIcon,
ResendIcon,
SlackIcon,
@@ -122,12 +121,6 @@ export const ShowNotifications = () => {
<TeamsIcon className="size-7 text-muted-foreground" />
</div>
)}
{notification.notificationType ===
"mattermost" && (
<div className="flex items-center justify-center rounded-lg">
<MattermostIcon className="size-7" />
</div>
)}
{notification.name}
</span>

View File

@@ -20,7 +20,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -410,10 +409,7 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
<FormDescription>
Use &quot;root&quot; or a non-root user with passwordless
sudo access.
</FormDescription>
<FormMessage />
</FormItem>
)}

View File

@@ -118,10 +118,9 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
</div>
) : (
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
<AlertBlock type="info">
You can connect as root or as a non-root user with passwordless
sudo access. If using a non-root user, ensure passwordless sudo is
configured.
<AlertBlock type="warning">
Using a root user is required to ensure everything works as
expected.
</AlertBlock>
<Tabs defaultValue="ssh-keys">

View File

@@ -163,29 +163,6 @@ export const ValidateServer = ({ serverId }: Props) => {
: "Not Created"
}
/>
<StatusRow
label="Privilege Mode"
isEnabled={
data?.privilegeMode === "root" ||
data?.privilegeMode === "sudo"
}
description={
data?.privilegeMode === "root"
? "Running as root"
: data?.privilegeMode === "sudo"
? "Running with sudo"
: "No sudo access (required for non-root)"
}
/>
<StatusRow
label="Docker Group"
isEnabled={data?.dockerGroupMember}
description={
data?.dockerGroupMember
? "User is in docker group"
: "User is not in docker group"
}
/>
</div>
</div>
</div>

View File

@@ -88,21 +88,6 @@ export const DiscordIcon = ({ className }: Props) => {
</svg>
);
};
export const MattermostIcon = ({ className }: Props) => {
return (
<svg
fill="#0061ff"
viewBox="0 0 501 501"
xmlns="http://www.w3.org/2000/svg"
className={cn("size-8", className)}
>
<path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z" />
<path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z" />
</svg>
);
};
export const TeamsIcon = ({ className }: Props) => {
return (
<svg

View File

@@ -1,10 +0,0 @@
ALTER TYPE "public"."notificationType" ADD VALUE 'mattermost' BEFORE 'pushover';--> statement-breakpoint
CREATE TABLE "mattermost" (
"mattermostId" text PRIMARY KEY NOT NULL,
"webhookUrl" text NOT NULL,
"channel" text,
"username" text
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "mattermostId" text;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_mattermostId_mattermost_mattermostId_fk" FOREIGN KEY ("mattermostId") REFERENCES "public"."mattermost"("mattermostId") ON DELETE cascade ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -1079,13 +1079,6 @@
"when": 1774322599182,
"tag": "0153_motionless_mastermind",
"breakpoints": true
},
{
"idx": 154,
"version": "7",
"when": 1774337356154,
"tag": "0154_careful_eternals",
"breakpoints": true
}
]
}

View File

@@ -1,5 +1,4 @@
import {
containerRemove,
containerRestart,
findServerById,
getConfig,
@@ -53,32 +52,6 @@ export const dockerRouter = createTRPCRouter({
return result;
}),
removeContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await containerRemove(input.containerId, input.serverId);
await audit(ctx, {
action: "delete",
resourceType: "docker",
resourceId: input.containerId,
resourceName: input.containerId,
});
}),
getConfig: withPermission("docker", "read")
.input(
z.object({

View File

@@ -4,7 +4,6 @@ import {
createEmailNotification,
createGotifyNotification,
createLarkNotification,
createMattermostNotification,
createNtfyNotification,
createPushoverNotification,
createResendNotification,
@@ -20,7 +19,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -33,7 +31,6 @@ import {
updateEmailNotification,
updateGotifyNotification,
updateLarkNotification,
updateMattermostNotification,
updateNtfyNotification,
updatePushoverNotification,
updateResendNotification,
@@ -57,7 +54,6 @@ import {
apiCreateEmail,
apiCreateGotify,
apiCreateLark,
apiCreateMattermost,
apiCreateNtfy,
apiCreatePushover,
apiCreateResend,
@@ -70,7 +66,6 @@ import {
apiTestEmailConnection,
apiTestGotifyConnection,
apiTestLarkConnection,
apiTestMattermostConnection,
apiTestNtfyConnection,
apiTestPushoverConnection,
apiTestResendConnection,
@@ -82,7 +77,6 @@ import {
apiUpdateEmail,
apiUpdateGotify,
apiUpdateLark,
apiUpdateMattermost,
apiUpdateNtfy,
apiUpdatePushover,
apiUpdateResend,
@@ -479,7 +473,6 @@ export const notificationRouter = createTRPCRouter({
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -682,74 +675,6 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createMattermost: withPermission("notification", "create")
.input(apiCreateMattermost)
.mutation(async ({ input, ctx }) => {
try {
await createMattermostNotification(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
cause: error,
});
}
}),
updateMattermost: withPermission("notification", "update")
.input(apiUpdateMattermost)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (
IS_CLOUD &&
notification.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
const result = await updateMattermostNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testMattermostConnection: withPermission("notification", "create")
.input(apiTestMattermostConnection)
.mutation(async ({ input }) => {
try {
await sendMattermostNotification(input, {
text: "Hi, From Dokploy 👋",
channel: input.channel,
username: input.username || "Dokploy Bot",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
cause: error,
});
}
}),
createCustom: withPermission("notification", "create")
.input(apiCreateCustom)
.mutation(async ({ input, ctx }) => {

View File

@@ -252,8 +252,6 @@ export const serverRouter = createTRPCRouter({
isDokployNetworkInstalled: boolean;
isSwarmInstalled: boolean;
isMainDirectoryInstalled: boolean;
privilegeMode: string;
dockerGroupMember: boolean;
};
} catch (error) {
throw new TRPCError({

View File

@@ -20,7 +20,6 @@ export const notificationType = pgEnum("notificationType", [
"resend",
"gotify",
"ntfy",
"mattermost",
"pushover",
"custom",
"lark",
@@ -65,9 +64,6 @@ export const notifications = pgTable("notification", {
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
mattermostId: text("mattermostId").references(() => mattermost.mattermostId, {
onDelete: "cascade",
}),
customId: text("customId").references(() => custom.customId, {
onDelete: "cascade",
}),
@@ -158,16 +154,6 @@ export const ntfy = pgTable("ntfy", {
priority: integer("priority").notNull().default(3),
});
export const mattermost = pgTable("mattermost", {
mattermostId: text("mattermostId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
channel: text("channel"),
username: text("username"),
});
export const custom = pgTable("custom", {
customId: text("customId")
.notNull()
@@ -234,10 +220,6 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
mattermost: one(mattermost, {
fields: [notifications.mattermostId],
references: [mattermost.mattermostId],
}),
custom: one(custom, {
fields: [notifications.customId],
references: [custom.customId],
@@ -482,51 +464,6 @@ export const apiTestNtfyConnection = apiCreateNtfy.pick({
priority: true,
});
export const apiCreateMattermost = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().url(),
channel: z.string().optional(),
username: z.string().optional(),
})
.required({
name: true,
webhookUrl: true,
appBuildError: true,
databaseBackup: true,
volumeBackup: true,
dokployRestart: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
});
export const apiUpdateMattermost = apiCreateMattermost.partial().extend({
notificationId: z.string().min(1),
mattermostId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestMattermostConnection = apiCreateMattermost
.pick({
webhookUrl: true,
channel: true,
username: true,
})
.extend({
channel: z.string().optional(),
username: z.string().optional(),
});
export const apiFindOneNotification = z.object({
notificationId: z.string().min(1),
});

View File

@@ -371,21 +371,6 @@ export const containerRestart = async (containerId: string) => {
} catch {}
};
export const containerRemove = async (
containerId: string,
serverId?: string,
) => {
const command = `docker rm -f ${containerId}`;
const { stderr } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
if (stderr) {
console.error(`Error: ${stderr}`);
throw new Error(stderr);
}
};
export const getSwarmNodes = async (serverId?: string) => {
try {
let stdout = "";

View File

@@ -5,7 +5,6 @@ import {
type apiCreateEmail,
type apiCreateGotify,
type apiCreateLark,
type apiCreateMattermost,
type apiCreateNtfy,
type apiCreatePushover,
type apiCreateResend,
@@ -17,7 +16,6 @@ import {
type apiUpdateEmail,
type apiUpdateGotify,
type apiUpdateLark,
type apiUpdateMattermost,
type apiUpdateNtfy,
type apiUpdatePushover,
type apiUpdateResend,
@@ -29,7 +27,6 @@ import {
email,
gotify,
lark,
mattermost,
notifications,
ntfy,
pushover,
@@ -800,7 +797,6 @@ export const findNotificationById = async (notificationId: string) => {
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -1019,98 +1015,6 @@ export const updateNotificationById = async (
return result[0];
};
export const createMattermostNotification = async (
input: z.infer<typeof apiCreateMattermost>,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newMattermost = await tx
.insert(mattermost)
.values({
webhookUrl: input.webhookUrl,
channel: input.channel,
username: input.username,
})
.returning()
.then((value) => value[0]);
if (!newMattermost) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mattermost",
});
}
const newDestination = await tx
.insert(notifications)
.values({
mattermostId: newMattermost.mattermostId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "mattermost",
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateMattermostNotification = async (
input: z.infer<typeof apiUpdateMattermost>,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(mattermost)
.set({
webhookUrl: input.webhookUrl,
channel: input.channel,
username: input.username,
})
.where(eq(mattermost.mattermostId, input.mattermostId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const createPushoverNotification = async (
input: z.infer<typeof apiCreatePushover>,
organizationId: string,

View File

@@ -115,20 +115,9 @@ SYS_ARCH=$(uname -m)
CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE"
# Auto-detect sudo requirement
if [ "$EUID" -eq 0 ]; then
SUDO_CMD=""
echo "Running as root"
else
if sudo -n true 2>/dev/null; then
SUDO_CMD="sudo"
echo "Running as $CURRENT_USER with sudo privileges"
else
echo "Error: Non-root user requires passwordless sudo access. ❌"
echo "Configure with: echo '$CURRENT_USER ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/$CURRENT_USER"
exit 1
fi
if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌"
exit
fi
# Check if the OS is manjaro, if so, change it to arch
@@ -163,7 +152,7 @@ else
fi
if [ "$OS_TYPE" = 'amzn' ]; then
$SUDO_CMD dnf install -y findutils >/dev/null
dnf install -y findutils >/dev/null
fi
case "$OS_TYPE" in
@@ -229,9 +218,6 @@ ${installBuildpacks()}
echo -e "13. Installing Railpack"
${installRailpack()}
echo -e "14. Configuring permissions"
${setupPermissions()}
`
: `
echo -e "2. Installing Docker. "
@@ -249,9 +235,6 @@ ${installBuildpacks()}
echo -e "6. Installing Railpack"
${installRailpack()}
echo -e "7. Configuring permissions"
${setupPermissions()}
`
}
`;
@@ -369,18 +352,16 @@ const setupMainDirectory = () => `
echo "/etc/dokploy already exists ✅"
else
# Create the /etc/dokploy directory
$SUDO_CMD mkdir -p /etc/dokploy
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
echo "Directory /etc/dokploy created ✅"
fi
# Ensure the current user owns the directory
if [ -n "$SUDO_CMD" ]; then
$SUDO_CMD chown -R $CURRENT_USER:$CURRENT_USER /etc/dokploy
fi
`;
export const setupSwarm = () => `
# Check if the node is already part of a Docker Swarm
if $SUDO_CMD docker info | grep -q 'Swarm: active'; then
if docker info | grep -q 'Swarm: active'; then
echo "Already part of a Docker Swarm ✅"
else
# Get IP address
@@ -430,18 +411,18 @@ export const setupSwarm = () => `
echo "Advertise address: \$advertise_addr"
# Initialize Docker Swarm
$SUDO_CMD docker swarm init --advertise-addr \$advertise_addr
docker swarm init --advertise-addr \$advertise_addr
echo "Swarm initialized ✅"
fi
`;
const setupNetwork = () => `
# Check if the dokploy-network already exists
if $SUDO_CMD docker network ls | grep -q 'dokploy-network'; then
if docker network ls | grep -q 'dokploy-network'; then
echo "Network dokploy-network already exists ✅"
else
# Create the dokploy-network if it doesn't exist
if $SUDO_CMD docker network create --driver overlay --attachable dokploy-network; then
if docker network create --driver overlay --attachable dokploy-network; then
echo "Network created ✅"
else
echo "Failed to create dokploy-network ❌" >&2
@@ -466,34 +447,33 @@ const installUtilities = () => `
case "$OS_TYPE" in
arch)
$SUDO_CMD pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;;
alpine)
$SUDO_CMD sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
$SUDO_CMD apk update >/dev/null
$SUDO_CMD apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;;
ubuntu | debian | raspbian)
export DEBIAN_FRONTEND=noninteractive
$SUDO_CMD apt-get update -y >/dev/null
$SUDO_CMD apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
$SUDO_CMD dnf install -y wget git git-lfs jq openssl >/dev/null
dnf install -y wget git git-lfs jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
$SUDO_CMD yum install -y dnf >/dev/null
yum install -y dnf >/dev/null
fi
if ! command -v curl >/dev/null; then
$SUDO_CMD dnf install -y curl >/dev/null
dnf install -y curl >/dev/null
fi
$SUDO_CMD dnf install -y wget git git-lfs jq openssl unzip >/dev/null
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
$SUDO_CMD zypper refresh >/dev/null
$SUDO_CMD zypper install -y curl wget git git-lfs jq openssl >/dev/null
zypper refresh >/dev/null
zypper install -y curl wget git git-lfs jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -520,41 +500,41 @@ if ! [ -x "$(command -v docker)" ]; then
echo " - Docker is not installed. Installing Docker. It may take a while."
case "$OS_TYPE" in
"almalinux")
$SUDO_CMD dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
$SUDO_CMD dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
$SUDO_CMD systemctl start docker >/dev/null 2>&1
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
"opencloudos")
# Special handling for OpenCloud OS
echo " - Installing Docker for OpenCloud OS..."
$SUDO_CMD dnf install -y docker >/dev/null 2>&1
dnf install -y docker >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
# Remove --live-restore parameter from Docker configuration if it exists
if [ -f "/etc/sysconfig/docker" ]; then
echo " - Removing --live-restore parameter from Docker configuration..."
$SUDO_CMD sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
$SUDO_CMD sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
# Clean up any double spaces that might be left
$SUDO_CMD sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
fi
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
$SUDO_CMD systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
echo " - Docker configured for OpenCloud OS"
;;
"alpine")
$SUDO_CMD apk add docker docker-cli-compose >/dev/null 2>&1
$SUDO_CMD rc-update add docker default >/dev/null 2>&1
$SUDO_CMD service docker start >/dev/null 2>&1
apk add docker docker-cli-compose >/dev/null 2>&1
rc-update add docker default >/dev/null 2>&1
service docker start >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with apk. Try to install it manually."
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
@@ -562,8 +542,8 @@ if ! [ -x "$(command -v docker)" ]; then
fi
;;
"arch")
$SUDO_CMD pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
$SUDO_CMD systemctl enable docker.service >/dev/null 2>&1
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
systemctl enable docker.service >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with pacman. Try to install it manually."
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
@@ -571,13 +551,13 @@ if ! [ -x "$(command -v docker)" ]; then
fi
;;
"amzn")
$SUDO_CMD dnf install docker -y >/dev/null 2>&1
dnf install docker -y >/dev/null 2>&1
DOCKER_CONFIG=/usr/local/lib/docker
$SUDO_CMD mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
$SUDO_CMD curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
$SUDO_CMD chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
$SUDO_CMD systemctl start docker >/dev/null 2>&1
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with dnf. Try to install it manually."
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
@@ -587,18 +567,18 @@ if ! [ -x "$(command -v docker)" ]; then
"fedora")
if [ -x "$(command -v dnf5)" ]; then
# dnf5 is available
$SUDO_CMD dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
else
# dnf5 is not available, use dnf
$SUDO_CMD dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
fi
$SUDO_CMD dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
$SUDO_CMD systemctl start docker >/dev/null 2>&1
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
*)
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
@@ -606,9 +586,9 @@ if ! [ -x "$(command -v docker)" ]; then
echo "Please install Docker manually."
exit 1
fi
if ! [ -x "$(command -v docker)" ]; then
curl -s https://get.docker.com | $SUDO_CMD sh -s -- --version $DOCKER_VERSION 2>&1
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker installation failed."
echo " Maybe your OS is not supported?"
@@ -617,13 +597,13 @@ if ! [ -x "$(command -v docker)" ]; then
fi
fi
if [ "$OS_TYPE" = "rocky" ]; then
$SUDO_CMD systemctl start docker >/dev/null 2>&1
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
fi
if [ "$OS_TYPE" = "centos" ]; then
$SUDO_CMD systemctl start docker >/dev/null 2>&1
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
fi
@@ -667,7 +647,7 @@ export const installRClone = () => `
if command_exists rclone; then
echo "RClone already installed ✅"
else
curl https://rclone.org/install.sh | $SUDO_CMD bash
curl https://rclone.org/install.sh | sudo bash
RCLONE_VERSION=$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//')
echo "RClone version $RCLONE_VERSION installed ✅"
fi
@@ -676,19 +656,19 @@ export const installRClone = () => `
export const createTraefikInstance = () => {
const command = `
# Check if dokpyloy-traefik exists
if $SUDO_CMD docker service inspect dokploy-traefik > /dev/null 2>&1; then
if docker service inspect dokploy-traefik > /dev/null 2>&1; then
echo "Migrating Traefik to Standalone..."
$SUDO_CMD docker service rm dokploy-traefik
docker service rm dokploy-traefik
sleep 8
echo "Traefik migrated to Standalone ✅"
fi
if $SUDO_CMD docker inspect dokploy-traefik > /dev/null 2>&1; then
if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅"
else
# Create the dokploy-traefik container
TRAEFIK_VERSION=${TRAEFIK_VERSION}
$SUDO_CMD docker run -d \
docker run -d \
--name dokploy-traefik \
--restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
@@ -699,7 +679,7 @@ export const createTraefikInstance = () => {
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
traefik:v$TRAEFIK_VERSION
$SUDO_CMD docker network connect dokploy-network dokploy-traefik;
docker network connect dokploy-network dokploy-traefik;
echo "Traefik version $TRAEFIK_VERSION installed ✅"
fi
`;
@@ -712,7 +692,7 @@ const installNixpacks = () => `
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.41.0
$SUDO_CMD bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi
`;
@@ -722,28 +702,11 @@ const installRailpack = () => `
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=0.15.4
$SUDO_CMD bash -c "$(curl -fsSL https://railpack.com/install.sh)"
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi
`;
const setupPermissions = () => `
# Add user to docker group if not root
if [ -n "$SUDO_CMD" ]; then
if ! groups $CURRENT_USER | grep -qw docker; then
$SUDO_CMD usermod -aG docker $CURRENT_USER
echo "User $CURRENT_USER added to docker group ✅"
else
echo "User $CURRENT_USER already in docker group ✅"
fi
# Ensure the user owns the dokploy directory
$SUDO_CMD chown -R $CURRENT_USER:$CURRENT_USER /etc/dokploy
echo "Permissions configured for $CURRENT_USER ✅"
else
echo "Running as root, no extra permissions needed ✅"
fi
`;
const installBuildpacks = () => `
SUFFIX=""
if [ "$SYS_ARCH" = "aarch64" ] || [ "$SYS_ARCH" = "arm64" ]; then
@@ -753,7 +716,7 @@ const installBuildpacks = () => `
echo "Buildpacks already installed ✅"
else
BUILDPACKS_VERSION=0.39.1
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | $SUDO_CMD tar -C /usr/local/bin/ --no-same-owner -xzv pack
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
fi
`;

View File

@@ -79,24 +79,6 @@ export const validateDokployNetwork = () => `
fi
`;
export const validateSudoAccess = () => `
if [ "$(id -u)" -eq 0 ]; then
echo "root true"
elif sudo -n true 2>/dev/null; then
echo "sudo true"
else
echo "none false"
fi
`;
export const validateDockerGroup = () => `
if groups | grep -qw docker; then
echo true
else
echo false
fi
`;
export const serverValidate = async (serverId: string) => {
const client = new Client();
const server = await findServerById(serverId);
@@ -136,11 +118,7 @@ export const serverValidate = async (serverId: string) => {
isSwarmInstalled=$(${validateSwarm()})
isMainDirectoryInstalled=$(${validateMainDirectory()})
sudoAccessResult=$(${validateSudoAccess()})
privilegeMode=$(echo $sudoAccessResult | awk '{print $1}')
isDockerGroupMember=$(${validateDockerGroup()})
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled, \\"privilegeMode\\": \\"$privilegeMode\\", \\"dockerGroupMember\\": $isDockerGroupMember}"
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
`;
client.exec(bashCommand, (err, stream) => {
if (err) {

View File

@@ -19,7 +19,7 @@ export const runComposeBackup = async (
const project = await findProjectById(environment.projectId);
const { prefix, databaseType, serviceName } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.${databaseType === "mongo" ? "bson" : "sql"}.gz`;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({

View File

@@ -135,8 +135,8 @@ export const keepLatestNBackups = async (
const appName = getServiceAppName(backup);
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
// --include "*.bson.gz" or "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" ${backupFilesPath}`;
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
// when we pipe the above command with this one, we only get the list of files we want to delete
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
// this command deletes the files

View File

@@ -16,7 +16,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.bson.gz`;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -10,7 +10,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -51,7 +50,6 @@ export const sendBuildErrorNotifications = async ({
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -68,7 +66,6 @@ export const sendBuildErrorNotifications = async ({
slack,
gotify,
ntfy,
mattermost,
custom,
lark,
pushover,
@@ -253,26 +250,6 @@ export const sendBuildErrorNotifications = async ({
});
}
if (mattermost) {
await sendMattermostNotification(mattermost, {
text: `:warning: **Build Failed**
**Project:** ${projectName}
**Application:** ${applicationName}
**Type:** ${applicationType}
**Time:** ${date.toLocaleString()}
**Error:**
\`\`\`
${errorMessage}
\`\`\`
[View Build Details](${buildLink})`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy Bot",
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Error",

View File

@@ -11,7 +11,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -54,7 +53,6 @@ export const sendBuildSuccessNotifications = async ({
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -71,7 +69,6 @@ export const sendBuildSuccessNotifications = async ({
slack,
gotify,
ntfy,
mattermost,
custom,
lark,
pushover,
@@ -269,14 +266,6 @@ export const sendBuildSuccessNotifications = async ({
});
}
if (mattermost) {
await sendMattermostNotification(mattermost, {
text: `**✅ Build Success**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${applicationType}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}\n\n[View Build Details](${buildLink})`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy",
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Success",

View File

@@ -10,7 +10,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -51,7 +50,6 @@ export const sendDatabaseBackupNotifications = async ({
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -68,7 +66,6 @@ export const sendDatabaseBackupNotifications = async ({
slack,
gotify,
ntfy,
mattermost,
custom,
lark,
pushover,
@@ -275,21 +272,6 @@ export const sendDatabaseBackupNotifications = async ({
});
}
if (mattermost) {
const statusEmoji = type === "success" ? "✅" : "❌";
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg =
type === "error" && errorMessage
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
: "";
await sendMattermostNotification(mattermost, {
text: `**${statusEmoji} Database Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${databaseType}\n**Database Name:** ${databaseName}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy",
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,

View File

@@ -10,7 +10,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -38,7 +37,6 @@ export const sendDockerCleanupNotifications = async (
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -55,7 +53,6 @@ export const sendDockerCleanupNotifications = async (
slack,
gotify,
ntfy,
mattermost,
custom,
lark,
pushover,
@@ -171,14 +168,6 @@ export const sendDockerCleanupNotifications = async (
});
}
if (mattermost) {
await sendMattermostNotification(mattermost, {
text: `**✅ Docker Cleanup**\n\n**Message:** ${message}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy",
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Docker Cleanup",

View File

@@ -10,7 +10,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -33,7 +32,6 @@ export const sendDokployRestartNotifications = async () => {
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -50,7 +48,6 @@ export const sendDokployRestartNotifications = async () => {
slack,
gotify,
ntfy,
mattermost,
custom,
lark,
pushover,
@@ -161,14 +158,6 @@ export const sendDokployRestartNotifications = async () => {
});
}
if (mattermost) {
await sendMattermostNotification(mattermost, {
text: `**✅ Dokploy Server Restarted**\n\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy",
});
}
if (custom) {
try {
await sendCustomNotification(custom, {

View File

@@ -5,7 +5,6 @@ import {
sendCustomNotification,
sendDiscordNotification,
sendLarkNotification,
sendMattermostNotification,
sendPushoverNotification,
sendSlackNotification,
sendTeamsNotification,
@@ -39,7 +38,6 @@ export const sendServerThresholdNotifications = async (
discord: true,
telegram: true,
slack: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -51,72 +49,63 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) {
const {
discord,
telegram,
slack,
mattermost,
custom,
lark,
pushover,
teams,
} = notification;
const { discord, telegram, slack, custom, lark, pushover, teams } =
notification;
try {
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(">", `\`⚠️\` Server ${payload.Type} Alert`),
color: typeColor,
fields: [
{
name: decorate("`🏷️`", "Server Name"),
value: payload.ServerName,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate(typeEmoji, "Type"),
value: payload.Type,
inline: true,
},
{
name: decorate("📊", "Current Value"),
value: `${payload.Value.toFixed(2)}%`,
inline: true,
},
{
name: decorate("⚠️", "Threshold"),
value: `${payload.Threshold.toFixed(2)}%`,
inline: true,
},
{
name: decorate("`📜`", "Message"),
value: `\`\`\`${payload.Message}\`\`\``,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Server Monitoring Alert",
await sendDiscordNotification(discord, {
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
color: typeColor,
fields: [
{
name: decorate("`🏷️`", "Server Name"),
value: payload.ServerName,
inline: true,
},
});
}
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate(typeEmoji, "Type"),
value: payload.Type,
inline: true,
},
{
name: decorate("📊", "Current Value"),
value: `${payload.Value.toFixed(2)}%`,
inline: true,
},
{
name: decorate("⚠️", "Threshold"),
value: `${payload.Threshold.toFixed(2)}%`,
inline: true,
},
{
name: decorate("`📜`", "Message"),
value: `\`\`\`${payload.Message}\`\`\``,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Server Monitoring Alert",
},
});
}
if (telegram) {
await sendTelegramNotification(
telegram,
`
if (telegram) {
await sendTelegramNotification(
telegram,
`
<b>⚠️ Server ${payload.Type} Alert</b>
<b>Server Name:</b> ${payload.ServerName}
<b>Type:</b> ${payload.Type}
@@ -125,181 +114,170 @@ export const sendServerThresholdNotifications = async (
<b>Message:</b> ${payload.Message}
<b>Time:</b> ${date.toLocaleString()}
`,
);
}
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: `:warning: *Server ${payload.Type} Alert*`,
fields: [
{
title: "Server Name",
value: payload.ServerName,
short: true,
},
{
title: "Type",
value: payload.Type,
short: true,
},
{
title: "Current Value",
value: `${payload.Value.toFixed(2)}%`,
short: true,
},
{
title: "Threshold",
value: `${payload.Threshold.toFixed(2)}%`,
short: true,
},
{
title: "Message",
value: payload.Message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: `:warning: *Server ${payload.Type} Alert*`,
fields: [
{
title: "Server Name",
value: payload.ServerName,
short: true,
},
{
title: "Type",
value: payload.Type,
short: true,
},
{
title: "Current Value",
value: `${payload.Value.toFixed(2)}%`,
short: true,
},
{
title: "Threshold",
value: `${payload.Threshold.toFixed(2)}%`,
short: true,
},
{
title: "Message",
value: payload.Message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
}
if (mattermost) {
await sendMattermostNotification(mattermost, {
text: `**⚠️ Server ${payload.Type} Alert**\n\n**Server Name:** ${payload.ServerName}\n**Type:** ${payload.Type}\n**Current Value:** ${payload.Value.toFixed(2)}%\n**Threshold:** ${payload.Threshold.toFixed(2)}%\n**Message:** ${payload.Message}\n**Time:** ${date.toLocaleString()}`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy",
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Server ${payload.Type} Alert`,
message: payload.Message,
serverName: payload.ServerName,
type: payload.Type,
currentValue: payload.Value,
threshold: payload.Threshold,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "alert",
alertType: "server-threshold",
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Server ${payload.Type} Alert`,
message: payload.Message,
serverName: payload.ServerName,
type: payload.Type,
currentValue: payload.Value,
threshold: payload.Threshold,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "alert",
alertType: "server-threshold",
});
}
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: `⚠️ Server ${payload.Type} Alert`,
},
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: `**Server Name:**\n${payload.ServerName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Message:**\n${payload.Message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Time:**\n${date.toLocaleString()}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
}
header: {
title: {
tag: "plain_text",
content: `⚠️ Server ${payload.Type} Alert`,
},
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: `**Server Name:**\n${payload.ServerName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Message:**\n${payload.Message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Time:**\n${date.toLocaleString()}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
`Server ${payload.Type} Alert`,
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
);
}
} catch (error) {
console.log(error);
if (pushover) {
await sendPushoverNotification(
pushover,
`Server ${payload.Type} Alert`,
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
);
}
if (teams) {

View File

@@ -4,7 +4,6 @@ import type {
email,
gotify,
lark,
mattermost,
ntfy,
pushover,
resend,
@@ -207,33 +206,6 @@ export const sendNtfyNotification = async (
}
};
export const sendMattermostNotification = async (
connection: typeof mattermost.$inferInsert,
message: any,
) => {
const payload = {
...message,
// Only include username if it's provided and not empty
...(message.username?.trim() && { username: message.username }),
// Only include channel if it's provided and not empty
...(message.channel?.trim() && {
channel: `#${message.channel.replace("#", "")}`,
}),
};
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(
`Failed to send Mattermost notification: ${response.statusText}`,
);
}
};
export const sendCustomNotification = async (
connection: typeof custom.$inferInsert,
payload: Record<string, any>,

View File

@@ -9,8 +9,6 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -61,11 +59,9 @@ export const sendVolumeBackupNotifications = async ({
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
teams: true,
custom: true,
},
});
@@ -78,420 +74,278 @@ export const sendVolumeBackupNotifications = async ({
slack,
gotify,
ntfy,
mattermost,
custom,
lark,
pushover,
teams,
custom,
} = notification;
try {
if (email || resend) {
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
const htmlContent = await renderAsync(
VolumeBackupEmail({
projectName,
applicationName,
volumeName,
serviceType,
type,
errorMessage,
backupSize,
date: date.toISOString(),
}),
);
if (email) {
await sendEmailNotification(email, subject, htmlContent);
}
if (resend) {
await sendResendNotification(resend, subject, htmlContent);
}
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Volume Backup Successful")
: decorate(">", "`❌` Volume Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`💾`", "Volume Name"),
value: volumeName,
inline: true,
},
{
name: decorate("`🔧`", "Service Type"),
value: serviceType,
inline: true,
},
...(backupSize
? [
{
name: decorate("`📊`", "Backup Size"),
value: backupSize,
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 Volume Backup Notification",
},
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`💾Volume Name: ${volumeName}\n` +
`🔧Service Type: ${serviceType}\n` +
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${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 sizeInfo = backupSize
? `\n<b>Backup Size:</b> ${backupSize}`
: "";
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
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: *Volume Backup Successful*"
: ":x: *Volume 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: "Volume Name",
value: volumeName,
short: true,
},
{
title: "Service Type",
value: serviceType,
short: true,
},
...(backupSize
? [
{
title: "Backup Size",
value: backupSize,
short: true,
},
]
: []),
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
short: true,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
short: true,
},
],
},
],
});
}
if (mattermost) {
const statusEmoji = type === "success" ? "✅" : "❌";
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg =
type === "error" && errorMessage
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
: "";
const sizeInfo = backupSize ? `\n**Backup Size:** ${backupSize}` : "";
await sendMattermostNotification(mattermost, {
text: `**${statusEmoji} Volume Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Volume Name:** ${volumeName}\n**Service Type:** ${serviceType}${sizeInfo}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
channel: mattermost.channel,
username: mattermost.username || "Dokploy",
});
}
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",
},
},
},
},
header: {
title: {
tag: "plain_text",
content:
type === "success"
? "✅ Volume Backup Successful"
: "❌ Volume 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: `**Volume Name:**\n${volumeName}`,
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: `**Service Type:**\n${serviceType}`,
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",
},
]
: []),
],
},
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
);
}
if (teams) {
const facts = [
{ name: "Project", value: projectName },
{ name: "Application", value: applicationName },
{ name: "Volume Name", value: volumeName },
{ name: "Service Type", value: serviceType },
{ name: "Date", value: format(date, "PP pp") },
{
name: "Status",
value: type === "success" ? "Successful" : "Failed",
},
];
if (backupSize) {
facts.push({ name: "Backup Size", value: backupSize });
}
if (type === "error" && errorMessage) {
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
}
await sendTeamsNotification(teams, {
title:
type === "success"
? "✅ Volume Backup Successful"
: "❌ Volume Backup Failed",
facts,
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
message:
type === "success"
? "Volume backup completed successfully"
: "Volume backup failed",
if (email || resend) {
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
const htmlContent = await renderAsync(
VolumeBackupEmail({
projectName,
applicationName,
volumeName,
serviceType,
type,
errorMessage: errorMessage ?? "",
backupSize: backupSize ?? "",
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: type,
});
errorMessage,
backupSize,
date: date.toISOString(),
}),
);
if (email) {
await sendEmailNotification(email, subject, htmlContent);
}
} catch (error) {
console.log(error);
if (resend) {
await sendResendNotification(resend, subject, htmlContent);
}
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Volume Backup Successful")
: decorate(">", "`❌` Volume Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`💾`", "Volume Name"),
value: volumeName,
inline: true,
},
{
name: decorate("`🔧`", "Service Type"),
value: serviceType,
inline: true,
},
...(backupSize
? [
{
name: decorate("`📊`", "Backup Size"),
value: backupSize,
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.substring(0, 1010)}\`\`\``,
},
]
: []),
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Volume Backup Notification",
},
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`💾Volume Name: ${volumeName}\n` +
`🔧Service Type: ${serviceType}\n` +
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${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 sizeInfo = backupSize ? `\n<b>Backup Size:</b> ${backupSize}` : "";
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
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: *Volume Backup Successful*"
: ":x: *Volume 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: "Volume Name",
value: volumeName,
short: true,
},
{
title: "Service Type",
value: serviceType,
short: true,
},
...(backupSize
? [
{
title: "Backup Size",
value: backupSize,
short: true,
},
]
: []),
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
short: true,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
short: true,
},
],
},
],
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
);
}
if (teams) {
const facts = [
{ name: "Project", value: projectName },
{ name: "Application", value: applicationName },
{ name: "Volume Name", value: volumeName },
{ name: "Service Type", value: serviceType },
{ name: "Date", value: format(date, "PP pp") },
{ name: "Status", value: type === "success" ? "Successful" : "Failed" },
];
if (backupSize) {
facts.push({ name: "Backup Size", value: backupSize });
}
if (type === "error" && errorMessage) {
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
}
await sendTeamsNotification(teams, {
title:
type === "success"
? "✅ Volume Backup Successful"
: "❌ Volume Backup Failed",
facts,
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
message:
type === "success"
? "Volume backup completed successfully"
: "Volume backup failed",
projectName,
applicationName,
volumeName,
serviceType,
type,
errorMessage: errorMessage ?? "",
backupSize: backupSize ?? "",
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: type,
});
}
}
};