Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfdc73f8d1 | ||
|
|
525a711c55 | ||
|
|
e0a0f97f70 | ||
|
|
45aae520d8 | ||
|
|
c8f14c322e | ||
|
|
334466f28f | ||
|
|
22328eeb83 | ||
|
|
c2201a304e | ||
|
|
f77de1bc52 | ||
|
|
74d6b2d591 | ||
|
|
b7cc3283e2 | ||
|
|
5dfa6c5194 | ||
|
|
59d662396e | ||
|
|
cbfcae4ab6 | ||
|
|
152a54b251 | ||
|
|
1113669bfd | ||
|
|
4d251271b9 | ||
|
|
21263a217c | ||
|
|
3322fdf937 | ||
|
|
b415e2fba1 | ||
|
|
8888a4ce5c | ||
|
|
dd3ba7e836 | ||
|
|
cb6d8fceb4 | ||
|
|
d8641b7b4e | ||
|
|
997a8395b1 | ||
|
|
d2420ed6e8 | ||
|
|
64ada7020a | ||
|
|
faf24dfa25 | ||
|
|
6f4bf428c7 | ||
|
|
a43627d869 | ||
|
|
addd102d39 | ||
|
|
0d85fd0e3c | ||
|
|
86fc59d850 | ||
|
|
3cd3db6828 | ||
|
|
2c6fd0f52b | ||
|
|
5fb682b58d | ||
|
|
b77f330222 | ||
|
|
f0b8f3eaa0 | ||
|
|
36af22630b | ||
|
|
2d53a700f6 | ||
|
|
00eeffee13 | ||
|
|
7a32698031 | ||
|
|
9d834e1a79 | ||
|
|
91819c2488 | ||
|
|
f64392469d | ||
|
|
889e72d21e | ||
|
|
86165d1104 | ||
|
|
54adab16cf | ||
|
|
2e3b0ddcde | ||
|
|
ed0f3cadd6 | ||
|
|
898880634a | ||
|
|
b4e154fb28 | ||
|
|
2e6489d315 | ||
|
|
210fed30a2 | ||
|
|
60521c1025 | ||
|
|
1a496e35c0 | ||
|
|
f37f98aade | ||
|
|
0eb7b3ecb1 | ||
|
|
1a7c602861 | ||
|
|
4706adc0c0 | ||
|
|
85f025c729 | ||
|
|
06005eb333 | ||
|
|
0c01efb249 | ||
|
|
7e9e9dc865 | ||
|
|
b28bf5f9ec | ||
|
|
c071be6ad9 | ||
|
|
059a98c575 | ||
|
|
d2a07195b0 | ||
|
|
4ff1b3c19f | ||
|
|
39abd7e374 | ||
|
|
899d7565f6 | ||
|
|
3cfc2d6cd8 | ||
|
|
817fa91173 | ||
|
|
4d8a4f713d | ||
|
|
4865f4f969 | ||
|
|
0b7feb5483 | ||
|
|
1c139b9503 | ||
|
|
d47b7e62e6 | ||
|
|
33940a345a | ||
|
|
f230bda1f7 | ||
|
|
687524d154 |
@@ -26,7 +26,7 @@ Dokploy include multiples features to make your life easier.
|
||||
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
* **Docker Management**: Easily deploy and manage Docker containers.
|
||||
* **CLI (Soon⌛)**: Manage your applications and databases using the command line.
|
||||
* **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||
* **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="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">
|
||||
|
||||
79
components/dashboard/application/domains/generate-domain.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { GenerateTraefikMe } from "./generate-traefikme";
|
||||
import { GenerateWildCard } from "./generate-wildcard";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const GenerateDomain = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="secondary">
|
||||
Generate Domain
|
||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Generate Domain</DialogTitle>
|
||||
<DialogDescription>
|
||||
Generate Domains for your applications
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-bold">
|
||||
1. Generate TraefikMe Domain
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
This option generates a free domain provided by{" "}
|
||||
<Link
|
||||
href="https://traefik.me"
|
||||
className="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
TraefikMe
|
||||
</Link>
|
||||
. We recommend using this for quick domain testing or if you
|
||||
don't have a domain yet.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-bold">
|
||||
2. Use Wildcard Domain
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
To use this option, you need to set up an 'A' record in your
|
||||
domain provider. For example, create a record for
|
||||
*.yourdomain.com.
|
||||
</div>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<div className="flex flex-row gap-4 w-full">
|
||||
<GenerateTraefikMe applicationId={applicationId} />
|
||||
{/* <GenerateWildCard applicationId={applicationId} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
export const GenerateTraefikMe = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Generate Domain
|
||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to generate a new domain?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will generate a new domain and will be used to access to the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
toast.success("Generated Domain succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to generate Domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { SquareAsterisk } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
export const GenerateWildCard = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Generate Wildcard Domain
|
||||
<SquareAsterisk className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to generate a new wildcard domain?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will generate a new domain and will be used to access to the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
toast.success("Generated Domain succesfully");
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(`Error to generate Domain: ${e.message}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { ExternalLink, GlobeIcon } from "lucide-react";
|
||||
import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,6 +14,7 @@ import { DeleteDomain } from "./delete-domain";
|
||||
import Link from "next/link";
|
||||
import { AddDomain } from "./add-domain";
|
||||
import { UpdateDomain } from "./update-domain";
|
||||
import { GenerateDomain } from "./generate-domain";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
@@ -31,7 +32,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Domains</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -39,11 +40,16 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
)}
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
)}
|
||||
{data && data?.length > 0 && (
|
||||
<GenerateDomain applicationId={applicationId} />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
{data?.length === 0 ? (
|
||||
@@ -53,9 +59,13 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
To access to the application is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
|
||||
<GenerateDomain applicationId={applicationId} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
@@ -26,8 +25,6 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: markRunning } =
|
||||
api.application.markRunning.useMutation();
|
||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||
|
||||
return (
|
||||
@@ -48,24 +45,16 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
toast.success("Deploying Application....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Deploying Application....");
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.message || "Error to deploy Application");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -25,8 +25,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { mutateAsync: markRunning } =
|
||||
api.application.markRunning.useMutation();
|
||||
|
||||
const { mutateAsync } = api.application.redeploy.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
@@ -54,22 +53,14 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
toast.success("Redeploying Application....");
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await mutateAsync({
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Application rebuild succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the application");
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the application");
|
||||
|
||||
@@ -9,14 +9,11 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// import { CancelQueues } from "./cancel-queues";
|
||||
// import { ShowDeployment } from "./show-deployment-compose";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { ShowDeploymentCompose } from "./show-deployment-compose";
|
||||
import { RefreshTokenCompose } from "./refresh-token-compose";
|
||||
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
||||
// import { RefreshToken } from "./refresh-token";//
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
@@ -90,6 +87,11 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="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">
|
||||
|
||||
@@ -25,7 +25,6 @@ export const DeployCompose = ({ composeId }: Props) => {
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const { mutateAsync: markRunning } = api.compose.update.useMutation();
|
||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||
|
||||
return (
|
||||
@@ -44,25 +43,16 @@ export const DeployCompose = ({ composeId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
toast.success("Deploying Compose....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
composeId,
|
||||
composeStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Deploying Compose....");
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
composeId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.message || "Error to deploy Compose");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -25,7 +25,6 @@ export const RedbuildCompose = ({ composeId }: Props) => {
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync: markRunning } = api.compose.update.useMutation();
|
||||
const { mutateAsync } = api.compose.redeploy.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
@@ -53,23 +52,14 @@ export const RedbuildCompose = ({ composeId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
toast.success("Redeploying Compose....");
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
composeStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
await mutateAsync({
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Compose rebuild succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the compose");
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the compose");
|
||||
|
||||
@@ -25,7 +25,6 @@ export const StopCompose = ({ composeId }: Props) => {
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync: markRunning } = api.compose.update.useMutation();
|
||||
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
@@ -47,26 +46,17 @@ export const StopCompose = ({ composeId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
composeStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
await mutateAsync({
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Compose rebuild succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the compose");
|
||||
});
|
||||
});
|
||||
toast.success("Compose stopped succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the compose");
|
||||
toast.error("Error to stop the compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -161,10 +161,6 @@ export const ShowContainers = () => {
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="space-x-2 flex flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -141,7 +141,13 @@ export const DockerMonitoring = ({
|
||||
network: data.network[data.network.length - 1] ?? currentData.network,
|
||||
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
||||
});
|
||||
setAcummulativeData(data);
|
||||
setAcummulativeData({
|
||||
block: data?.block || [],
|
||||
cpu: data?.cpu || [],
|
||||
disk: data?.disk || [],
|
||||
memory: data?.memory || [],
|
||||
network: data?.network || [],
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -54,7 +54,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
||||
<div className="sticky top-0 z-10 flex flex-col gap-4 bg-black p-6 border-b">
|
||||
<div className="sticky top-0 z-10 flex flex-col gap-4 bg-background p-6 border-b">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -76,8 +76,8 @@ export const ShowProjects = () => {
|
||||
project?.compose.length;
|
||||
return (
|
||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||
<Button
|
||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||
size="sm"
|
||||
@@ -85,113 +85,122 @@ export const ShowProjects = () => {
|
||||
>
|
||||
<ExternalLinkIcon className="size-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2">
|
||||
<span className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookIcon className="size-4 text-muted-foreground" />
|
||||
<Link
|
||||
className="text-base font-medium leading-none"
|
||||
href={`/dashboard/project/${project.projectId}`}
|
||||
>
|
||||
{project.name}
|
||||
</Link>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2">
|
||||
<span className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-base font-medium leading-none">
|
||||
{project.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{project.description}
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{project.description}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex self-start space-x-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="px-2">
|
||||
<MoreHorizontalIcon className="size-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[200px] space-y-2">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<div className="flex self-start space-x-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="px-2"
|
||||
>
|
||||
<MoreHorizontalIcon className="size-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[200px] space-y-2">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<UpdateProject projectId={project.projectId} />
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<UpdateProject projectId={project.projectId} />
|
||||
</div>
|
||||
|
||||
{(auth?.rol === "admin" ||
|
||||
user?.canDeleteProjects) && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to delete this project?
|
||||
</AlertDialogTitle>
|
||||
{!emptyServices ? (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
You have active services, please delete
|
||||
them first
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone
|
||||
</AlertDialogDescription>
|
||||
)}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={!emptyServices}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
projectId: project.projectId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Project delete succesfully",
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to delete this project",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
utils.project.all.invalidate();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
{(auth?.rol === "admin" ||
|
||||
user?.canDeleteProjects) && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to delete this project?
|
||||
</AlertDialogTitle>
|
||||
{!emptyServices ? (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
You have active services, please
|
||||
delete them first
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone
|
||||
</AlertDialogDescription>
|
||||
)}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={!emptyServices}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
projectId: project.projectId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Project delete succesfully",
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to delete this project",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
utils.project.all.invalidate();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-4">
|
||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||
<DateTooltip date={project.createdAt}>
|
||||
Created
|
||||
</DateTooltip>
|
||||
<span>
|
||||
{totalServices}{" "}
|
||||
{totalServices === 1 ? "service" : "services"}
|
||||
</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-4">
|
||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||
<DateTooltip date={project.createdAt}>Created</DateTooltip>
|
||||
<span>
|
||||
{totalServices}{" "}
|
||||
{totalServices === 1 ? "service" : "services"}
|
||||
</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const GithubSetup = () => {
|
||||
const [organizationName, setOrganization] = useState<string>("");
|
||||
const { data } = api.admin.one.useQuery();
|
||||
useEffect(() => {
|
||||
const url = document.location.origin;
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
||||
@@ -55,7 +56,7 @@ export const GithubSetup = () => {
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
// JUST FOR TESTING
|
||||
url: "https://webhook.site/b6a167c0-ceb5-4f0c-a257-97c0fd163977",
|
||||
url: `${url}/api/deploy/github`,
|
||||
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
||||
},
|
||||
callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación
|
||||
@@ -92,8 +93,18 @@ export const GithubSetup = () => {
|
||||
</span>
|
||||
<BadgeCheck className="size-4 text-green-700" />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<div className="flex items-end gap-4 flex-wrap">
|
||||
<RemoveGithubApp />
|
||||
{/* <Link
|
||||
href={`https://github.com/settings/apps/${data?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage Github App</span>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipProvider,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
export const RemoveGithubApp = () => {
|
||||
const { refetch } = api.auth.get.useQuery();
|
||||
@@ -22,7 +29,20 @@ export const RemoveGithubApp = () => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">Remove Current Github App</Button>
|
||||
<Button variant="destructive">
|
||||
Remove Current Github App
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
We recommend deleting the GitHub app first, and then removing
|
||||
the current one from here.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
||||
1
drizzle/0017_minor_post.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "deployment" ADD COLUMN "description" text;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL;
|
||||
1
drizzle/0018_careful_killmonger.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "admin" ADD COLUMN "githubWebhookSecret" text;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "7610c85e-c3e4-4a32-8ce9-7f48b298f956",
|
||||
"id": "ec852f38-886a-43b4-9295-73984ed8ef45",
|
||||
"prevId": "2d8d7670-b942-4573-9c44-6e81d2a2fa16",
|
||||
"version": "6",
|
||||
"dialect": "postgresql",
|
||||
@@ -465,7 +465,7 @@
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true
|
||||
},
|
||||
"isRegistered": {
|
||||
"name": "isRegistered",
|
||||
@@ -1585,6 +1585,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "deploymentStatus",
|
||||
|
||||
2632
drizzle/meta/0018_snapshot.json
Normal file
@@ -124,8 +124,15 @@
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "6",
|
||||
"when": 1719109531147,
|
||||
"tag": "0017_yummy_norrin_radd",
|
||||
"when": 1719547174326,
|
||||
"tag": "0017_minor_post",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1719928377858,
|
||||
"tag": "0018_careful_killmonger",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.2.4",
|
||||
"version": "v0.3.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run build-server && npm run build-next",
|
||||
@@ -19,7 +19,7 @@
|
||||
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
||||
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
|
||||
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"lint": "biome lint",
|
||||
"db:seed": "tsx -r dotenv/config ./server/db/seed.ts",
|
||||
"db:clean": "tsx -r dotenv/config ./server/db/reset.ts",
|
||||
@@ -36,10 +36,12 @@
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"@octokit/webhooks": "^13.2.7",
|
||||
"@radix-ui/react-accordion": "1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@@ -113,7 +115,6 @@
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tar-fs": "3.0.5",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"use-resize-observer": "9.1.0",
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
|
||||
29
pages/api/[...trpc].ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { createTRPCContext } from "@/server/api/trpc";
|
||||
import { validateBearerToken } from "@/server/auth/token";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let { session, user } = await validateBearerToken(req);
|
||||
|
||||
if (!session) {
|
||||
const cookieResult = await validateRequest(req, res);
|
||||
session = cookieResult.session;
|
||||
user = cookieResult.user;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return createOpenApiNextHandler({
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
})(req, res);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { updateApplicationStatus } from "@/server/api/services/application";
|
||||
import { db } from "@/server/db";
|
||||
import { applications } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
@@ -33,6 +32,7 @@ export default async function handler(
|
||||
}
|
||||
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
|
||||
const sourceType = application.sourceType;
|
||||
|
||||
@@ -67,14 +67,10 @@ export default async function handler(
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApplicationStatus(
|
||||
application.applicationId as string,
|
||||
"running",
|
||||
);
|
||||
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: application.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
@@ -166,6 +162,37 @@ export const extractCommitMessage = (headers: any, body: any) => {
|
||||
return "NEW CHANGES";
|
||||
};
|
||||
|
||||
export const extractHash = (headers: any, body: any) => {
|
||||
// GitHub
|
||||
if (headers["x-github-event"]) {
|
||||
return body.head_commit ? body.head_commit.id : "";
|
||||
}
|
||||
|
||||
// GitLab
|
||||
if (headers["x-gitlab-event"]) {
|
||||
return (
|
||||
body.checkout_sha ||
|
||||
(body.commits && body.commits.length > 0
|
||||
? body.commits[0].id
|
||||
: "NEW COMMIT")
|
||||
);
|
||||
}
|
||||
|
||||
// Bitbucket
|
||||
if (headers["x-event-key"]?.includes("repo:push")) {
|
||||
return body.push.changes && body.push.changes.length > 0
|
||||
? body.push.changes[0].new.target.hash
|
||||
: "NEW COMMIT";
|
||||
}
|
||||
|
||||
// Gitea
|
||||
if (headers["x-gitea-event"]) {
|
||||
return body.after || "NEW COMMIT";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const extractBranchName = (headers: any, body: any) => {
|
||||
if (headers["x-github-event"] || headers["x-gitea-event"]) {
|
||||
return body?.ref?.replace("refs/heads/", "");
|
||||
|
||||
@@ -4,8 +4,11 @@ import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { extractBranchName, extractCommitMessage } from "../[refreshToken]";
|
||||
import { updateCompose } from "@/server/api/services/compose";
|
||||
import {
|
||||
extractBranchName,
|
||||
extractCommitMessage,
|
||||
extractHash,
|
||||
} from "../[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@@ -34,7 +37,7 @@ export default async function handler(
|
||||
}
|
||||
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
const sourceType = composeResult.sourceType;
|
||||
|
||||
if (sourceType === "github") {
|
||||
@@ -52,15 +55,12 @@ export default async function handler(
|
||||
}
|
||||
|
||||
try {
|
||||
await updateCompose(composeResult.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeResult.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
126
pages/api/deploy/github.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { Webhooks } from "@octokit/webhooks";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { applications, compose } from "@/server/db/schema";
|
||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { findAdmin } from "@/server/api/services/admin";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (!admin) {
|
||||
res.status(200).json({ message: "Could not find admin" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!admin.githubWebhookSecret) {
|
||||
res.status(200).json({ message: "Github Webhook Secret not set" });
|
||||
return;
|
||||
}
|
||||
|
||||
const webhooks = new Webhooks({
|
||||
secret: admin.githubWebhookSecret,
|
||||
});
|
||||
|
||||
const signature = req.headers["x-hub-signature-256"];
|
||||
const github = req.body;
|
||||
|
||||
const verified = await webhooks.verify(
|
||||
JSON.stringify(github),
|
||||
signature as string,
|
||||
);
|
||||
|
||||
if (!verified) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers["x-github-event"] === "ping") {
|
||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers["x-github-event"] !== "push") {
|
||||
res.status(400).json({ message: "We only accept push events" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const branchName = github?.ref?.replace("refs/heads/", "");
|
||||
const repository = github?.repository?.name;
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
|
||||
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),
|
||||
),
|
||||
});
|
||||
|
||||
for (const app of apps) {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
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),
|
||||
),
|
||||
});
|
||||
|
||||
for (const composeApp of composeApps) {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export default async function handler(
|
||||
githubAppName: data.name,
|
||||
githubClientId: data.client_id,
|
||||
githubClientSecret: data.client_secret,
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
githubPrivateKey: data.pem,
|
||||
})
|
||||
.where(eq(admins.authId, authId as string))
|
||||
|
||||
@@ -6,15 +6,32 @@ import type { GetServerSidePropsContext, NextPage } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
import superjson from "superjson";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { data } = api.settings.getOpenApiDocument.useQuery();
|
||||
const [spec, setSpec] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
// Esto solo se ejecutará en el cliente
|
||||
if (data) {
|
||||
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
|
||||
const newSpec = {
|
||||
...data,
|
||||
servers: [{ url: protocolAndHost }],
|
||||
externalDocs: {
|
||||
url: `${protocolAndHost}/settings.getOpenApiDocument`,
|
||||
},
|
||||
};
|
||||
setSpec(newSpec);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white">
|
||||
<SwaggerUI spec={data || {}} />
|
||||
<SwaggerUI spec={spec} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
139
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ dependencies:
|
||||
'@octokit/auth-app':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4
|
||||
'@octokit/webhooks':
|
||||
specifier: ^13.2.7
|
||||
version: 13.2.7
|
||||
'@radix-ui/react-accordion':
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -100,7 +103,7 @@ dependencies:
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/next':
|
||||
specifier: ^10.43.6
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.4)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@trpc/react-query':
|
||||
specifier: ^10.43.6
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -193,10 +196,10 @@ dependencies:
|
||||
version: 3.3.7
|
||||
next:
|
||||
specifier: ^14.1.3
|
||||
version: 14.2.3(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 14.2.4(react-dom@18.2.0)(react@18.2.0)
|
||||
next-themes:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(next@14.2.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 0.2.1(next@14.2.4)(react-dom@18.2.0)(react@18.2.0)
|
||||
node-os-utils:
|
||||
specifier: 1.3.7
|
||||
version: 1.3.7
|
||||
@@ -2059,12 +2062,12 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/env@14.2.3:
|
||||
resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==}
|
||||
/@next/env@14.2.4:
|
||||
resolution: {integrity: sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==}
|
||||
dev: false
|
||||
|
||||
/@next/swc-darwin-arm64@14.2.3:
|
||||
resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==}
|
||||
/@next/swc-darwin-arm64@14.2.4:
|
||||
resolution: {integrity: sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@@ -2072,8 +2075,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-darwin-x64@14.2.3:
|
||||
resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==}
|
||||
/@next/swc-darwin-x64@14.2.4:
|
||||
resolution: {integrity: sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@@ -2081,48 +2084,44 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-gnu@14.2.3:
|
||||
resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==}
|
||||
/@next/swc-linux-arm64-gnu@14.2.4:
|
||||
resolution: {integrity: sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-musl@14.2.3:
|
||||
resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==}
|
||||
/@next/swc-linux-arm64-musl@14.2.4:
|
||||
resolution: {integrity: sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-gnu@14.2.3:
|
||||
resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==}
|
||||
/@next/swc-linux-x64-gnu@14.2.4:
|
||||
resolution: {integrity: sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-musl@14.2.3:
|
||||
resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==}
|
||||
/@next/swc-linux-x64-musl@14.2.4:
|
||||
resolution: {integrity: sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-arm64-msvc@14.2.3:
|
||||
resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==}
|
||||
/@next/swc-win32-arm64-msvc@14.2.4:
|
||||
resolution: {integrity: sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@@ -2130,8 +2129,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-ia32-msvc@14.2.3:
|
||||
resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==}
|
||||
/@next/swc-win32-ia32-msvc@14.2.4:
|
||||
resolution: {integrity: sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
@@ -2139,8 +2138,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-x64-msvc@14.2.3:
|
||||
resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==}
|
||||
/@next/swc-win32-x64-msvc@14.2.4:
|
||||
resolution: {integrity: sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -2608,6 +2607,14 @@ packages:
|
||||
resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==}
|
||||
dev: false
|
||||
|
||||
/@octokit/openapi-types@22.2.0:
|
||||
resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==}
|
||||
dev: false
|
||||
|
||||
/@octokit/openapi-webhooks-types@8.2.1:
|
||||
resolution: {integrity: sha512-msAU1oTSm0ZmvAE0xDemuF4tVs5i0xNnNGtNmr4EuATi+1Rn8cZDetj6NXioSf5LwnxEc209COa/WOSbjuhLUA==}
|
||||
dev: false
|
||||
|
||||
/@octokit/plugin-paginate-graphql@4.0.1(@octokit/core@5.1.0):
|
||||
resolution: {integrity: sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -2669,6 +2676,13 @@ packages:
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/@octokit/request-error@6.1.1:
|
||||
resolution: {integrity: sha512-1mw1gqT3fR/WFvnoVpY/zUM2o/XkMs/2AszUUG9I69xn0JFLv6PGkPhNk5lbfvROs79wiS0bqiJNxfCZcRJJdg==}
|
||||
engines: {node: '>= 18'}
|
||||
dependencies:
|
||||
'@octokit/types': 13.5.0
|
||||
dev: false
|
||||
|
||||
/@octokit/request@8.2.0:
|
||||
resolution: {integrity: sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -2685,11 +2699,22 @@ packages:
|
||||
'@octokit/openapi-types': 20.0.0
|
||||
dev: false
|
||||
|
||||
/@octokit/types@13.5.0:
|
||||
resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==}
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 22.2.0
|
||||
dev: false
|
||||
|
||||
/@octokit/webhooks-methods@4.1.0:
|
||||
resolution: {integrity: sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==}
|
||||
engines: {node: '>= 18'}
|
||||
dev: false
|
||||
|
||||
/@octokit/webhooks-methods@5.1.0:
|
||||
resolution: {integrity: sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==}
|
||||
engines: {node: '>= 18'}
|
||||
dev: false
|
||||
|
||||
/@octokit/webhooks-types@7.3.2:
|
||||
resolution: {integrity: sha512-JWOoOgtWTFnTSAamPXXyjTY5/apttvNxF+vPBnwdSu5cj5snrd7FO0fyw4+wTXy8fHduq626JjhO+TwCyyA6vA==}
|
||||
dev: false
|
||||
@@ -2704,6 +2729,16 @@ packages:
|
||||
aggregate-error: 3.1.0
|
||||
dev: false
|
||||
|
||||
/@octokit/webhooks@13.2.7:
|
||||
resolution: {integrity: sha512-sPHCyi9uZuCs1gg0yF53FFocM+GsiiBEhQQV/itGzzQ8gjyv2GMJ1YvgdDY4lC0ePZeiV3juEw4GbS6w1VHhRw==}
|
||||
engines: {node: '>= 18'}
|
||||
dependencies:
|
||||
'@octokit/openapi-webhooks-types': 8.2.1
|
||||
'@octokit/request-error': 6.1.1
|
||||
'@octokit/webhooks-methods': 5.1.0
|
||||
aggregate-error: 5.0.0
|
||||
dev: false
|
||||
|
||||
/@pkgjs/parseargs@0.11.0:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -4952,7 +4987,7 @@ packages:
|
||||
'@trpc/server': 10.45.2
|
||||
dev: false
|
||||
|
||||
/@trpc/next@10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.3)(react-dom@18.2.0)(react@18.2.0):
|
||||
/@trpc/next@10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.4)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-RSORmfC+/nXdmRY1pQ0AalsVgSzwNAFbZLYHiTvPM5QQ8wmMEHilseCYMXpu0se/TbPt9zVR6Ka2d7O6zxKkXg==}
|
||||
peerDependencies:
|
||||
'@tanstack/react-query': ^4.18.0
|
||||
@@ -4967,7 +5002,7 @@ packages:
|
||||
'@trpc/client': 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@trpc/server': 10.45.2
|
||||
next: 14.2.3(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.2.4(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
@@ -5570,6 +5605,14 @@ packages:
|
||||
indent-string: 5.0.0
|
||||
dev: false
|
||||
|
||||
/aggregate-error@5.0.0:
|
||||
resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
clean-stack: 5.2.0
|
||||
indent-string: 5.0.0
|
||||
dev: false
|
||||
|
||||
/ajv-formats@2.1.1(ajv@8.14.0):
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -6076,6 +6119,13 @@ packages:
|
||||
escape-string-regexp: 5.0.0
|
||||
dev: false
|
||||
|
||||
/clean-stack@5.2.0:
|
||||
resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
escape-string-regexp: 5.0.0
|
||||
dev: false
|
||||
|
||||
/cli-boxes@3.0.0:
|
||||
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8104,14 +8154,14 @@ packages:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
dev: false
|
||||
|
||||
/next-themes@0.2.1(next@14.2.3)(react-dom@18.2.0)(react@18.2.0):
|
||||
/next-themes@0.2.1(next@14.2.4)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
||||
peerDependencies:
|
||||
next: '*'
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
dependencies:
|
||||
next: 14.2.3(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.2.4(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
@@ -8120,8 +8170,8 @@ packages:
|
||||
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
|
||||
dev: true
|
||||
|
||||
/next@14.2.3(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==}
|
||||
/next@14.2.4(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -8138,7 +8188,7 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@next/env': 14.2.3
|
||||
'@next/env': 14.2.4
|
||||
'@swc/helpers': 0.5.5
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001598
|
||||
@@ -8148,15 +8198,15 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
styled-jsx: 5.1.1(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 14.2.3
|
||||
'@next/swc-darwin-x64': 14.2.3
|
||||
'@next/swc-linux-arm64-gnu': 14.2.3
|
||||
'@next/swc-linux-arm64-musl': 14.2.3
|
||||
'@next/swc-linux-x64-gnu': 14.2.3
|
||||
'@next/swc-linux-x64-musl': 14.2.3
|
||||
'@next/swc-win32-arm64-msvc': 14.2.3
|
||||
'@next/swc-win32-ia32-msvc': 14.2.3
|
||||
'@next/swc-win32-x64-msvc': 14.2.3
|
||||
'@next/swc-darwin-arm64': 14.2.4
|
||||
'@next/swc-darwin-x64': 14.2.4
|
||||
'@next/swc-linux-arm64-gnu': 14.2.4
|
||||
'@next/swc-linux-arm64-musl': 14.2.4
|
||||
'@next/swc-linux-x64-gnu': 14.2.4
|
||||
'@next/swc-linux-x64-musl': 14.2.4
|
||||
'@next/swc-win32-arm64-msvc': 14.2.4
|
||||
'@next/swc-win32-ia32-msvc': 14.2.4
|
||||
'@next/swc-win32-x64-msvc': 14.2.4
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
@@ -8551,7 +8601,7 @@ packages:
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
source-map-js: 1.2.0
|
||||
dev: false
|
||||
|
||||
/postcss@8.4.35:
|
||||
@@ -9404,7 +9454,6 @@ packages:
|
||||
/source-map-js@1.2.0:
|
||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
|
||||
BIN
public/templates/appsmith.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/templates/baserow.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/templates/directus.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/templates/documenso.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/templates/excalidraw.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/templates/ghost.jpeg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/templates/glitchtip.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
70
public/templates/grafana.svg
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 142.5 145.6" style="enable-background:new 0 0 142.5 145.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#565656;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M28.7,131.5c-0.3,7.9-6.6,14.1-14.4,14.1C6.1,145.6,0,139,0,130.9s6.6-14.7,14.7-14.7c3.6,0,7.2,1.6,10.2,4.4
|
||||
l-2.3,2.9c-2.3-2-5.1-3.4-7.9-3.4c-5.9,0-10.8,4.8-10.8,10.8c0,6.1,4.6,10.8,10.4,10.8c5.2,0,9.3-3.8,10.2-8.8H12.6v-3.5h16.1
|
||||
V131.5z"/>
|
||||
<path class="st0" d="M42.3,129.5h-2.2c-2.4,0-4.4,2-4.4,4.4v11.4h-3.9v-19.6H35v1.6c1.1-1.1,2.7-1.6,4.6-1.6h4.2L42.3,129.5z"/>
|
||||
<path class="st0" d="M63.7,145.3h-3.4v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4V145.3z M59.7,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||
C57.1,141.2,59.1,139.3,59.7,137z"/>
|
||||
<path class="st0" d="M71.5,124.7v1.1h6.2v3.4h-6.2v16.1h-3.8v-20.5c0-4.3,3.1-6.8,7-6.8h4.7l-1.6,3.7h-3.1
|
||||
C72.9,121.6,71.5,123,71.5,124.7z"/>
|
||||
<path class="st0" d="M98.5,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H98.5z M94.5,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||
C92,141.2,93.9,139.3,94.5,137z"/>
|
||||
<path class="st0" d="M119.4,133.8v11.5h-3.9v-11.6c0-2.4-2-4.4-4.4-4.4c-2.5,0-4.4,2-4.4,4.4v11.6h-3.9v-19.6h3.2v1.7
|
||||
c1.4-1.3,3.3-2,5.2-2C115.8,125.5,119.4,129.2,119.4,133.8z"/>
|
||||
<path class="st0" d="M142.4,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H142.4z M138.4,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||
C135.9,141.2,137.8,139.3,138.4,137z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="71.25" y1="10.4893" x2="71.25" y2="113.3415" gradientTransform="matrix(1 0 0 -1 0 148.6)">
|
||||
<stop offset="0" style="stop-color:#FCEE1F"/>
|
||||
<stop offset="1" style="stop-color:#F15B2A"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M122.9,49.9c-0.2-1.9-0.5-4.1-1.1-6.5c-0.6-2.4-1.6-5-2.9-7.8c-1.4-2.7-3.1-5.6-5.4-8.3
|
||||
c-0.9-1.1-1.9-2.1-2.9-3.2c1.6-6.3-1.9-11.8-1.9-11.8c-6.1-0.4-9.9,1.9-11.3,2.9c-0.2-0.1-0.5-0.2-0.7-0.3c-1-0.4-2.1-0.8-3.2-1.2
|
||||
c-1.1-0.3-2.2-0.7-3.3-0.9c-1.1-0.3-2.3-0.5-3.5-0.7c-0.2,0-0.4-0.1-0.6-0.1C83.5,3.6,75.9,0,75.9,0c-8.7,5.6-10.4,13.1-10.4,13.1
|
||||
s0,0.2-0.1,0.4c-0.5,0.1-0.9,0.3-1.4,0.4c-0.6,0.2-1.3,0.4-1.9,0.7c-0.6,0.3-1.3,0.5-1.9,0.8c-1.3,0.6-2.5,1.2-3.8,1.9
|
||||
c-1.2,0.7-2.4,1.4-3.5,2.2c-0.2-0.1-0.3-0.2-0.3-0.2c-11.7-4.5-22.1,0.9-22.1,0.9c-0.9,12.5,4.7,20.3,5.8,21.7
|
||||
c-0.3,0.8-0.5,1.5-0.8,2.3c-0.9,2.8-1.5,5.7-1.9,8.7c-0.1,0.4-0.1,0.9-0.2,1.3c-10.8,5.3-14,16.3-14,16.3c9,10.4,19.6,11,19.6,11
|
||||
l0,0c1.3,2.4,2.9,4.7,4.6,6.8c0.7,0.9,1.5,1.7,2.3,2.6c-3.3,9.4,0.5,17.3,0.5,17.3c10.1,0.4,16.7-4.4,18.1-5.5c1,0.3,2,0.6,3,0.9
|
||||
c3.1,0.8,6.3,1.3,9.4,1.4c0.8,0,1.6,0,2.4,0h0.4H80h0.5H81l0,0c4.7,6.8,13.1,7.7,13.1,7.7c5.9-6.3,6.3-12.4,6.3-13.8l0,0
|
||||
c0,0,0,0,0-0.1s0-0.2,0-0.2l0,0c0-0.1,0-0.2,0-0.3c1.2-0.9,2.4-1.8,3.6-2.8c2.4-2.1,4.4-4.6,6.2-7.2c0.2-0.2,0.3-0.5,0.5-0.7
|
||||
c6.7,0.4,11.4-4.2,11.4-4.2c-1.1-7-5.1-10.4-5.9-11l0,0c0,0,0,0-0.1-0.1l-0.1-0.1l0,0l-0.1-0.1c0-0.4,0.1-0.8,0.1-1.3
|
||||
c0.1-0.8,0.1-1.5,0.1-2.3v-0.6v-0.3v-0.1c0-0.2,0-0.1,0-0.2v-0.5v-0.6c0-0.2,0-0.4,0-0.6s0-0.4-0.1-0.6l-0.1-0.6l-0.1-0.6
|
||||
c-0.1-0.8-0.3-1.5-0.4-2.3c-0.7-3-1.9-5.9-3.4-8.4c-1.6-2.6-3.5-4.8-5.7-6.8c-2.2-1.9-4.6-3.5-7.2-4.6c-2.6-1.2-5.2-1.9-7.9-2.2
|
||||
c-1.3-0.2-2.7-0.2-4-0.2h-0.5h-0.1h-0.2h-0.2h-0.5c-0.2,0-0.4,0-0.5,0c-0.7,0.1-1.4,0.2-2,0.3c-2.7,0.5-5.2,1.5-7.4,2.8
|
||||
c-2.2,1.3-4.1,3-5.7,4.9s-2.8,3.9-3.6,6.1c-0.8,2.1-1.3,4.4-1.4,6.5c0,0.5,0,1.1,0,1.6c0,0.1,0,0.3,0,0.4v0.4c0,0.3,0,0.5,0.1,0.8
|
||||
c0.1,1.1,0.3,2.1,0.6,3.1c0.6,2,1.5,3.8,2.7,5.4s2.5,2.8,4,3.8s3,1.7,4.6,2.2c1.6,0.5,3.1,0.7,4.5,0.6c0.2,0,0.4,0,0.5,0
|
||||
c0.1,0,0.2,0,0.3,0s0.2,0,0.3,0c0.2,0,0.3,0,0.5,0h0.1h0.1c0.1,0,0.2,0,0.3,0c0.2,0,0.4-0.1,0.5-0.1c0.2,0,0.3-0.1,0.5-0.1
|
||||
c0.3-0.1,0.7-0.2,1-0.3c0.6-0.2,1.2-0.5,1.8-0.7c0.6-0.3,1.1-0.6,1.5-0.9c0.1-0.1,0.3-0.2,0.4-0.3c0.5-0.4,0.6-1.1,0.2-1.6
|
||||
c-0.4-0.4-1-0.5-1.5-0.3C88,74,87.9,74,87.7,74.1c-0.4,0.2-0.9,0.4-1.3,0.5c-0.5,0.1-1,0.3-1.5,0.4c-0.3,0-0.5,0.1-0.8,0.1
|
||||
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.3,0-0.4,0s-0.3,0-0.4,0c-0.2,0-0.3,0-0.5,0c0,0-0.1,0,0,0h-0.1h-0.1c-0.1,0-0.1,0-0.2,0
|
||||
s-0.3,0-0.4-0.1c-1.1-0.2-2.3-0.5-3.4-1c-1.1-0.5-2.2-1.2-3.1-2.1c-1-0.9-1.8-1.9-2.5-3.1c-0.7-1.2-1.1-2.5-1.3-3.8
|
||||
c-0.1-0.7-0.2-1.4-0.1-2.1c0-0.2,0-0.4,0-0.6c0,0.1,0,0,0,0v-0.1v-0.1c0-0.1,0-0.2,0-0.3c0-0.4,0.1-0.7,0.2-1.1c0.5-3,2-5.9,4.3-8.1
|
||||
c0.6-0.6,1.2-1.1,1.9-1.5c0.7-0.5,1.4-0.9,2.1-1.2c0.7-0.3,1.5-0.6,2.3-0.8s1.6-0.4,2.4-0.4c0.4,0,0.8-0.1,1.2-0.1
|
||||
c0.1,0,0.2,0,0.3,0h0.3h0.2c0.1,0,0,0,0,0h0.1h0.3c0.9,0.1,1.8,0.2,2.6,0.4c1.7,0.4,3.4,1,5,1.9c3.2,1.8,5.9,4.5,7.5,7.8
|
||||
c0.8,1.6,1.4,3.4,1.7,5.3c0.1,0.5,0.1,0.9,0.2,1.4v0.3V66c0,0.1,0,0.2,0,0.3c0,0.1,0,0.2,0,0.3v0.3v0.3c0,0.2,0,0.6,0,0.8
|
||||
c0,0.5-0.1,1-0.1,1.5c-0.1,0.5-0.1,1-0.2,1.5s-0.2,1-0.3,1.5c-0.2,1-0.6,1.9-0.9,2.9c-0.7,1.9-1.7,3.7-2.9,5.3
|
||||
c-2.4,3.3-5.7,6-9.4,7.7c-1.9,0.8-3.8,1.5-5.8,1.8c-1,0.2-2,0.3-3,0.3H81h-0.2h-0.3H80h-0.3c0.1,0,0,0,0,0h-0.1
|
||||
c-0.5,0-1.1,0-1.6-0.1c-2.2-0.2-4.3-0.6-6.4-1.2c-2.1-0.6-4.1-1.4-6-2.4c-3.8-2-7.2-4.9-9.9-8.2c-1.3-1.7-2.5-3.5-3.5-5.4
|
||||
s-1.7-3.9-2.3-5.9c-0.6-2-0.9-4.1-1-6.2v-0.4v-0.1v-0.1v-0.2V60v-0.1v-0.1v-0.2v-0.5V59l0,0v-0.2c0-0.3,0-0.5,0-0.8
|
||||
c0-1,0.1-2.1,0.3-3.2c0.1-1.1,0.3-2.1,0.5-3.2c0.2-1.1,0.5-2.1,0.8-3.2c0.6-2.1,1.3-4.1,2.2-6c1.8-3.8,4.1-7.2,6.8-9.9
|
||||
c0.7-0.7,1.4-1.3,2.2-1.9c0.3-0.3,1-0.9,1.8-1.4c0.8-0.5,1.6-1,2.5-1.4c0.4-0.2,0.8-0.4,1.3-0.6c0.2-0.1,0.4-0.2,0.7-0.3
|
||||
c0.2-0.1,0.4-0.2,0.7-0.3c0.9-0.4,1.8-0.7,2.7-1c0.2-0.1,0.5-0.1,0.7-0.2c0.2-0.1,0.5-0.1,0.7-0.2c0.5-0.1,0.9-0.2,1.4-0.4
|
||||
c0.2-0.1,0.5-0.1,0.7-0.2c0.2,0,0.5-0.1,0.7-0.1c0.2,0,0.5-0.1,0.7-0.1l0.4-0.1l0.4-0.1c0.2,0,0.5-0.1,0.7-0.1
|
||||
c0.3,0,0.5-0.1,0.8-0.1c0.2,0,0.6-0.1,0.8-0.1c0.2,0,0.3,0,0.5-0.1h0.3h0.2h0.2c0.3,0,0.5,0,0.8-0.1h0.4c0,0,0.1,0,0,0h0.1h0.2
|
||||
c0.2,0,0.5,0,0.7,0c0.9,0,1.8,0,2.7,0c1.8,0.1,3.6,0.3,5.3,0.6c3.4,0.6,6.7,1.7,9.6,3.2c2.9,1.4,5.6,3.2,7.8,5.1
|
||||
c0.1,0.1,0.3,0.2,0.4,0.4c0.1,0.1,0.3,0.2,0.4,0.4c0.3,0.2,0.5,0.5,0.8,0.7c0.3,0.2,0.5,0.5,0.8,0.7c0.2,0.3,0.5,0.5,0.7,0.8
|
||||
c1,1,1.9,2.1,2.7,3.1c1.6,2.1,2.9,4.2,3.9,6.2c0.1,0.1,0.1,0.2,0.2,0.4c0.1,0.1,0.1,0.2,0.2,0.4s0.2,0.5,0.4,0.7
|
||||
c0.1,0.2,0.2,0.5,0.3,0.7c0.1,0.2,0.2,0.5,0.3,0.7c0.4,0.9,0.7,1.8,1,2.7c0.5,1.4,0.8,2.6,1.1,3.6c0.1,0.4,0.5,0.7,0.9,0.7
|
||||
c0.5,0,0.8-0.4,0.8-0.9C123,52.7,123,51.4,122.9,49.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/templates/meilisearch.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/templates/metabase.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/templates/minio.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/templates/n8n.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/templates/nocodb.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
public/templates/odoo.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
public/templates/open-webui.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/templates/phpmyadmin.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
public/templates/rocketchat.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/templates/uptime-kuma.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
public/templates/wordpress.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
@@ -162,6 +162,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Rebuild deployment",
|
||||
descriptionLog: "",
|
||||
type: "redeploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
@@ -294,6 +295,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
descriptionLog: "",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
|
||||
@@ -138,6 +138,7 @@ export const composeRouter = createTRPCRouter({
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: "",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
@@ -156,6 +157,7 @@ export const composeRouter = createTRPCRouter({
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: "",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
createDomain,
|
||||
findDomainById,
|
||||
findDomainsByApplicationId,
|
||||
generateDomain,
|
||||
generateWildcard,
|
||||
removeDomainById,
|
||||
updateDomainById,
|
||||
} from "../services/domain";
|
||||
@@ -35,6 +37,16 @@ export const domainRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
return await findDomainsByApplicationId(input.applicationId);
|
||||
}),
|
||||
generateDomain: protectedProcedure
|
||||
.input(apiFindDomainByApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
return generateDomain(input);
|
||||
}),
|
||||
generateWildcard: protectedProcedure
|
||||
.input(apiFindDomainByApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
return generateWildcard(input);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
@@ -248,7 +248,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
getOpenApiDocument: protectedProcedure.query(
|
||||
async ({ ctx }): Promise<unknown> => {
|
||||
const protocol = ctx.req.headers["x-forwarded-proto"];
|
||||
const url = `${protocol}://${ctx.req.headers.host}/api/trpc`;
|
||||
const url = `${protocol}://${ctx.req.headers.host}/api`;
|
||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: "tRPC OpenAPI",
|
||||
version: "1.0.0",
|
||||
|
||||
@@ -130,15 +130,18 @@ export const updateApplicationStatus = async (
|
||||
export const deployApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const admin = await findAdmin();
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -173,14 +176,17 @@ export const deployApplication = async ({
|
||||
export const rebuildApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -134,15 +134,18 @@ export const updateCompose = async (
|
||||
export const deployCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const admin = await findAdmin();
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -170,14 +173,17 @@ export const deployCompose = async ({
|
||||
export const rebuildCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const createDeployment = async (
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@@ -100,6 +101,7 @@ export const createDeploymentCompose = async (
|
||||
.values({
|
||||
composeId: deployment.composeId,
|
||||
title: deployment.title || "Deployment",
|
||||
description: deployment.description || "",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
})
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateDomain, domains } from "@/server/db/schema";
|
||||
import {
|
||||
type apiCreateDomain,
|
||||
type apiFindDomainByApplication,
|
||||
domains,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findApplicationById } from "./application";
|
||||
import { manageDomain } from "@/server/utils/traefik/domain";
|
||||
import { findAdmin } from "./admin";
|
||||
import { generateRandomDomain } from "@/templates/utils";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
@@ -29,6 +35,58 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
await manageDomain(application, domain);
|
||||
});
|
||||
};
|
||||
|
||||
export const generateDomain = async (
|
||||
input: typeof apiFindDomainByApplication._type,
|
||||
) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
const admin = await findAdmin();
|
||||
const domain = await createDomain({
|
||||
applicationId: application.applicationId,
|
||||
host: generateRandomDomain({
|
||||
serverIp: admin.serverIp || "",
|
||||
projectName: application.appName,
|
||||
}),
|
||||
port: 3000,
|
||||
certificateType: "none",
|
||||
https: false,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
export const generateWildcard = async (
|
||||
input: typeof apiFindDomainByApplication._type,
|
||||
) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (!admin.host) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "We need a host to generate a wildcard domain",
|
||||
});
|
||||
}
|
||||
const domain = await createDomain({
|
||||
applicationId: application.applicationId,
|
||||
host: generateWildcardDomain(application.appName, admin.host || ""),
|
||||
port: 3000,
|
||||
certificateType: "none",
|
||||
https: false,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
export const generateWildcardDomain = (
|
||||
appName: string,
|
||||
serverDomain: string,
|
||||
) => {
|
||||
return `${appName}-${serverDomain}`;
|
||||
};
|
||||
|
||||
export const findDomainById = async (domainId: string) => {
|
||||
const domain = await db.query.domains.findFirst({
|
||||
where: eq(domains.domainId, domainId),
|
||||
|
||||
@@ -33,8 +33,10 @@ export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||
});
|
||||
}
|
||||
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
if (newRegistry.registryType === "cloud") {
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
}
|
||||
|
||||
return newRegistry;
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ export const admins = pgTable("admin", {
|
||||
githubClientSecret: text("githubClientSecret"),
|
||||
githubInstallationId: text("githubInstallationId"),
|
||||
githubPrivateKey: text("githubPrivateKey"),
|
||||
githubWebhookSecret: text("githubWebhookSecret"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
|
||||
@@ -18,6 +18,7 @@ export const deployments = pgTable("deployment", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
status: deploymentStatus("status").default("running"),
|
||||
logPath: text("logPath").notNull(),
|
||||
applicationId: text("applicationId").references(
|
||||
@@ -49,6 +50,7 @@ const schema = createInsertSchema(deployments, {
|
||||
logPath: z.string().min(1),
|
||||
applicationId: z.string(),
|
||||
composeId: z.string(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
@@ -57,6 +59,7 @@ export const apiCreateDeployment = schema
|
||||
status: true,
|
||||
logPath: true,
|
||||
applicationId: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
@@ -68,6 +71,7 @@ export const apiCreateDeploymentCompose = schema
|
||||
status: true,
|
||||
logPath: true,
|
||||
composeId: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
composeId: z.string().min(1),
|
||||
|
||||
@@ -2,20 +2,27 @@ import { type Job, Worker } from "bullmq";
|
||||
import {
|
||||
deployApplication,
|
||||
rebuildApplication,
|
||||
updateApplicationStatus,
|
||||
} from "../api/services/application";
|
||||
import { myQueue, redisConfig } from "./queueSetup";
|
||||
import { deployCompose, rebuildCompose } from "../api/services/compose";
|
||||
import {
|
||||
deployCompose,
|
||||
rebuildCompose,
|
||||
updateCompose,
|
||||
} from "../api/services/compose";
|
||||
|
||||
type DeployJob =
|
||||
| {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application";
|
||||
}
|
||||
| {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "compose";
|
||||
};
|
||||
@@ -27,27 +34,35 @@ export const deploymentWorker = new Worker(
|
||||
async (job: Job<DeploymentJob>) => {
|
||||
try {
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
await updateCompose(job.data.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,9 @@ export const buildMariadb = async (mariadb: MariadbWithMounts) => {
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
|
||||
@@ -63,6 +63,9 @@ export const buildMongo = async (mongo: MongoWithMounts) => {
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
|
||||
@@ -69,6 +69,9 @@ export const buildMysql = async (mysql: MysqlWithMounts) => {
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
|
||||
@@ -63,6 +63,9 @@ export const buildPostgres = async (postgres: PostgresWithMounts) => {
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
|
||||
@@ -50,17 +50,19 @@ export const buildRedis = async (redis: RedisWithMounts) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Command: ["/bin/sh"],
|
||||
Args: [
|
||||
"-c",
|
||||
command ? command : `redis-server --requirepass ${databasePassword}`,
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
|
||||
@@ -64,8 +64,8 @@ export const containerExists = async (containerName: string) => {
|
||||
export const stopService = async (appName: string) => {
|
||||
try {
|
||||
await execAsync(`docker service scale ${appName}=0 `);
|
||||
console.log("Service stopped ✅");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
@@ -93,6 +93,7 @@ export const cleanUpUnusedImages = async () => {
|
||||
try {
|
||||
await execAsync("docker image prune --all --force");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -101,6 +102,7 @@ export const cleanStoppedContainers = async () => {
|
||||
try {
|
||||
await execAsync("docker container prune --force");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -109,6 +111,7 @@ export const cleanUpUnusedVolumes = async () => {
|
||||
try {
|
||||
await execAsync("docker volume prune --force");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -142,6 +145,7 @@ export const startService = async (appName: string) => {
|
||||
try {
|
||||
await execAsync(`docker service scale ${appName}=1 `);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,7 +87,6 @@ const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeRepoPathSSH = (input: string) => {
|
||||
const SSH_PATH_RE = new RegExp(
|
||||
[
|
||||
@@ -113,7 +112,7 @@ const sanitizeRepoPathSSH = (input: string) => {
|
||||
return {
|
||||
user: found.groups?.user ?? "git",
|
||||
domain: found.groups?.domain,
|
||||
port: Number(found.groups?.port ?? 22),
|
||||
port: 22,
|
||||
owner: found.groups?.owner ?? "",
|
||||
repo: found.groups?.repo,
|
||||
get repoPath() {
|
||||
|
||||
18
templates/appsmith/docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
appsmith:
|
||||
image: index.docker.io/appsmith/appsmith-ee:v1.29
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
- ${APP_SMITH_PORT}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.${HASH}.rule=Host(`${APP_SMITH_HOST}`)"
|
||||
- "traefik.http.services.${HASH}.loadbalancer.server.port=${APP_SMITH_PORT}"
|
||||
volumes:
|
||||
- ./stacks:/appsmith-stacks
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
20
templates/appsmith/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`APP_SMITH_HOST=${randomDomain}`,
|
||||
"APP_SMITH_PORT=80",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
22
templates/baserow/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
baserow:
|
||||
image: baserow/baserow:1.25.2
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
BASEROW_PUBLIC_URL: "http://${BASEROW_HOST}"
|
||||
ports:
|
||||
- ${BASEROW_PORT}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${BASEROW_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${BASEROW_PORT}
|
||||
volumes:
|
||||
- baserow_data:/baserow/data
|
||||
volumes:
|
||||
baserow_data:
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
20
templates/baserow/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`BASEROW_HOST=${randomDomain}`,
|
||||
"BASEROW_PORT=80",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
@@ -17,8 +16,8 @@ services:
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
- NEXTAUTH_SECRET=asklmdaklsmdklasmdklasd
|
||||
- CALENDSO_ENCRYPTION_KEY=asklmdaklsmdklasmdklasd
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY}
|
||||
- DATABASE_URL=postgres://postgres:password@postgres:5432/db
|
||||
- NEXT_PUBLIC_WEBAPP_URL=http://${CALCOM_HOST}
|
||||
- NEXTAUTH_URL=http://${CALCOM_HOST}/api/auth
|
||||
|
||||
@@ -3,16 +3,22 @@ import {
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
generateBase64,
|
||||
} from "../utils";
|
||||
|
||||
// https://cal.com/
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const calcomEncryptionKey = generateBase64(32);
|
||||
const nextAuthSecret = generateBase64(32);
|
||||
|
||||
const envs = [
|
||||
`CALCOM_HOST=${randomDomain}`,
|
||||
"CALCOM_PORT=3000",
|
||||
`HASH=${mainServiceHash}`,
|
||||
`NEXTAUTH_SECRET=${nextAuthSecret}`,
|
||||
`CALENDSO_ENCRYPTION_KEY=${calcomEncryptionKey}`,
|
||||
];
|
||||
|
||||
return {
|
||||
|
||||
56
templates/directus/docker-compose.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
database:
|
||||
image: postgis/postgis:13-master
|
||||
volumes:
|
||||
- directus:/var/lib/postgresql/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
POSTGRES_USER: "directus"
|
||||
POSTGRES_PASSWORD: "directus"
|
||||
POSTGRES_DB: "directus"
|
||||
|
||||
cache:
|
||||
image: redis:6
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
directus:
|
||||
image: directus/directus:10.12.1
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
- 8055
|
||||
volumes:
|
||||
- ./uploads:/directus/uploads
|
||||
- ./extensions:/directus/extensions
|
||||
depends_on:
|
||||
- cache
|
||||
- database
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${DIRECTUS_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${DIRECTUS_PORT}
|
||||
environment:
|
||||
SECRET: "replace-with-secure-random-value"
|
||||
|
||||
DB_CLIENT: "pg"
|
||||
DB_HOST: "database"
|
||||
DB_PORT: "5432"
|
||||
DB_DATABASE: "directus"
|
||||
DB_USER: "directus"
|
||||
DB_PASSWORD: "directus"
|
||||
|
||||
CACHE_ENABLED: "true"
|
||||
CACHE_AUTO_PURGE: "true"
|
||||
CACHE_STORE: "redis"
|
||||
REDIS: "redis://cache:6379"
|
||||
|
||||
ADMIN_EMAIL: "admin@example.com"
|
||||
ADMIN_PASSWORD: "d1r3ctu5"
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
volumes:
|
||||
directus:
|
||||
20
templates/directus/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`DIRECTUS_HOST=${randomDomain}`,
|
||||
"DIRECTUS_PORT=8055",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
53
templates/documenso/docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- documenso-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=documenso
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=documenso
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U documenso"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
documenso:
|
||||
image: documenso/documenso:1.5.6-rc.2
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- PORT=${DOCUMENSO_PORT}
|
||||
- NEXTAUTH_URL=http://${DOCUMENSO_HOST}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY}
|
||||
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY}
|
||||
- NEXT_PUBLIC_WEBAPP_URL=http://${DOCUMENSO_HOST}
|
||||
- NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@postgres:5432/documenso
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@postgres:5432/documenso
|
||||
- NEXT_PUBLIC_UPLOAD_TRANSPORT=database
|
||||
- NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
|
||||
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
|
||||
ports:
|
||||
- ${DOCUMENSO_PORT}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.${HASH}.rule=Host(`${DOCUMENSO_HOST}`)"
|
||||
- "traefik.http.services.${HASH}.loadbalancer.server.port=${DOCUMENSO_PORT}"
|
||||
volumes:
|
||||
- /opt/documenso/cert.p12:/opt/documenso/cert.p12
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
documenso-data:
|
||||
30
templates/documenso/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
generateBase64,
|
||||
generatePassword,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
|
||||
const nextAuthSecret = generateBase64(32);
|
||||
const documensoEncryptionKey = generatePassword(32);
|
||||
const documensoSecondaryEncryptionKey = generatePassword(64);
|
||||
|
||||
const envs = [
|
||||
`DOCUMENSO_HOST=${randomDomain}`,
|
||||
"DOCUMENSO_PORT=3000",
|
||||
`HASH=${mainServiceHash}`,
|
||||
`NEXTAUTH_SECRET=${nextAuthSecret}`,
|
||||
`NEXT_PRIVATE_ENCRYPTION_KEY=${documensoEncryptionKey}`,
|
||||
`NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${documensoSecondaryEncryptionKey}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
17
templates/excalidraw/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
excalidraw:
|
||||
networks:
|
||||
- dokploy-network
|
||||
image: excalidraw/excalidraw:latest
|
||||
ports:
|
||||
- ${EXCALIDRAW_PORT}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${EXCALIDRAW_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${EXCALIDRAW_PORT}
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
20
templates/excalidraw/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`EXCALIDRAW_HOST=${randomDomain}`,
|
||||
"EXCALIDRAW_PORT=80",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
41
templates/ghost/docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
|
||||
ghost:
|
||||
image: ghost:5-alpine
|
||||
restart: always
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
- ${GHOST_PORT}
|
||||
environment:
|
||||
database__client: mysql
|
||||
database__connection__host: db
|
||||
database__connection__user: root
|
||||
database__connection__password: example
|
||||
database__connection__database: ghost
|
||||
url: http://${GHOST_HOST}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${GHOST_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${GHOST_PORT}
|
||||
volumes:
|
||||
- ghost:/var/lib/ghost/content
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
restart: always
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: example
|
||||
volumes:
|
||||
- db:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
ghost:
|
||||
db:
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
20
templates/ghost/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`GHOST_HOST=${randomDomain}`,
|
||||
"GHOST_PORT=2368",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
71
templates/glitchtip/docker-compose.yml
Normal file
@@ -0,0 +1,71 @@
|
||||
x-environment:
|
||||
&default-environment
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
PORT: ${GLITCHTIP_PORT}
|
||||
EMAIL_URL: consolemail://
|
||||
GLITCHTIP_DOMAIN: http://${GLITCHTIP_HOST}
|
||||
DEFAULT_FROM_EMAIL: email@glitchtip.com
|
||||
CELERY_WORKER_AUTOSCALE: "1,3"
|
||||
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
|
||||
|
||||
x-depends_on:
|
||||
&default-depends_on
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: "trust"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pg-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
redis:
|
||||
image: redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dokploy-network
|
||||
web:
|
||||
image: glitchtip/glitchtip:v4.0
|
||||
depends_on: *default-depends_on
|
||||
ports:
|
||||
- ${GLITCHTIP_PORT}
|
||||
environment: *default-environment
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- uploads:/code/uploads
|
||||
networks:
|
||||
- dokploy-network
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${GLITCHTIP_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${GLITCHTIP_PORT}
|
||||
worker:
|
||||
image: glitchtip/glitchtip:v4.0
|
||||
command: ./bin/run-celery-with-beat.sh
|
||||
depends_on: *default-depends_on
|
||||
environment: *default-environment
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- uploads:/code/uploads
|
||||
networks:
|
||||
- dokploy-network
|
||||
migrate:
|
||||
image: glitchtip/glitchtip:v4.0
|
||||
depends_on: *default-depends_on
|
||||
command: "./manage.py migrate"
|
||||
environment: *default-environment
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
volumes:
|
||||
pg-data:
|
||||
uploads:
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
23
templates/glitchtip/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
generateBase64,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const secretKey = generateBase64(32);
|
||||
const envs = [
|
||||
`GLITCHTIP_HOST=${randomDomain}`,
|
||||
"GLITCHTIP_PORT=8000",
|
||||
`SECRET_KEY=${secretKey}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
20
templates/grafana/docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
grafana:
|
||||
networks:
|
||||
- dokploy-network
|
||||
image: grafana/grafana-enterprise:9.5.20
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${GRAFANA_PORT}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${GRAFANA_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${GRAFANA_PORT}
|
||||
volumes:
|
||||
- grafana-storage:/var/lib/grafana
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
volumes:
|
||||
grafana-storage: {}
|
||||
20
templates/grafana/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`GRAFANA_HOST=${randomDomain}`,
|
||||
"GRAFANA_PORT=3000",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
25
templates/meilisearch/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
meilisearch:
|
||||
networks:
|
||||
- dokploy-network
|
||||
image: getmeili/meilisearch:v1.8.3
|
||||
ports:
|
||||
- ${MEILISEARCH_PORT}
|
||||
volumes:
|
||||
- meili_data:/meili_data
|
||||
environment:
|
||||
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
|
||||
MEILI_ENV: ${MEILI_ENV}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${MEILISEARCH_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${MEILISEARCH_PORT}
|
||||
|
||||
volumes:
|
||||
meili_data:
|
||||
driver: local
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
24
templates/meilisearch/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
generateBase64,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const masterKey = generateBase64(32);
|
||||
const envs = [
|
||||
`MEILISEARCH_HOST=${randomDomain}`,
|
||||
"MEILISEARCH_PORT=7700",
|
||||
"MEILI_ENV=development",
|
||||
`MEILI_MASTER_KEY=${masterKey}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
38
templates/metabase/docker-compose.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
metabase:
|
||||
image: metabase/metabase:v0.50.8
|
||||
volumes:
|
||||
- /dev/urandom:/dev/random:ro
|
||||
ports:
|
||||
- ${METABASE_PORT}
|
||||
environment:
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_DBNAME: metabaseappdb
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_USER: metabase
|
||||
MB_DB_PASS: mysecretpassword
|
||||
MB_DB_HOST: postgres
|
||||
networks:
|
||||
- dokploy-network
|
||||
healthcheck:
|
||||
test: curl --fail -I http://localhost:3000/api/health || exit 1
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${METABASE_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${METABASE_PORT}
|
||||
postgres:
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_USER: metabase
|
||||
POSTGRES_DB: metabaseappdb
|
||||
POSTGRES_PASSWORD: mysecretpassword
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
20
templates/metabase/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`METABASE_HOST=${randomDomain}`,
|
||||
"METABASE_PORT=3000",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
31
templates/minio/docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio
|
||||
ports:
|
||||
- ${MINIO_API_PORT}
|
||||
- ${MINIO_DASHBOARD_PORT}
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
environment:
|
||||
- MINIO_ROOT_USER=minioadmin
|
||||
- MINIO_ROOT_PASSWORD=minioadmin123
|
||||
command: server /data --console-address ":9001"
|
||||
networks:
|
||||
- dokploy-network
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.service=${HASH}
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${MINIO_DASHBOARD_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${MINIO_DASHBOARD_PORT}
|
||||
# API router and service
|
||||
- traefik.http.routers.${HASH}-api.service=${HASH}-api
|
||||
- traefik.http.routers.${HASH}-api.rule=Host(`${MINIO_API_HOST}`)
|
||||
- traefik.http.services.${HASH}-api.loadbalancer.server.port=${MINIO_API_PORT}
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
23
templates/minio/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const apiDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`MINIO_DASHBOARD_HOST=${randomDomain}`,
|
||||
"MINIO_DASHBOARD_PORT=9001",
|
||||
`MINIO_API_HOST=${apiDomain}`,
|
||||
"MINIO_API_PORT=9000",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
29
templates/n8n/docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n:1.48.1
|
||||
restart: always
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
- ${N8N_PORT}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${N8N_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${N8N_PORT}
|
||||
environment:
|
||||
- N8N_HOST=${N8N_HOST}
|
||||
- N8N_PORT=5678
|
||||
- N8N_PROTOCOL=http
|
||||
- NODE_ENV=production
|
||||
- WEBHOOK_URL=https://${N8N_HOST}/
|
||||
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
|
||||
- N8N_SECURE_COOKIE=false
|
||||
volumes:
|
||||
- n8n_data:/home/node/.n8n
|
||||
|
||||
volumes:
|
||||
n8n_data:
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
21
templates/n8n/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`N8N_HOST=${randomDomain}`,
|
||||
"N8N_PORT=5678",
|
||||
`HASH=${mainServiceHash}`,
|
||||
"GENERIC_TIMEZONE=Europe/Berlin",
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
44
templates/nocodb/docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
nocodb:
|
||||
image: nocodb/nocodb:0.251.1
|
||||
restart: always
|
||||
networks:
|
||||
- dokploy-network
|
||||
ports:
|
||||
- ${NOCODB_PORT}
|
||||
environment:
|
||||
NC_DB : "pg://root_db?u=postgres&p=password&d=root_db"
|
||||
PORT : ${NOCODB_PORT}
|
||||
NC_REDIS_URL: ${NC_REDIS_URL}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.${HASH}.rule=Host(`${NOCODB_HOST}`)
|
||||
- traefik.http.services.${HASH}.loadbalancer.server.port=${NOCODB_PORT}
|
||||
volumes:
|
||||
- nc_data:/usr/app/data
|
||||
|
||||
root_db:
|
||||
image: postgres:14.7
|
||||
restart: always
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
POSTGRES_DB: root_db
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_USER: postgres
|
||||
healthcheck:
|
||||
interval: 10s
|
||||
retries: 10
|
||||
test: "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""
|
||||
timeout: 2s
|
||||
volumes:
|
||||
- "db_data:/var/lib/postgresql/data"
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
db_data: {}
|
||||
nc_data: {}
|
||||
28
templates/nocodb/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// EXAMPLE
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
generateBase64,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const secretBase = generateBase64(64);
|
||||
const toptKeyBase = generateBase64(32);
|
||||
|
||||
const envs = [
|
||||
`NOCODB_HOST=${randomDomain}`,
|
||||
"NOCODB_PORT=8000",
|
||||
`NC_AUTH_JWT_SECRET=${secretBase}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||
42
templates/odoo/docker-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
image: odoo:16.0
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- ${ODOO_PORT}
|
||||
environment:
|
||||
- HOST=db
|
||||
- USER=odoo
|
||||
- PASSWORD=odoo
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.${HASH}.rule=Host(`${ODOO_HOST}`)"
|
||||
- "traefik.http.services.${HASH}.loadbalancer.server.port=${ODOO_PORT}"
|
||||
volumes:
|
||||
- odoo-web-data:/var/lib/odoo
|
||||
- ./config:/etc/odoo
|
||||
- ./addons:/mnt/extra-addons
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
- POSTGRES_DB=postgres
|
||||
- POSTGRES_USER=odoo
|
||||
- POSTGRES_PASSWORD=odoo
|
||||
volumes:
|
||||
- odoo-db-data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
odoo-web-data:
|
||||
odoo-db-data:
|
||||
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
20
templates/odoo/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
`ODOO_HOST=${randomDomain}`,
|
||||
"ODOO_PORT=8069",
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
||||