Compare commits

..

9 Commits

Author SHA1 Message Date
dosubot[bot]
10c6b2bd73 docs: reorganize README with improved structure and clarity 2026-03-29 17:44:02 +00:00
Mauricio Siu
290a03ccfb Merge pull request #4093 from Dokploy/4084-gotify-ntfy-lark-mattermost-and-custom-notification-providers-silently-drop-volumebackup-on-creation
feat(notification): add volumeBackup parameter to notification creati…
2026-03-29 09:10:28 -06:00
Mauricio Siu
63aa60f7e2 feat(notification): add volumeBackup parameter to notification creation functions
- Updated createCustomNotification, createLarkNotification, createMattermostNotification, and updateMattermostNotification to include volumeBackup as a parameter, enhancing notification capabilities.
2026-03-29 09:08:46 -06:00
Mauricio Siu
fe9b0ebcea Merge pull request #4092 from Dokploy/2023-add-support-for-rclone-sign_accept_encoding-option-to-fix-s3-compatible-services-behind-proxies-blocked-until-rclone-170
feat(destinations): add additionalFlags field for destination settings
2026-03-29 09:06:44 -06:00
Mauricio Siu
8ccdb66ced feat(destinations): enhance validation for additionalFlags in destination settings
- Introduced regex validation for the `additionalFlags` field to ensure proper flag formatting.
- Updated error handling in the API router to provide clearer feedback on validation issues.
- Refactored the database schema to align with the new validation rules for additionalFlags.
- Added a new validation module for destination-related checks.
2026-03-29 08:58:42 -06:00
Mauricio Siu
e38f07d286 fix(dashboard): handle optional serverId in RemoveContainerDialog
- Updated the serverId prop in RemoveContainerDialog to default to undefined if not provided, ensuring better handling of optional values.
2026-03-29 08:46:05 -06:00
autofix-ci[bot]
035d39e3b7 [autofix.ci] apply automated fixes 2026-03-29 14:43:41 +00:00
Mauricio Siu
82a908a865 feat(destinations): enhance additionalFlags handling in destination settings
- Refactored the `additionalFlags` field to use a structured object format, allowing for better validation and management of flag values.
- Replaced the textarea input with a dynamic list of input fields, enabling users to add or remove flags easily.
- Updated form handling to accommodate the new structure, ensuring proper data mapping during form submission.
2026-03-29 08:43:16 -06:00
Mauricio Siu
4bbb2ece49 feat(destinations): add additionalFlags field for destination settings
- Introduced an optional `additionalFlags` field in the destination schema to allow users to specify extra parameters.
- Updated the form in the dashboard to include a textarea for entering additional flags.
- Modified the API router to handle the new `additionalFlags` input when creating or updating destinations.
- Adjusted database schema to accommodate the new field in the destination table.
2026-03-29 08:39:27 -06:00
13 changed files with 8371 additions and 9 deletions

View File

@@ -7,7 +7,7 @@ Please describe in a short paragraph what this PR is about.
Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/MkinG2k0/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.
## Issues related (if applicable)

View File

@@ -20,7 +20,7 @@ Dokploy includes multiple features to make your life easier.
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, libsql, and Redis.
- **Backups**: Automate backups for databases to an external storage destination.
- **Backups**: Automate backups for databases to external storage destinations (S3, SFTP, FTP, Google Drive).
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.

View File

@@ -130,7 +130,7 @@ export const columns: ColumnDef<Container>[] = [
</DockerTerminalModal>
<RemoveContainerDialog
containerId={container.containerId}
serverId={container.serverId}
serverId={container.serverId ?? undefined}
/>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -1,7 +1,7 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -35,6 +35,10 @@ import {
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "@dokploy/server/db/validations/destination";
import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({
@@ -46,6 +50,16 @@ const addDestination = z.object({
region: z.string(),
endpoint: z.string().min(1, "Endpoint is required"),
serverId: z.string().optional(),
additionalFlags: z
.array(
z.object({
value: z
.string()
.min(1, "Flag cannot be empty")
.regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR),
}),
)
.optional(),
});
type AddDestination = z.infer<typeof addDestination>;
@@ -89,9 +103,16 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: "",
secretAccessKey: "",
endpoint: "",
additionalFlags: [],
},
resolver: zodResolver(addDestination),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "additionalFlags",
});
useEffect(() => {
if (destination) {
form.reset({
@@ -102,6 +123,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
bucket: destination.bucket,
region: destination.region,
endpoint: destination.endpoint,
additionalFlags:
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
});
} else {
form.reset();
@@ -118,6 +141,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: data.region,
secretAccessKey: data.secretAccessKey,
destinationId: destinationId || "",
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
})
.then(async () => {
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
@@ -127,9 +151,12 @@ export const HandleDestinations = ({ destinationId }: Props) => {
}
setOpen(false);
})
.catch(() => {
.catch((e) => {
toast.error(
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
{
description: e.message,
},
);
});
};
@@ -141,6 +168,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
"secretAccessKey",
"bucket",
"endpoint",
"additionalFlags",
]);
if (!result) {
@@ -179,6 +207,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region,
secretAccessKey: secretKey,
serverId,
additionalFlags:
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
})
.then(() => {
toast.success("Connection Success");
@@ -358,6 +388,48 @@ export const HandleDestinations = ({ destinationId }: Props) => {
</FormItem>
)}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>Additional Flags (Optional)</FormLabel>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ value: "" })}
>
<PlusIcon className="size-4" />
Add Flag
</Button>
</div>
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`additionalFlags.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormControl>
<Input
placeholder="--s3-sign-accept-encoding=false"
{...field}
/>
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</form>
<DialogFooter

View File

@@ -0,0 +1 @@
ALTER TABLE "destination" ADD COLUMN "additionalFlags" text[];

File diff suppressed because it is too large Load Diff

View File

@@ -1086,6 +1086,13 @@
"when": 1774337356154,
"tag": "0154_careful_eternals",
"breakpoints": true
},
{
"idx": 155,
"version": "7",
"when": 1774794547865,
"tag": "0155_careless_clea",
"breakpoints": true
}
]
}

View File

@@ -47,8 +47,15 @@ export const destinationRouter = createTRPCRouter({
testConnection: withPermission("destination", "create")
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
input;
const {
secretAccessKey,
bucket,
region,
endpoint,
accessKey,
provider,
additionalFlags,
} = input;
try {
const rcloneFlags = [
`--s3-access-key-id="${accessKey}"`,
@@ -65,6 +72,9 @@ export const destinationRouter = createTRPCRouter({
if (provider) {
rcloneFlags.unshift(`--s3-provider="${provider}"`);
}
if (additionalFlags?.length) {
rcloneFlags.push(...additionalFlags);
}
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
@@ -159,7 +169,14 @@ export const destinationRouter = createTRPCRouter({
});
return result;
} catch (error) {
throw error;
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error?.message
: "Error connecting to bucket",
cause: error,
});
}
}),
});

View File

@@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "../validations/destination";
import { organization } from "./account";
import { backups } from "./backups";
@@ -18,6 +22,7 @@ export const destinations = pgTable("destination", {
bucket: text("bucket").notNull(),
region: text("region").notNull(),
endpoint: text("endpoint").notNull(),
additionalFlags: text("additionalFlags").array(),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, {
endpoint: z.string(),
secretAccessKey: z.string(),
region: z.string(),
additionalFlags: z
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
.default([]),
});
export const apiCreateDestination = createSchema
@@ -55,6 +63,7 @@ export const apiCreateDestination = createSchema
region: true,
endpoint: true,
secretAccessKey: true,
additionalFlags: true,
})
.required()
.extend({
@@ -81,6 +90,7 @@ export const apiUpdateDestination = createSchema
secretAccessKey: true,
destinationId: true,
provider: true,
additionalFlags: true,
})
.required()
.extend({

View File

@@ -0,0 +1,3 @@
export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/;
export const ADDITIONAL_FLAG_ERROR =
"Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)";

View File

@@ -1,6 +1,7 @@
export * from "./auth/random-password";
export * from "./constants/index";
export * from "./db/constants";
export * from "./db/validations/destination";
export * from "./db/validations/domain";
export * from "./db/validations/index";
export * from "./lib/auth";

View File

@@ -729,6 +729,7 @@ export const createCustomNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "custom",
@@ -853,6 +854,7 @@ export const createLarkNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "lark",
@@ -1049,6 +1051,7 @@ export const createMattermostNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "mattermost",
@@ -1080,6 +1083,7 @@ export const updateMattermostNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,

View File

@@ -79,6 +79,10 @@ export const getS3Credentials = (destination: Destination) => {
rcloneFlags.unshift(`--s3-provider="${provider}"`);
}
if (destination.additionalFlags?.length) {
rcloneFlags.push(...destination.additionalFlags);
}
return rcloneFlags;
};