diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx
index 1531942f..0fa5ae38 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";
@@ -51,7 +52,10 @@ export const ComposeActions = ({ composeId }: Props) => {
- {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 00000000..20f990bb
--- /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 00000000..3080e755
--- /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/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 257f374f..6d04e815 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 63d29539..604c2150 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 {