mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into 560-self-hosted-gitlab
This commit is contained in:
@@ -99,14 +99,14 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- refactor/enhancement-languages
|
- fix/nixpacks-version
|
||||||
- build-arm64:
|
- build-arm64:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- refactor/enhancement-languages
|
- fix/nixpacks-version
|
||||||
- combine-manifests:
|
- combine-manifests:
|
||||||
requires:
|
requires:
|
||||||
- build-amd64
|
- build-amd64
|
||||||
@@ -116,4 +116,4 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- refactor/enhancement-languages
|
- fix/nixpacks-version
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ 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.29.1
|
||||||
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 \
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
applicationType: z.literal("compose"),
|
applicationType: z.literal("compose"),
|
||||||
serverId: z.string().min(1),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
applicationId: z.string(),
|
||||||
|
previewDeploymentId: z.string(),
|
||||||
|
titleLog: z.string(),
|
||||||
|
descriptionLog: z.string(),
|
||||||
|
server: z.boolean().optional(),
|
||||||
|
type: z.enum(["deploy"]),
|
||||||
|
applicationType: z.literal("application-preview"),
|
||||||
|
serverId: z.string().min(1),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
deployRemoteApplication,
|
deployRemoteApplication,
|
||||||
deployRemoteCompose,
|
deployRemoteCompose,
|
||||||
|
deployRemotePreviewApplication,
|
||||||
rebuildRemoteApplication,
|
rebuildRemoteApplication,
|
||||||
rebuildRemoteCompose,
|
rebuildRemoteCompose,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
|
updatePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import type { DeployJob } from "./schema";
|
import type { DeployJob } from "./schema";
|
||||||
|
|
||||||
@@ -47,6 +49,20 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (job.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||||
|
previewStatus: "running",
|
||||||
|
});
|
||||||
|
if (job.server) {
|
||||||
|
if (job.type === "deploy") {
|
||||||
|
await deployRemotePreviewApplication({
|
||||||
|
applicationId: job.applicationId,
|
||||||
|
titleLog: job.titleLog,
|
||||||
|
descriptionLog: job.descriptionLog,
|
||||||
|
previewDeploymentId: job.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (job.applicationType === "application") {
|
if (job.applicationType === "application") {
|
||||||
@@ -55,6 +71,10 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
await updateCompose(job.composeId, {
|
await updateCompose(job.composeId, {
|
||||||
composeStatus: "error",
|
composeStatus: "error",
|
||||||
});
|
});
|
||||||
|
} else if (job.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||||
|
previewStatus: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe("createDomainLabels", () => {
|
|||||||
domainId: "",
|
domainId: "",
|
||||||
path: "/",
|
path: "/",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
previewDeploymentId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should create basic labels for web entrypoint", async () => {
|
it("should create basic labels for web entrypoint", async () => {
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ const baseApp: ApplicationNested = {
|
|||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
isPreviewDeploymentsActive: false,
|
||||||
|
previewBuildArgs: null,
|
||||||
|
previewCertificateType: "none",
|
||||||
|
previewEnv: null,
|
||||||
|
previewHttps: false,
|
||||||
|
previewPath: "/",
|
||||||
|
previewPort: 3000,
|
||||||
|
previewLimit: 0,
|
||||||
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
adminId: "",
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
isPreviewDeploymentsActive: false,
|
||||||
|
previewBuildArgs: null,
|
||||||
|
previewCertificateType: "none",
|
||||||
|
previewEnv: null,
|
||||||
|
previewHttps: false,
|
||||||
|
previewPath: "/",
|
||||||
|
previewPort: 3000,
|
||||||
|
previewLimit: 0,
|
||||||
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
adminId: "",
|
||||||
@@ -96,6 +105,7 @@ const baseDomain: Domain = {
|
|||||||
composeId: "",
|
composeId: "",
|
||||||
domainType: "application",
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
|
previewDeploymentId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseRedirect: Redirect = {
|
const baseRedirect: Redirect = {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
refetchInterval: 1000,
|
refetchInterval: 1000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrl(document.location.origin);
|
setUrl(document.location.origin);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex w-full flex-col gap-5 "
|
className="flex w-full flex-col gap-5 "
|
||||||
>
|
>
|
||||||
<Card className="bg-background">
|
<Card className="bg-background p-6">
|
||||||
<Secrets
|
<Secrets
|
||||||
name="env"
|
name="env"
|
||||||
title="Environment Settings"
|
title="Environment Settings"
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { domain } from "@/server/db/validations/domain";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Dices } from "lucide-react";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
previewDeploymentId: string;
|
||||||
|
domainId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddPreviewDomain = ({
|
||||||
|
previewDeploymentId,
|
||||||
|
domainId = "",
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
|
{
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!domainId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: previewDeployment } = api.previewDeployment.one.useQuery(
|
||||||
|
{
|
||||||
|
previewDeploymentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!previewDeploymentId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
|
? api.domain.update.useMutation()
|
||||||
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Domain>({
|
||||||
|
resolver: zodResolver(domain),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
path: data?.path || undefined,
|
||||||
|
port: data?.port || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
form.reset({});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data, isLoading]);
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
|
error: domainId
|
||||||
|
? "Error to update the domain"
|
||||||
|
: "Error to create the domain",
|
||||||
|
submit: domainId ? "Update" : "Create",
|
||||||
|
dialogDescription: domainId
|
||||||
|
? "In this section you can edit a domain"
|
||||||
|
: "In this section you can add domains",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Domain) => {
|
||||||
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
|
previewDeploymentId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(dictionary.success);
|
||||||
|
await utils.previewDeployment.all.invalidate({
|
||||||
|
applicationId: previewDeployment?.applicationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (domainId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(dictionary.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Domain</DialogTitle>
|
||||||
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Host</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="api.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingGenerate}
|
||||||
|
onClick={() => {
|
||||||
|
generateDomain({
|
||||||
|
appName: previewDeployment?.appName || "",
|
||||||
|
serverId:
|
||||||
|
previewDeployment?.application
|
||||||
|
?.serverId || "",
|
||||||
|
})
|
||||||
|
.then((domain) => {
|
||||||
|
field.onChange(domain);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dices className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>Generate traefik.me domain</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Container Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.getValues().https && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Certificate</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Letsencrypt (Default)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||||
|
{dictionary.submit}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deployments: RouterOutputs["deployment"]["all"];
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">View Builds</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Preview Builds</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See all the preview builds for this application on this Pull Request
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{deployments?.map((deployment) => (
|
||||||
|
<div
|
||||||
|
key={deployment.deploymentId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
|
{deployment.status}
|
||||||
|
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment?.status}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.title}
|
||||||
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment.logPath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<ShowDeployment
|
||||||
|
serverId={serverId || ""}
|
||||||
|
open={activeLog !== null}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Pencil, RocketIcon } from "lucide-react";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
import { ShowPreviewBuilds } from "./show-preview-builds";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
|
api.previewDeployment.delete.useMutation();
|
||||||
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
|
api.previewDeployment.all.useQuery(
|
||||||
|
{ applicationId },
|
||||||
|
{
|
||||||
|
enabled: !!applicationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// const [url, setUrl] = React.useState("");
|
||||||
|
// useEffect(() => {
|
||||||
|
// setUrl(document.location.origin);
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl">Preview Deployments</CardTitle>
|
||||||
|
<CardDescription>See all the preview deployments</CardDescription>
|
||||||
|
</div>
|
||||||
|
{data?.isPreviewDeploymentsActive && (
|
||||||
|
<ShowPreviewSettings applicationId={applicationId} />
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{data?.isPreviewDeploymentsActive ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
|
<span>
|
||||||
|
Preview deployments are a way to test your application before it
|
||||||
|
is deployed to production. It will create a new deployment for
|
||||||
|
each pull request you create.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{data?.previewDeployments?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No preview deployments found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{previewDeployments?.map((previewDeployment) => {
|
||||||
|
const { deployments, domain } = previewDeployment;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={previewDeployment?.previewDeploymentId}
|
||||||
|
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between gap-2 max-sm:flex-wrap">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{deployments?.length === 0 ? (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
No deployments found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
|
{previewDeployment?.pullRequestTitle}
|
||||||
|
</span>
|
||||||
|
<StatusTooltip
|
||||||
|
status={previewDeployment.previewStatus}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{previewDeployment?.pullRequestTitle && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="break-all text-sm text-muted-foreground w-fit">
|
||||||
|
Title: {previewDeployment?.pullRequestTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{previewDeployment?.pullRequestURL && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GithubIcon />
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={previewDeployment?.pullRequestURL}
|
||||||
|
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
||||||
|
>
|
||||||
|
Pull Request URL
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col ">
|
||||||
|
<span>Domain </span>
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={`http://${domain?.host}`}
|
||||||
|
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
||||||
|
>
|
||||||
|
{domain?.host}
|
||||||
|
</Link>
|
||||||
|
<AddPreviewDomain
|
||||||
|
previewDeploymentId={
|
||||||
|
previewDeployment.previewDeploymentId
|
||||||
|
}
|
||||||
|
domainId={domain?.domainId}
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Pencil className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</AddPreviewDomain>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
|
||||||
|
{previewDeployment?.createdAt && (
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip
|
||||||
|
date={previewDeployment?.createdAt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ShowPreviewBuilds
|
||||||
|
deployments={previewDeployment?.deployments || []}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShowModalLogs
|
||||||
|
appName={previewDeployment.appName}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
|
<Button variant="outline">View Logs</Button>
|
||||||
|
</ShowModalLogs>
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Preview"
|
||||||
|
description="Are you sure you want to delete this preview?"
|
||||||
|
onClick={() => {
|
||||||
|
deletePreviewDeployment({
|
||||||
|
previewDeploymentId:
|
||||||
|
previewDeployment.previewDeploymentId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetchPreviewDeployments();
|
||||||
|
toast.success("Preview deployment deleted");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="destructive" isLoading={isLoading}>
|
||||||
|
Delete Preview
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Preview deployments are disabled for this application, please
|
||||||
|
enable it
|
||||||
|
</span>
|
||||||
|
<ShowPreviewSettings applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
env: z.string(),
|
||||||
|
buildArgs: z.string(),
|
||||||
|
wildcardDomain: z.string(),
|
||||||
|
port: z.number(),
|
||||||
|
previewLimit: z.number(),
|
||||||
|
previewHttps: z.boolean(),
|
||||||
|
previewPath: z.string(),
|
||||||
|
previewCertificateType: z.enum(["letsencrypt", "none"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
|
const { mutateAsync: updateApplication, isLoading } =
|
||||||
|
api.application.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
env: "",
|
||||||
|
wildcardDomain: "*.traefik.me",
|
||||||
|
port: 3000,
|
||||||
|
previewLimit: 3,
|
||||||
|
previewHttps: false,
|
||||||
|
previewPath: "/",
|
||||||
|
previewCertificateType: "none",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewHttps = form.watch("previewHttps");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||||
|
}, [data?.isPreviewDeploymentsActive]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
env: data.previewEnv || "",
|
||||||
|
buildArgs: data.previewBuildArgs || "",
|
||||||
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
|
port: data.previewPort || 3000,
|
||||||
|
previewLimit: data.previewLimit || 3,
|
||||||
|
previewHttps: data.previewHttps || false,
|
||||||
|
previewPath: data.previewPath || "/",
|
||||||
|
previewCertificateType: data.previewCertificateType || "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
updateApplication({
|
||||||
|
previewEnv: formData.env,
|
||||||
|
previewBuildArgs: formData.buildArgs,
|
||||||
|
previewWildcard: formData.wildcardDomain,
|
||||||
|
previewPort: formData.port,
|
||||||
|
applicationId,
|
||||||
|
previewLimit: formData.previewLimit,
|
||||||
|
previewHttps: formData.previewHttps,
|
||||||
|
previewPath: formData.previewPath,
|
||||||
|
previewCertificateType: formData.previewCertificateType,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Preview Deployments settings updated");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">View Settings</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Adjust the settings for preview deployments of this application,
|
||||||
|
including environment variables, build options, and deployment
|
||||||
|
rules.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="hook-form-delete-application"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="wildcardDomain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Wildcard Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="*.traefik.me" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Preview Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="/" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder="3000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewLimit"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Preview Limit</FormLabel>
|
||||||
|
{/* <FormDescription>
|
||||||
|
Set the limit of preview deployments that can be
|
||||||
|
created for this app.
|
||||||
|
</FormDescription> */}
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder="3000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewHttps"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{previewHttps && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewCertificateType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Certificate</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Letsencrypt (Default)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<div className="flex flex-row items-center justify-between rounded-lg border p-4 col-span-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">
|
||||||
|
Enable preview deployments
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable or disable preview deployments for this
|
||||||
|
application.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isEnabled}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateApplication({
|
||||||
|
isPreviewDeploymentsActive: checked,
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Preview deployments enabled");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="env"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Secrets
|
||||||
|
name="env"
|
||||||
|
title="Environment Settings"
|
||||||
|
description="You can add environment variables to your resource."
|
||||||
|
placeholder={[
|
||||||
|
"NODE_ENV=production",
|
||||||
|
"PORT=3000",
|
||||||
|
].join("\n")}
|
||||||
|
/>
|
||||||
|
{/* <CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
language="properties"
|
||||||
|
wrapperClassName="h-[25rem] font-mono"
|
||||||
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/> */}
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{data?.buildType === "dockerfile" && (
|
||||||
|
<Secrets
|
||||||
|
name="buildArgs"
|
||||||
|
title="Build-time Variables"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Available only at build-time. See documentation
|
||||||
|
<a
|
||||||
|
className="text-primary"
|
||||||
|
href="https://docs.docker.com/build/guide/build-args/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placeholder="NPM_TOKEN=xyz"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-application"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/* */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -33,6 +33,7 @@ import { useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
import { GPUSupport } from "./gpu-support";
|
import { GPUSupport } from "./gpu-support";
|
||||||
|
import { ValidateServer } from "./validate-server";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -90,9 +91,10 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
<TabsList className="grid grid-cols-3 w-[400px]">
|
<TabsList className="grid grid-cols-4 w-[600px]">
|
||||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="validate">Validate</TabsTrigger>
|
||||||
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
@@ -203,7 +205,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
|
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<CardTitle className="text-xl">
|
<CardTitle className="text-xl">
|
||||||
Deployments
|
Deployments
|
||||||
@@ -293,6 +295,14 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="validate"
|
||||||
|
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||||
|
<ValidateServer serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="gpu-setup"
|
value="gpu-setup"
|
||||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2, PcCase, RefreshCw } from "lucide-react";
|
||||||
|
import { StatusRow } from "./gpu-support";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidateServer = ({ serverId }: Props) => {
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const { data, refetch, error, isLoading, isError } =
|
||||||
|
api.server.validate.useQuery(
|
||||||
|
{ serverId },
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PcCase className="size-5" />
|
||||||
|
<CardTitle className="text-xl">Setup Validation</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Check if your server is ready for deployment
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isLoading={isRefreshing}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await refetch();
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="w-full">
|
||||||
|
{error.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground py-4">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
<span>Checking Server Configuration</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid w-full gap-4">
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Status</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Shows the server configuration status
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="Docker Installed"
|
||||||
|
isEnabled={data?.docker?.enabled}
|
||||||
|
description={
|
||||||
|
data?.docker?.enabled
|
||||||
|
? `Installed: ${data?.docker?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="RClone Installed"
|
||||||
|
isEnabled={data?.rclone?.enabled}
|
||||||
|
description={
|
||||||
|
data?.rclone?.enabled
|
||||||
|
? `Installed: ${data?.rclone?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Nixpacks Installed"
|
||||||
|
isEnabled={data?.nixpacks?.enabled}
|
||||||
|
description={
|
||||||
|
data?.nixpacks?.enabled
|
||||||
|
? `Installed: ${data?.nixpacks?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Buildpacks Installed"
|
||||||
|
isEnabled={data?.buildpacks?.enabled}
|
||||||
|
description={
|
||||||
|
data?.buildpacks?.enabled
|
||||||
|
? `Installed: ${data?.buildpacks?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Dokploy Network Installed"
|
||||||
|
isEnabled={data?.isDokployNetworkInstalled}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Swarm Installed"
|
||||||
|
isEnabled={data?.isSwarmInstalled}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Main Directory Created"
|
||||||
|
isEnabled={data?.isMainDirectoryInstalled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,7 +29,7 @@ export const Secrets = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
<CardHeader className="flex flex-row w-full items-center justify-between px-0">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">{props.title}</CardTitle>
|
<CardTitle className="text-xl">{props.title}</CardTitle>
|
||||||
<CardDescription>{props.description}</CardDescription>
|
<CardDescription>{props.description}</CardDescription>
|
||||||
@@ -47,7 +47,7 @@ export const Secrets = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Toggle>
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="w-full space-y-4">
|
<CardContent className="w-full space-y-4 p-0">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
|
|||||||
53
apps/dokploy/drizzle/0049_dark_leopardon.sql
Normal file
53
apps/dokploy/drizzle/0049_dark_leopardon.sql
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
ALTER TYPE "domainType" ADD VALUE 'preview';--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "preview_deployments" (
|
||||||
|
"previewDeploymentId" text PRIMARY KEY NOT NULL,
|
||||||
|
"branch" text NOT NULL,
|
||||||
|
"pullRequestId" text NOT NULL,
|
||||||
|
"pullRequestNumber" text NOT NULL,
|
||||||
|
"pullRequestURL" text NOT NULL,
|
||||||
|
"pullRequestTitle" text NOT NULL,
|
||||||
|
"pullRequestCommentId" text NOT NULL,
|
||||||
|
"previewStatus" "applicationStatus" DEFAULT 'idle' NOT NULL,
|
||||||
|
"appName" text NOT NULL,
|
||||||
|
"applicationId" text NOT NULL,
|
||||||
|
"domainId" text,
|
||||||
|
"createdAt" text NOT NULL,
|
||||||
|
"expiresAt" text,
|
||||||
|
CONSTRAINT "preview_deployments_appName_unique" UNIQUE("appName")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewEnv" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewBuildArgs" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewWildcard" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewPort" integer DEFAULT 3000;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewHttps" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewPath" text DEFAULT '/';--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "certificateType" "certificateType" DEFAULT 'none' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "previewLimit" integer DEFAULT 3;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "isPreviewDeploymentsActive" boolean DEFAULT false;--> statement-breakpoint
|
||||||
|
ALTER TABLE "domain" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "isPreviewDeployment" boolean DEFAULT false;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_domainId_domain_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domain"("domainId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "domain" ADD CONSTRAINT "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "47638eb6-3fb5-4a1e-a0d1-911eb3256976",
|
"id": "db518175-259d-4f4a-b6d0-fe95067bba61",
|
||||||
"prevId": "928417c8-2e7b-43ba-bc19-44b4d70107f1",
|
"prevId": "928417c8-2e7b-43ba-bc19-44b4d70107f1",
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -38,6 +38,67 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"previewEnv": {
|
||||||
|
"name": "previewEnv",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"previewBuildArgs": {
|
||||||
|
"name": "previewBuildArgs",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"previewWildcard": {
|
||||||
|
"name": "previewWildcard",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"previewPort": {
|
||||||
|
"name": "previewPort",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 3000
|
||||||
|
},
|
||||||
|
"previewHttps": {
|
||||||
|
"name": "previewHttps",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"previewPath": {
|
||||||
|
"name": "previewPath",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'/'"
|
||||||
|
},
|
||||||
|
"certificateType": {
|
||||||
|
"name": "certificateType",
|
||||||
|
"type": "certificateType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'none'"
|
||||||
|
},
|
||||||
|
"previewLimit": {
|
||||||
|
"name": "previewLimit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": 3
|
||||||
|
},
|
||||||
|
"isPreviewDeploymentsActive": {
|
||||||
|
"name": "isPreviewDeploymentsActive",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"buildArgs": {
|
"buildArgs": {
|
||||||
"name": "buildArgs",
|
"name": "buildArgs",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1162,6 +1223,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"previewDeploymentId": {
|
||||||
|
"name": "previewDeploymentId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"certificateType": {
|
"certificateType": {
|
||||||
"name": "certificateType",
|
"name": "certificateType",
|
||||||
"type": "certificateType",
|
"type": "certificateType",
|
||||||
@@ -1198,6 +1265,19 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
|
||||||
|
"name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
|
||||||
|
"tableFrom": "domain",
|
||||||
|
"tableTo": "preview_deployments",
|
||||||
|
"columnsFrom": [
|
||||||
|
"previewDeploymentId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"previewDeploymentId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -1967,6 +2047,19 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"isPreviewDeployment": {
|
||||||
|
"name": "isPreviewDeployment",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"previewDeploymentId": {
|
||||||
|
"name": "previewDeploymentId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"name": "createdAt",
|
"name": "createdAt",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -2014,6 +2107,19 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
|
||||||
|
"name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
|
||||||
|
"tableFrom": "deployment",
|
||||||
|
"tableTo": "preview_deployments",
|
||||||
|
"columnsFrom": [
|
||||||
|
"previewDeploymentId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"previewDeploymentId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -3641,13 +3747,6 @@
|
|||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"gitlabUrl": {
|
|
||||||
"name": "gitlabUrl",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'https://gitlab.com'"
|
|
||||||
},
|
|
||||||
"application_id": {
|
"application_id": {
|
||||||
"name": "application_id",
|
"name": "application_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -3828,6 +3927,131 @@
|
|||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"public.preview_deployments": {
|
||||||
|
"name": "preview_deployments",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"previewDeploymentId": {
|
||||||
|
"name": "previewDeploymentId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"branch": {
|
||||||
|
"name": "branch",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pullRequestId": {
|
||||||
|
"name": "pullRequestId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pullRequestNumber": {
|
||||||
|
"name": "pullRequestNumber",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pullRequestURL": {
|
||||||
|
"name": "pullRequestURL",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pullRequestTitle": {
|
||||||
|
"name": "pullRequestTitle",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"pullRequestCommentId": {
|
||||||
|
"name": "pullRequestCommentId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"previewStatus": {
|
||||||
|
"name": "previewStatus",
|
||||||
|
"type": "applicationStatus",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'idle'"
|
||||||
|
},
|
||||||
|
"appName": {
|
||||||
|
"name": "appName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"applicationId": {
|
||||||
|
"name": "applicationId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"domainId": {
|
||||||
|
"name": "domainId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"preview_deployments_applicationId_application_applicationId_fk": {
|
||||||
|
"name": "preview_deployments_applicationId_application_applicationId_fk",
|
||||||
|
"tableFrom": "preview_deployments",
|
||||||
|
"tableTo": "application",
|
||||||
|
"columnsFrom": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"preview_deployments_domainId_domain_domainId_fk": {
|
||||||
|
"name": "preview_deployments_domainId_domain_domainId_fk",
|
||||||
|
"tableFrom": "preview_deployments",
|
||||||
|
"tableTo": "domain",
|
||||||
|
"columnsFrom": [
|
||||||
|
"domainId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"domainId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"preview_deployments_appName_unique": {
|
||||||
|
"name": "preview_deployments_appName_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"appName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -3867,7 +4091,8 @@
|
|||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"compose",
|
"compose",
|
||||||
"application"
|
"application",
|
||||||
|
"preview"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.databaseType": {
|
"public.databaseType": {
|
||||||
|
|||||||
@@ -348,8 +348,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 49,
|
"idx": 49,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1733610324506,
|
"when": 1733628762978,
|
||||||
"tag": "0049_fluffy_expediter",
|
"tag": "0049_dark_leopardon",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.13.1",
|
"version": "v0.14.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"build-next": "next build",
|
"build-next": "next build",
|
||||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||||
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||||
|
"dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json",
|
||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ import { applications, compose, github } from "@/server/db/schema";
|
|||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import {
|
||||||
|
createPreviewDeployment,
|
||||||
|
type Domain,
|
||||||
|
findPreviewDeploymentByApplicationId,
|
||||||
|
findPreviewDeploymentsByPullRequestId,
|
||||||
|
IS_CLOUD,
|
||||||
|
removePreviewDeployment,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
|
import { generateRandomDomain } from "@/templates/utils";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
@@ -53,36 +61,183 @@ export default async function handler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers["x-github-event"] !== "push") {
|
if (
|
||||||
res.status(400).json({ message: "We only accept push events" });
|
req.headers["x-github-event"] !== "push" &&
|
||||||
|
req.headers["x-github-event"] !== "pull_request"
|
||||||
|
) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "We only accept push events or pull_request events" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (req.headers["x-github-event"] === "push") {
|
||||||
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
try {
|
||||||
|
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
||||||
|
const repository = githubBody?.repository?.name;
|
||||||
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
|
const owner = githubBody?.repository?.owner?.name;
|
||||||
|
|
||||||
|
const apps = await db.query.applications.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(applications.sourceType, "github"),
|
||||||
|
eq(applications.autoDeploy, true),
|
||||||
|
eq(applications.branch, branchName),
|
||||||
|
eq(applications.repository, repository),
|
||||||
|
eq(applications.owner, owner),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const app of apps) {
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
titleLog: deploymentTitle,
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "application",
|
||||||
|
server: !!app.serverId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IS_CLOUD && app.serverId) {
|
||||||
|
jobData.serverId = app.serverId;
|
||||||
|
await deploy(jobData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeApps = await db.query.compose.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(compose.sourceType, "github"),
|
||||||
|
eq(compose.autoDeploy, true),
|
||||||
|
eq(compose.branch, branchName),
|
||||||
|
eq(compose.repository, repository),
|
||||||
|
eq(compose.owner, owner),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const composeApp of composeApps) {
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
composeId: composeApp.composeId as string,
|
||||||
|
titleLog: deploymentTitle,
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "compose",
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
server: !!composeApp.serverId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IS_CLOUD && composeApp.serverId) {
|
||||||
|
jobData.serverId = composeApp.serverId;
|
||||||
|
await deploy(jobData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalApps = apps.length + composeApps.length;
|
||||||
|
const emptyApps = totalApps === 0;
|
||||||
|
|
||||||
|
if (emptyApps) {
|
||||||
|
res.status(200).json({ message: "No apps to deploy" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: "Error To Deploy Application", error });
|
||||||
|
}
|
||||||
|
} else if (req.headers["x-github-event"] === "pull_request") {
|
||||||
|
const prId = githubBody?.pull_request?.id;
|
||||||
|
|
||||||
|
if (githubBody?.action === "closed") {
|
||||||
|
const previewDeploymentResult =
|
||||||
|
await findPreviewDeploymentsByPullRequestId(prId);
|
||||||
|
|
||||||
|
if (previewDeploymentResult.length > 0) {
|
||||||
|
for (const previewDeployment of previewDeploymentResult) {
|
||||||
|
try {
|
||||||
|
await removePreviewDeployment(
|
||||||
|
previewDeployment.previewDeploymentId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.status(200).json({ message: "Preview Deployment Closed" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// opened or synchronize or reopened
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const branch = githubBody?.pull_request?.base?.ref;
|
||||||
const owner = githubBody?.repository?.owner?.name;
|
const owner = githubBody?.repository?.owner?.login;
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(applications.sourceType, "github"),
|
eq(applications.sourceType, "github"),
|
||||||
eq(applications.autoDeploy, true),
|
|
||||||
eq(applications.branch, branchName),
|
|
||||||
eq(applications.repository, repository),
|
eq(applications.repository, repository),
|
||||||
|
eq(applications.branch, branch),
|
||||||
|
eq(applications.isPreviewDeploymentsActive, true),
|
||||||
eq(applications.owner, owner),
|
eq(applications.owner, owner),
|
||||||
),
|
),
|
||||||
|
with: {
|
||||||
|
previewDeployments: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const prBranch = githubBody?.pull_request?.head?.ref;
|
||||||
|
|
||||||
|
const prNumber = githubBody?.pull_request?.number;
|
||||||
|
const prTitle = githubBody?.pull_request?.title;
|
||||||
|
const prURL = githubBody?.pull_request?.html_url;
|
||||||
|
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
|
const previewLimit = app?.previewLimit || 0;
|
||||||
|
if (app?.previewDeployments?.length > previewLimit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previewDeploymentResult =
|
||||||
|
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
||||||
|
|
||||||
|
let previewDeploymentId =
|
||||||
|
previewDeploymentResult?.previewDeploymentId || "";
|
||||||
|
|
||||||
|
if (!previewDeploymentResult) {
|
||||||
|
const previewDeployment = await createPreviewDeployment({
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
branch: prBranch,
|
||||||
|
pullRequestId: prId,
|
||||||
|
pullRequestNumber: prNumber,
|
||||||
|
pullRequestTitle: prTitle,
|
||||||
|
pullRequestURL: prURL,
|
||||||
|
});
|
||||||
|
previewDeploymentId = previewDeployment.previewDeploymentId;
|
||||||
|
}
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: app.applicationId as string,
|
applicationId: app.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: "Preview Deployment",
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application-preview",
|
||||||
server: !!app.serverId,
|
server: !!app.serverId,
|
||||||
|
previewDeploymentId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_CLOUD && app.serverId) {
|
if (IS_CLOUD && app.serverId) {
|
||||||
@@ -99,52 +254,8 @@ export default async function handler(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return res.status(200).json({ message: "Apps Deployed" });
|
||||||
const composeApps = await db.query.compose.findMany({
|
|
||||||
where: and(
|
|
||||||
eq(compose.sourceType, "github"),
|
|
||||||
eq(compose.autoDeploy, true),
|
|
||||||
eq(compose.branch, branchName),
|
|
||||||
eq(compose.repository, repository),
|
|
||||||
eq(compose.owner, owner),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const composeApp of composeApps) {
|
|
||||||
const jobData: DeploymentJob = {
|
|
||||||
composeId: composeApp.composeId as string,
|
|
||||||
titleLog: deploymentTitle,
|
|
||||||
type: "deploy",
|
|
||||||
applicationType: "compose",
|
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
|
||||||
server: !!composeApp.serverId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (IS_CLOUD && composeApp.serverId) {
|
|
||||||
jobData.serverId = composeApp.serverId;
|
|
||||||
await deploy(jobData);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await myQueue.add(
|
|
||||||
"deployments",
|
|
||||||
{ ...jobData },
|
|
||||||
{
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalApps = apps.length + composeApps.length;
|
|
||||||
const emptyApps = totalApps === 0;
|
|
||||||
|
|
||||||
if (emptyApps) {
|
|
||||||
res.status(200).json({ message: "No apps to deploy" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).json({ message: "Error To Deploy Application", error });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ message: "No Actions matched" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ShowDomains } from "@/components/dashboard/application/domains/show-dom
|
|||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show";
|
||||||
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
|
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
|
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
|
||||||
import { UpdateApplication } from "@/components/dashboard/application/update-application";
|
import { UpdateApplication } from "@/components/dashboard/application/update-application";
|
||||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
@@ -51,7 +52,8 @@ type TabState =
|
|||||||
| "advanced"
|
| "advanced"
|
||||||
| "deployments"
|
| "deployments"
|
||||||
| "domains"
|
| "domains"
|
||||||
| "monitoring";
|
| "monitoring"
|
||||||
|
| "preview-deployments";
|
||||||
|
|
||||||
const Service = (
|
const Service = (
|
||||||
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
||||||
@@ -191,8 +193,8 @@ const Service = (
|
|||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",
|
||||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
data?.serverId ? "md:grid-cols-7" : "md:grid-cols-8",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@@ -202,6 +204,9 @@ const Service = (
|
|||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="preview-deployments">
|
||||||
|
Preview Deployments
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -244,6 +249,11 @@ const Service = (
|
|||||||
<ShowDeployments applicationId={applicationId} />
|
<ShowDeployments applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="preview-deployments" className="w-full">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowPreviewDeployments applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="domains" className="w-full">
|
<TabsContent value="domains" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomains applicationId={applicationId} />
|
<ShowDomains applicationId={applicationId} />
|
||||||
|
|||||||
13
apps/dokploy/public/templates/browserless.svg
Normal file
13
apps/dokploy/public/templates/browserless.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||||
|
<title>favicon</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g1" x1="220.3" y1="854.7" x2="760.4" y2="517.2" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ff3c95"/>
|
||||||
|
<stop offset="1" stop-color="#ffc550"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<style>
|
||||||
|
.s0 { fill: url(#g1) }
|
||||||
|
</style>
|
||||||
|
<path id="Path 0" class="s0" d="m243.9 0c0.3 0.1 26.4 15 115.1 66.5v411.8c0 391.6 0.1 411.7 1.8 411.2 0.9-0.3 69.7-34 304.1-149l0.1-61.8c0-48.9-0.3-61.6-1.3-61.3-0.6 0.2-43.6 20-95.5 44-51.8 24-94.4 43.5-94.7 43.3-0.2-0.1-0.3-128.3 0-569.6l115.5 68 1.1 256.4 191.4 111.4 0.5 243.6-213.1 104.5c-117.2 57.5-213.9 104.6-214.8 104.8-0.9 0.2-26.5-16.3-112.2-73.3l0.1-296.5c0-163.1 0.3-376.9 0.7-475.2 0.3-98.4 0.9-178.8 1.2-178.8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 836 B |
13
apps/dokploy/public/templates/budibase.svg
Normal file
13
apps/dokploy/public/templates/budibase.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg width="265" height="265" viewBox="0 0 265 265" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1_1799)">
|
||||||
|
<path d="M158.2 8.6V116.6C158.2 121.3 162 125.2 166.8 125.2H213.8C218 125.2 222 123.2 224.6 119.8L262.9 68.9C265.7 65.2 265.7 60.1 262.9 56.4L224.6 5.4C222 2 218 0 213.8 0H166.8C162 0 158.2 3.8 158.2 8.6Z" fill="#FF4E4E"/>
|
||||||
|
<path d="M158.2 148.4V256.4C158.2 261.1 162 265 166.8 265H213.8C218 265 222 263 224.6 259.6L262.9 208.7C265.7 205 265.7 199.9 262.9 196.2L224.6 145.3C222.1 141.9 218.1 139.9 213.8 139.9H166.8C162 139.8 158.2 143.7 158.2 148.4Z" fill="#6E56FF"/>
|
||||||
|
<path d="M0 8.6V116.6C0 121.3 3.8 125.2 8.6 125.2H109.6C113.8 125.2 117.8 123.2 120.4 119.8L155.9 72.5C160.3 66.6 160.3 58.5 155.9 52.6L120.3 5.4C117.8 2 113.8 0 109.5 0H8.6C3.8 0 0 3.8 0 8.6Z" fill="#F97777"/>
|
||||||
|
<path d="M0 148.4V256.4C0 261.1 3.8 265 8.6 265H109.6C113.8 265 117.8 263 120.4 259.6L155.9 212.3C160.3 206.4 160.3 198.3 155.9 192.4L120.4 145.1C117.9 141.7 113.9 139.7 109.6 139.7H8.6C3.8 139.8 0 143.7 0 148.4Z" fill="#9F8FFF"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_1799">
|
||||||
|
<rect width="265" height="265" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
17
apps/dokploy/public/templates/drawio.svg
Normal file
17
apps/dokploy/public/templates/drawio.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 161.6 161.6" style="enable-background:new 0 0 161.6 161.6;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#F08705;}
|
||||||
|
.st1{fill:#DF6C0C;}
|
||||||
|
.st2{fill:#FFFFFF;}
|
||||||
|
.st3{fill:#333333;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M161.6,154.7c0,3.9-3.2,6.9-6.9,6.9H6.9c-3.9,0-6.9-3.2-6.9-6.9V6.9C0,3,3.2,0,6.9,0h147.8 c3.9,0,6.9,3.2,6.9,6.9L161.6,154.7L161.6,154.7z"/>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M161.6,154.7c0,3.9-3.2,6.9-6.9,6.9H55.3l-32.2-32.7l20-32.7l59.4-73.8l58.9,60.7L161.6,154.7z"/>
|
||||||
|
</g>
|
||||||
|
<path class="st2" d="M132.7,90.3h-17l-18-30.6c4-0.8,7-4.4,7-8.6V28c0-4.9-3.9-8.8-8.8-8.8h-30c-4.9,0-8.8,3.9-8.8,8.8v23.1 c0,4.3,3,7.8,6.9,8.6L46,90.4H29c-4.9,0-8.8,3.9-8.8,8.8v23.1c0,4.9,3.9,8.8,8.8,8.8h30c4.9,0,8.8-3.9,8.8-8.8V99.2 c0-4.9-3.9-8.8-8.8-8.8h-2.9L73.9,60h13.9l17.9,30.4h-3c-4.9,0-8.8,3.9-8.8,8.8v23.1c0,4.9,3.9,8.8,8.8,8.8h30 c4.9,0,8.8-3.9,8.8-8.8V99.2C141.5,94.3,137.6,90.3,132.7,90.3z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
apps/dokploy/public/templates/kimai.svg
Normal file
1
apps/dokploy/public/templates/kimai.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg cursor="default" enable-background="new" height="512" viewBox="0 0 440 440" width="512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><radialGradient id="a" cx="611" cy="41.266644" gradientTransform="matrix(1.1673343 0 0 1.4196623 -102.24121 -19.722475)" gradientUnits="userSpaceOnUse" r="160.5"><stop offset="0" stop-color="#01fd00"/><stop offset="1" stop-color="#009d39"/></radialGradient><radialGradient id="b" cx="873.78265" cy="-16.37981" gradientTransform="matrix(1.9295131 -1.1140049 1.1550268 2.0005649 -1087.6858 1104.8947)" gradientUnits="userSpaceOnUse" r="55.747124"><stop offset="0" stop-color="#f9f9f9"/><stop offset="1" stop-color="#fff"/></radialGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="611.14288" x2="610.57141" y1="234.57143" y2="-110.91601"><stop offset="0" stop-color="#f2f2f2"/><stop offset="1" stop-color="#f2f2f2"/></linearGradient><g transform="translate(-390.56693 181.56689)"><g><path d="m391.020508-182.838852h439.95895v439.95895h-439.95895z" fill="#fff" stroke="#58dd58" stroke-linejoin="round" stroke-width=".040996" visibility="hidden"/><circle cx="611" cy="38" fill="url(#c)" r="187.776825"/><circle cx="611" cy="38" fill="url(#a)" r="166.285645" stroke="#fff" stroke-linejoin="round" stroke-width="3"/><path d="m571.156433 108.308197 110-63.999998-110-64z" fill="url(#b)" fill-rule="evenodd"/></g><g fill="#c7efc4" fill-rule="evenodd"><rect height="32" opacity=".5" ry="7" transform="matrix(-.83616052 -.5484848 .5484848 -.83616052 0 0)" width="14" x="-542.46875" y="419.410309"/><rect height="32" opacity=".5" ry="7" transform="matrix(-.45720822 -.88935968 .88935968 -.45720822 0 0)" width="14" x="-320.158203" y="642.978027"/><rect height="41" opacity=".5" ry="9" transform="rotate(-90)" width="18" x="-48.536366" y="718.754395"/><rect height="32" opacity=".5" ry="7" transform="matrix(.47159375 -.88181593 .88181593 .47159375 0 0)" width="14" x="244.405457" y="673.798523"/><rect height="32" opacity=".5" ry="7" transform="matrix(-.8503618 .52619845 -.52619845 -.8503618 0 0)" width="14" x="-505.884949" y="-503.867859"/><rect height="41" opacity=".5" ry="9" transform="scale(-1)" width="18" x="-620.297424" y="-187.942719"/><rect height="32" opacity=".5" ry="7" transform="matrix(-.84033379 -.54206929 .54206929 -.84033379 0 0)" width="14" x="-539.16571" y="148.581909"/><rect height="32" opacity=".5" ry="7" transform="matrix(-.45720822 -.88935968 .88935968 -.45720822 0 0)" width="14" x="-319.728088" y="374.804901"/><rect height="41" opacity=".5" ry="9" transform="rotate(-90)" width="18" x="-48.536366" y="460.964569"/><rect height="32" opacity=".5" ry="7" transform="matrix(.47159375 -.88181593 .88181593 .47159375 0 0)" width="14" x="250.841736" y="406.195648"/><rect height="32" opacity=".5" ry="7" transform="matrix(-.849305 .52790248 -.52790248 -.849305 0 0)" width="14" x="-505.957611" y="-236.959518"/><g transform="scale(-1)"><rect height="41" opacity=".5" ry="9" width="18" x="-633.02533" y="69.120789"/><rect height="41" opacity=".5" ry="9" width="18" x="-606.761353" y="69.120789"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -31,6 +31,7 @@ import { settingsRouter } from "./routers/settings";
|
|||||||
import { sshRouter } from "./routers/ssh-key";
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
import { stripeRouter } from "./routers/stripe";
|
import { stripeRouter } from "./routers/stripe";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
|
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -55,6 +56,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
destination: destinationRouter,
|
destination: destinationRouter,
|
||||||
backup: backupRouter,
|
backup: backupRouter,
|
||||||
deployment: deploymentRouter,
|
deployment: deploymentRouter,
|
||||||
|
previewDeployment: previewDeploymentRouter,
|
||||||
mounts: mountRouter,
|
mounts: mountRouter,
|
||||||
certificates: certificateRouter,
|
certificates: certificateRouter,
|
||||||
settings: settingsRouter,
|
settings: settingsRouter,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
findDomainById,
|
findDomainById,
|
||||||
findDomainsByApplicationId,
|
findDomainsByApplicationId,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
|
findPreviewDeploymentById,
|
||||||
generateTraefikMeDomain,
|
generateTraefikMeDomain,
|
||||||
manageDomain,
|
manageDomain,
|
||||||
removeDomain,
|
removeDomain,
|
||||||
@@ -108,12 +109,33 @@ export const domainRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to access this compose",
|
message: "You are not authorized to access this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (currentDomain.previewDeploymentId) {
|
||||||
|
const newPreviewDeployment = await findPreviewDeploymentById(
|
||||||
|
currentDomain.previewDeploymentId,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
newPreviewDeployment.application.project.adminId !== ctx.user.adminId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to access this preview deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const result = await updateDomainById(input.domainId, input);
|
const result = await updateDomainById(input.domainId, input);
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
if (domain.applicationId) {
|
if (domain.applicationId) {
|
||||||
const application = await findApplicationById(domain.applicationId);
|
const application = await findApplicationById(domain.applicationId);
|
||||||
await manageDomain(application, domain);
|
await manageDomain(application, domain);
|
||||||
|
} else if (domain.previewDeploymentId) {
|
||||||
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
|
domain.previewDeploymentId,
|
||||||
|
);
|
||||||
|
const application = await findApplicationById(
|
||||||
|
previewDeployment.applicationId,
|
||||||
|
);
|
||||||
|
application.appName = previewDeployment.appName;
|
||||||
|
await manageDomain(application, domain);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|||||||
54
apps/dokploy/server/api/routers/preview-deployment.ts
Normal file
54
apps/dokploy/server/api/routers/preview-deployment.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||||
|
import {
|
||||||
|
findApplicationById,
|
||||||
|
findPreviewDeploymentById,
|
||||||
|
findPreviewDeploymentsByApplicationId,
|
||||||
|
removePreviewDeployment,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
|
export const previewDeploymentRouter = createTRPCRouter({
|
||||||
|
all: protectedProcedure
|
||||||
|
.input(apiFindAllByApplication)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
if (application.project.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to access this application",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await findPreviewDeploymentsByApplicationId(input.applicationId);
|
||||||
|
}),
|
||||||
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ previewDeploymentId: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
|
input.previewDeploymentId,
|
||||||
|
);
|
||||||
|
if (previewDeployment.application.project.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to delete this preview deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await removePreviewDeployment(input.previewDeploymentId);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
one: protectedProcedure
|
||||||
|
.input(z.object({ previewDeploymentId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
|
input.previewDeploymentId,
|
||||||
|
);
|
||||||
|
if (previewDeployment.application.project.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to access this preview deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return previewDeployment;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
haveActiveServices,
|
haveActiveServices,
|
||||||
removeDeploymentsByServerId,
|
removeDeploymentsByServerId,
|
||||||
serverSetup,
|
serverSetup,
|
||||||
|
serverValidate,
|
||||||
updateServerById,
|
updateServerById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -118,6 +119,47 @@ export const serverRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
validate: protectedProcedure
|
||||||
|
.input(apiFindOneServer)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to validate this server",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const response = await serverValidate(input.serverId);
|
||||||
|
return response as unknown as {
|
||||||
|
docker: {
|
||||||
|
enabled: boolean;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
rclone: {
|
||||||
|
enabled: boolean;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
nixpacks: {
|
||||||
|
enabled: boolean;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
buildpacks: {
|
||||||
|
enabled: boolean;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
isDokployNetworkInstalled: boolean;
|
||||||
|
isSwarmInstalled: boolean;
|
||||||
|
isMainDirectoryInstalled: boolean;
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
||||||
|
cause: error as Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiRemoveServer)
|
.input(apiRemoveServer)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
deployApplication,
|
deployApplication,
|
||||||
deployCompose,
|
deployCompose,
|
||||||
|
deployPreviewApplication,
|
||||||
deployRemoteApplication,
|
deployRemoteApplication,
|
||||||
deployRemoteCompose,
|
deployRemoteCompose,
|
||||||
|
deployRemotePreviewApplication,
|
||||||
rebuildApplication,
|
rebuildApplication,
|
||||||
rebuildCompose,
|
rebuildCompose,
|
||||||
rebuildRemoteApplication,
|
rebuildRemoteApplication,
|
||||||
rebuildRemoteCompose,
|
rebuildRemoteCompose,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
|
updatePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { type Job, Worker } from "bullmq";
|
import { type Job, Worker } from "bullmq";
|
||||||
import type { DeploymentJob } from "./queue-types";
|
import type { DeploymentJob } from "./queue-types";
|
||||||
@@ -20,6 +23,7 @@ export const deploymentWorker = new Worker(
|
|||||||
try {
|
try {
|
||||||
if (job.data.applicationType === "application") {
|
if (job.data.applicationType === "application") {
|
||||||
await updateApplicationStatus(job.data.applicationId, "running");
|
await updateApplicationStatus(job.data.applicationId, "running");
|
||||||
|
|
||||||
if (job.data.server) {
|
if (job.data.server) {
|
||||||
if (job.data.type === "redeploy") {
|
if (job.data.type === "redeploy") {
|
||||||
await rebuildRemoteApplication({
|
await rebuildRemoteApplication({
|
||||||
@@ -83,6 +87,29 @@ export const deploymentWorker = new Worker(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (job.data.applicationType === "application-preview") {
|
||||||
|
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
||||||
|
previewStatus: "running",
|
||||||
|
});
|
||||||
|
if (job.data.server) {
|
||||||
|
if (job.data.type === "deploy") {
|
||||||
|
await deployRemotePreviewApplication({
|
||||||
|
applicationId: job.data.applicationId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (job.data.type === "deploy") {
|
||||||
|
await deployPreviewApplication({
|
||||||
|
applicationId: job.data.applicationId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error", error);
|
console.log("Error", error);
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ type DeployJob =
|
|||||||
type: "deploy" | "redeploy";
|
type: "deploy" | "redeploy";
|
||||||
applicationType: "compose";
|
applicationType: "compose";
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
applicationId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy";
|
||||||
|
applicationType: "application-preview";
|
||||||
|
previewDeploymentId: string;
|
||||||
|
serverId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
export type DeploymentJob = DeployJob;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
|
|||||||
config({ path: ".env" });
|
config({ path: ".env" });
|
||||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
const app = next({ dev, turbopack: dev });
|
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
void app.prepare().then(async () => {
|
void app.prepare().then(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
16
apps/dokploy/templates/browserless/docker-compose.yml
Normal file
16
apps/dokploy/templates/browserless/docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
browserless:
|
||||||
|
image: ghcr.io/browserless/chromium:v2.23.0
|
||||||
|
environment:
|
||||||
|
TOKEN: ${BROWSERLESS_TOKEN}
|
||||||
|
expose:
|
||||||
|
- 3000
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://127.0.0.1:3000/docs'
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 15
|
||||||
28
apps/dokploy/templates/browserless/index.ts
Normal file
28
apps/dokploy/templates/browserless/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainHost = generateRandomDomain(schema);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainHost,
|
||||||
|
port: 3000,
|
||||||
|
serviceName: "browserless",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const envs = [
|
||||||
|
`BROWERLESS_HOST=${mainHost}`,
|
||||||
|
`BROWSERLESS_TOKEN=${generatePassword(16)}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
199
apps/dokploy/templates/budibase/docker-compose.yml
Normal file
199
apps/dokploy/templates/budibase/docker-compose.yml
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
services:
|
||||||
|
apps:
|
||||||
|
image: budibase.docker.scarf.sh/budibase/apps:3.2.25
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
SELF_HOSTED: 1
|
||||||
|
LOG_LEVEL: info
|
||||||
|
PORT: 4002
|
||||||
|
INTERNAL_API_KEY: ${BB_INTERNAL_API_KEY}
|
||||||
|
API_ENCRYPTION_KEY: ${BB_API_ENCRYPTION_KEY}
|
||||||
|
JWT_SECRET: ${BB_JWT_SECRET}
|
||||||
|
MINIO_ACCESS_KEY: ${BB_MINIO_ACCESS_KEY}
|
||||||
|
MINIO_SECRET_KEY: ${BB_MINIO_SECRET_KEY}
|
||||||
|
MINIO_URL: http://minio:9000
|
||||||
|
REDIS_URL: redis:6379
|
||||||
|
REDIS_PASSWORD: ${BB_REDIS_PASSWORD}
|
||||||
|
WORKER_URL: http://worker:4003
|
||||||
|
COUCH_DB_USERNAME: budibase
|
||||||
|
COUCH_DB_PASSWORD: ${BB_COUCHDB_PASSWORD}
|
||||||
|
COUCH_DB_URL: http://budibase:${BB_COUCHDB_PASSWORD}@couchdb:5984
|
||||||
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT:-PRODUCTION}
|
||||||
|
ENABLE_ANALYTICS: ${ENABLE_ANALYTICS:-true}
|
||||||
|
BB_ADMIN_USER_EMAIL: ''
|
||||||
|
BB_ADMIN_USER_PASSWORD: ''
|
||||||
|
depends_on:
|
||||||
|
worker:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- '--spider'
|
||||||
|
- '-qO-'
|
||||||
|
- 'http://localhost:4002/health'
|
||||||
|
interval: 15s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
worker:
|
||||||
|
image: budibase.docker.scarf.sh/budibase/worker:3.2.25
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
SELF_HOSTED: 1
|
||||||
|
LOG_LEVEL: info
|
||||||
|
PORT: 4003
|
||||||
|
CLUSTER_PORT: 10000
|
||||||
|
INTERNAL_API_KEY: ${BB_INTERNAL_API_KEY}
|
||||||
|
API_ENCRYPTION_KEY: ${BB_API_ENCRYPTION_KEY}
|
||||||
|
JWT_SECRET: ${BB_JWT_SECRET}
|
||||||
|
MINIO_ACCESS_KEY: ${BB_MINIO_ACCESS_KEY}
|
||||||
|
MINIO_SECRET_KEY: ${BB_MINIO_SECRET_KEY}
|
||||||
|
APPS_URL: http://apps:4002
|
||||||
|
MINIO_URL: http://minio:9000
|
||||||
|
REDIS_URL: redis:6379
|
||||||
|
REDIS_PASSWORD: ${BB_REDIS_PASSWORD}
|
||||||
|
COUCH_DB_USERNAME: budibase
|
||||||
|
COUCH_DB_PASSWORD: ${BB_COUCHDB_PASSWORD}
|
||||||
|
COUCH_DB_URL: http://budibase:${BB_COUCHDB_PASSWORD}@couchdb:5984
|
||||||
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT:-PRODUCTION}
|
||||||
|
ENABLE_ANALYTICS: ${ENABLE_ANALYTICS:-true}
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- '--spider'
|
||||||
|
- '-qO-'
|
||||||
|
- 'http://localhost:4003/health'
|
||||||
|
interval: 15s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
minio:
|
||||||
|
image: minio/minio:RELEASE.2024-11-07T00-52-20Z
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- 'minio_data:/data'
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${BB_MINIO_ACCESS_KEY}
|
||||||
|
MINIO_ROOT_PASSWORD: ${BB_MINIO_SECRET_KEY}
|
||||||
|
MINIO_BROWSER: off
|
||||||
|
command: 'server /data --console-address ":9001"'
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://localhost:9000/minio/health/live'
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
proxy:
|
||||||
|
image: budibase/proxy:3.2.25
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND: 10
|
||||||
|
PROXY_RATE_LIMIT_API_PER_SECOND: 20
|
||||||
|
APPS_UPSTREAM_URL: http://apps:4002
|
||||||
|
WORKER_UPSTREAM_URL: http://worker:4003
|
||||||
|
MINIO_UPSTREAM_URL: http://minio:9000
|
||||||
|
COUCHDB_UPSTREAM_URL: http://couchdb:5984
|
||||||
|
WATCHTOWER_UPSTREAM_URL: http://watchtower:8080
|
||||||
|
RESOLVER: 127.0.0.11
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
worker:
|
||||||
|
condition: service_healthy
|
||||||
|
apps:
|
||||||
|
condition: service_healthy
|
||||||
|
couchdb:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://localhost:10000/'
|
||||||
|
interval: 15s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
couchdb:
|
||||||
|
image: budibase/couchdb:v3.3.3
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
COUCHDB_USER: budibase
|
||||||
|
COUCHDB_PASSWORD: ${BB_COUCHDB_PASSWORD}
|
||||||
|
TARGETBUILD: docker-compose
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://localhost:5984/'
|
||||||
|
interval: 15s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
volumes:
|
||||||
|
- 'couchdb3_data:/opt/couchdb/data'
|
||||||
|
redis:
|
||||||
|
image: redis:7.2-alpine
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
restart: unless-stopped
|
||||||
|
command: 'redis-server --requirepass "${BB_REDIS_PASSWORD}"'
|
||||||
|
volumes:
|
||||||
|
- 'redis_data:/data'
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- redis-cli
|
||||||
|
- '-a'
|
||||||
|
- ${BB_REDIS_PASSWORD}
|
||||||
|
- ping
|
||||||
|
interval: 15s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
watchtower:
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
image: containrrr/watchtower:1.7.1
|
||||||
|
volumes:
|
||||||
|
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||||
|
command: '--debug --http-api-update bbapps bbworker bbproxy'
|
||||||
|
environment:
|
||||||
|
WATCHTOWER_HTTP_API: true
|
||||||
|
WATCHTOWER_HTTP_API_TOKEN: ${BB_WATCHTOWER_PASSWORD}
|
||||||
|
WATCHTOWER_CLEANUP: true
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=false
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minio_data:
|
||||||
|
couchdb3_data:
|
||||||
|
redis_data:
|
||||||
45
apps/dokploy/templates/budibase/index.ts
Normal file
45
apps/dokploy/templates/budibase/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
|
||||||
|
const apiKey = generatePassword(32);
|
||||||
|
const encryptionKey = generatePassword(32);
|
||||||
|
const jwtSecret = generatePassword(32);
|
||||||
|
const couchDbPassword = generatePassword(32);
|
||||||
|
const redisPassword = generatePassword(32);
|
||||||
|
const minioAccessKey = generatePassword(32);
|
||||||
|
const minioSecretKey = generatePassword(32);
|
||||||
|
const watchtowerPassword = generatePassword(32);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 10000,
|
||||||
|
serviceName: "proxy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`BB_HOST=${mainDomain}`,
|
||||||
|
`BB_INTERNAL_API_KEY=${apiKey}`,
|
||||||
|
`BB_API_ENCRYPTION_KEY=${encryptionKey}`,
|
||||||
|
`BB_JWT_SECRET=${jwtSecret}`,
|
||||||
|
`BB_COUCHDB_PASSWORD=${couchDbPassword}`,
|
||||||
|
`BB_REDIS_PASSWORD=${redisPassword}`,
|
||||||
|
`BB_WATCHTOWER_PASSWORD=${watchtowerPassword}`,
|
||||||
|
`BB_MINIO_ACCESS_KEY=${minioAccessKey}`,
|
||||||
|
`BB_MINIO_SECRET_KEY=${minioSecretKey}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
62
apps/dokploy/templates/drawio/docker-compose.yml
Normal file
62
apps/dokploy/templates/drawio/docker-compose.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
plantuml-server:
|
||||||
|
image: plantuml/plantuml-server
|
||||||
|
ports:
|
||||||
|
- "8080"
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- fonts_volume:/usr/share/fonts/drawio
|
||||||
|
image-export:
|
||||||
|
image: jgraph/export-server
|
||||||
|
ports:
|
||||||
|
- "8000"
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- fonts_volume:/usr/share/fonts/drawio
|
||||||
|
environment:
|
||||||
|
- DRAWIO_BASE_URL=${DRAWIO_BASE_URL}
|
||||||
|
drawio:
|
||||||
|
image: jgraph/drawio:24.7.17
|
||||||
|
ports:
|
||||||
|
- "8080"
|
||||||
|
links:
|
||||||
|
- plantuml-server:plantuml-server
|
||||||
|
- image-export:image-export
|
||||||
|
depends_on:
|
||||||
|
- plantuml-server
|
||||||
|
- image-export
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
RAWIO_SELF_CONTAINED: 1
|
||||||
|
DRAWIO_USE_HTTP: 1
|
||||||
|
PLANTUML_URL: http://plantuml-server:8080/
|
||||||
|
EXPORT_URL: http://image-export:8000/
|
||||||
|
DRAWIO_BASE_URL: ${DRAWIO_BASE_URL}
|
||||||
|
DRAWIO_SERVER_URL: ${DRAWIO_SERVER_URL}
|
||||||
|
DRAWIO_CSP_HEADER: ${DRAWIO_CSP_HEADER}
|
||||||
|
DRAWIO_VIEWER_URL: ${DRAWIO_VIEWER_URL}
|
||||||
|
DRAWIO_LIGHTBOX_URL: ${DRAWIO_LIGHTBOX_URL}
|
||||||
|
DRAWIO_CONFIG: ${DRAWIO_CONFIG}
|
||||||
|
DRAWIO_GOOGLE_CLIENT_ID: ${DRAWIO_GOOGLE_CLIENT_ID}
|
||||||
|
DRAWIO_GOOGLE_APP_ID: ${DRAWIO_GOOGLE_APP_ID}
|
||||||
|
DRAWIO_GOOGLE_CLIENT_SECRET: ${DRAWIO_GOOGLE_CLIENT_SECRET}
|
||||||
|
DRAWIO_GOOGLE_VIEWER_CLIENT_ID: ${DRAWIO_GOOGLE_VIEWER_CLIENT_ID}
|
||||||
|
DRAWIO_GOOGLE_VIEWER_APP_ID: ${DRAWIO_GOOGLE_VIEWER_APP_ID}
|
||||||
|
DRAWIO_GOOGLE_VIEWER_CLIENT_SECRET: ${DRAWIO_GOOGLE_VIEWER_CLIENT_SECRET}
|
||||||
|
DRAWIO_MSGRAPH_CLIENT_ID: ${DRAWIO_MSGRAPH_CLIENT_ID}
|
||||||
|
DRAWIO_MSGRAPH_CLIENT_SECRET: ${DRAWIO_MSGRAPH_CLIENT_SECRET}
|
||||||
|
DRAWIO_MSGRAPH_TENANT_ID: ${DRAWIO_MSGRAPH_TENANT_ID}
|
||||||
|
DRAWIO_GITLAB_ID: ${DRAWIO_GITLAB_ID}
|
||||||
|
DRAWIO_GITLAB_SECRET: ${DRAWIO_GITLAB_SECRET}
|
||||||
|
DRAWIO_GITLAB_URL: ${DRAWIO_GITLAB_URL}
|
||||||
|
DRAWIO_CLOUD_CONVERT_APIKEY: ${DRAWIO_CLOUD_CONVERT_APIKEY}
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fonts_volume:
|
||||||
31
apps/dokploy/templates/drawio/index.ts
Normal file
31
apps/dokploy/templates/drawio/index.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateBase64,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const secretKeyBase = generateBase64(64);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 8080,
|
||||||
|
serviceName: "drawio",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`DRAWIO_HOST=${mainDomain}`,
|
||||||
|
`DRAWIO_BASE_URL=https://${mainDomain}`,
|
||||||
|
`DRAWIO_SERVER_URL=https://${mainDomain}/`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
51
apps/dokploy/templates/kimai/docker-compose.yml
Normal file
51
apps/dokploy/templates/kimai/docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: kimai/kimai2:apache-2.26.0
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
APP_ENV: prod
|
||||||
|
DATABASE_URL: mysql://kimai:${KI_MYSQL_PASSWORD:-kimai}@db/kimai
|
||||||
|
TRUSTED_PROXIES: localhost
|
||||||
|
APP_SECRET: ${KI_APP_SECRET}
|
||||||
|
MAILER_FROM: ${KI_MAILER_FROM:-admin@kimai.local}
|
||||||
|
MAILER_URL: ${KI_MAILER_URL:-null://null}
|
||||||
|
ADMINMAIL: ${KI_ADMINMAIL:-admin@kimai.local}
|
||||||
|
ADMINPASS: ${KI_ADMINPASS}
|
||||||
|
volumes:
|
||||||
|
- kimai-data:/opt/kimai/var
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
db:
|
||||||
|
image: mariadb:10.11
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MYSQL_DATABASE=kimai
|
||||||
|
- MYSQL_USER=kimai
|
||||||
|
- MYSQL_PASSWORD=${KI_MYSQL_PASSWORD}
|
||||||
|
- MYSQL_ROOT_PASSWORD=${KI_MYSQL_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
command:
|
||||||
|
- --character-set-server=utf8mb4
|
||||||
|
- --collation-server=utf8mb4_unicode_ci
|
||||||
|
- --innodb-buffer-pool-size=256M
|
||||||
|
- --innodb-flush-log-at-trx-commit=2
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "kimai", "-p${KI_MYSQL_PASSWORD}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
kimai-data:
|
||||||
|
mysql-data:
|
||||||
37
apps/dokploy/templates/kimai/index.ts
Normal file
37
apps/dokploy/templates/kimai/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const domain = generateRandomDomain(schema);
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: domain,
|
||||||
|
port: 8001,
|
||||||
|
serviceName: "app",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const adminPassword = generatePassword(32);
|
||||||
|
const mysqlPassword = generatePassword(32);
|
||||||
|
const mysqlRootPassword = generatePassword(32);
|
||||||
|
const appSecret = generatePassword(32);
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`KI_HOST=${domain}`,
|
||||||
|
"KI_ADMINMAIL=admin@kimai.local",
|
||||||
|
`KI_ADMINPASS=${adminPassword}`,
|
||||||
|
`KI_MYSQL_ROOT_PASSWORD=${mysqlRootPassword}`,
|
||||||
|
`KI_MYSQL_PASSWORD=${mysqlPassword}`,
|
||||||
|
`KI_APP_SECRET=${appSecret}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -107,6 +107,21 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["database"],
|
tags: ["database"],
|
||||||
load: () => import("./baserow/index").then((m) => m.generate),
|
load: () => import("./baserow/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "budibase",
|
||||||
|
name: "Budibase",
|
||||||
|
version: "3.2.25",
|
||||||
|
description:
|
||||||
|
"Budibase is an open-source low-code platform that saves engineers 100s of hours building forms, portals, and approval apps, securely.",
|
||||||
|
logo: "budibase.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/Budibase/budibase",
|
||||||
|
website: "https://budibase.com/",
|
||||||
|
docs: "https://docs.budibase.com/docs/",
|
||||||
|
},
|
||||||
|
tags: ["database", "low-code", "nocode", "applications"],
|
||||||
|
load: () => import("./budibase/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "ghost",
|
id: "ghost",
|
||||||
name: "Ghost",
|
name: "Ghost",
|
||||||
@@ -936,8 +951,8 @@ export const templates: TemplateData[] = [
|
|||||||
logo: "ryot.png",
|
logo: "ryot.png",
|
||||||
links: {
|
links: {
|
||||||
github: "https://github.com/IgnisDa/ryot",
|
github: "https://github.com/IgnisDa/ryot",
|
||||||
website: "https://ryot.dev/",
|
website: "https://ryot.io/",
|
||||||
docs: "https://ryot.dev/docs/getting-started",
|
docs: "https://docs.ryot.io/",
|
||||||
},
|
},
|
||||||
tags: ["media", "tracking", "self-hosted"],
|
tags: ["media", "tracking", "self-hosted"],
|
||||||
load: () => import("./ryot/index").then((m) => m.generate),
|
load: () => import("./ryot/index").then((m) => m.generate),
|
||||||
@@ -987,4 +1002,49 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["event-driven", "applications"],
|
tags: ["event-driven", "applications"],
|
||||||
load: () => import("./triggerdotdev/index").then((m) => m.generate),
|
load: () => import("./triggerdotdev/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "browserless",
|
||||||
|
name: "Browserless",
|
||||||
|
version: "2.23.0",
|
||||||
|
description:
|
||||||
|
"Browserless allows remote clients to connect and execute headless work, all inside of docker. It supports the standard, unforked Puppeteer and Playwright libraries, as well offering REST-based APIs for common actions like data collection, PDF generation and more.",
|
||||||
|
logo: "browserless.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/browserless/browserless",
|
||||||
|
website: "https://www.browserless.io/",
|
||||||
|
docs: "https://docs.browserless.io/",
|
||||||
|
},
|
||||||
|
tags: ["browser", "automation"],
|
||||||
|
load: () => import("./browserless/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "drawio",
|
||||||
|
name: "draw.io",
|
||||||
|
version: "24.7.17",
|
||||||
|
description:
|
||||||
|
"draw.io is a configurable diagramming/whiteboarding visualization application.",
|
||||||
|
logo: "drawio.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/jgraph/drawio",
|
||||||
|
website: "https://draw.io/",
|
||||||
|
docs: "https://www.drawio.com/doc/",
|
||||||
|
},
|
||||||
|
tags: ["drawing", "diagrams"],
|
||||||
|
load: () => import("./drawio/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kimai",
|
||||||
|
name: "Kimai",
|
||||||
|
version: "2.26.0",
|
||||||
|
description:
|
||||||
|
"Kimai is a web-based multi-user time-tracking application. Works great for everyone: freelancers, companies, organizations - everyone can track their times, generate reports, create invoices and do so much more.",
|
||||||
|
logo: "kimai.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/kimai/kimai",
|
||||||
|
website: "https://www.kimai.org",
|
||||||
|
docs: "https://www.kimai.org/documentation",
|
||||||
|
},
|
||||||
|
tags: ["invoice", "business", "finance"],
|
||||||
|
load: () => import("./kimai/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dokploy:setup": "pnpm --filter=dokploy run setup",
|
"dokploy:setup": "pnpm --filter=dokploy run setup",
|
||||||
"dokploy:dev": "pnpm --filter=dokploy run dev",
|
"dokploy:dev": "pnpm --filter=dokploy run dev",
|
||||||
|
"dokploy:dev:turbopack": "pnpm --filter=dokploy run dev-turbopack",
|
||||||
"dokploy:build": "pnpm --filter=dokploy run build",
|
"dokploy:build": "pnpm --filter=dokploy run build",
|
||||||
"dokploy:start": "pnpm --filter=dokploy run start",
|
"dokploy:start": "pnpm --filter=dokploy run start",
|
||||||
"test": "pnpm --filter=dokploy run test",
|
"test": "pnpm --filter=dokploy run test",
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ import { redirects } from "./redirects";
|
|||||||
import { registry } from "./registry";
|
import { registry } from "./registry";
|
||||||
import { security } from "./security";
|
import { security } from "./security";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus, certificateType } from "./shared";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { previewDeployments } from "./preview-deployments";
|
||||||
|
|
||||||
export const sourceType = pgEnum("sourceType", [
|
export const sourceType = pgEnum("sourceType", [
|
||||||
"docker",
|
"docker",
|
||||||
@@ -114,6 +115,19 @@ export const applications = pgTable("application", {
|
|||||||
.unique(),
|
.unique(),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
env: text("env"),
|
env: text("env"),
|
||||||
|
previewEnv: text("previewEnv"),
|
||||||
|
previewBuildArgs: text("previewBuildArgs"),
|
||||||
|
previewWildcard: text("previewWildcard"),
|
||||||
|
previewPort: integer("previewPort").default(3000),
|
||||||
|
previewHttps: boolean("previewHttps").notNull().default(false),
|
||||||
|
previewPath: text("previewPath").default("/"),
|
||||||
|
previewCertificateType: certificateType("certificateType")
|
||||||
|
.notNull()
|
||||||
|
.default("none"),
|
||||||
|
previewLimit: integer("previewLimit").default(3),
|
||||||
|
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||||
|
false,
|
||||||
|
),
|
||||||
buildArgs: text("buildArgs"),
|
buildArgs: text("buildArgs"),
|
||||||
memoryReservation: integer("memoryReservation"),
|
memoryReservation: integer("memoryReservation"),
|
||||||
memoryLimit: integer("memoryLimit"),
|
memoryLimit: integer("memoryLimit"),
|
||||||
@@ -240,6 +254,7 @@ export const applicationsRelations = relations(
|
|||||||
fields: [applications.serverId],
|
fields: [applications.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
|
previewDeployments: many(previewDeployments),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -349,6 +364,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
subtitle: z.string().optional(),
|
subtitle: z.string().optional(),
|
||||||
dockerImage: z.string().optional(),
|
dockerImage: z.string().optional(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
|
isPreviewDeploymentsActive: z.boolean().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
registryUrl: z.string().optional(),
|
registryUrl: z.string().optional(),
|
||||||
customGitSSHKeyId: z.string().optional(),
|
customGitSSHKeyId: z.string().optional(),
|
||||||
@@ -380,6 +396,14 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
|
previewPort: z.number().optional(),
|
||||||
|
previewEnv: z.string().optional(),
|
||||||
|
previewBuildArgs: z.string().optional(),
|
||||||
|
previewWildcard: z.string().optional(),
|
||||||
|
previewLimit: z.number().optional(),
|
||||||
|
previewHttps: z.boolean().optional(),
|
||||||
|
previewPath: z.string().optional(),
|
||||||
|
previewCertificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema.pick({
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { is, relations } from "drizzle-orm";
|
||||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
import {
|
||||||
|
type AnyPgColumn,
|
||||||
|
boolean,
|
||||||
|
pgEnum,
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
|
import { previewDeployments } from "./preview-deployments";
|
||||||
|
|
||||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||||
"running",
|
"running",
|
||||||
@@ -32,6 +39,11 @@ export const deployments = pgTable("deployment", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
|
isPreviewDeployment: boolean("isPreviewDeployment").default(false),
|
||||||
|
previewDeploymentId: text("previewDeploymentId").references(
|
||||||
|
(): AnyPgColumn => previewDeployments.previewDeploymentId,
|
||||||
|
{ onDelete: "cascade" },
|
||||||
|
),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
@@ -50,6 +62,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
|||||||
fields: [deployments.serverId],
|
fields: [deployments.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
|
previewDeployment: one(previewDeployments, {
|
||||||
|
fields: [deployments.previewDeploymentId],
|
||||||
|
references: [previewDeployments.previewDeploymentId],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const schema = createInsertSchema(deployments, {
|
const schema = createInsertSchema(deployments, {
|
||||||
@@ -59,6 +75,7 @@ const schema = createInsertSchema(deployments, {
|
|||||||
applicationId: z.string(),
|
applicationId: z.string(),
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
previewDeploymentId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateDeployment = schema
|
export const apiCreateDeployment = schema
|
||||||
@@ -68,11 +85,24 @@ export const apiCreateDeployment = schema
|
|||||||
logPath: true,
|
logPath: true,
|
||||||
applicationId: true,
|
applicationId: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
previewDeploymentId: true,
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
applicationId: z.string().min(1),
|
applicationId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiCreateDeploymentPreview = schema
|
||||||
|
.pick({
|
||||||
|
title: true,
|
||||||
|
status: true,
|
||||||
|
logPath: true,
|
||||||
|
description: true,
|
||||||
|
previewDeploymentId: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
previewDeploymentId: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiCreateDeploymentCompose = schema
|
export const apiCreateDeploymentCompose = schema
|
||||||
.pick({
|
.pick({
|
||||||
title: true,
|
title: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
type AnyPgColumn,
|
||||||
boolean,
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
pgEnum,
|
pgEnum,
|
||||||
@@ -14,8 +15,13 @@ import { domain } from "../validations/domain";
|
|||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { certificateType } from "./shared";
|
import { certificateType } from "./shared";
|
||||||
|
import { previewDeployments } from "./preview-deployments";
|
||||||
|
|
||||||
export const domainType = pgEnum("domainType", ["compose", "application"]);
|
export const domainType = pgEnum("domainType", [
|
||||||
|
"compose",
|
||||||
|
"application",
|
||||||
|
"preview",
|
||||||
|
]);
|
||||||
|
|
||||||
export const domains = pgTable("domain", {
|
export const domains = pgTable("domain", {
|
||||||
domainId: text("domainId")
|
domainId: text("domainId")
|
||||||
@@ -39,6 +45,10 @@ export const domains = pgTable("domain", {
|
|||||||
() => applications.applicationId,
|
() => applications.applicationId,
|
||||||
{ onDelete: "cascade" },
|
{ onDelete: "cascade" },
|
||||||
),
|
),
|
||||||
|
previewDeploymentId: text("previewDeploymentId").references(
|
||||||
|
(): AnyPgColumn => previewDeployments.previewDeploymentId,
|
||||||
|
{ onDelete: "cascade" },
|
||||||
|
),
|
||||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,6 +61,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({
|
|||||||
fields: [domains.composeId],
|
fields: [domains.composeId],
|
||||||
references: [compose.composeId],
|
references: [compose.composeId],
|
||||||
}),
|
}),
|
||||||
|
previewDeployment: one(previewDeployments, {
|
||||||
|
fields: [domains.previewDeploymentId],
|
||||||
|
references: [previewDeployments.previewDeploymentId],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
||||||
@@ -65,6 +79,7 @@ export const apiCreateDomain = createSchema.pick({
|
|||||||
composeId: true,
|
composeId: true,
|
||||||
serviceName: true,
|
serviceName: true,
|
||||||
domainType: true,
|
domainType: true,
|
||||||
|
previewDeploymentId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiFindDomain = createSchema
|
export const apiFindDomain = createSchema
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ export * from "./github";
|
|||||||
export * from "./gitlab";
|
export * from "./gitlab";
|
||||||
export * from "./server";
|
export * from "./server";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
export * from "./preview-deployments";
|
||||||
74
packages/server/src/db/schema/preview-deployments.ts
Normal file
74
packages/server/src/db/schema/preview-deployments.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { applications } from "./application";
|
||||||
|
import { domains } from "./domain";
|
||||||
|
import { deployments } from "./deployment";
|
||||||
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { generateAppName } from "./utils";
|
||||||
|
import { applicationStatus } from "./shared";
|
||||||
|
|
||||||
|
export const previewDeployments = pgTable("preview_deployments", {
|
||||||
|
previewDeploymentId: text("previewDeploymentId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
branch: text("branch").notNull(),
|
||||||
|
pullRequestId: text("pullRequestId").notNull(),
|
||||||
|
pullRequestNumber: text("pullRequestNumber").notNull(),
|
||||||
|
pullRequestURL: text("pullRequestURL").notNull(),
|
||||||
|
pullRequestTitle: text("pullRequestTitle").notNull(),
|
||||||
|
pullRequestCommentId: text("pullRequestCommentId").notNull(),
|
||||||
|
previewStatus: applicationStatus("previewStatus").notNull().default("idle"),
|
||||||
|
appName: text("appName")
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => generateAppName("preview"))
|
||||||
|
.unique(),
|
||||||
|
applicationId: text("applicationId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => applications.applicationId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
domainId: text("domainId").references(() => domains.domainId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
createdAt: text("createdAt")
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
expiresAt: text("expiresAt"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const previewDeploymentsRelations = relations(
|
||||||
|
previewDeployments,
|
||||||
|
({ one, many }) => ({
|
||||||
|
deployments: many(deployments),
|
||||||
|
domain: one(domains, {
|
||||||
|
fields: [previewDeployments.domainId],
|
||||||
|
references: [domains.domainId],
|
||||||
|
}),
|
||||||
|
application: one(applications, {
|
||||||
|
fields: [previewDeployments.applicationId],
|
||||||
|
references: [applications.applicationId],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createSchema = createInsertSchema(previewDeployments, {
|
||||||
|
applicationId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiCreatePreviewDeployment = createSchema
|
||||||
|
.pick({
|
||||||
|
applicationId: true,
|
||||||
|
domainId: true,
|
||||||
|
branch: true,
|
||||||
|
pullRequestId: true,
|
||||||
|
pullRequestNumber: true,
|
||||||
|
pullRequestURL: true,
|
||||||
|
pullRequestTitle: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
applicationId: z.string().min(1),
|
||||||
|
// deploymentId: z.string().min(1),
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@ export * from "./services/mount";
|
|||||||
export * from "./services/certificate";
|
export * from "./services/certificate";
|
||||||
export * from "./services/redirect";
|
export * from "./services/redirect";
|
||||||
export * from "./services/security";
|
export * from "./services/security";
|
||||||
|
export * from "./services/preview-deployment";
|
||||||
export * from "./services/port";
|
export * from "./services/port";
|
||||||
export * from "./services/redis";
|
export * from "./services/redis";
|
||||||
export * from "./services/compose";
|
export * from "./services/compose";
|
||||||
@@ -40,6 +41,7 @@ export * from "./setup/redis-setup";
|
|||||||
export * from "./setup/server-setup";
|
export * from "./setup/server-setup";
|
||||||
export * from "./setup/setup";
|
export * from "./setup/setup";
|
||||||
export * from "./setup/traefik-setup";
|
export * from "./setup/traefik-setup";
|
||||||
|
export * from "./setup/server-validate";
|
||||||
|
|
||||||
export * from "./utils/backups/index";
|
export * from "./utils/backups/index";
|
||||||
export * from "./utils/backups/mariadb";
|
export * from "./utils/backups/mariadb";
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getCustomGitCloneCommand,
|
getCustomGitCloneCommand,
|
||||||
} from "@dokploy/server/utils/providers/git";
|
} from "@dokploy/server/utils/providers/git";
|
||||||
import {
|
import {
|
||||||
|
authGithub,
|
||||||
cloneGithubRepository,
|
cloneGithubRepository,
|
||||||
getGithubCloneCommand,
|
getGithubCloneCommand,
|
||||||
} from "@dokploy/server/utils/providers/github";
|
} from "@dokploy/server/utils/providers/github";
|
||||||
@@ -40,8 +41,23 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { encodeBase64 } from "../utils/docker/utils";
|
import { encodeBase64 } from "../utils/docker/utils";
|
||||||
import { getDokployUrl } from "./admin";
|
import { getDokployUrl } from "./admin";
|
||||||
import { createDeployment, updateDeploymentStatus } from "./deployment";
|
import {
|
||||||
|
createDeployment,
|
||||||
|
createDeploymentPreview,
|
||||||
|
updateDeploymentStatus,
|
||||||
|
} from "./deployment";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
import {
|
||||||
|
findPreviewDeploymentById,
|
||||||
|
updatePreviewDeployment,
|
||||||
|
} from "./preview-deployment";
|
||||||
|
import {
|
||||||
|
createPreviewDeploymentComment,
|
||||||
|
getIssueComment,
|
||||||
|
issueCommentExists,
|
||||||
|
updateIssueComment,
|
||||||
|
} from "./github";
|
||||||
|
import { type Domain, getDomainHost } from "./domain";
|
||||||
export type Application = typeof applications.$inferSelect;
|
export type Application = typeof applications.$inferSelect;
|
||||||
|
|
||||||
export const createApplication = async (
|
export const createApplication = async (
|
||||||
@@ -100,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => {
|
|||||||
github: true,
|
github: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
server: true,
|
server: true,
|
||||||
|
previewDeployments: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@@ -168,7 +185,10 @@ export const deployApplication = async ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository(application, deployment.logPath);
|
await cloneGithubRepository({
|
||||||
|
...application,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
});
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
} else if (application.sourceType === "gitlab") {
|
} else if (application.sourceType === "gitlab") {
|
||||||
await cloneGitlabRepository(application, deployment.logPath);
|
await cloneGitlabRepository(application, deployment.logPath);
|
||||||
@@ -276,7 +296,11 @@ export const deployRemoteApplication = async ({
|
|||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
command += await getGithubCloneCommand(application, deployment.logPath);
|
command += await getGithubCloneCommand({
|
||||||
|
...application,
|
||||||
|
serverId: application.serverId,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
});
|
||||||
} else if (application.sourceType === "gitlab") {
|
} else if (application.sourceType === "gitlab") {
|
||||||
command += await getGitlabCloneCommand(application, deployment.logPath);
|
command += await getGitlabCloneCommand(application, deployment.logPath);
|
||||||
} else if (application.sourceType === "bitbucket") {
|
} else if (application.sourceType === "bitbucket") {
|
||||||
@@ -348,6 +372,225 @@ export const deployRemoteApplication = async ({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deployPreviewApplication = async ({
|
||||||
|
applicationId,
|
||||||
|
titleLog = "Preview Deployment",
|
||||||
|
descriptionLog = "",
|
||||||
|
previewDeploymentId,
|
||||||
|
}: {
|
||||||
|
applicationId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
previewDeploymentId: string;
|
||||||
|
}) => {
|
||||||
|
const application = await findApplicationById(applicationId);
|
||||||
|
const deployment = await createDeploymentPreview({
|
||||||
|
title: titleLog,
|
||||||
|
description: descriptionLog,
|
||||||
|
previewDeploymentId: previewDeploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewDeployment =
|
||||||
|
await findPreviewDeploymentById(previewDeploymentId);
|
||||||
|
|
||||||
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||||
|
const issueParams = {
|
||||||
|
owner: application?.owner || "",
|
||||||
|
repository: application?.repository || "",
|
||||||
|
issue_number: previewDeployment.pullRequestNumber,
|
||||||
|
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const commentExists = await issueCommentExists({
|
||||||
|
...issueParams,
|
||||||
|
});
|
||||||
|
if (!commentExists) {
|
||||||
|
const result = await createPreviewDeploymentComment({
|
||||||
|
...issueParams,
|
||||||
|
previewDomain,
|
||||||
|
appName: previewDeployment.appName,
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
previewDeploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Pull request comment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||||
|
}
|
||||||
|
const buildingComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"running",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
|
});
|
||||||
|
application.appName = previewDeployment.appName;
|
||||||
|
application.env = application.previewEnv;
|
||||||
|
application.buildArgs = application.previewBuildArgs;
|
||||||
|
|
||||||
|
if (application.sourceType === "github") {
|
||||||
|
await cloneGithubRepository({
|
||||||
|
...application,
|
||||||
|
appName: previewDeployment.appName,
|
||||||
|
branch: previewDeployment.branch,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
});
|
||||||
|
await buildApplication(application, deployment.logPath);
|
||||||
|
}
|
||||||
|
// 4eef09efc46009187d668cf1c25f768d0bde4f91
|
||||||
|
const successComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"success",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
|
});
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
previewStatus: "done",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
|
});
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
previewStatus: "error",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deployRemotePreviewApplication = async ({
|
||||||
|
applicationId,
|
||||||
|
titleLog = "Preview Deployment",
|
||||||
|
descriptionLog = "",
|
||||||
|
previewDeploymentId,
|
||||||
|
}: {
|
||||||
|
applicationId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
previewDeploymentId: string;
|
||||||
|
}) => {
|
||||||
|
const application = await findApplicationById(applicationId);
|
||||||
|
const deployment = await createDeploymentPreview({
|
||||||
|
title: titleLog,
|
||||||
|
description: descriptionLog,
|
||||||
|
previewDeploymentId: previewDeploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewDeployment =
|
||||||
|
await findPreviewDeploymentById(previewDeploymentId);
|
||||||
|
|
||||||
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||||
|
const issueParams = {
|
||||||
|
owner: application?.owner || "",
|
||||||
|
repository: application?.repository || "",
|
||||||
|
issue_number: previewDeployment.pullRequestNumber,
|
||||||
|
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const commentExists = await issueCommentExists({
|
||||||
|
...issueParams,
|
||||||
|
});
|
||||||
|
if (!commentExists) {
|
||||||
|
const result = await createPreviewDeploymentComment({
|
||||||
|
...issueParams,
|
||||||
|
previewDomain,
|
||||||
|
appName: previewDeployment.appName,
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
previewDeploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Pull request comment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||||
|
}
|
||||||
|
const buildingComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"running",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
|
});
|
||||||
|
application.appName = previewDeployment.appName;
|
||||||
|
application.env = application.previewEnv;
|
||||||
|
application.buildArgs = application.previewBuildArgs;
|
||||||
|
|
||||||
|
if (application.serverId) {
|
||||||
|
let command = "set -e;";
|
||||||
|
if (application.sourceType === "github") {
|
||||||
|
command += await getGithubCloneCommand({
|
||||||
|
...application,
|
||||||
|
serverId: application.serverId,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
command += getBuildCommand(application, deployment.logPath);
|
||||||
|
await execAsyncRemote(application.serverId, command);
|
||||||
|
await mechanizeDockerContainer(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
const successComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"success",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
|
});
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
previewStatus: "done",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
|
});
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
previewStatus: "error",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const rebuildRemoteApplication = async ({
|
export const rebuildRemoteApplication = async ({
|
||||||
applicationId,
|
applicationId,
|
||||||
titleLog = "Rebuild deployment",
|
titleLog = "Rebuild deployment",
|
||||||
|
|||||||
@@ -214,7 +214,11 @@ export const deployCompose = async ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
await cloneGithubRepository({
|
||||||
|
...compose,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
type: "compose",
|
||||||
|
});
|
||||||
} else if (compose.sourceType === "gitlab") {
|
} else if (compose.sourceType === "gitlab") {
|
||||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
} else if (compose.sourceType === "bitbucket") {
|
||||||
@@ -314,11 +318,12 @@ export const deployRemoteCompose = async ({
|
|||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
|
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
command += await getGithubCloneCommand(
|
command += await getGithubCloneCommand({
|
||||||
compose,
|
...compose,
|
||||||
deployment.logPath,
|
logPath: deployment.logPath,
|
||||||
true,
|
type: "compose",
|
||||||
);
|
serverId: compose.serverId,
|
||||||
|
});
|
||||||
} else if (compose.sourceType === "gitlab") {
|
} else if (compose.sourceType === "gitlab") {
|
||||||
command += await getGitlabCloneCommand(
|
command += await getGitlabCloneCommand(
|
||||||
compose,
|
compose,
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { db } from "@dokploy/server/db";
|
|||||||
import {
|
import {
|
||||||
type apiCreateDeployment,
|
type apiCreateDeployment,
|
||||||
type apiCreateDeploymentCompose,
|
type apiCreateDeploymentCompose,
|
||||||
|
type apiCreateDeploymentPreview,
|
||||||
type apiCreateDeploymentServer,
|
type apiCreateDeploymentServer,
|
||||||
deployments,
|
deployments,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
type Application,
|
type Application,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
@@ -21,6 +22,11 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
|
|||||||
import { type Server, findServerById } from "./server";
|
import { type Server, findServerById } from "./server";
|
||||||
|
|
||||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||||
|
import {
|
||||||
|
findPreviewDeploymentById,
|
||||||
|
type PreviewDeployment,
|
||||||
|
updatePreviewDeployment,
|
||||||
|
} from "./preview-deployment";
|
||||||
|
|
||||||
export type Deployment = typeof deployments.$inferSelect;
|
export type Deployment = typeof deployments.$inferSelect;
|
||||||
|
|
||||||
@@ -101,6 +107,74 @@ export const createDeployment = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createDeploymentPreview = async (
|
||||||
|
deployment: Omit<
|
||||||
|
typeof apiCreateDeploymentPreview._type,
|
||||||
|
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const previewDeployment = await findPreviewDeploymentById(
|
||||||
|
deployment.previewDeploymentId,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await removeLastTenPreviewDeploymenById(
|
||||||
|
deployment.previewDeploymentId,
|
||||||
|
previewDeployment?.application?.serverId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const appName = `${previewDeployment.appName}`;
|
||||||
|
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
||||||
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
|
const fileName = `${appName}-${formattedDateTime}.log`;
|
||||||
|
const logFilePath = path.join(LOGS_PATH, appName, fileName);
|
||||||
|
|
||||||
|
if (previewDeployment?.application?.serverId) {
|
||||||
|
const server = await findServerById(
|
||||||
|
previewDeployment?.application?.serverId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const command = `
|
||||||
|
mkdir -p ${LOGS_PATH}/${appName};
|
||||||
|
echo "Initializing deployment" >> ${logFilePath};
|
||||||
|
`;
|
||||||
|
|
||||||
|
await execAsyncRemote(server.serverId, command);
|
||||||
|
} else {
|
||||||
|
await fsPromises.mkdir(path.join(LOGS_PATH, appName), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deploymentCreate = await db
|
||||||
|
.insert(deployments)
|
||||||
|
.values({
|
||||||
|
title: deployment.title || "Deployment",
|
||||||
|
status: "running",
|
||||||
|
logPath: logFilePath,
|
||||||
|
description: deployment.description || "",
|
||||||
|
previewDeploymentId: deployment.previewDeploymentId,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return deploymentCreate[0];
|
||||||
|
} catch (error) {
|
||||||
|
await updatePreviewDeployment(deployment.previewDeploymentId, {
|
||||||
|
previewStatus: "error",
|
||||||
|
});
|
||||||
|
console.log(error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const createDeploymentCompose = async (
|
export const createDeploymentCompose = async (
|
||||||
deployment: Omit<
|
deployment: Omit<
|
||||||
typeof apiCreateDeploymentCompose._type,
|
typeof apiCreateDeploymentCompose._type,
|
||||||
@@ -257,6 +331,41 @@ const removeLastTenComposeDeployments = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeLastTenPreviewDeploymenById = async (
|
||||||
|
previewDeploymentId: string,
|
||||||
|
serverId: string | null,
|
||||||
|
) => {
|
||||||
|
const deploymentList = await db.query.deployments.findMany({
|
||||||
|
where: eq(deployments.previewDeploymentId, previewDeploymentId),
|
||||||
|
orderBy: desc(deployments.createdAt),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deploymentList.length > 10) {
|
||||||
|
const deploymentsToDelete = deploymentList.slice(10);
|
||||||
|
if (serverId) {
|
||||||
|
let command = "";
|
||||||
|
for (const oldDeployment of deploymentsToDelete) {
|
||||||
|
const logPath = path.join(oldDeployment.logPath);
|
||||||
|
|
||||||
|
command += `
|
||||||
|
rm -rf ${logPath};
|
||||||
|
`;
|
||||||
|
await removeDeployment(oldDeployment.deploymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
for (const oldDeployment of deploymentsToDelete) {
|
||||||
|
const logPath = path.join(oldDeployment.logPath);
|
||||||
|
if (existsSync(logPath)) {
|
||||||
|
await fsPromises.unlink(logPath);
|
||||||
|
}
|
||||||
|
await removeDeployment(oldDeployment.deploymentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const removeDeployments = async (application: Application) => {
|
export const removeDeployments = async (application: Application) => {
|
||||||
const { appName, applicationId } = application;
|
const { appName, applicationId } = application;
|
||||||
const { LOGS_PATH } = paths(!!application.serverId);
|
const { LOGS_PATH } = paths(!!application.serverId);
|
||||||
@@ -269,6 +378,30 @@ export const removeDeployments = async (application: Application) => {
|
|||||||
await removeDeploymentsByApplicationId(applicationId);
|
await removeDeploymentsByApplicationId(applicationId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeDeploymentsByPreviewDeploymentId = async (
|
||||||
|
previewDeployment: PreviewDeployment,
|
||||||
|
serverId: string | null,
|
||||||
|
) => {
|
||||||
|
const { appName } = previewDeployment;
|
||||||
|
const { LOGS_PATH } = paths(!!serverId);
|
||||||
|
const logsPath = path.join(LOGS_PATH, appName);
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, `rm -rf ${logsPath}`);
|
||||||
|
} else {
|
||||||
|
await removeDirectoryIfExistsContent(logsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(deployments)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
deployments.previewDeploymentId,
|
||||||
|
previewDeployment.previewDeploymentId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
};
|
||||||
|
|
||||||
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
||||||
const { appName } = compose;
|
const { appName } = compose;
|
||||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const getContainersByAppNameMatch = async (
|
|||||||
const command =
|
const command =
|
||||||
appType === "docker-compose"
|
appType === "docker-compose"
|
||||||
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
||||||
: `${cmd} | grep ${appName}`;
|
: `${cmd} | grep '^.*Name: ${appName}'`;
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||||
|
|
||||||
|
|||||||
@@ -134,3 +134,7 @@ export const removeDomainById = async (domainId: string) => {
|
|||||||
|
|
||||||
return result[0];
|
return result[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDomainHost = (domain: Domain) => {
|
||||||
|
return `${domain.https ? "https" : "http"}://${domain.host}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { authGithub } from "../utils/providers/github";
|
||||||
|
import { updatePreviewDeployment } from "./preview-deployment";
|
||||||
|
|
||||||
export type Github = typeof github.$inferSelect;
|
export type Github = typeof github.$inferSelect;
|
||||||
export const createGithub = async (
|
export const createGithub = async (
|
||||||
@@ -72,3 +74,119 @@ export const updateGithub = async (
|
|||||||
.returning()
|
.returning()
|
||||||
.then((response) => response[0]);
|
.then((response) => response[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getIssueComment = (
|
||||||
|
appName: string,
|
||||||
|
status: "success" | "error" | "running" | "initializing",
|
||||||
|
previewDomain: string,
|
||||||
|
) => {
|
||||||
|
let statusMessage = "";
|
||||||
|
if (status === "success") {
|
||||||
|
statusMessage = "✅ Done";
|
||||||
|
} else if (status === "error") {
|
||||||
|
statusMessage = "❌ Failed";
|
||||||
|
} else if (status === "initializing") {
|
||||||
|
statusMessage = "🔄 Building";
|
||||||
|
} else {
|
||||||
|
statusMessage = "🔄 Building";
|
||||||
|
}
|
||||||
|
const finished = `
|
||||||
|
| Name | Status | Preview | Updated (UTC) |
|
||||||
|
|------------|--------------|-------------------------------------|-----------------------|
|
||||||
|
| ${appName} | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} |
|
||||||
|
`;
|
||||||
|
|
||||||
|
return finished;
|
||||||
|
};
|
||||||
|
interface CommentExists {
|
||||||
|
owner: string;
|
||||||
|
repository: string;
|
||||||
|
comment_id: number;
|
||||||
|
githubId: string;
|
||||||
|
}
|
||||||
|
export const issueCommentExists = async ({
|
||||||
|
owner,
|
||||||
|
repository,
|
||||||
|
comment_id,
|
||||||
|
githubId,
|
||||||
|
}: CommentExists) => {
|
||||||
|
const github = await findGithubById(githubId);
|
||||||
|
const octokit = authGithub(github);
|
||||||
|
try {
|
||||||
|
await octokit.rest.issues.getComment({
|
||||||
|
owner: owner || "",
|
||||||
|
repo: repository || "",
|
||||||
|
comment_id: comment_id,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
interface Comment {
|
||||||
|
owner: string;
|
||||||
|
repository: string;
|
||||||
|
issue_number: string;
|
||||||
|
body: string;
|
||||||
|
comment_id: number;
|
||||||
|
githubId: string;
|
||||||
|
}
|
||||||
|
export const updateIssueComment = async ({
|
||||||
|
owner,
|
||||||
|
repository,
|
||||||
|
issue_number,
|
||||||
|
body,
|
||||||
|
comment_id,
|
||||||
|
githubId,
|
||||||
|
}: Comment) => {
|
||||||
|
const github = await findGithubById(githubId);
|
||||||
|
const octokit = authGithub(github);
|
||||||
|
|
||||||
|
await octokit.rest.issues.updateComment({
|
||||||
|
owner: owner || "",
|
||||||
|
repo: repository || "",
|
||||||
|
issue_number: issue_number,
|
||||||
|
body,
|
||||||
|
comment_id: comment_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CommentCreate {
|
||||||
|
appName: string;
|
||||||
|
owner: string;
|
||||||
|
repository: string;
|
||||||
|
issue_number: string;
|
||||||
|
previewDomain: string;
|
||||||
|
githubId: string;
|
||||||
|
previewDeploymentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPreviewDeploymentComment = async ({
|
||||||
|
owner,
|
||||||
|
repository,
|
||||||
|
issue_number,
|
||||||
|
previewDomain,
|
||||||
|
appName,
|
||||||
|
githubId,
|
||||||
|
previewDeploymentId,
|
||||||
|
}: CommentCreate) => {
|
||||||
|
const github = await findGithubById(githubId);
|
||||||
|
const octokit = authGithub(github);
|
||||||
|
|
||||||
|
const runningComment = getIssueComment(
|
||||||
|
appName,
|
||||||
|
"initializing",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
|
||||||
|
const issue = await octokit.rest.issues.createComment({
|
||||||
|
owner: owner || "",
|
||||||
|
repo: repository || "",
|
||||||
|
issue_number: Number.parseInt(issue_number),
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await updatePreviewDeployment(previewDeploymentId, {
|
||||||
|
pullRequestCommentId: `${issue.data.id}`,
|
||||||
|
}).then((response) => response[0]);
|
||||||
|
};
|
||||||
|
|||||||
283
packages/server/src/services/preview-deployment.ts
Normal file
283
packages/server/src/services/preview-deployment.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import {
|
||||||
|
type apiCreatePreviewDeployment,
|
||||||
|
deployments,
|
||||||
|
previewDeployments,
|
||||||
|
} from "@dokploy/server/db/schema";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
|
import { slugify } from "../setup/server-setup";
|
||||||
|
import { findApplicationById } from "./application";
|
||||||
|
import { createDomain } from "./domain";
|
||||||
|
import { generatePassword, generateRandomDomain } from "../templates/utils";
|
||||||
|
import { manageDomain } from "../utils/traefik/domain";
|
||||||
|
import {
|
||||||
|
removeDeployments,
|
||||||
|
removeDeploymentsByPreviewDeploymentId,
|
||||||
|
} from "./deployment";
|
||||||
|
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||||
|
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||||
|
import { removeService } from "../utils/docker/utils";
|
||||||
|
import { authGithub } from "../utils/providers/github";
|
||||||
|
import { getIssueComment, type Github } from "./github";
|
||||||
|
import { findAdminById } from "./admin";
|
||||||
|
|
||||||
|
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||||
|
|
||||||
|
export const findPreviewDeploymentById = async (
|
||||||
|
previewDeploymentId: string,
|
||||||
|
) => {
|
||||||
|
const application = await db.query.previewDeployments.findFirst({
|
||||||
|
where: eq(previewDeployments.previewDeploymentId, previewDeploymentId),
|
||||||
|
with: {
|
||||||
|
domain: true,
|
||||||
|
application: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!application) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Preview Deployment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return application;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findApplicationByPreview = async (applicationId: string) => {
|
||||||
|
const application = await db.query.applications.findFirst({
|
||||||
|
with: {
|
||||||
|
previewDeployments: {
|
||||||
|
where: eq(previewDeployments.applicationId, applicationId),
|
||||||
|
},
|
||||||
|
project: true,
|
||||||
|
domains: true,
|
||||||
|
deployments: true,
|
||||||
|
mounts: true,
|
||||||
|
redirects: true,
|
||||||
|
security: true,
|
||||||
|
ports: true,
|
||||||
|
registry: true,
|
||||||
|
gitlab: true,
|
||||||
|
github: true,
|
||||||
|
bitbucket: true,
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Applicationnot found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return application;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
|
try {
|
||||||
|
const application = await findApplicationByPreview(previewDeploymentId);
|
||||||
|
const previewDeployment =
|
||||||
|
await findPreviewDeploymentById(previewDeploymentId);
|
||||||
|
|
||||||
|
const deployment = await db
|
||||||
|
.delete(previewDeployments)
|
||||||
|
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
application.appName = previewDeployment.appName;
|
||||||
|
const cleanupOperations = [
|
||||||
|
async () =>
|
||||||
|
await removeDeploymentsByPreviewDeploymentId(
|
||||||
|
previewDeployment,
|
||||||
|
application.serverId,
|
||||||
|
),
|
||||||
|
async () =>
|
||||||
|
await removeDirectoryCode(application.appName, application.serverId),
|
||||||
|
async () =>
|
||||||
|
await removeTraefikConfig(application.appName, application.serverId),
|
||||||
|
async () =>
|
||||||
|
await removeService(application?.appName, application.serverId),
|
||||||
|
];
|
||||||
|
for (const operation of cleanupOperations) {
|
||||||
|
try {
|
||||||
|
await operation();
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
return deployment[0];
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to delete this preview deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// testing-tesoitnmg-ddq0ul-preview-ihl44o
|
||||||
|
export const updatePreviewDeployment = async (
|
||||||
|
previewDeploymentId: string,
|
||||||
|
previewDeploymentData: Partial<PreviewDeployment>,
|
||||||
|
) => {
|
||||||
|
const application = await db
|
||||||
|
.update(previewDeployments)
|
||||||
|
.set({
|
||||||
|
...previewDeploymentData,
|
||||||
|
})
|
||||||
|
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return application;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findPreviewDeploymentsByApplicationId = async (
|
||||||
|
applicationId: string,
|
||||||
|
) => {
|
||||||
|
const deploymentsList = await db.query.previewDeployments.findMany({
|
||||||
|
where: eq(previewDeployments.applicationId, applicationId),
|
||||||
|
orderBy: desc(previewDeployments.createdAt),
|
||||||
|
with: {
|
||||||
|
deployments: {
|
||||||
|
orderBy: desc(deployments.createdAt),
|
||||||
|
},
|
||||||
|
domain: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return deploymentsList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPreviewDeployment = async (
|
||||||
|
schema: typeof apiCreatePreviewDeployment._type,
|
||||||
|
) => {
|
||||||
|
const application = await findApplicationById(schema.applicationId);
|
||||||
|
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||||
|
|
||||||
|
const generateDomain = await generateWildcardDomain(
|
||||||
|
application.previewWildcard || "*.traefik.me",
|
||||||
|
appName,
|
||||||
|
application.server?.ipAddress || "",
|
||||||
|
application.project.adminId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const octokit = authGithub(application?.github as Github);
|
||||||
|
|
||||||
|
const runningComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"initializing",
|
||||||
|
generateDomain,
|
||||||
|
);
|
||||||
|
|
||||||
|
const issue = await octokit.rest.issues.createComment({
|
||||||
|
owner: application?.owner || "",
|
||||||
|
repo: application?.repository || "",
|
||||||
|
issue_number: Number.parseInt(schema.pullRequestNumber),
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewDeployment = await db
|
||||||
|
.insert(previewDeployments)
|
||||||
|
.values({
|
||||||
|
...schema,
|
||||||
|
appName: appName,
|
||||||
|
pullRequestCommentId: `${issue.data.id}`,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!previewDeployment) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the preview deployment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDomain = await createDomain({
|
||||||
|
host: generateDomain,
|
||||||
|
path: application.previewPath,
|
||||||
|
port: application.previewPort,
|
||||||
|
https: application.previewHttps,
|
||||||
|
certificateType: application.previewCertificateType,
|
||||||
|
domainType: "preview",
|
||||||
|
previewDeploymentId: previewDeployment.previewDeploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
application.appName = appName;
|
||||||
|
|
||||||
|
await manageDomain(application, newDomain);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(previewDeployments)
|
||||||
|
.set({
|
||||||
|
domainId: newDomain.domainId,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
previewDeployments.previewDeploymentId,
|
||||||
|
previewDeployment.previewDeploymentId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return previewDeployment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findPreviewDeploymentsByPullRequestId = async (
|
||||||
|
pullRequestId: string,
|
||||||
|
) => {
|
||||||
|
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
||||||
|
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return previewDeploymentResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findPreviewDeploymentByApplicationId = async (
|
||||||
|
applicationId: string,
|
||||||
|
pullRequestId: string,
|
||||||
|
) => {
|
||||||
|
const previewDeploymentResult = await db.query.previewDeployments.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(previewDeployments.applicationId, applicationId),
|
||||||
|
eq(previewDeployments.pullRequestId, pullRequestId),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return previewDeploymentResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateWildcardDomain = async (
|
||||||
|
baseDomain: string,
|
||||||
|
appName: string,
|
||||||
|
serverIp: string,
|
||||||
|
adminId: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
if (!baseDomain.startsWith("*.")) {
|
||||||
|
throw new Error('The base domain must start with "*."');
|
||||||
|
}
|
||||||
|
const hash = `${appName}`;
|
||||||
|
if (baseDomain.includes("traefik.me")) {
|
||||||
|
let ip = "";
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
ip = "127.0.0.1";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverIp) {
|
||||||
|
ip = serverIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
const admin = await findAdminById(adminId);
|
||||||
|
ip = admin?.serverIp || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugIp = ip.replaceAll(".", "-");
|
||||||
|
return baseDomain.replace(
|
||||||
|
"*",
|
||||||
|
`${hash}${slugIp === "" ? "" : `-${slugIp}`}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDomain.replace("*", hash);
|
||||||
|
};
|
||||||
@@ -74,25 +74,106 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
|||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
|
set -e;
|
||||||
|
# Thanks to coolify <3
|
||||||
|
|
||||||
|
DOCKER_VERSION=27.0.3
|
||||||
|
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
|
CURRENT_USER=$USER
|
||||||
|
|
||||||
|
echo "Installing requirements for: OS: $OS_TYPE"
|
||||||
|
if [ $EUID != 0 ]; then
|
||||||
|
echo "Please run this script as root or with sudo ❌"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
${validatePorts()}
|
# Check if the OS is manjaro, if so, change it to arch
|
||||||
|
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||||
|
OS_TYPE="arch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||||
|
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||||
|
OS_TYPE="fedora"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is popOS, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "pop" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#Check if the OS is zorin, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "zorin" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||||
|
OS_VERSION="rolling"
|
||||||
|
else
|
||||||
|
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||||
|
*)
|
||||||
|
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "---------------------------------------------"
|
||||||
|
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||||
|
echo "| Docker | $DOCKER_VERSION"
|
||||||
|
echo -e "---------------------------------------------\n"
|
||||||
|
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||||
|
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${installUtilities()}
|
||||||
|
|
||||||
|
echo -e "2. Validating ports. "
|
||||||
|
${validatePorts()}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo -e "3. Installing RClone. "
|
||||||
${installRClone()}
|
${installRClone()}
|
||||||
|
|
||||||
|
echo -e "4. Installing Docker. "
|
||||||
${installDocker()}
|
${installDocker()}
|
||||||
|
|
||||||
|
echo -e "5. Setting up Docker Swarm"
|
||||||
${setupSwarm()}
|
${setupSwarm()}
|
||||||
|
|
||||||
|
echo -e "6. Setting up Network"
|
||||||
${setupNetwork()}
|
${setupNetwork()}
|
||||||
|
|
||||||
|
echo -e "7. Setting up Directories"
|
||||||
${setupMainDirectory()}
|
${setupMainDirectory()}
|
||||||
${setupDirectories()}
|
${setupDirectories()}
|
||||||
|
|
||||||
|
echo -e "8. Setting up Traefik"
|
||||||
${createTraefikConfig()}
|
${createTraefikConfig()}
|
||||||
|
|
||||||
|
echo -e "9. Setting up Middlewares"
|
||||||
${createDefaultMiddlewares()}
|
${createDefaultMiddlewares()}
|
||||||
|
|
||||||
|
echo -e "10. Setting up Traefik Instance"
|
||||||
${createTraefikInstance()}
|
${createTraefikInstance()}
|
||||||
|
|
||||||
|
echo -e "11. Installing Nixpacks"
|
||||||
${installNixpacks()}
|
${installNixpacks()}
|
||||||
|
|
||||||
|
echo -e "12. Installing Buildpacks"
|
||||||
${installBuildpacks()}
|
${installBuildpacks()}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
client.exec(bashCommand, (err, stream) => {
|
client.exec(bashCommand, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
writeStream.write(err);
|
writeStream.write(err);
|
||||||
@@ -204,17 +285,12 @@ const setupNetwork = () => `
|
|||||||
echo "Network dokploy-network already exists ✅"
|
echo "Network dokploy-network already exists ✅"
|
||||||
else
|
else
|
||||||
# Create the dokploy-network if it doesn't exist
|
# Create the dokploy-network if it doesn't exist
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
if docker network create --driver overlay --attachable dokploy-network; then
|
||||||
echo "Network created ✅"
|
echo "Network created ✅"
|
||||||
fi
|
else
|
||||||
`;
|
echo "Failed to create dokploy-network ❌" >&2
|
||||||
|
exit 1
|
||||||
const installDocker = () => `
|
fi
|
||||||
if command_exists docker; then
|
|
||||||
echo "Docker already installed ✅"
|
|
||||||
else
|
|
||||||
echo "Installing Docker ✅"
|
|
||||||
curl -sSL https://get.docker.com | sh -s -- --version 27.2.0
|
|
||||||
fi
|
fi
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -230,6 +306,155 @@ const validatePorts = () => `
|
|||||||
fi
|
fi
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const installUtilities = () => `
|
||||||
|
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
arch)
|
||||||
|
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||||
|
;;
|
||||||
|
alpine)
|
||||||
|
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||||
|
apk update >/dev/null
|
||||||
|
apk add curl wget git jq openssl >/dev/null
|
||||||
|
;;
|
||||||
|
ubuntu | debian | raspbian)
|
||||||
|
apt-get update -y >/dev/null
|
||||||
|
apt-get install -y curl wget git jq openssl >/dev/null
|
||||||
|
;;
|
||||||
|
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||||
|
if [ "$OS_TYPE" = "amzn" ]; then
|
||||||
|
dnf install -y wget git jq openssl >/dev/null
|
||||||
|
else
|
||||||
|
if ! command -v dnf >/dev/null; then
|
||||||
|
yum install -y dnf >/dev/null
|
||||||
|
fi
|
||||||
|
if ! command -v curl >/dev/null; then
|
||||||
|
dnf install -y curl >/dev/null
|
||||||
|
fi
|
||||||
|
dnf install -y wget git jq openssl unzip >/dev/null
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
sles | opensuse-leap | opensuse-tumbleweed)
|
||||||
|
zypper refresh >/dev/null
|
||||||
|
zypper install -y curl wget git jq openssl >/dev/null
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
`;
|
||||||
|
|
||||||
|
const installDocker = () => `
|
||||||
|
|
||||||
|
# Detect if docker is installed via snap
|
||||||
|
if [ -x "$(command -v snap)" ]; then
|
||||||
|
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
|
||||||
|
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
|
||||||
|
echo " - Docker is installed via snap."
|
||||||
|
echo " Please note that Dokploy does not support Docker installed via snap."
|
||||||
|
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "3. Check Docker Installation. "
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
"almalinux")
|
||||||
|
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||||
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
systemctl start docker >/dev/null 2>&1
|
||||||
|
systemctl enable docker >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
"alpine")
|
||||||
|
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||||
|
rc-update add docker default >/dev/null 2>&1
|
||||||
|
service docker start >/dev/null 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||||
|
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"arch")
|
||||||
|
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||||
|
systemctl enable docker.service >/dev/null 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||||
|
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"amzn")
|
||||||
|
dnf install docker -y >/dev/null 2>&1
|
||||||
|
DOCKER_CONFIG=/usr/local/lib/docker
|
||||||
|
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||||
|
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||||
|
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||||
|
systemctl start docker >/dev/null 2>&1
|
||||||
|
systemctl enable docker >/dev/null 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||||
|
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"fedora")
|
||||||
|
if [ -x "$(command -v dnf5)" ]; then
|
||||||
|
# dnf5 is available
|
||||||
|
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
# dnf5 is not available, use dnf
|
||||||
|
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
systemctl start docker >/dev/null 2>&1
|
||||||
|
systemctl enable docker >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||||
|
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||||
|
echo "Please install Docker manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
curl -s https://releases.rancher.com/install-docker/$DOCKER_VERSION.sh | sh 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
|
||||||
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
echo " - Docker installation failed."
|
||||||
|
echo " Maybe your OS is not supported?"
|
||||||
|
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$OS_TYPE" = "rocky" ]; then
|
||||||
|
systemctl start docker >/dev/null 2>&1
|
||||||
|
systemctl enable docker >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OS_TYPE" = "centos" ]; then
|
||||||
|
systemctl start docker >/dev/null 2>&1
|
||||||
|
systemctl enable docker >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
esac
|
||||||
|
echo " - Docker installed successfully."
|
||||||
|
else
|
||||||
|
echo " - Docker is installed."
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
const createTraefikConfig = () => {
|
const createTraefikConfig = () => {
|
||||||
const config = getDefaultServerTraefikConfig();
|
const config = getDefaultServerTraefikConfig();
|
||||||
|
|
||||||
@@ -260,7 +485,12 @@ const createDefaultMiddlewares = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const installRClone = () => `
|
export const installRClone = () => `
|
||||||
curl https://rclone.org/install.sh | sudo bash
|
if command_exists rclone; then
|
||||||
|
echo "RClone already installed ✅"
|
||||||
|
else
|
||||||
|
curl https://rclone.org/install.sh | sudo bash
|
||||||
|
echo "RClone installed successfully ✅"
|
||||||
|
fi
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const createTraefikInstance = () => {
|
export const createTraefikInstance = () => {
|
||||||
@@ -292,7 +522,8 @@ const installNixpacks = () => `
|
|||||||
if command_exists nixpacks; then
|
if command_exists nixpacks; then
|
||||||
echo "Nixpacks already installed ✅"
|
echo "Nixpacks already installed ✅"
|
||||||
else
|
else
|
||||||
VERSION=1.28.1 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
export NIXPACKS_VERSION=1.29.1
|
||||||
|
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||||
echo "Nixpacks version 1.28.1 installed ✅"
|
echo "Nixpacks version 1.28.1 installed ✅"
|
||||||
fi
|
fi
|
||||||
`;
|
`;
|
||||||
|
|||||||
144
packages/server/src/setup/server-validate.ts
Normal file
144
packages/server/src/setup/server-validate.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { Client } from "ssh2";
|
||||||
|
import { findServerById } from "../services/server";
|
||||||
|
|
||||||
|
export const validateDocker = () => `
|
||||||
|
if command_exists docker; then
|
||||||
|
echo "$(docker --version | awk '{print $3}' | sed 's/,//') true"
|
||||||
|
else
|
||||||
|
echo "0.0.0 false"
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const validateRClone = () => `
|
||||||
|
if command_exists rclone; then
|
||||||
|
echo "$(rclone --version | head -n 1 | awk '{print $2}') true"
|
||||||
|
else
|
||||||
|
echo "0.0.0 false"
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const validateSwarm = () => `
|
||||||
|
if docker info --format '{{.Swarm.LocalNodeState}}' | grep -q 'active'; then
|
||||||
|
echo true
|
||||||
|
else
|
||||||
|
echo false
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const validateNixpacks = () => `
|
||||||
|
if command_exists nixpacks; then
|
||||||
|
echo "$(nixpacks --version | awk '{print $2}') true"
|
||||||
|
else
|
||||||
|
echo "0.0.0 false"
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const validateBuildpacks = () => `
|
||||||
|
if command_exists pack; then
|
||||||
|
echo "$(pack --version | awk '{print $1}') true"
|
||||||
|
else
|
||||||
|
echo "0.0.0 false"
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const validateMainDirectory = () => `
|
||||||
|
if [ -d "/etc/dokploy" ]; then
|
||||||
|
echo true
|
||||||
|
else
|
||||||
|
echo false
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const validateDokployNetwork = () => `
|
||||||
|
if docker network ls | grep -q 'dokploy-network'; then
|
||||||
|
echo true
|
||||||
|
else
|
||||||
|
echo false
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const serverValidate = async (serverId: string) => {
|
||||||
|
const client = new Client();
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
if (!server.sshKeyId) {
|
||||||
|
throw new Error("No SSH Key found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
client
|
||||||
|
.once("ready", () => {
|
||||||
|
const bashCommand = `
|
||||||
|
command_exists() {
|
||||||
|
command -v "$@" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerVersionEnabled=$(${validateDocker()})
|
||||||
|
rcloneVersionEnabled=$(${validateRClone()})
|
||||||
|
nixpacksVersionEnabled=$(${validateNixpacks()})
|
||||||
|
buildpacksVersionEnabled=$(${validateBuildpacks()})
|
||||||
|
|
||||||
|
dockerVersion=$(echo $dockerVersionEnabled | awk '{print $1}')
|
||||||
|
dockerEnabled=$(echo $dockerVersionEnabled | awk '{print $2}')
|
||||||
|
|
||||||
|
rcloneVersion=$(echo $rcloneVersionEnabled | awk '{print $1}')
|
||||||
|
rcloneEnabled=$(echo $rcloneVersionEnabled | awk '{print $2}')
|
||||||
|
|
||||||
|
nixpacksVersion=$(echo $nixpacksVersionEnabled | awk '{print $1}')
|
||||||
|
nixpacksEnabled=$(echo $nixpacksVersionEnabled | awk '{print $2}')
|
||||||
|
|
||||||
|
buildpacksVersion=$(echo $buildpacksVersionEnabled | awk '{print $1}')
|
||||||
|
buildpacksEnabled=$(echo $buildpacksVersionEnabled | awk '{print $2}')
|
||||||
|
|
||||||
|
isDokployNetworkInstalled=$(${validateDokployNetwork()})
|
||||||
|
isSwarmInstalled=$(${validateSwarm()})
|
||||||
|
isMainDirectoryInstalled=$(${validateMainDirectory()})
|
||||||
|
|
||||||
|
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
|
||||||
|
`;
|
||||||
|
client.exec(bashCommand, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let output = "";
|
||||||
|
stream
|
||||||
|
.on("close", () => {
|
||||||
|
client.end();
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(output.trim());
|
||||||
|
resolve(result);
|
||||||
|
} catch (parseError) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("data", (data: string) => {
|
||||||
|
output += data;
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data) => {});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
client.end();
|
||||||
|
if (err.level === "client-authentication") {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`SSH connection error: ${err.message}`));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: server.sshKey?.privateKey,
|
||||||
|
timeout: 99999,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@ import { buildHeroku, getHerokuCommand } from "./heroku";
|
|||||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||||
import { buildStatic, getStaticCommand } from "./static";
|
import { buildStatic, getStaticCommand } from "./static";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||||
// HEROKU codeDirectory = where is the path of the code directory
|
// HEROKU codeDirectory = where is the path of the code directory
|
||||||
@@ -33,6 +34,7 @@ export type ApplicationNested = InferResultType<
|
|||||||
project: true;
|
project: true;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const buildApplication = async (
|
export const buildApplication = async (
|
||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const buildNixpacks = async (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName, publishDirectory, serverId } = application;
|
const { env, appName, publishDirectory } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ const setupLocalServer = async (daemonConfig: any) => {
|
|||||||
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||||
|
|
||||||
const setupCommands = [
|
const setupCommands = [
|
||||||
`pkexec sh -c '
|
`sudo sh -c '
|
||||||
cp ${configFile} /etc/docker/daemon.json &&
|
cp ${configFile} /etc/docker/daemon.json &&
|
||||||
mkdir -p /etc/nvidia-container-runtime &&
|
mkdir -p /etc/nvidia-container-runtime &&
|
||||||
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
||||||
@@ -314,7 +314,14 @@ const setupLocalServer = async (daemonConfig: any) => {
|
|||||||
`rm ${configFile}`,
|
`rm ${configFile}`,
|
||||||
].join(" && ");
|
].join(" && ");
|
||||||
|
|
||||||
await execAsync(setupCommands);
|
try {
|
||||||
|
await execAsync(setupCommands);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Setup failed:", error);
|
||||||
|
throw new Error(
|
||||||
|
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
||||||
|
|||||||
@@ -74,11 +74,22 @@ export type ApplicationWithGithub = InferResultType<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
||||||
export const cloneGithubRepository = async (
|
|
||||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
interface CloneGithubRepository {
|
||||||
logPath: string,
|
appName: string;
|
||||||
isCompose = false,
|
owner: string | null;
|
||||||
) => {
|
branch: string | null;
|
||||||
|
githubId: string | null;
|
||||||
|
repository: string | null;
|
||||||
|
logPath: string;
|
||||||
|
type?: "application" | "compose";
|
||||||
|
}
|
||||||
|
export const cloneGithubRepository = async ({
|
||||||
|
logPath,
|
||||||
|
type = "application",
|
||||||
|
...entity
|
||||||
|
}: CloneGithubRepository) => {
|
||||||
|
const isCompose = type === "compose";
|
||||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||||
const { appName, repository, owner, branch, githubId } = entity;
|
const { appName, repository, owner, branch, githubId } = entity;
|
||||||
@@ -145,13 +156,13 @@ export const cloneGithubRepository = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGithubCloneCommand = async (
|
export const getGithubCloneCommand = async ({
|
||||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
logPath,
|
||||||
logPath: string,
|
type = "application",
|
||||||
isCompose = false,
|
...entity
|
||||||
) => {
|
}: CloneGithubRepository & { serverId: string }) => {
|
||||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||||
|
const isCompose = type === "compose";
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
|
|||||||
Reference in New Issue
Block a user