Merge pull request #197 from Dokploy/canary

v0.3.1
This commit is contained in:
Mauricio Siu
2024-07-06 12:01:07 -06:00
committed by GitHub
28 changed files with 2989 additions and 107 deletions

View File

@@ -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

View File

@@ -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");

View File

@@ -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">

View File

@@ -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

View File

@@ -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");

View File

@@ -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,23 +46,14 @@ 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 stop the compose");
});
});
toast.success("Compose stopped succesfully");
})
.catch(() => {
toast.error("Error to stop the compose");

View File

@@ -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 dark: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>

View File

@@ -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>
) : (

View File

@@ -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>

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" ADD COLUMN "githubWebhookSecret" text;

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,13 @@
"when": 1719547174326,
"tag": "0017_minor_post",
"breakpoints": true
},
{
"idx": 18,
"version": "6",
"when": 1719928377858,
"tag": "0018_careful_killmonger",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.3.0",
"version": "v0.3.1",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -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",

View File

@@ -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";
@@ -68,11 +67,6 @@ export default async function handler(
}
try {
await updateApplicationStatus(
application.applicationId as string,
"running",
);
const jobData: DeploymentJob = {
applicationId: application.applicationId as string,
titleLog: deploymentTitle,

View File

@@ -9,7 +9,6 @@ import {
extractCommitMessage,
extractHash,
} from "../[refreshToken]";
import { updateCompose } from "@/server/api/services/compose";
export default async function handler(
req: NextApiRequest,
@@ -56,10 +55,6 @@ export default async function handler(
}
try {
await updateCompose(composeResult.composeId, {
composeStatus: "running",
});
const jobData: DeploymentJob = {
composeId: composeResult.composeId as string,
titleLog: deploymentTitle,

126
pages/api/deploy/github.ts Normal file
View 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 });
}
}

View File

@@ -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))

54
pnpm-lock.yaml generated
View File

@@ -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)
@@ -2604,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'}
@@ -2665,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'}
@@ -2681,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
@@ -2700,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'}
@@ -5566,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:
@@ -6072,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'}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -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;
});

View File

@@ -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),

View File

@@ -2,9 +2,14 @@ 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 =
| {
@@ -29,6 +34,7 @@ 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,
@@ -43,6 +49,9 @@ export const deploymentWorker = new Worker(
});
}
} else if (job.data.applicationType === "compose") {
await updateCompose(job.data.composeId, {
composeStatus: "running",
});
if (job.data.type === "deploy") {
await deployCompose({
composeId: job.data.composeId,

View File

@@ -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;
}
};

View File

@@ -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() {

View File

@@ -1,7 +1,7 @@
version: "3.8"
services:
nocodb:
image: nocodb/nocodb:0.251.0
image: nocodb/nocodb:0.251.1
restart: always
networks:
- dokploy-network

View File

@@ -0,0 +1,37 @@
version: '3.8'
services:
ollama:
volumes:
- ollama:/root/.ollama
networks:
- dokploy-network
pull_policy: always
tty: true
restart: unless-stopped
image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
open-webui:
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
networks:
- dokploy-network
volumes:
- open-webui:/app/backend/data
depends_on:
- ollama
environment:
- 'OLLAMA_BASE_URL=http://ollama:11434'
- 'WEBUI_SECRET_KEY='
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${OPEN_WEBUI_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${OPEN_WEBUI_PORT}"
networks:
dokploy-network:
external: true
volumes:
ollama: {}
open-webui: {}

View File

@@ -0,0 +1,22 @@
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 = [
`OPEN_WEBUI_HOST=${randomDomain}`,
"OPEN_WEBUI_PORT=8080",
`HASH=${mainServiceHash}`,
'OLLAMA_DOCKER_TAG=0.1.47',
'WEBUI_DOCKER_TAG=0.3.7'
];
return {
envs,
};
}

View File

@@ -216,7 +216,7 @@ export const templates: TemplateData[] = [
{
id: "nocodb",
name: "NocoDB",
version: "0.251.0",
version: "0.251.1",
description:
"NocoDB is an opensource Airtable alternative that turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadsheet.",
@@ -318,4 +318,18 @@ export const templates: TemplateData[] = [
tags: ["hosting"],
load: () => import("./glitchtip/index").then((m) => m.generate),
},
{
id: 'open-webui',
name: 'Open WebUI',
version: 'v0.3.7',
description: 'Open WebUI is a free and open source chatgpt alternative. Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. The template include ollama and webui services.',
logo: 'open-webui.png',
links: {
github: 'https://github.com/open-webui/open-webui',
website: 'https://openwebui.com/',
docs: 'https://docs.openwebui.com/',
},
tags: ['chat'],
load: () => import('./open-webui/index').then((m) => m.generate),
}
];