diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx
index 365e37f51..140b8d96b 100644
--- a/apps/dokploy/components/dashboard/compose/general/actions.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx
@@ -14,6 +14,7 @@ import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
+import { StartCompose } from "../start-compose";
import { DeployCompose } from "./deploy-compose";
import { RedbuildCompose } from "./rebuild-compose";
import { StopCompose } from "./stop-compose";
@@ -71,7 +72,10 @@ export const ComposeActions = ({ composeId }: Props) => {
Autodeploy {data?.autoDeploy && }
- {data?.composeType === "docker-compose" && (
+ {data?.composeType === "docker-compose" &&
+ data?.composeStatus === "idle" ? (
+
+ ) : (
)}
diff --git a/apps/dokploy/components/dashboard/compose/start-compose.tsx b/apps/dokploy/components/dashboard/compose/start-compose.tsx
new file mode 100644
index 000000000..20f990bb3
--- /dev/null
+++ b/apps/dokploy/components/dashboard/compose/start-compose.tsx
@@ -0,0 +1,65 @@
+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 { CheckCircle2 } from "lucide-react";
+import { toast } from "sonner";
+
+interface Props {
+ composeId: string;
+}
+
+export const StartCompose = ({ composeId }: Props) => {
+ const { mutateAsync, isLoading } = api.compose.start.useMutation();
+ const utils = api.useUtils();
+ return (
+
+
+
+
+
+
+
+ Are you sure to start the compose?
+
+
+ This will start the compose
+
+
+
+ Cancel
+ {
+ await mutateAsync({
+ composeId,
+ })
+ .then(async () => {
+ await utils.compose.one.invalidate({
+ composeId,
+ });
+ toast.success("Compose started succesfully");
+ })
+ .catch(() => {
+ toast.error("Error to start the Compose");
+ });
+ }}
+ >
+ Confirm
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/compose/stop-compose.tsx b/apps/dokploy/components/dashboard/compose/stop-compose.tsx
new file mode 100644
index 000000000..3080e755a
--- /dev/null
+++ b/apps/dokploy/components/dashboard/compose/stop-compose.tsx
@@ -0,0 +1,65 @@
+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 { Ban } from "lucide-react";
+import { toast } from "sonner";
+
+interface Props {
+ composeId: string;
+}
+
+export const StopCompose = ({ composeId }: Props) => {
+ const { mutateAsync, isLoading } = api.compose.stop.useMutation();
+ const utils = api.useUtils();
+ return (
+
+
+
+
+
+
+
+ Are you absolutely sure to stop the compose?
+
+
+ This will stop the compose
+
+
+
+ Cancel
+ {
+ await mutateAsync({
+ composeId,
+ })
+ .then(async () => {
+ await utils.compose.one.invalidate({
+ composeId,
+ });
+ toast.success("Compose stopped succesfully");
+ })
+ .catch(() => {
+ toast.error("Error to stop the Compose");
+ });
+ }}
+ >
+ Confirm
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts
index 1d8c094af..08589fae1 100644
--- a/apps/dokploy/pages/api/deploy/github.ts
+++ b/apps/dokploy/pages/api/deploy/github.ts
@@ -10,138 +10,138 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { extractCommitMessage, extractHash } from "./[refreshToken]";
export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse
+ req: NextApiRequest,
+ res: NextApiResponse,
) {
- const signature = req.headers["x-hub-signature-256"];
- const githubBody = req.body;
+ const signature = req.headers["x-hub-signature-256"];
+ const githubBody = req.body;
- if (!githubBody?.installation?.id) {
- res.status(400).json({ message: "Github Installation not found" });
- return;
- }
+ if (!githubBody?.installation?.id) {
+ res.status(400).json({ message: "Github Installation not found" });
+ return;
+ }
- const githubResult = await db.query.github.findFirst({
- where: eq(github.githubInstallationId, githubBody.installation.id),
- });
+ const githubResult = await db.query.github.findFirst({
+ where: eq(github.githubInstallationId, githubBody.installation.id),
+ });
- if (!githubResult) {
- res.status(400).json({ message: "Github Installation not found" });
- return;
- }
+ if (!githubResult) {
+ res.status(400).json({ message: "Github Installation not found" });
+ return;
+ }
- if (!githubResult.githubWebhookSecret) {
- res.status(400).json({ message: "Github Webhook Secret not set" });
- return;
- }
- const webhooks = new Webhooks({
- secret: githubResult.githubWebhookSecret,
- });
+ if (!githubResult.githubWebhookSecret) {
+ res.status(400).json({ message: "Github Webhook Secret not set" });
+ return;
+ }
+ const webhooks = new Webhooks({
+ secret: githubResult.githubWebhookSecret,
+ });
- const verified = await webhooks.verify(
- JSON.stringify(githubBody),
- signature as string
- );
+ const verified = await webhooks.verify(
+ JSON.stringify(githubBody),
+ signature as string,
+ );
- if (!verified) {
- res.status(401).json({ message: "Unauthorized" });
- return;
- }
+ 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"] === "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;
- }
+ if (req.headers["x-github-event"] !== "push") {
+ res.status(400).json({ message: "We only accept push events" });
+ return;
+ }
- try {
- const branchName = githubBody?.ref?.replace("refs/heads/", "");
- const repository = githubBody?.repository?.name;
- const deploymentTitle = extractCommitMessage(req.headers, req.body);
- const deploymentHash = extractHash(req.headers, req.body);
+ try {
+ const branchName = githubBody?.ref?.replace("refs/heads/", "");
+ const repository = githubBody?.repository?.name;
+ const deploymentTitle = extractCommitMessage(req.headers, req.body);
+ const deploymentHash = extractHash(req.headers, req.body);
- const apps = await db.query.applications.findMany({
- where: and(
- eq(applications.sourceType, "github"),
- eq(applications.autoDeploy, true),
- eq(applications.branch, branchName),
- eq(applications.repository, repository)
- ),
- });
+ 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",
- server: !!app.serverId,
- };
+ for (const app of apps) {
+ const jobData: DeploymentJob = {
+ applicationId: app.applicationId as string,
+ titleLog: deploymentTitle,
+ descriptionLog: `Hash: ${deploymentHash}`,
+ type: "deploy",
+ applicationType: "application",
+ server: !!app.serverId,
+ };
- if (IS_CLOUD && app.serverId) {
- jobData.serverId = app.serverId;
- await deploy(jobData);
- return true;
- }
- await myQueue.add(
- "deployments",
- { ...jobData },
- {
- removeOnComplete: true,
- removeOnFail: true,
- }
- );
- }
+ if (IS_CLOUD && app.serverId) {
+ jobData.serverId = app.serverId;
+ await deploy(jobData);
+ return true;
+ }
+ await myQueue.add(
+ "deployments",
+ { ...jobData },
+ {
+ removeOnComplete: true,
+ removeOnFail: true,
+ },
+ );
+ }
- const composeApps = await db.query.compose.findMany({
- where: and(
- eq(compose.sourceType, "github"),
- eq(compose.autoDeploy, true),
- eq(compose.branch, branchName),
- eq(compose.repository, repository)
- ),
- });
+ 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}`,
- server: !!composeApp.serverId,
- };
+ for (const composeApp of composeApps) {
+ const jobData: DeploymentJob = {
+ composeId: composeApp.composeId as string,
+ titleLog: deploymentTitle,
+ type: "deploy",
+ applicationType: "compose",
+ descriptionLog: `Hash: ${deploymentHash}`,
+ server: !!composeApp.serverId,
+ };
- if (IS_CLOUD && composeApp.serverId) {
- jobData.serverId = composeApp.serverId;
- await deploy(jobData);
- return true;
- }
+ if (IS_CLOUD && composeApp.serverId) {
+ jobData.serverId = composeApp.serverId;
+ await deploy(jobData);
+ return true;
+ }
- await myQueue.add(
- "deployments",
- { ...jobData },
- {
- removeOnComplete: true,
- removeOnFail: true,
- }
- );
- }
+ await myQueue.add(
+ "deployments",
+ { ...jobData },
+ {
+ removeOnComplete: true,
+ removeOnFail: true,
+ },
+ );
+ }
- const totalApps = apps.length + composeApps.length;
- const emptyApps = totalApps === 0;
+ 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 });
- }
+ 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 });
+ }
}
diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 257f374f2..6d04e815f 100644
--- a/apps/dokploy/server/api/routers/compose.ts
+++ b/apps/dokploy/server/api/routers/compose.ts
@@ -48,6 +48,7 @@ import {
removeCompose,
removeComposeDirectory,
removeDeploymentsByComposeId,
+ startCompose,
stopCompose,
updateCompose,
} from "@dokploy/server";
@@ -309,6 +310,20 @@ export const composeRouter = createTRPCRouter({
}
await stopCompose(input.composeId);
+ return true;
+ }),
+ start: protectedProcedure
+ .input(apiFindCompose)
+ .mutation(async ({ input, ctx }) => {
+ const compose = await findComposeById(input.composeId);
+ if (compose.project.adminId !== ctx.user.adminId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to stop this compose",
+ });
+ }
+ await startCompose(input.composeId);
+
return true;
}),
getDefaultCommand: protectedProcedure
diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts
index 63d29539d..604c2150e 100644
--- a/packages/server/src/services/compose.ts
+++ b/packages/server/src/services/compose.ts
@@ -463,6 +463,36 @@ export const removeCompose = async (compose: Compose) => {
return true;
};
+export const startCompose = async (composeId: string) => {
+ const compose = await findComposeById(composeId);
+ try {
+ const { COMPOSE_PATH } = paths(!!compose.serverId);
+ if (compose.composeType === "docker-compose") {
+ if (compose.serverId) {
+ await execAsyncRemote(
+ compose.serverId,
+ `cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
+ );
+ } else {
+ await execAsync(`docker compose -p ${compose.appName} up -d`, {
+ cwd: join(COMPOSE_PATH, compose.appName, "code"),
+ });
+ }
+ }
+
+ await updateCompose(composeId, {
+ composeStatus: "done",
+ });
+ } catch (error) {
+ await updateCompose(composeId, {
+ composeStatus: "idle",
+ });
+ throw error;
+ }
+
+ return true;
+};
+
export const stopCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {