mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
23 Commits
1888-docke
...
v0.22.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe163dd33 | ||
|
|
19b56771b8 | ||
|
|
cff01ed438 | ||
|
|
10fa3c8cf1 | ||
|
|
6c5497ed21 | ||
|
|
380656efee | ||
|
|
c64d2245ce | ||
|
|
a985998b93 | ||
|
|
4f3ba16dfa | ||
|
|
6c788429f1 | ||
|
|
3176a9d7e3 | ||
|
|
94a6a9587e | ||
|
|
911681f389 | ||
|
|
5992688e85 | ||
|
|
425061e481 | ||
|
|
08c0bf8a21 | ||
|
|
64a2c9e0a1 | ||
|
|
21e46f5382 | ||
|
|
52b2158309 | ||
|
|
178d84d438 | ||
|
|
80016b57a8 | ||
|
|
d4032f34bf | ||
|
|
5156b45ffc |
@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
ARG NIXPACKS_VERSION=1.35.0
|
ARG NIXPACKS_VERSION=1.39.0
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
@@ -63,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash
|
|||||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD [ "pnpm", "start" ]
|
CMD [ "pnpm", "start" ]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ COPY . /usr/src/app
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
{templateInfo.template.envs.map((env, index) => (
|
{templateInfo.template.envs.map((env, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="rounded-lg border bg-card p-2 font-mono text-sm"
|
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
|
||||||
>
|
>
|
||||||
{env}
|
{env}
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
<DialogDescription>Mount File Content</DialogDescription>
|
<DialogDescription>Mount File Content</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<ScrollArea className="h-[25vh] pr-4">
|
<ScrollArea className="h-[45vh] pr-4">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="yaml"
|
language="yaml"
|
||||||
value={selectedMount?.content || ""}
|
value={selectedMount?.content || ""}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const ShowDeployments = ({
|
|||||||
<span>Webhook URL: </span>
|
<span>Webhook URL: </span>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span className="break-all text-muted-foreground">
|
<span className="break-all text-muted-foreground">
|
||||||
{`${url}/api/deploy${type === 'compose' ? '/compose' : ''}/${refreshToken}`}
|
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
|
||||||
</span>
|
</span>
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<RefreshToken id={id} type={type} />
|
<RefreshToken id={id} type={type} />
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules || false,
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: BitbucketProvider) => {
|
const onSubmit = async (data: BitbucketProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
registryURL: data.registryUrl || "",
|
registryURL: data.registryUrl || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules || false,
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GiteaProvider) => {
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GithubProvider) => {
|
const onSubmit = async (data: GithubProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.applicationId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GitlabProvider) => {
|
const onSubmit = async (data: GitlabProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.composeId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: BitbucketProvider) => {
|
const onSubmit = async (data: BitbucketProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.composeId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GiteaProvider) => {
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.composeId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GithubProvider) => {
|
const onSubmit = async (data: GithubProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data?.composeId, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GitlabProvider) => {
|
const onSubmit = async (data: GitlabProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Copy, Loader2 } from "lucide-react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
|
|||||||
api.project.duplicate.useMutation({
|
api.project.duplicate.useMutation({
|
||||||
onSuccess: async (newProject) => {
|
onSuccess: async (newProject) => {
|
||||||
await utils.project.all.invalidate();
|
await utils.project.all.invalidate();
|
||||||
toast.success("Project duplicated successfully");
|
toast.success(
|
||||||
|
duplicateType === "new-project"
|
||||||
|
? "Project duplicated successfully"
|
||||||
|
: "Services duplicated successfully",
|
||||||
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
if (duplicateType === "new-project") {
|
||||||
|
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleDuplicate = async () => {
|
const handleDuplicate = async () => {
|
||||||
if (!name) {
|
if (duplicateType === "new-project" && !name) {
|
||||||
toast.error("Project name is required");
|
toast.error("Project name is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
|
|||||||
id: service.id,
|
id: service.id,
|
||||||
type: service.type,
|
type: service.type,
|
||||||
})),
|
})),
|
||||||
|
duplicateInSameProject: duplicateType === "same-project",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
|
|||||||
// Reset form when closing
|
// Reset form when closing
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setDuplicateType("new-project");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Duplicate Project</DialogTitle>
|
<DialogTitle>Duplicate Services</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Create a new project with the selected services
|
Choose where to duplicate the selected services
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="name">Name</Label>
|
<Label>Duplicate to</Label>
|
||||||
<Input
|
<RadioGroup
|
||||||
id="name"
|
value={duplicateType}
|
||||||
value={name}
|
onValueChange={setDuplicateType}
|
||||||
onChange={(e) => setName(e.target.value)}
|
className="grid gap-2"
|
||||||
placeholder="New project name"
|
>
|
||||||
/>
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="new-project" id="new-project" />
|
||||||
|
<Label htmlFor="new-project">New project</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="same-project" id="same-project" />
|
||||||
|
<Label htmlFor="same-project">Same project</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{duplicateType === "new-project" && (
|
||||||
<Label htmlFor="description">Description</Label>
|
<>
|
||||||
<Input
|
<div className="grid gap-2">
|
||||||
id="description"
|
<Label htmlFor="name">Name</Label>
|
||||||
value={description}
|
<Input
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
id="name"
|
||||||
placeholder="Project description (optional)"
|
value={name}
|
||||||
/>
|
onChange={(e) => setName(e.target.value)}
|
||||||
</div>
|
placeholder="New project name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Project description (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Selected services to duplicate</Label>
|
<Label>Selected services to duplicate</Label>
|
||||||
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Duplicating...
|
{duplicateType === "new-project"
|
||||||
|
? "Duplicating project..."
|
||||||
|
: "Duplicating services..."}
|
||||||
</>
|
</>
|
||||||
|
) : duplicateType === "new-project" ? (
|
||||||
|
"Duplicate project"
|
||||||
) : (
|
) : (
|
||||||
"Duplicate"
|
"Duplicate services"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
|
|||||||
remotely.
|
remotely.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
<div>
|
||||||
|
<p className="text-primary text-sm font-medium">
|
||||||
|
You will need to purchase or rent a Virtual Private Server (VPS) to
|
||||||
|
proceed, we recommend to use one of these providers since has been
|
||||||
|
heavily tested.
|
||||||
|
</p>
|
||||||
|
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Hostinger - Get 20% Discount
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href=" https://app.americancloud.com/register?ref=dokploy"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
American Cloud - Get $20 Credits
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://m.do.co/c/db24efd43f35"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
DigitalOcean - Get $200 Credits
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Hetzner - Get €20 Credits
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.vultr.com/?ref=9679828"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Vultr
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.linode.com/es/pricing/#compute-shared"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Linode
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<AlertBlock className="mt-4 px-4">
|
||||||
|
You are free to use whatever provider, but we recommend to use one
|
||||||
|
of the above, to avoid issues.
|
||||||
|
</AlertBlock>
|
||||||
|
</div>
|
||||||
{!canCreateMoreServers && (
|
{!canCreateMoreServers && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You cannot create more servers,{" "}
|
You cannot create more servers,{" "}
|
||||||
|
|||||||
@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
|
|||||||
Hostinger - Get 20% Discount
|
Hostinger - Get 20% Discount
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href=" https://app.americancloud.com/register?ref=dokploy"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
American Cloud - Get $20 Credits
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://m.do.co/c/db24efd43f35"
|
href="https://m.do.co/c/db24efd43f35"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.22.4",
|
"version": "v0.22.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -80,7 +80,11 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
<footer className="mt-auto text-center py-5">
|
<footer className="mt-auto text-center py-5">
|
||||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
<Link href="https://github.com/Dokploy/dokploy/issues" target="_blank" className="underline hover:text-primary transition-colors" >
|
<Link
|
||||||
|
href="https://github.com/Dokploy/dokploy/issues"
|
||||||
|
target="_blank"
|
||||||
|
className="underline hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
Submit Log in issue on Github
|
Submit Log in issue on Github
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const generate = processTemplate(template.config, {
|
const appName = `${projectName}-${generatePassword(6)}`;
|
||||||
|
const config = {
|
||||||
|
...template.config,
|
||||||
|
variables: {
|
||||||
|
APP_NAME: appName,
|
||||||
|
...template.config.variables,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const generate = processTemplate(config, {
|
||||||
serverIp: serverIp,
|
serverIp: serverIp,
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
});
|
});
|
||||||
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: appName,
|
||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedTemplate = processTemplate(config, {
|
const configModified = {
|
||||||
|
...config,
|
||||||
|
variables: {
|
||||||
|
APP_NAME: compose.appName,
|
||||||
|
...config.variables,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedTemplate = processTemplate(configModified, {
|
||||||
serverIp: serverIp,
|
serverIp: serverIp,
|
||||||
projectName: compose.appName,
|
projectName: compose.appName,
|
||||||
});
|
});
|
||||||
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedTemplate = processTemplate(config, {
|
const configModified = {
|
||||||
|
...config,
|
||||||
|
variables: {
|
||||||
|
APP_NAME: compose.appName,
|
||||||
|
...config.variables,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedTemplate = processTemplate(configModified, {
|
||||||
serverIp: serverIp,
|
serverIp: serverIp,
|
||||||
projectName: compose.appName,
|
projectName: compose.appName,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
duplicateInSameProject: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new project
|
// Create new project or use existing one
|
||||||
const newProject = await createProject(
|
const targetProject = input.duplicateInSameProject
|
||||||
{
|
? sourceProject
|
||||||
name: input.name,
|
: await createProject(
|
||||||
description: input.description,
|
{
|
||||||
env: sourceProject.env,
|
name: input.name,
|
||||||
},
|
description: input.description,
|
||||||
ctx.session.activeOrganizationId,
|
env: sourceProject.env,
|
||||||
);
|
},
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
if (input.includeServices) {
|
if (input.includeServices) {
|
||||||
const servicesToDuplicate = input.selectedServices || [];
|
const servicesToDuplicate = input.selectedServices || [];
|
||||||
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const newApplication = await createApplication({
|
const newApplication = await createApplication({
|
||||||
...application,
|
...application,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${application.name} (copy)`
|
||||||
|
: application.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const newPostgres = await createPostgres({
|
const newPostgres = await createPostgres({
|
||||||
...postgres,
|
...postgres,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${postgres.name} (copy)`
|
||||||
|
: postgres.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findMariadbById(id);
|
await findMariadbById(id);
|
||||||
const newMariadb = await createMariadb({
|
const newMariadb = await createMariadb({
|
||||||
...mariadb,
|
...mariadb,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${mariadb.name} (copy)`
|
||||||
|
: mariadb.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findMongoById(id);
|
await findMongoById(id);
|
||||||
const newMongo = await createMongo({
|
const newMongo = await createMongo({
|
||||||
...mongo,
|
...mongo,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${mongo.name} (copy)`
|
||||||
|
: mongo.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findMySqlById(id);
|
await findMySqlById(id);
|
||||||
const newMysql = await createMysql({
|
const newMysql = await createMysql({
|
||||||
...mysql,
|
...mysql,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${mysql.name} (copy)`
|
||||||
|
: mysql.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
const { redisId, mounts, ...redis } = await findRedisById(id);
|
const { redisId, mounts, ...redis } = await findRedisById(id);
|
||||||
const newRedis = await createRedis({
|
const newRedis = await createRedis({
|
||||||
...redis,
|
...redis,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${redis.name} (copy)`
|
||||||
|
: redis.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await findComposeById(id);
|
await findComposeById(id);
|
||||||
const newCompose = await createCompose({
|
const newCompose = await createCompose({
|
||||||
...compose,
|
...compose,
|
||||||
projectId: newProject.projectId,
|
name: input.duplicateInSameProject
|
||||||
|
? `${compose.name} (copy)`
|
||||||
|
: compose.name,
|
||||||
|
projectId: targetProject.projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate selected services
|
// Duplicate selected services
|
||||||
|
|
||||||
for (const service of servicesToDuplicate) {
|
for (const service of servicesToDuplicate) {
|
||||||
await duplicateService(service.id, service.type);
|
await duplicateService(service.id, service.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newProject.projectId,
|
targetProject.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newProject;
|
return targetProject;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
|
|||||||
@@ -356,20 +356,20 @@ const installUtilities = () => `
|
|||||||
|
|
||||||
case "$OS_TYPE" in
|
case "$OS_TYPE" in
|
||||||
arch)
|
arch)
|
||||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
||||||
;;
|
;;
|
||||||
alpine)
|
alpine)
|
||||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||||
apk update >/dev/null
|
apk update >/dev/null
|
||||||
apk add curl wget git jq openssl sudo unzip tar >/dev/null
|
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
|
||||||
;;
|
;;
|
||||||
ubuntu | debian | raspbian)
|
ubuntu | debian | raspbian)
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
|
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||||
;;
|
;;
|
||||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||||
if [ "$OS_TYPE" = "amzn" ]; then
|
if [ "$OS_TYPE" = "amzn" ]; then
|
||||||
dnf install -y wget git jq openssl >/dev/null
|
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||||
else
|
else
|
||||||
if ! command -v dnf >/dev/null; then
|
if ! command -v dnf >/dev/null; then
|
||||||
yum install -y dnf >/dev/null
|
yum install -y dnf >/dev/null
|
||||||
@@ -377,12 +377,12 @@ const installUtilities = () => `
|
|||||||
if ! command -v curl >/dev/null; then
|
if ! command -v curl >/dev/null; then
|
||||||
dnf install -y curl >/dev/null
|
dnf install -y curl >/dev/null
|
||||||
fi
|
fi
|
||||||
dnf install -y wget git jq openssl unzip >/dev/null
|
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
sles | opensuse-leap | opensuse-tumbleweed)
|
sles | opensuse-leap | opensuse-tumbleweed)
|
||||||
zypper refresh >/dev/null
|
zypper refresh >/dev/null
|
||||||
zypper install -y curl wget git jq openssl >/dev/null
|
zypper install -y curl wget git git-lfs jq openssl >/dev/null
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||||
@@ -577,7 +577,7 @@ const installNixpacks = () => `
|
|||||||
if command_exists nixpacks; then
|
if command_exists nixpacks; then
|
||||||
echo "Nixpacks already installed ✅"
|
echo "Nixpacks already installed ✅"
|
||||||
else
|
else
|
||||||
export NIXPACKS_VERSION=1.35.0
|
export NIXPACKS_VERSION=1.39.0
|
||||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { execAsync } from "../process/execAsync";
|
|||||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||||
import { mkdtemp } from "node:fs/promises";
|
import { mkdtemp, rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import {
|
import {
|
||||||
@@ -51,10 +51,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
|
|
||||||
const postgresContainerId = containerId.trim();
|
const postgresContainerId = containerId.trim();
|
||||||
|
|
||||||
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
|
// First dump the database inside the container
|
||||||
|
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
|
||||||
|
writeStream.write(`Running dump command: ${dumpCommand}\n`);
|
||||||
|
await execAsync(dumpCommand);
|
||||||
|
|
||||||
writeStream.write(`Running command: ${postgresCommand}\n`);
|
// Then copy the file from the container to host
|
||||||
await execAsync(postgresCommand);
|
const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
|
||||||
|
writeStream.write(`Copying database dump: ${copyCommand}\n`);
|
||||||
|
await execAsync(copyCommand);
|
||||||
|
|
||||||
|
// Clean up the temp file in the container
|
||||||
|
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
|
||||||
|
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
|
||||||
|
await execAsync(cleanupCommand);
|
||||||
|
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||||
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
await execAsync(`rm -rf ${tempDir}`);
|
try {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error("Cleanup error:", cleanupError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Backup error:", error);
|
console.error("Backup error:", error);
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => {
|
|||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
let envContent = env || "";
|
let envContent = `APP_NAME=${appName}\n`;
|
||||||
|
envContent += env || "";
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||||
}
|
}
|
||||||
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
|||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
|
|
||||||
let envContent = env || "";
|
let envContent = `APP_NAME=${appName}\n`;
|
||||||
|
envContent += env || "";
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,32 +246,16 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
|||||||
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabId);
|
const gitlabProvider = await findGitlabById(gitlabId);
|
||||||
|
|
||||||
const response = await fetch(
|
const allProjects = await validateGitlabProvider(gitlabProvider);
|
||||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const filteredRepos = allProjects.filter((repo: any) => {
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const repositories = await response.json();
|
|
||||||
|
|
||||||
const filteredRepos = repositories.filter((repo: any) => {
|
|
||||||
const { full_path, kind } = repo.namespace;
|
const { full_path, kind } = repo.namespace;
|
||||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
const groupName = gitlabProvider.groupName?.toLowerCase();
|
||||||
|
|
||||||
if (groupName) {
|
if (groupName) {
|
||||||
const isIncluded = groupName
|
const isIncluded = groupName
|
||||||
.split(",")
|
.split(",")
|
||||||
.some((name) => full_path.toLowerCase().includes(name));
|
.some((name) => full_path === name);
|
||||||
|
|
||||||
return isIncluded && kind === "group";
|
return isIncluded && kind === "group";
|
||||||
}
|
}
|
||||||
@@ -432,34 +416,60 @@ export const testGitlabConnection = async (
|
|||||||
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabId);
|
const gitlabProvider = await findGitlabById(gitlabId);
|
||||||
|
|
||||||
const response = await fetch(
|
const repositories = await validateGitlabProvider(gitlabProvider);
|
||||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const repositories = await response.json();
|
|
||||||
|
|
||||||
const filteredRepos = repositories.filter((repo: any) => {
|
const filteredRepos = repositories.filter((repo: any) => {
|
||||||
const { full_path, kind } = repo.namespace;
|
const { full_path, kind } = repo.namespace;
|
||||||
|
|
||||||
if (groupName) {
|
if (groupName) {
|
||||||
return groupName
|
return groupName.split(",").some((name) => full_path === name);
|
||||||
.split(",")
|
|
||||||
.some((name) => full_path.toLowerCase().includes(name));
|
|
||||||
}
|
}
|
||||||
return kind === "user";
|
return kind === "user";
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredRepos.length;
|
return filteredRepos.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
||||||
|
try {
|
||||||
|
const allProjects = [];
|
||||||
|
let page = 1;
|
||||||
|
const perPage = 100; // GitLab's max per page is 100
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = await response.json();
|
||||||
|
|
||||||
|
if (projects.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
allProjects.push(...projects);
|
||||||
|
page++;
|
||||||
|
|
||||||
|
const total = response.headers.get("x-total");
|
||||||
|
if (total && allProjects.length >= Number.parseInt(total)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allProjects;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user