mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'Dokploy:canary' into feat/quick-service-switcher
This commit is contained in:
@@ -23,7 +23,6 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { StatusTooltip } from "../shared/status-tooltip";
|
import { StatusTooltip } from "../shared/status-tooltip";
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ export const SearchCommand = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = api.user.session.useQuery();
|
||||||
const { data } = api.project.all.useQuery(undefined, {
|
const { data } = api.project.all.useQuery(undefined, {
|
||||||
enabled: !!session,
|
enabled: !!session,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||||
|
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = api.user.session.useQuery();
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
@@ -99,8 +98,8 @@ export const AddGithubProvider = () => {
|
|||||||
<form
|
<form
|
||||||
action={
|
action={
|
||||||
isOrganization
|
isOrganization
|
||||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
? `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}`
|
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
||||||
}
|
}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const ShowUsers = () => {
|
|||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = api.user.session.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ function SidebarLogo() {
|
|||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = api.user.session.useQuery();
|
||||||
const {
|
const {
|
||||||
data: organizations,
|
data: organizations,
|
||||||
refetch,
|
refetch,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { yaml } from "@codemirror/lang-yaml";
|
|||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { search, searchKeymap } from "@codemirror/search";
|
||||||
|
import { EditorView, keymap } from "@codemirror/view";
|
||||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
@@ -155,6 +156,8 @@ export const CodeEditor = ({
|
|||||||
}}
|
}}
|
||||||
theme={resolvedTheme === "dark" ? githubDark : githubLight}
|
theme={resolvedTheme === "dark" ? githubDark : githubLight}
|
||||||
extensions={[
|
extensions={[
|
||||||
|
search(),
|
||||||
|
keymap.of(searchKeymap),
|
||||||
language === "yaml"
|
language === "yaml"
|
||||||
? yaml()
|
? yaml()
|
||||||
: language === "json"
|
: language === "json"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.28.3",
|
"version": "v0.28.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -53,7 +53,8 @@
|
|||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/language": "^6.11.0",
|
"@codemirror/language": "^6.11.0",
|
||||||
"@codemirror/legacy-modes": "6.4.0",
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
"@codemirror/view": "6.29.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
|
"@codemirror/view": "^6.39.15",
|
||||||
"@dokploy/server": "workspace:*",
|
"@dokploy/server": "workspace:*",
|
||||||
"@dokploy/trpc-openapi": "0.0.17",
|
"@dokploy/trpc-openapi": "0.0.17",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
|
|||||||
@@ -10,22 +10,29 @@ type Query = {
|
|||||||
state: string;
|
state: string;
|
||||||
installation_id: string;
|
installation_id: string;
|
||||||
setup_action: string;
|
setup_action: string;
|
||||||
userId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const { code, state, installation_id, userId }: Query = req.query as Query;
|
const { code, state, installation_id }: Query = req.query as Query;
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return res.status(400).json({ error: "Missing code parameter" });
|
return res.status(400).json({ error: "Missing code parameter" });
|
||||||
}
|
}
|
||||||
const [action, value] = state?.split(":");
|
const [action, ...rest] = state?.split(":");
|
||||||
// Value could be the organizationId or the githubProviderId
|
// For gh_init: rest[0] = organizationId, rest[1] = userId
|
||||||
|
// For gh_setup: rest[0] = githubProviderId
|
||||||
|
|
||||||
if (action === "gh_init") {
|
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 octokit = new Octokit({});
|
||||||
const { data } = await octokit.request(
|
const { data } = await octokit.request(
|
||||||
"POST /app-manifests/{code}/conversions",
|
"POST /app-manifests/{code}/conversions",
|
||||||
@@ -44,7 +51,7 @@ export default async function handler(
|
|||||||
githubWebhookSecret: data.webhook_secret,
|
githubWebhookSecret: data.webhook_secret,
|
||||||
githubPrivateKey: data.pem,
|
githubPrivateKey: data.pem,
|
||||||
},
|
},
|
||||||
value as string,
|
organizationId as string,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
} else if (action === "gh_setup") {
|
} else if (action === "gh_setup") {
|
||||||
@@ -53,7 +60,7 @@ export default async function handler(
|
|||||||
.set({
|
.set({
|
||||||
githubInstallationId: installation_id,
|
githubInstallationId: installation_id,
|
||||||
})
|
})
|
||||||
.where(eq(github.githubId, value as string))
|
.where(eq(github.githubId, rest[0] as string))
|
||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -777,7 +777,7 @@ const EnvironmentPage = (
|
|||||||
}
|
}
|
||||||
if (success > 0) {
|
if (success > 0) {
|
||||||
toast.success(
|
toast.success(
|
||||||
`${success} service${success !== 1 ? "s" : ""} deployed successfully`,
|
`${success} service${success !== 1 ? "s" : ""} queued for deployment`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
|
|||||||
@@ -149,12 +149,12 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
// Check if port 8080 is already in use before enabling dashboard
|
// Check if port 8080 is already in use before enabling dashboard
|
||||||
const portCheck = await checkPortInUse(8080, input.serverId);
|
const portCheck = await checkPortInUse(8080, input.serverId);
|
||||||
if (portCheck.isInUse) {
|
if (portCheck.isInUse) {
|
||||||
const conflictingContainer = portCheck.conflictingContainer
|
const conflictInfo = portCheck.conflictingContainer
|
||||||
? ` by container "${portCheck.conflictingContainer}"`
|
? ` by ${portCheck.conflictingContainer}`
|
||||||
: "";
|
: "";
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "CONFLICT",
|
code: "CONFLICT",
|
||||||
message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
|
message: `Port 8080 is already in use${conflictInfo}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
newPorts.push({
|
newPorts.push({
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return memberResult;
|
return memberResult;
|
||||||
}),
|
}),
|
||||||
|
session: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
activeOrganizationId: ctx.session.activeOrganizationId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
get: protectedProcedure.query(async ({ ctx }) => {
|
get: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const memberResult = await db.query.member.findFirst({
|
const memberResult = await db.query.member.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
|
|||||||
WITH recent_metrics AS (
|
WITH recent_metrics AS (
|
||||||
SELECT metrics_json
|
SELECT metrics_json
|
||||||
FROM container_metrics
|
FROM container_metrics
|
||||||
WHERE container_name = ?
|
WHERE container_name = ? OR container_name LIKE ?
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
)
|
)
|
||||||
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
|
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
|
||||||
`
|
`
|
||||||
rows, err := db.Query(query, containerName, limit)
|
rows, err := db.Query(query, containerName, containerName+".%", limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -90,12 +90,12 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
|
|||||||
WITH recent_metrics AS (
|
WITH recent_metrics AS (
|
||||||
SELECT metrics_json
|
SELECT metrics_json
|
||||||
FROM container_metrics
|
FROM container_metrics
|
||||||
WHERE container_name = ?
|
WHERE container_name = ? OR container_name LIKE ?
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
)
|
)
|
||||||
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
|
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
|
||||||
`
|
`
|
||||||
rows, err := db.Query(query, containerName)
|
rows, err := db.Query(query, containerName, containerName+".%")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,24 @@ import path from "node:path";
|
|||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
|
|
||||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
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 CLEANUP_CRON_JOB = "50 23 * * *";
|
||||||
export const docker = new Docker();
|
export const docker = new Docker({
|
||||||
|
...(DOCKER_API_VERSION && {
|
||||||
|
version: DOCKER_API_VERSION,
|
||||||
|
}),
|
||||||
|
...(DOCKER_HOST && {
|
||||||
|
host: DOCKER_HOST,
|
||||||
|
}),
|
||||||
|
...(DOCKER_PORT && {
|
||||||
|
port: DOCKER_PORT,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
// When not set, use the legacy default so 2FA remains working for users who
|
// When not set, use the legacy default so 2FA remains working for users who
|
||||||
// enabled it before BETTER_AUTH_SECRET was introduced .
|
// enabled it before BETTER_AUTH_SECRET was introduced .
|
||||||
|
|||||||
@@ -365,12 +365,13 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewPath: z.string().optional(),
|
previewPath: z.string().optional(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional().optional(),
|
||||||
previewLabels: z.array(z.string()).optional(),
|
previewLabels: z.array(z.string()).optional(),
|
||||||
cleanCache: z.boolean().optional(),
|
cleanCache: z.boolean().optional(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||||
|
enableSubmodules: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema.pick({
|
||||||
@@ -433,13 +434,13 @@ export const apiSaveGithubProvider = createSchema
|
|||||||
owner: true,
|
owner: true,
|
||||||
buildPath: true,
|
buildPath: true,
|
||||||
githubId: true,
|
githubId: true,
|
||||||
watchPaths: true,
|
|
||||||
enableSubmodules: true,
|
|
||||||
})
|
})
|
||||||
.required()
|
.required()
|
||||||
.extend({
|
.extend({
|
||||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||||
});
|
})
|
||||||
|
.required()
|
||||||
|
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||||
|
|
||||||
export const apiSaveGitlabProvider = createSchema
|
export const apiSaveGitlabProvider = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
@@ -451,10 +452,9 @@ export const apiSaveGitlabProvider = createSchema
|
|||||||
gitlabId: true,
|
gitlabId: true,
|
||||||
gitlabProjectId: true,
|
gitlabProjectId: true,
|
||||||
gitlabPathNamespace: true,
|
gitlabPathNamespace: true,
|
||||||
watchPaths: true,
|
|
||||||
enableSubmodules: true,
|
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||||
|
|
||||||
export const apiSaveBitbucketProvider = createSchema
|
export const apiSaveBitbucketProvider = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
@@ -465,10 +465,9 @@ export const apiSaveBitbucketProvider = createSchema
|
|||||||
bitbucketRepositorySlug: true,
|
bitbucketRepositorySlug: true,
|
||||||
bitbucketId: true,
|
bitbucketId: true,
|
||||||
applicationId: true,
|
applicationId: true,
|
||||||
watchPaths: true,
|
|
||||||
enableSubmodules: true,
|
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||||
|
|
||||||
export const apiSaveGiteaProvider = createSchema
|
export const apiSaveGiteaProvider = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
@@ -478,10 +477,9 @@ export const apiSaveGiteaProvider = createSchema
|
|||||||
giteaOwner: true,
|
giteaOwner: true,
|
||||||
giteaRepository: true,
|
giteaRepository: true,
|
||||||
giteaId: true,
|
giteaId: true,
|
||||||
watchPaths: true,
|
|
||||||
enableSubmodules: true,
|
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||||
|
|
||||||
export const apiSaveDockerProvider = createSchema
|
export const apiSaveDockerProvider = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
@@ -506,6 +504,7 @@ export const apiSaveGitProvider = createSchema
|
|||||||
.merge(
|
.merge(
|
||||||
createSchema.pick({
|
createSchema.pick({
|
||||||
customGitSSHKeyId: true,
|
customGitSSHKeyId: true,
|
||||||
|
enableSubmodules: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -135,15 +135,25 @@ export const getTrustedOrigins = async () => {
|
|||||||
if (trustedOriginsCache && now < trustedOriginsCache.expiresAt) {
|
if (trustedOriginsCache && now < trustedOriginsCache.expiresAt) {
|
||||||
return trustedOriginsCache.data;
|
return trustedOriginsCache.data;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const trustedOrigins = await runQuery();
|
const trustedOrigins = await runQuery();
|
||||||
trustedOriginsCache = {
|
trustedOriginsCache = {
|
||||||
data: trustedOrigins,
|
data: trustedOrigins,
|
||||||
expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS,
|
expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS,
|
||||||
};
|
};
|
||||||
return trustedOrigins;
|
return trustedOrigins;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch trusted origins:", error);
|
||||||
|
return trustedOriginsCache?.data ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return runQuery();
|
try {
|
||||||
|
return await runQuery();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch trusted origins:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTrustedProviders = async () => {
|
export const getTrustedProviders = async () => {
|
||||||
|
|||||||
@@ -117,12 +117,12 @@ export const createDeployment = async (
|
|||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const application = await findApplicationById(deployment.applicationId);
|
const application = await findApplicationById(deployment.applicationId);
|
||||||
try {
|
|
||||||
await removeLastTenDeployments(
|
await removeLastTenDeployments(
|
||||||
deployment.applicationId,
|
deployment.applicationId,
|
||||||
"application",
|
"application",
|
||||||
application.serverId,
|
application.serverId,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
const serverId = application.buildServerId || application.serverId;
|
const serverId = application.buildServerId || application.serverId;
|
||||||
|
|
||||||
const { LOGS_PATH } = paths(!!serverId);
|
const { LOGS_PATH } = paths(!!serverId);
|
||||||
@@ -200,13 +200,12 @@ export const createDeploymentPreview = async (
|
|||||||
const previewDeployment = await findPreviewDeploymentById(
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
deployment.previewDeploymentId,
|
deployment.previewDeploymentId,
|
||||||
);
|
);
|
||||||
try {
|
|
||||||
await removeLastTenDeployments(
|
await removeLastTenDeployments(
|
||||||
deployment.previewDeploymentId,
|
deployment.previewDeploymentId,
|
||||||
"previewDeployment",
|
"previewDeployment",
|
||||||
previewDeployment?.application?.serverId,
|
previewDeployment?.application?.serverId,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
const appName = `${previewDeployment.appName}`;
|
const appName = `${previewDeployment.appName}`;
|
||||||
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
@@ -281,12 +280,12 @@ export const createDeploymentCompose = async (
|
|||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const compose = await findComposeById(deployment.composeId);
|
const compose = await findComposeById(deployment.composeId);
|
||||||
try {
|
|
||||||
await removeLastTenDeployments(
|
await removeLastTenDeployments(
|
||||||
deployment.composeId,
|
deployment.composeId,
|
||||||
"compose",
|
"compose",
|
||||||
compose.serverId,
|
compose.serverId,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
const fileName = `${compose.appName}-${formattedDateTime}.log`;
|
const fileName = `${compose.appName}-${formattedDateTime}.log`;
|
||||||
@@ -369,8 +368,8 @@ export const createDeploymentBackup = async (
|
|||||||
} else if (backup.backupType === "compose") {
|
} else if (backup.backupType === "compose") {
|
||||||
serverId = backup.compose?.serverId;
|
serverId = backup.compose?.serverId;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
||||||
|
try {
|
||||||
const { LOGS_PATH } = paths(!!serverId);
|
const { LOGS_PATH } = paths(!!serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
const fileName = `${backup.appName}-${formattedDateTime}.log`;
|
const fileName = `${backup.appName}-${formattedDateTime}.log`;
|
||||||
@@ -439,12 +438,12 @@ export const createDeploymentSchedule = async (
|
|||||||
) => {
|
) => {
|
||||||
const schedule = await findScheduleById(deployment.scheduleId);
|
const schedule = await findScheduleById(deployment.scheduleId);
|
||||||
|
|
||||||
try {
|
|
||||||
const serverId =
|
const serverId =
|
||||||
schedule.application?.serverId ||
|
schedule.application?.serverId ||
|
||||||
schedule.compose?.serverId ||
|
schedule.compose?.serverId ||
|
||||||
schedule.server?.serverId;
|
schedule.server?.serverId;
|
||||||
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
|
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
|
||||||
|
try {
|
||||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
||||||
@@ -515,7 +514,6 @@ export const createDeploymentVolumeBackup = async (
|
|||||||
) => {
|
) => {
|
||||||
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
|
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
|
||||||
|
|
||||||
try {
|
|
||||||
const serverId =
|
const serverId =
|
||||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||||
await removeLastTenDeployments(
|
await removeLastTenDeployments(
|
||||||
@@ -523,6 +521,7 @@ export const createDeploymentVolumeBackup = async (
|
|||||||
"volumeBackup",
|
"volumeBackup",
|
||||||
serverId,
|
serverId,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
|
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
|
||||||
@@ -601,24 +600,23 @@ export const removeDeployment = async (deploymentId: string) => {
|
|||||||
.then((result) => result[0]);
|
.then((result) => result[0]);
|
||||||
|
|
||||||
if (!deployment) {
|
if (!deployment) {
|
||||||
throw new TRPCError({
|
return null;
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Deployment not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const command = `
|
|
||||||
rm -f ${deployment.logPath};
|
const logPath = path.join(deployment.logPath);
|
||||||
`;
|
if (logPath && logPath !== ".") {
|
||||||
|
const command = `rm -f ${logPath};`;
|
||||||
if (deployment.serverId) {
|
if (deployment.serverId) {
|
||||||
await execAsyncRemote(deployment.serverId, command);
|
await execAsyncRemote(deployment.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(command);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return deployment;
|
return deployment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error creating the deployment";
|
error instanceof Error ? error.message : "Error removing the deployment";
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message,
|
message,
|
||||||
@@ -686,34 +684,49 @@ const removeLastTenDeployments = async (
|
|||||||
if (serverId) {
|
if (serverId) {
|
||||||
let command = "";
|
let command = "";
|
||||||
for (const oldDeployment of deploymentsToDelete) {
|
for (const oldDeployment of deploymentsToDelete) {
|
||||||
|
try {
|
||||||
const logPath = path.join(oldDeployment.logPath);
|
const logPath = path.join(oldDeployment.logPath);
|
||||||
if (oldDeployment.rollbackId) {
|
if (oldDeployment.rollbackId) {
|
||||||
await removeRollbackById(oldDeployment.rollbackId);
|
await removeRollbackById(oldDeployment.rollbackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logPath !== ".") {
|
if (logPath && logPath !== ".") {
|
||||||
command += `
|
command += `rm -rf ${logPath};`;
|
||||||
rm -rf ${logPath};
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
await removeDeployment(oldDeployment.deploymentId);
|
await removeDeployment(oldDeployment.deploymentId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command) {
|
||||||
await execAsyncRemote(serverId, command);
|
await execAsyncRemote(serverId, command);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const oldDeployment of deploymentsToDelete) {
|
for (const oldDeployment of deploymentsToDelete) {
|
||||||
|
try {
|
||||||
if (oldDeployment.rollbackId) {
|
if (oldDeployment.rollbackId) {
|
||||||
await removeRollbackById(oldDeployment.rollbackId);
|
await removeRollbackById(oldDeployment.rollbackId);
|
||||||
}
|
}
|
||||||
const logPath = path.join(oldDeployment.logPath);
|
const logPath = path.join(oldDeployment.logPath);
|
||||||
if (
|
if (
|
||||||
|
logPath &&
|
||||||
|
logPath !== "." &&
|
||||||
existsSync(logPath) &&
|
existsSync(logPath) &&
|
||||||
!oldDeployment.errorMessage &&
|
!oldDeployment.errorMessage
|
||||||
logPath !== "."
|
|
||||||
) {
|
) {
|
||||||
await fsPromises.unlink(logPath);
|
await fsPromises.unlink(logPath);
|
||||||
}
|
}
|
||||||
await removeDeployment(oldDeployment.deploymentId);
|
await removeDeployment(oldDeployment.deploymentId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string {
|
|||||||
return `'${s.replace(/'/g, `'\\''`)}'`;
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeDockerLoginCommand(
|
export function safeDockerLoginCommand(
|
||||||
registry: string | undefined,
|
registry: string | undefined,
|
||||||
user: string | undefined,
|
user: string | undefined,
|
||||||
pass: string | undefined,
|
pass: string | undefined,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment";
|
|||||||
import type { Mount } from "./mount";
|
import type { Mount } from "./mount";
|
||||||
import type { Port } from "./port";
|
import type { Port } from "./port";
|
||||||
import type { Project } from "./project";
|
import type { Project } from "./project";
|
||||||
import type { Registry } from "./registry";
|
import { type Registry, safeDockerLoginCommand } from "./registry";
|
||||||
|
|
||||||
export const createRollback = async (
|
export const createRollback = async (
|
||||||
input: z.infer<typeof createRollbackSchema>,
|
input: z.infer<typeof createRollbackSchema>,
|
||||||
@@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
|||||||
const command = `docker image rm ${image} --force`;
|
const command = `docker image rm ${image} --force`;
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(command, serverId);
|
await execAsyncRemote(serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(command);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,23 @@ 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 (
|
const rollbackApplication = async (
|
||||||
appName: string,
|
appName: string,
|
||||||
image: string,
|
image: string,
|
||||||
@@ -188,6 +205,14 @@ const rollbackApplication = async (
|
|||||||
throw new Error("Full context is required for rollback");
|
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);
|
const docker = await getRemoteDocker(serverId);
|
||||||
|
|
||||||
// Use the same configuration as mechanizeDockerContainer
|
// Use the same configuration as mechanizeDockerContainer
|
||||||
|
|||||||
@@ -413,17 +413,38 @@ export const checkPortInUse = async (
|
|||||||
serverId?: string,
|
serverId?: string,
|
||||||
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
|
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
|
||||||
try {
|
try {
|
||||||
const command = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
|
// Check if port is in use by a Docker container
|
||||||
const { stdout } = serverId
|
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`;
|
||||||
? await execAsyncRemote(serverId, command)
|
const { stdout: dockerOut } = serverId
|
||||||
: await execAsync(command);
|
? await execAsyncRemote(serverId, dockerCommand)
|
||||||
|
: await execAsync(dockerCommand);
|
||||||
|
|
||||||
const container = stdout.trim();
|
const container = dockerOut.trim();
|
||||||
|
|
||||||
|
if (container) {
|
||||||
return {
|
return {
|
||||||
isInUse: !!container,
|
isInUse: true,
|
||||||
conflictingContainer: container || undefined,
|
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 };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking port availability:", error);
|
console.error("Error checking port availability:", error);
|
||||||
return { isInUse: false };
|
return { isInUse: false };
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
|||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
||||||
});
|
});
|
||||||
case "azure":
|
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({
|
return createAzure({
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ export const runComposeBackup = async (
|
|||||||
compose: Compose,
|
compose: Compose,
|
||||||
backup: BackupSchedule,
|
backup: BackupSchedule,
|
||||||
) => {
|
) => {
|
||||||
const { environmentId, name } = compose;
|
const { environmentId, name, appName } = compose;
|
||||||
const environment = await findEnvironmentById(environmentId);
|
const environment = await findEnvironmentById(environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
const { prefix, databaseType } = backup;
|
const { prefix, databaseType, serviceName } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
|
||||||
|
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
const deployment = await createDeploymentBackup({
|
const deployment = await createDeploymentBackup({
|
||||||
backupId: backup.backupId,
|
backupId: backup.backupId,
|
||||||
title: "Compose Backup",
|
title: "Compose Backup",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
|
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
|
||||||
import { member } from "@dokploy/server/db/schema";
|
import { member } from "@dokploy/server/db/schema";
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
@@ -11,7 +10,7 @@ import { startLogCleanup } from "../access-log/handler";
|
|||||||
import { cleanupAll } from "../docker/utils";
|
import { cleanupAll } from "../docker/utils";
|
||||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils";
|
||||||
|
|
||||||
export const initCronJobs = async () => {
|
export const initCronJobs = async () => {
|
||||||
console.log("Setting up cron jobs....");
|
console.log("Setting up cron jobs....");
|
||||||
@@ -107,6 +106,20 @@ 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 (
|
export const keepLatestNBackups = async (
|
||||||
backup: BackupSchedule,
|
backup: BackupSchedule,
|
||||||
serverId?: string | null,
|
serverId?: string | null,
|
||||||
@@ -117,18 +130,16 @@ export const keepLatestNBackups = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(backup.destination);
|
const rcloneFlags = getS3Credentials(backup.destination);
|
||||||
const backupFilesPath = path.join(
|
const appName = getServiceAppName(backup);
|
||||||
`:s3:${backup.destination.bucket}`,
|
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
|
||||||
backup.prefix,
|
|
||||||
);
|
|
||||||
|
|
||||||
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
// --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}`;
|
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
|
// 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{}`;
|
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
|
||||||
// this command deletes the files
|
// this command deletes the files
|
||||||
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}/{}
|
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}{}
|
||||||
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}/{}`;
|
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
|
||||||
|
|
||||||
const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`;
|
const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`;
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ export const runMariadbBackup = async (
|
|||||||
mariadb: Mariadb,
|
mariadb: Mariadb,
|
||||||
backup: BackupSchedule,
|
backup: BackupSchedule,
|
||||||
) => {
|
) => {
|
||||||
const { environmentId, name } = mariadb;
|
const { environmentId, name, appName } = mariadb;
|
||||||
const environment = await findEnvironmentById(environmentId);
|
const environment = await findEnvironmentById(environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
const { prefix } = backup;
|
const { prefix } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
const deployment = await createDeploymentBackup({
|
const deployment = await createDeploymentBackup({
|
||||||
backupId: backup.backupId,
|
backupId: backup.backupId,
|
||||||
title: "MariaDB Backup",
|
title: "MariaDB Backup",
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|||||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
|
|
||||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||||
const { environmentId, name } = mongo;
|
const { environmentId, name, appName } = mongo;
|
||||||
const environment = await findEnvironmentById(environmentId);
|
const environment = await findEnvironmentById(environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
const { prefix } = backup;
|
const { prefix } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
const deployment = await createDeploymentBackup({
|
const deployment = await createDeploymentBackup({
|
||||||
backupId: backup.backupId,
|
backupId: backup.backupId,
|
||||||
title: "MongoDB Backup",
|
title: "MongoDB Backup",
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|||||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
|
|
||||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||||
const { environmentId, name } = mysql;
|
const { environmentId, name, appName } = mysql;
|
||||||
const environment = await findEnvironmentById(environmentId);
|
const environment = await findEnvironmentById(environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
const { prefix } = backup;
|
const { prefix } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
const deployment = await createDeploymentBackup({
|
const deployment = await createDeploymentBackup({
|
||||||
backupId: backup.backupId,
|
backupId: backup.backupId,
|
||||||
title: "MySQL Backup",
|
title: "MySQL Backup",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const runPostgresBackup = async (
|
|||||||
postgres: Postgres,
|
postgres: Postgres,
|
||||||
backup: BackupSchedule,
|
backup: BackupSchedule,
|
||||||
) => {
|
) => {
|
||||||
const { name, environmentId } = postgres;
|
const { name, environmentId, appName } = postgres;
|
||||||
const environment = await findEnvironmentById(environmentId);
|
const environment = await findEnvironmentById(environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export const runPostgresBackup = async (
|
|||||||
const { prefix } = backup;
|
const { prefix } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
const { BASE_PATH } = paths();
|
const { BASE_PATH } = paths();
|
||||||
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
||||||
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
||||||
const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
|
const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(`mkdir -p ${tempDir}/filesystem`);
|
await execAsync(`mkdir -p ${tempDir}/filesystem`);
|
||||||
@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
await execAsync(cleanupCommand);
|
await execAsync(cleanupCommand);
|
||||||
|
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
`rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||||
);
|
);
|
||||||
|
|
||||||
writeStream.write("Copied filesystem to temp directory\n");
|
writeStream.write("Copied filesystem to temp directory\n");
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Compose Type: ${composeType} ✅`;
|
|||||||
|
|
||||||
cd "${projectPath}";
|
cd "${projectPath}";
|
||||||
|
|
||||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""}
|
||||||
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export const randomizeComposeFile = async (
|
|||||||
) => {
|
) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const composeFile = compose.composeFile;
|
const composeFile = compose.composeFile;
|
||||||
const composeData = parse(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile, {
|
||||||
|
maxAliasCount: 10000,
|
||||||
|
}) as ComposeSpecification;
|
||||||
|
|
||||||
const randomSuffix = suffix || generateRandomHash();
|
const randomSuffix = suffix || generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export const loadDockerCompose = async (
|
|||||||
|
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
const yamlStr = readFileSync(path, "utf8");
|
const yamlStr = readFileSync(path, "utf8");
|
||||||
const parsedConfig = parse(yamlStr) as ComposeSpecification;
|
const parsedConfig = parse(yamlStr, {
|
||||||
|
maxAliasCount: 10000,
|
||||||
|
}) as ComposeSpecification;
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -86,7 +88,9 @@ export const loadDockerComposeRemote = async (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!stdout) return null;
|
if (!stdout) return null;
|
||||||
const parsedConfig = parse(stdout) as ComposeSpecification;
|
const parsedConfig = parse(stdout, {
|
||||||
|
maxAliasCount: 10000,
|
||||||
|
}) as ComposeSpecification;
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -211,7 +211,10 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
const baseUrl = (provider.giteaInternalUrl || provider.giteaUrl).replace(
|
||||||
|
/\/+$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
// Use /user/repos to get authenticated user's repositories with pagination
|
// Use /user/repos to get authenticated user's repositories with pagination
|
||||||
let allRepos = 0;
|
let allRepos = 0;
|
||||||
@@ -268,7 +271,9 @@ export const getGiteaRepositories = async (giteaId?: string) => {
|
|||||||
await refreshGiteaToken(giteaId);
|
await refreshGiteaToken(giteaId);
|
||||||
const giteaProvider = await findGiteaById(giteaId);
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
|
||||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
const baseUrl = (
|
||||||
|
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl
|
||||||
|
).replace(/\/+$/, "");
|
||||||
|
|
||||||
// Use /user/repos to get authenticated user's repositories with pagination
|
// Use /user/repos to get authenticated user's repositories with pagination
|
||||||
let allRepositories: any[] = [];
|
let allRepositories: any[] = [];
|
||||||
@@ -333,7 +338,9 @@ export const getGiteaBranches = async (input: {
|
|||||||
|
|
||||||
const giteaProvider = await findGiteaById(input.giteaId);
|
const giteaProvider = await findGiteaById(input.giteaId);
|
||||||
|
|
||||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
const baseUrl = (
|
||||||
|
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl
|
||||||
|
).replace(/\/+$/, "");
|
||||||
|
|
||||||
// Handle pagination for branches
|
// Handle pagination for branches
|
||||||
let allBranches: any[] = [];
|
let allBranches: any[] = [];
|
||||||
|
|||||||
@@ -214,10 +214,13 @@ export const getGitlabBranches = async (input: {
|
|||||||
const allBranches = [];
|
const allBranches = [];
|
||||||
let page = 1;
|
let page = 1;
|
||||||
const perPage = 100; // GitLab's max per page is 100
|
const perPage = 100; // GitLab's max per page is 100
|
||||||
|
const baseUrl = (
|
||||||
|
gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl
|
||||||
|
).replace(/\/+$/, "");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const branchesResponse = await fetch(
|
const branchesResponse = await fetch(
|
||||||
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
|
`${baseUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||||
@@ -292,10 +295,13 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
|||||||
const allProjects = [];
|
const allProjects = [];
|
||||||
let page = 1;
|
let page = 1;
|
||||||
const perPage = 100; // GitLab's max per page is 100
|
const perPage = 100; // GitLab's max per page is 100
|
||||||
|
const baseUrl = (
|
||||||
|
gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl
|
||||||
|
).replace(/\/+$/, "");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
|
`${baseUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export const restoreComposeBackup = async (
|
|||||||
},
|
},
|
||||||
restoreType: composeType,
|
restoreType: composeType,
|
||||||
rcloneCommand,
|
rcloneCommand,
|
||||||
|
backupFile: backupInput.backupFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
emit("Starting restore...");
|
emit("Starting restore...");
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
|
|||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive --drop"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getComposeSearchCommand = (
|
export const getComposeSearchCommand = (
|
||||||
|
|||||||
@@ -152,17 +152,14 @@ export const createRouterConfig = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((entryPoint === "websecure" && https) || !https) {
|
if ((entryPoint === "websecure" && https) || !https) {
|
||||||
// redirects
|
// 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) {
|
for (const redirect of redirects) {
|
||||||
let middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
|
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
|
||||||
if (domain.domainType === "preview") {
|
|
||||||
middlewareName = `redirect-${appName.replace(
|
|
||||||
/^preview-(.+)-[^-]+$/,
|
|
||||||
"$1",
|
|
||||||
)}-${redirect.uniqueConfigKey}`;
|
|
||||||
}
|
|
||||||
routerConfig.middlewares?.push(middlewareName);
|
routerConfig.middlewares?.push(middlewareName);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// security
|
// security
|
||||||
if (security.length > 0) {
|
if (security.length > 0) {
|
||||||
|
|||||||
@@ -4,6 +4,24 @@ import { findComposeById } from "@dokploy/server/services/compose";
|
|||||||
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
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 (
|
export const backupVolume = async (
|
||||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||||
) => {
|
) => {
|
||||||
@@ -12,8 +30,9 @@ export const backupVolume = async (
|
|||||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||||
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
|
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
|
||||||
const destination = volumeBackup.destination;
|
const destination = volumeBackup.destination;
|
||||||
|
const s3AppName = getVolumeServiceAppName(volumeBackup);
|
||||||
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
||||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`;
|
||||||
const rcloneFlags = getS3Credentials(volumeBackup.destination);
|
const rcloneFlags = getS3Credentials(volumeBackup.destination);
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);
|
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||||
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
|
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
|
||||||
import { backupVolume } from "./backup";
|
import { backupVolume, getVolumeServiceAppName } from "./backup";
|
||||||
|
|
||||||
// Helper functions to extract project info from volume backup
|
// Helper functions to extract project info from volume backup
|
||||||
const getProjectName = (
|
const getProjectName = (
|
||||||
@@ -81,9 +81,9 @@ const cleanupOldVolumeBackups = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const normalizedPrefix = normalizeS3Path(prefix);
|
const s3AppName = getVolumeServiceAppName(volumeBackup);
|
||||||
const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`;
|
const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`;
|
||||||
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`;
|
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`;
|
||||||
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
|
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
|
||||||
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
|
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
|
||||||
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
|
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
|
||||||
@@ -131,6 +131,7 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
|||||||
? "mongodb"
|
? "mongodb"
|
||||||
: volumeBackup.serviceType;
|
: volumeBackup.serviceType;
|
||||||
|
|
||||||
|
try {
|
||||||
await sendVolumeBackupNotifications({
|
await sendVolumeBackupNotifications({
|
||||||
projectName,
|
projectName,
|
||||||
applicationName: volumeBackup.name,
|
applicationName: volumeBackup.name,
|
||||||
@@ -139,6 +140,12 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
organizationId,
|
organizationId,
|
||||||
});
|
});
|
||||||
|
} catch (notificationError) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send volume backup success notification",
|
||||||
|
notificationError,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||||
const volumeBackupPath = path.join(
|
const volumeBackupPath = path.join(
|
||||||
@@ -160,6 +167,7 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
|||||||
? "mongodb"
|
? "mongodb"
|
||||||
: volumeBackup.serviceType;
|
: volumeBackup.serviceType;
|
||||||
|
|
||||||
|
try {
|
||||||
await sendVolumeBackupNotifications({
|
await sendVolumeBackupNotifications({
|
||||||
projectName,
|
projectName,
|
||||||
applicationName: volumeBackup.name,
|
applicationName: volumeBackup.name,
|
||||||
@@ -169,5 +177,11 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
|||||||
organizationId,
|
organizationId,
|
||||||
errorMessage: error instanceof Error ? error.message : String(error),
|
errorMessage: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
} catch (notificationError) {
|
||||||
|
console.error(
|
||||||
|
"Failed to send volume backup error notification",
|
||||||
|
notificationError,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -131,9 +131,12 @@ importers:
|
|||||||
'@codemirror/legacy-modes':
|
'@codemirror/legacy-modes':
|
||||||
specifier: 6.4.0
|
specifier: 6.4.0
|
||||||
version: 6.4.0
|
version: 6.4.0
|
||||||
|
'@codemirror/search':
|
||||||
|
specifier: ^6.6.0
|
||||||
|
version: 6.6.0
|
||||||
'@codemirror/view':
|
'@codemirror/view':
|
||||||
specifier: 6.29.0
|
specifier: ^6.39.15
|
||||||
version: 6.29.0
|
version: 6.39.15
|
||||||
'@dokploy/server':
|
'@dokploy/server':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/server
|
version: link:../../packages/server
|
||||||
@@ -241,10 +244,10 @@ importers:
|
|||||||
version: 11.10.0(typescript@5.9.3)
|
version: 11.10.0(typescript@5.9.3)
|
||||||
'@uiw/codemirror-theme-github':
|
'@uiw/codemirror-theme-github':
|
||||||
specifier: ^4.23.12
|
specifier: ^4.23.12
|
||||||
version: 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)
|
version: 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)
|
||||||
'@uiw/react-codemirror':
|
'@uiw/react-codemirror':
|
||||||
specifier: ^4.23.12
|
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.29.0)(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.39.15)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@xterm/addon-attach':
|
'@xterm/addon-attach':
|
||||||
specifier: 0.10.0
|
specifier: 0.10.0
|
||||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||||
@@ -1285,9 +1288,6 @@ packages:
|
|||||||
'@codemirror/theme-one-dark@6.1.3':
|
'@codemirror/theme-one-dark@6.1.3':
|
||||||
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
|
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
|
||||||
|
|
||||||
'@codemirror/view@6.29.0':
|
|
||||||
resolution: {integrity: sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==}
|
|
||||||
|
|
||||||
'@codemirror/view@6.39.15':
|
'@codemirror/view@6.39.15':
|
||||||
resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
|
resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
|
||||||
|
|
||||||
@@ -8793,14 +8793,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.12.1
|
'@codemirror/language': 6.12.1
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@codemirror/view': 6.39.15
|
||||||
'@lezer/common': 1.5.1
|
'@lezer/common': 1.5.1
|
||||||
|
|
||||||
'@codemirror/commands@6.10.2':
|
'@codemirror/commands@6.10.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.12.1
|
'@codemirror/language': 6.12.1
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@codemirror/view': 6.39.15
|
||||||
'@lezer/common': 1.5.1
|
'@lezer/common': 1.5.1
|
||||||
|
|
||||||
'@codemirror/lang-json@6.0.2':
|
'@codemirror/lang-json@6.0.2':
|
||||||
@@ -8821,7 +8821,7 @@ snapshots:
|
|||||||
'@codemirror/language@6.12.1':
|
'@codemirror/language@6.12.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@codemirror/view': 6.39.15
|
||||||
'@lezer/common': 1.5.1
|
'@lezer/common': 1.5.1
|
||||||
'@lezer/highlight': 1.2.3
|
'@lezer/highlight': 1.2.3
|
||||||
'@lezer/lr': 1.4.8
|
'@lezer/lr': 1.4.8
|
||||||
@@ -8851,15 +8851,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.12.1
|
'@codemirror/language': 6.12.1
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@codemirror/view': 6.39.15
|
||||||
'@lezer/highlight': 1.2.3
|
'@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':
|
'@codemirror/view@6.39.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
@@ -12094,7 +12088,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.13
|
'@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.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.39.15)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete': 6.20.0
|
'@codemirror/autocomplete': 6.20.0
|
||||||
'@codemirror/commands': 6.10.2
|
'@codemirror/commands': 6.10.2
|
||||||
@@ -12102,30 +12096,30 @@ snapshots:
|
|||||||
'@codemirror/lint': 6.9.4
|
'@codemirror/lint': 6.9.4
|
||||||
'@codemirror/search': 6.6.0
|
'@codemirror/search': 6.6.0
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@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)':
|
'@uiw/codemirror-theme-github@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@uiw/codemirror-themes': 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)
|
'@uiw/codemirror-themes': 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@codemirror/language'
|
- '@codemirror/language'
|
||||||
- '@codemirror/state'
|
- '@codemirror/state'
|
||||||
- '@codemirror/view'
|
- '@codemirror/view'
|
||||||
|
|
||||||
'@uiw/codemirror-themes@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)':
|
'@uiw/codemirror-themes@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/language': 6.12.1
|
'@codemirror/language': 6.12.1
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@codemirror/view': 6.39.15
|
||||||
|
|
||||||
'@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)':
|
'@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)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.28.6
|
||||||
'@codemirror/commands': 6.10.2
|
'@codemirror/commands': 6.10.2
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/theme-one-dark': 6.1.3
|
'@codemirror/theme-one-dark': 6.1.3
|
||||||
'@codemirror/view': 6.29.0
|
'@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)
|
'@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: 6.0.2
|
codemirror: 6.0.2
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
@@ -12772,7 +12766,7 @@ snapshots:
|
|||||||
'@codemirror/lint': 6.9.4
|
'@codemirror/lint': 6.9.4
|
||||||
'@codemirror/search': 6.6.0
|
'@codemirror/search': 6.6.0
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.29.0
|
'@codemirror/view': 6.39.15
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user