Compare commits

..

1 Commits

Author SHA1 Message Date
dosubot[bot]
f233ae0978 docs: Dosu updates for PR #3868 2026-03-03 20:13:44 +00:00
45 changed files with 473 additions and 409 deletions

View File

@@ -6,3 +6,237 @@ 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
## 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.
## 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

View File

@@ -1,4 +1,4 @@
import { logger } from "./logger.js";
import { logger } from "./logger";
const baseUrl = process.env.INNGEST_BASE_URL ?? "";
const signingKey = process.env.INNGEST_SIGNING_KEY ?? "";

View File

@@ -9,7 +9,7 @@ import {
updateCompose,
updatePreviewDeployment,
} from "@dokploy/server";
import type { DeployJob } from "./schema.js";
import type { DeployJob } from "./schema";
export const deploy = async (job: DeployJob) => {
try {

View File

@@ -24,6 +24,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
const organizationSchema = z.object({
@@ -54,6 +55,8 @@ export function AddOrganization({ organizationId }: Props) {
const { mutateAsync, isPending } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const { refetch: refetchActiveOrganization } =
authClient.useActiveOrganization();
const form = useForm<OrganizationFormValues>({
resolver: zodResolver(organizationSchema),
@@ -86,7 +89,7 @@ export function AddOrganization({ organizationId }: Props) {
utils.organization.all.invalidate();
if (organizationId) {
utils.organization.one.invalidate({ organizationId });
utils.organization.active.invalidate();
refetchActiveOrganization();
}
setOpen(false);
})

View File

@@ -23,6 +23,7 @@ import {
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { StatusTooltip } from "../shared/status-tooltip";
@@ -55,7 +56,7 @@ export const SearchCommand = () => {
const router = useRouter();
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState("");
const { data: session } = api.user.session.useQuery();
const { data: session } = authClient.useSession();
const { data } = api.project.all.useQuery(undefined, {
enabled: !!session,
});

View File

@@ -12,13 +12,13 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: session } = api.user.session.useQuery();
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: session } = authClient.useSession();
const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false);
@@ -52,7 +52,7 @@ export const AddGithubProvider = () => {
);
setManifest(manifest);
}, [activeOrganization?.id, session?.user?.id]);
}, [data?.id, activeOrganization?.id, session?.user?.id]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -98,8 +98,8 @@ export const AddGithubProvider = () => {
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
}
method="post"
>
@@ -131,7 +131,11 @@ export const AddGithubProvider = () => {
Unsure if you already have an app?
</a>
<Button
disabled={isOrganization && organizationName.length < 1}
disabled={
(isOrganization && organizationName.length < 1) ||
!activeOrganization?.id ||
!session?.user?.id
}
type="submit"
className="self-end"
>

View File

@@ -55,7 +55,7 @@ export const AddInvitation = () => {
api.notification.getEmailProviders.useQuery();
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
const [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: activeOrganization } = authClient.useActiveOrganization();
const form = useForm<AddInvitation>({
defaultValues: {

View File

@@ -37,7 +37,7 @@ export const ShowUsers = () => {
const { mutateAsync } = api.user.remove.useMutation();
const utils = api.useUtils();
const { data: session } = api.user.session.useQuery();
const { data: session } = authClient.useSession();
return (
<div className="w-full">

View File

@@ -546,7 +546,7 @@ function SidebarLogo() {
const { state } = useSidebar();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.get.useQuery();
const { data: session } = api.user.session.useQuery();
const { data: session } = authClient.useSession();
const {
data: organizations,
refetch,
@@ -557,7 +557,8 @@ function SidebarLogo() {
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
api.organization.setDefault.useMutation();
const { isMobile } = useSidebar();
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: activeOrganization } = authClient.useActiveOrganization();
const _utils = api.useUtils();
const { data: invitations, refetch: refetchInvitations } =
api.user.getInvitations.useQuery();

View File

@@ -9,8 +9,7 @@ import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { search, searchKeymap } from "@codemirror/search";
import { EditorView, keymap } from "@codemirror/view";
import { EditorView } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
@@ -156,8 +155,6 @@ export const CodeEditor = ({
}}
theme={resolvedTheme === "dark" ? githubDark : githubLight}
extensions={[
search(),
keymap.of(searchKeymap),
language === "yaml"
? yaml()
: language === "json"

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.28.5",
"version": "v0.28.3",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -53,8 +53,7 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "^6.6.0",
"@codemirror/view": "^6.39.15",
"@codemirror/view": "6.29.0",
"@dokploy/server": "workspace:*",
"@dokploy/trpc-openapi": "0.0.17",
"@faker-js/faker": "^8.4.1",

View File

@@ -10,29 +10,22 @@ type Query = {
state: string;
installation_id: string;
setup_action: string;
userId: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { code, state, installation_id }: Query = req.query as Query;
const { code, state, installation_id, userId }: Query = req.query as Query;
if (!code) {
return res.status(400).json({ error: "Missing code parameter" });
}
const [action, ...rest] = state?.split(":");
// For gh_init: rest[0] = organizationId, rest[1] = userId
// For gh_setup: rest[0] = githubProviderId
const [action, value] = state?.split(":");
// Value could be the organizationId or the githubProviderId
if (action === "gh_init") {
const organizationId = rest[0];
const userId = rest[1] || (req.query.userId as string);
if (!userId) {
return res.status(400).json({ error: "Missing userId parameter" });
}
const octokit = new Octokit({});
const { data } = await octokit.request(
"POST /app-manifests/{code}/conversions",
@@ -51,7 +44,7 @@ export default async function handler(
githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem,
},
organizationId as string,
value as string,
userId,
);
} else if (action === "gh_setup") {
@@ -60,7 +53,7 @@ export default async function handler(
.set({
githubInstallationId: installation_id,
})
.where(eq(github.githubId, rest[0] as string))
.where(eq(github.githubId, value as string))
.returning();
}

View File

@@ -777,7 +777,7 @@ const EnvironmentPage = (
}
if (success > 0) {
toast.success(
`${success} service${success !== 1 ? "s" : ""} queued for deployment`,
`${success} service${success !== 1 ? "s" : ""} deployed successfully`,
);
}
if (failed > 0) {

View File

@@ -69,7 +69,8 @@ export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
return await createMount(input);
await createMount(input);
return true;
}),
remove: protectedProcedure
.input(apiRemoveMount)

View File

@@ -355,13 +355,4 @@ export const organizationRouter = createTRPCRouter({
return { success: true };
}),
active: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.session.activeOrganizationId) {
return null;
}
return await db.query.organization.findFirst({
where: eq(organization.id, ctx.session.activeOrganizationId),
});
}),
});

View File

@@ -149,12 +149,12 @@ export const settingsRouter = createTRPCRouter({
// Check if port 8080 is already in use before enabling dashboard
const portCheck = await checkPortInUse(8080, input.serverId);
if (portCheck.isInUse) {
const conflictInfo = portCheck.conflictingContainer
? ` by ${portCheck.conflictingContainer}`
const conflictingContainer = portCheck.conflictingContainer
? ` by container "${portCheck.conflictingContainer}"`
: "";
throw new TRPCError({
code: "CONFLICT",
message: `Port 8080 is already in use${conflictInfo}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
});
}
newPorts.push({

View File

@@ -101,16 +101,6 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
session: protectedProcedure.query(async ({ ctx }) => {
return {
user: {
id: ctx.user.id,
},
session: {
activeOrganizationId: ctx.session.activeOrganizationId,
},
};
}),
get: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(

View File

@@ -54,13 +54,13 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
WITH recent_metrics AS (
SELECT metrics_json
FROM container_metrics
WHERE container_name = ? OR container_name LIKE ?
WHERE container_name = ?
ORDER BY timestamp DESC
LIMIT ?
)
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
`
rows, err := db.Query(query, containerName, containerName+".%", limit)
rows, err := db.Query(query, containerName, limit)
if err != nil {
return nil, err
}
@@ -90,12 +90,12 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
WITH recent_metrics AS (
SELECT metrics_json
FROM container_metrics
WHERE container_name = ? OR container_name LIKE ?
WHERE container_name = ?
ORDER BY timestamp DESC
)
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
`
rows, err := db.Query(query, containerName, containerName+".%")
rows, err := db.Query(query, containerName)
if err != nil {
return nil, err
}

View File

@@ -2,24 +2,8 @@ import path from "node:path";
import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION;
export const DOCKER_HOST = process.env.DOCKER_HOST;
export const DOCKER_PORT = process.env.DOCKER_PORT
? Number(process.env.DOCKER_PORT)
: undefined;
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const docker = new Docker({
...(DOCKER_API_VERSION && {
version: DOCKER_API_VERSION,
}),
...(DOCKER_HOST && {
host: DOCKER_HOST,
}),
...(DOCKER_PORT && {
port: DOCKER_PORT,
}),
});
export const docker = new Docker();
// When not set, use the legacy default so 2FA remains working for users who
// enabled it before BETTER_AUTH_SECRET was introduced .

View File

@@ -365,13 +365,12 @@ const createSchema = createInsertSchema(applications, {
previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional().optional(),
watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
enableSubmodules: z.boolean().optional(),
});
export const apiCreateApplication = createSchema.pick({
@@ -434,13 +433,13 @@ export const apiSaveGithubProvider = createSchema
owner: true,
buildPath: true,
githubId: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.extend({
triggerType: z.enum(["push", "tag"]).default("push"),
})
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
});
export const apiSaveGitlabProvider = createSchema
.pick({
@@ -452,9 +451,10 @@ export const apiSaveGitlabProvider = createSchema
gitlabId: true,
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
.required();
export const apiSaveBitbucketProvider = createSchema
.pick({
@@ -465,9 +465,10 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketRepositorySlug: true,
bitbucketId: true,
applicationId: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
.required();
export const apiSaveGiteaProvider = createSchema
.pick({
@@ -477,9 +478,10 @@ export const apiSaveGiteaProvider = createSchema
giteaOwner: true,
giteaRepository: true,
giteaId: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
.required();
export const apiSaveDockerProvider = createSchema
.pick({
@@ -504,7 +506,6 @@ export const apiSaveGitProvider = createSchema
.merge(
createSchema.pick({
customGitSSHKeyId: true,
enableSubmodules: true,
}),
);

View File

@@ -99,15 +99,17 @@ const createSchema = createInsertSchema(mounts, {
mountPath: z.string().min(1),
mountId: z.string().optional(),
filePath: z.string().optional(),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
serviceType: z
.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
])
.default("application"),
});
export type ServiceType = NonNullable<

View File

@@ -135,25 +135,15 @@ export const getTrustedOrigins = async () => {
if (trustedOriginsCache && now < trustedOriginsCache.expiresAt) {
return trustedOriginsCache.data;
}
try {
const trustedOrigins = await runQuery();
trustedOriginsCache = {
data: trustedOrigins,
expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS,
};
return trustedOrigins;
} catch (error) {
console.error("Failed to fetch trusted origins:", error);
return trustedOriginsCache?.data ?? [];
}
const trustedOrigins = await runQuery();
trustedOriginsCache = {
data: trustedOrigins,
expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS,
};
return trustedOrigins;
}
try {
return await runQuery();
} catch (error) {
console.error("Failed to fetch trusted origins:", error);
return [];
}
return runQuery();
};
export const getTrustedProviders = async () => {

View File

@@ -117,12 +117,12 @@ export const createDeployment = async (
>,
) => {
const application = await findApplicationById(deployment.applicationId);
await removeLastTenDeployments(
deployment.applicationId,
"application",
application.serverId,
);
try {
await removeLastTenDeployments(
deployment.applicationId,
"application",
application.serverId,
);
const serverId = application.buildServerId || application.serverId;
const { LOGS_PATH } = paths(!!serverId);
@@ -200,12 +200,13 @@ export const createDeploymentPreview = async (
const previewDeployment = await findPreviewDeploymentById(
deployment.previewDeploymentId,
);
await removeLastTenDeployments(
deployment.previewDeploymentId,
"previewDeployment",
previewDeployment?.application?.serverId,
);
try {
await removeLastTenDeployments(
deployment.previewDeploymentId,
"previewDeployment",
previewDeployment?.application?.serverId,
);
const appName = `${previewDeployment.appName}`;
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
@@ -280,12 +281,12 @@ export const createDeploymentCompose = async (
>,
) => {
const compose = await findComposeById(deployment.composeId);
await removeLastTenDeployments(
deployment.composeId,
"compose",
compose.serverId,
);
try {
await removeLastTenDeployments(
deployment.composeId,
"compose",
compose.serverId,
);
const { LOGS_PATH } = paths(!!compose.serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${compose.appName}-${formattedDateTime}.log`;
@@ -368,8 +369,8 @@ export const createDeploymentBackup = async (
} else if (backup.backupType === "compose") {
serverId = backup.compose?.serverId;
}
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
try {
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
const { LOGS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${backup.appName}-${formattedDateTime}.log`;
@@ -438,12 +439,12 @@ export const createDeploymentSchedule = async (
) => {
const schedule = await findScheduleById(deployment.scheduleId);
const serverId =
schedule.application?.serverId ||
schedule.compose?.serverId ||
schedule.server?.serverId;
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
try {
const serverId =
schedule.application?.serverId ||
schedule.compose?.serverId ||
schedule.server?.serverId;
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
const { SCHEDULES_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
@@ -514,14 +515,14 @@ export const createDeploymentVolumeBackup = async (
) => {
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
const serverId =
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
await removeLastTenDeployments(
deployment.volumeBackupId,
"volumeBackup",
serverId,
);
try {
const serverId =
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
await removeLastTenDeployments(
deployment.volumeBackupId,
"volumeBackup",
serverId,
);
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
@@ -600,23 +601,24 @@ export const removeDeployment = async (deploymentId: string) => {
.then((result) => result[0]);
if (!deployment) {
return null;
throw new TRPCError({
code: "BAD_REQUEST",
message: "Deployment not found",
});
}
const logPath = path.join(deployment.logPath);
if (logPath && logPath !== ".") {
const command = `rm -f ${logPath};`;
if (deployment.serverId) {
await execAsyncRemote(deployment.serverId, command);
} else {
await execAsync(command);
}
const command = `
rm -f ${deployment.logPath};
`;
if (deployment.serverId) {
await execAsyncRemote(deployment.serverId, command);
} else {
await execAsync(command);
}
return deployment;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error removing the deployment";
error instanceof Error ? error.message : "Error creating the deployment";
throw new TRPCError({
code: "BAD_REQUEST",
message,
@@ -684,49 +686,34 @@ const removeLastTenDeployments = async (
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
try {
const logPath = path.join(oldDeployment.logPath);
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
if (logPath && logPath !== ".") {
command += `rm -rf ${logPath};`;
}
await removeDeployment(oldDeployment.deploymentId);
} catch (err) {
console.error(
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
err,
);
const logPath = path.join(oldDeployment.logPath);
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
if (logPath !== ".") {
command += `
rm -rf ${logPath};
`;
}
await removeDeployment(oldDeployment.deploymentId);
}
if (command) {
await execAsyncRemote(serverId, command);
}
await execAsyncRemote(serverId, command);
} else {
for (const oldDeployment of deploymentsToDelete) {
try {
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
const logPath = path.join(oldDeployment.logPath);
if (
logPath &&
logPath !== "." &&
existsSync(logPath) &&
!oldDeployment.errorMessage
) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
} catch (err) {
console.error(
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
err,
);
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
const logPath = path.join(oldDeployment.logPath);
if (
existsSync(logPath) &&
!oldDeployment.errorMessage &&
logPath !== "."
) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
}

View File

@@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string {
return `'${s.replace(/'/g, `'\\''`)}'`;
}
export function safeDockerLoginCommand(
function safeDockerLoginCommand(
registry: string | undefined,
user: string | undefined,
pass: string | undefined,

View File

@@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import { type Registry, safeDockerLoginCommand } from "./registry";
import type { Registry } from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
@@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
if (serverId) {
await execAsyncRemote(serverId, command);
await execAsyncRemote(command, serverId);
} else {
await execAsync(command);
}
@@ -171,23 +171,6 @@ export const rollback = async (rollbackId: string) => {
);
};
const dockerLoginForRegistry = async (
registry: Registry,
serverId?: string | null,
) => {
const loginCommand = safeDockerLoginCommand(
registry.registryUrl,
registry.username,
registry.password,
);
if (serverId) {
await execAsyncRemote(serverId, loginCommand);
} else {
await execAsync(loginCommand);
}
};
const rollbackApplication = async (
appName: string,
image: string,
@@ -205,14 +188,6 @@ const rollbackApplication = async (
throw new Error("Full context is required for rollback");
}
// Ensure Docker daemon is authenticated with the rollback registry
// before updating the swarm service. The authconfig in CreateServiceOptions
// alone is not sufficient — Docker Swarm also relies on the daemon's
// cached credentials (~/.docker/config.json) to distribute auth to nodes.
if (fullContext.rollbackRegistry) {
await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId);
}
const docker = await getRemoteDocker(serverId);
// Use the same configuration as mechanizeDockerContainer

View File

@@ -413,38 +413,17 @@ export const checkPortInUse = async (
serverId?: string,
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
try {
// Check if port is in use by a Docker container
const dockerCommand = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
const { stdout: dockerOut } = serverId
? await execAsyncRemote(serverId, dockerCommand)
: await execAsync(dockerCommand);
const command = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
const container = dockerOut.trim();
const container = stdout.trim();
if (container) {
return {
isInUse: true,
conflictingContainer: `container "${container}"`,
};
}
// Check if port is in use by a host-level service (non-Docker)
// Dokploy runs inside a container, so we spawn an ephemeral container
// with --net=host to share the host's network stack and use nc -z to
// check if something is listening on the port
const hostCommand = `docker run --rm --net=host busybox sh -c 'nc -z 0.0.0.0 ${port} 2>/dev/null && echo in_use || echo free'`;
const { stdout: hostOut } = serverId
? await execAsyncRemote(serverId, hostCommand)
: await execAsync(hostCommand);
if (hostOut.includes("in_use")) {
return {
isInUse: true,
conflictingContainer: "a host-level service",
};
}
return { isInUse: false };
return {
isInUse: !!container,
conflictingContainer: container || undefined,
};
} catch (error) {
console.error("Error checking port availability:", error);
return { isInUse: false };

View File

@@ -30,18 +30,6 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
baseURL: config.apiUrl,
});
case "azure":
// Azure OpenAI-compatible endpoints already include /v1 in the path.
// Using createAzure with such URLs causes a doubled /v1//v1/ suffix.
if (config.apiUrl.includes("/v1")) {
return createOpenAICompatible({
name: "azure",
baseURL: config.apiUrl,
headers: {
"api-key": config.apiKey,
Authorization: `Bearer ${config.apiKey}`,
},
});
}
return createAzure({
apiKey: config.apiKey,
baseURL: config.apiUrl,

View File

@@ -14,14 +14,13 @@ export const runComposeBackup = async (
compose: Compose,
backup: BackupSchedule,
) => {
const { environmentId, name, appName } = compose;
const { environmentId, name } = compose;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix, databaseType, serviceName } = backup;
const { prefix, databaseType } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
title: "Compose Backup",

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
@@ -10,7 +11,7 @@ import { startLogCleanup } from "../access-log/handler";
import { cleanupAll } from "../docker/utils";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils";
import { getS3Credentials, scheduleBackup } from "./utils";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");
@@ -106,20 +107,6 @@ export const initCronJobs = async () => {
}
};
const getServiceAppName = (backup: BackupSchedule): string => {
if (backup.compose?.appName) {
return backup.serviceName
? `${backup.compose.appName}_${backup.serviceName}`
: backup.compose.appName;
}
const serviceAppName =
backup.postgres?.appName ||
backup.mysql?.appName ||
backup.mariadb?.appName ||
backup.mongo?.appName;
return serviceAppName || backup.appName;
};
export const keepLatestNBackups = async (
backup: BackupSchedule,
serverId?: string | null,
@@ -130,16 +117,18 @@ export const keepLatestNBackups = async (
try {
const rcloneFlags = getS3Credentials(backup.destination);
const appName = getServiceAppName(backup);
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
const backupFilesPath = path.join(
`:s3:${backup.destination.bucket}`,
backup.prefix,
);
// --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
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}{}
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}/{}
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}/{}`;
const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`;

View File

@@ -14,13 +14,13 @@ export const runMariadbBackup = async (
mariadb: Mariadb,
backup: BackupSchedule,
) => {
const { environmentId, name, appName } = mariadb;
const { environmentId, name } = mariadb;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
title: "MariaDB Backup",

View File

@@ -11,13 +11,13 @@ import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { environmentId, name, appName } = mongo;
const { environmentId, name } = mongo;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
title: "MongoDB Backup",

View File

@@ -11,13 +11,13 @@ import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { environmentId, name, appName } = mysql;
const { environmentId, name } = mysql;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
title: "MySQL Backup",

View File

@@ -14,7 +14,7 @@ export const runPostgresBackup = async (
postgres: Postgres,
backup: BackupSchedule,
) => {
const { name, environmentId, appName } = postgres;
const { name, environmentId } = postgres;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
@@ -26,7 +26,7 @@ export const runPostgresBackup = async (
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;

View File

@@ -31,7 +31,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const { BASE_PATH } = paths();
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
const backupFileName = `webserver-backup-${timestamp}.zip`;
const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
try {
await execAsync(`mkdir -p ${tempDir}/filesystem`);
@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await execAsync(cleanupCommand);
await execAsync(
`rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`,
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
);
writeStream.write("Copied filesystem to temp directory\n");

View File

@@ -53,7 +53,7 @@ Compose Type: ${composeType} ✅`;
cd "${projectPath}";
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""}
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}

View File

@@ -18,9 +18,7 @@ export const randomizeComposeFile = async (
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = parse(composeFile, {
maxAliasCount: 10000,
}) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const randomSuffix = suffix || generateRandomHash();

View File

@@ -63,9 +63,7 @@ export const loadDockerCompose = async (
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
const parsedConfig = parse(yamlStr, {
maxAliasCount: 10000,
}) as ComposeSpecification;
const parsedConfig = parse(yamlStr) as ComposeSpecification;
return parsedConfig;
}
return null;
@@ -88,9 +86,7 @@ export const loadDockerComposeRemote = async (
return null;
}
if (!stdout) return null;
const parsedConfig = parse(stdout, {
maxAliasCount: 10000,
}) as ComposeSpecification;
const parsedConfig = parse(stdout) as ComposeSpecification;
return parsedConfig;
} catch {
return null;

View File

@@ -211,10 +211,7 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
});
}
const baseUrl = (provider.giteaInternalUrl || provider.giteaUrl).replace(
/\/+$/,
"",
);
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
// Use /user/repos to get authenticated user's repositories with pagination
let allRepos = 0;
@@ -271,9 +268,7 @@ export const getGiteaRepositories = async (giteaId?: string) => {
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
const baseUrl = (
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl
).replace(/\/+$/, "");
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
// Use /user/repos to get authenticated user's repositories with pagination
let allRepositories: any[] = [];
@@ -338,9 +333,7 @@ export const getGiteaBranches = async (input: {
const giteaProvider = await findGiteaById(input.giteaId);
const baseUrl = (
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl
).replace(/\/+$/, "");
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
// Handle pagination for branches
let allBranches: any[] = [];

View File

@@ -214,13 +214,10 @@ export const getGitlabBranches = async (input: {
const allBranches = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
const baseUrl = (
gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl
).replace(/\/+$/, "");
while (true) {
const branchesResponse = await fetch(
`${baseUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
@@ -295,13 +292,10 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
const allProjects = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
const baseUrl = (
gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl
).replace(/\/+$/, "");
while (true) {
const response = await fetch(
`${baseUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,

View File

@@ -69,7 +69,6 @@ export const restoreComposeBackup = async (
},
restoreType: composeType,
rcloneCommand,
backupFile: backupInput.backupFile,
});
emit("Starting restore...");

View File

@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
databaseUser: string,
databasePassword: string,
) => {
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive --drop"`;
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
};
export const getComposeSearchCommand = (

View File

@@ -152,13 +152,16 @@ export const createRouterConfig = async (
}
if ((entryPoint === "websecure" && https) || !https) {
// redirects - skip for preview deployments as wildcard subdomains
// should not inherit parent redirect rules (e.g., www-redirect)
if (domain.domainType !== "preview") {
for (const redirect of redirects) {
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
routerConfig.middlewares?.push(middlewareName);
// redirects
for (const redirect of redirects) {
let middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
if (domain.domainType === "preview") {
middlewareName = `redirect-${appName.replace(
/^preview-(.+)-[^-]+$/,
"$1",
)}-${redirect.uniqueConfigKey}`;
}
routerConfig.middlewares?.push(middlewareName);
}
// security

View File

@@ -4,24 +4,6 @@ import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
export const getVolumeServiceAppName = (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
): string => {
if (volumeBackup.compose?.appName) {
return volumeBackup.serviceName
? `${volumeBackup.compose.appName}_${volumeBackup.serviceName}`
: volumeBackup.compose.appName;
}
const serviceAppName =
volumeBackup.application?.appName ||
volumeBackup.postgres?.appName ||
volumeBackup.mysql?.appName ||
volumeBackup.mariadb?.appName ||
volumeBackup.mongo?.appName ||
volumeBackup.redis?.appName;
return serviceAppName || volumeBackup.appName;
};
export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
) => {
@@ -30,9 +12,8 @@ export const backupVolume = async (
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
const destination = volumeBackup.destination;
const s3AppName = getVolumeServiceAppName(volumeBackup);
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const rcloneFlags = getS3Credentials(volumeBackup.destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);

View File

@@ -12,7 +12,7 @@ import {
import { scheduledJobs, scheduleJob } from "node-schedule";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
import { backupVolume, getVolumeServiceAppName } from "./backup";
import { backupVolume } from "./backup";
// Helper functions to extract project info from volume backup
const getProjectName = (
@@ -81,9 +81,9 @@ const cleanupOldVolumeBackups = async (
try {
const rcloneFlags = getS3Credentials(destination);
const s3AppName = getVolumeServiceAppName(volumeBackup);
const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`;
const normalizedPrefix = normalizeS3Path(prefix);
const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`;
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
@@ -131,21 +131,14 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
? "mongodb"
: volumeBackup.serviceType;
try {
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "success",
organizationId,
});
} catch (notificationError) {
console.error(
"Failed to send volume backup success notification",
notificationError,
);
}
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "success",
organizationId,
});
} catch (error) {
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
const volumeBackupPath = path.join(
@@ -167,21 +160,14 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
? "mongodb"
: volumeBackup.serviceType;
try {
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "error",
organizationId,
errorMessage: error instanceof Error ? error.message : String(error),
});
} catch (notificationError) {
console.error(
"Failed to send volume backup error notification",
notificationError,
);
}
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "error",
organizationId,
errorMessage: error instanceof Error ? error.message : String(error),
});
}
};

48
pnpm-lock.yaml generated
View File

@@ -131,12 +131,9 @@ importers:
'@codemirror/legacy-modes':
specifier: 6.4.0
version: 6.4.0
'@codemirror/search':
specifier: ^6.6.0
version: 6.6.0
'@codemirror/view':
specifier: ^6.39.15
version: 6.39.15
specifier: 6.29.0
version: 6.29.0
'@dokploy/server':
specifier: workspace:*
version: link:../../packages/server
@@ -244,10 +241,10 @@ importers:
version: 11.10.0(typescript@5.9.3)
'@uiw/codemirror-theme-github':
specifier: ^4.23.12
version: 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)
version: 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)
'@uiw/react-codemirror':
specifier: ^4.23.12
version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.29.0)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@xterm/addon-attach':
specifier: 0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -1288,6 +1285,9 @@ packages:
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
'@codemirror/view@6.29.0':
resolution: {integrity: sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==}
'@codemirror/view@6.39.15':
resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
@@ -8793,14 +8793,14 @@ snapshots:
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
'@lezer/common': 1.5.1
'@codemirror/commands@6.10.2':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
'@lezer/common': 1.5.1
'@codemirror/lang-json@6.0.2':
@@ -8821,7 +8821,7 @@ snapshots:
'@codemirror/language@6.12.1':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
'@lezer/common': 1.5.1
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
@@ -8851,9 +8851,15 @@ snapshots:
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
'@lezer/highlight': 1.2.3
'@codemirror/view@6.29.0':
dependencies:
'@codemirror/state': 6.5.4
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@codemirror/view@6.39.15':
dependencies:
'@codemirror/state': 6.5.4
@@ -12088,7 +12094,7 @@ snapshots:
dependencies:
'@types/node': 24.10.13
'@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)':
'@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/commands': 6.10.2
@@ -12096,30 +12102,30 @@ snapshots:
'@codemirror/lint': 6.9.4
'@codemirror/search': 6.6.0
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
'@uiw/codemirror-theme-github@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)':
'@uiw/codemirror-theme-github@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)':
dependencies:
'@uiw/codemirror-themes': 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)
'@uiw/codemirror-themes': 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)
transitivePeerDependencies:
- '@codemirror/language'
- '@codemirror/state'
- '@codemirror/view'
'@uiw/codemirror-themes@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)':
'@uiw/codemirror-themes@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
'@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
'@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.29.0)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.28.6
'@codemirror/commands': 6.10.2
'@codemirror/state': 6.5.4
'@codemirror/theme-one-dark': 6.1.3
'@codemirror/view': 6.39.15
'@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)
'@codemirror/view': 6.29.0
'@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)
codemirror: 6.0.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -12766,7 +12772,7 @@ snapshots:
'@codemirror/lint': 6.9.4
'@codemirror/search': 6.6.0
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@codemirror/view': 6.29.0
color-convert@2.0.1:
dependencies: