mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: deploy compose on external servers
This commit is contained in:
@@ -11,7 +11,7 @@ interface Props {
|
||||
logPath: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
serverId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
|
||||
@@ -9,10 +9,16 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
logPath: string | null;
|
||||
serverId?: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
export const ShowDeploymentCompose = ({
|
||||
logPath,
|
||||
open,
|
||||
onClose,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -21,7 +27,7 @@ export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
|
||||
@@ -111,6 +111,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<ShowDeploymentCompose
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
|
||||
@@ -116,6 +116,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{data?.server?.name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -173,9 +173,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Registries ({servers?.length})
|
||||
</SelectLabel>
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -22,7 +22,9 @@ import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
@@ -51,6 +53,9 @@ const AddComposeSchema = z.object({
|
||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().min(1, {
|
||||
message: "Server is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||
@@ -63,6 +68,7 @@ interface Props {
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.all.useQuery();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.create.useMutation();
|
||||
|
||||
@@ -72,6 +78,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
description: "",
|
||||
composeType: "docker-compose",
|
||||
appName: `${slug}-`,
|
||||
serverId: "",
|
||||
},
|
||||
resolver: zodResolver(AddComposeSchema),
|
||||
});
|
||||
@@ -87,6 +94,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
projectId,
|
||||
composeType: data.composeType,
|
||||
appName: data.appName,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
@@ -148,6 +156,37 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
|
||||
@@ -59,7 +59,6 @@ import { addNewService, checkServiceAccess } from "../services/user";
|
||||
|
||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||
import { uploadFileSchema } from "@/utils/schema";
|
||||
import { Queue } from "bullmq";
|
||||
|
||||
export const applicationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByCompose,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { enqueueDeploymentJob, myQueue } from "@/server/queues/queueSetup";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import {
|
||||
randomizeComposeFile,
|
||||
@@ -49,6 +49,7 @@ import { createMount } from "../services/mount";
|
||||
import { findProjectById } from "../services/project";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { findApplicationById } from "../services/application";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -162,6 +163,22 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
// const jobData: DeploymentJob = {
|
||||
// composeId: input.composeId,
|
||||
// titleLog: "Manual deployment",
|
||||
// type: "deploy",
|
||||
// applicationType: "compose",
|
||||
// descriptionLog: "",
|
||||
// };
|
||||
// await myQueue.add(
|
||||
// "deployments",
|
||||
// { ...jobData },
|
||||
// {
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true,
|
||||
// },
|
||||
// );
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Manual deployment",
|
||||
@@ -169,14 +186,10 @@ export const composeRouter = createTRPCRouter({
|
||||
applicationType: "compose",
|
||||
descriptionLog: "",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
if (!compose.serverId) {
|
||||
} else {
|
||||
await enqueueDeploymentJob(compose.serverId, jobData);
|
||||
}
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
|
||||
@@ -3,23 +3,43 @@ import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateCompose, compose } from "@/server/db/schema";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
import { buildCompose } from "@/server/utils/builders/compose";
|
||||
import {
|
||||
buildCompose,
|
||||
getBuildComposeCommand,
|
||||
} from "@/server/utils/builders/compose";
|
||||
import { randomizeSpecificationFile } from "@/server/utils/docker/compose";
|
||||
import { cloneCompose, loadDockerCompose } from "@/server/utils/docker/domain";
|
||||
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
import { cloneBitbucketRepository } from "@/server/utils/providers/bitbucket";
|
||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||
import { cloneGitlabRepository } from "@/server/utils/providers/gitlab";
|
||||
import { createComposeFile } from "@/server/utils/providers/raw";
|
||||
import {
|
||||
cloneBitbucketRepository,
|
||||
getBitbucketCloneCommand,
|
||||
} from "@/server/utils/providers/bitbucket";
|
||||
import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
} from "@/server/utils/providers/git";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@/server/utils/providers/github";
|
||||
import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@/server/utils/providers/gitlab";
|
||||
import {
|
||||
createComposeFile,
|
||||
getCreateComposeFileCommand,
|
||||
} from "@/server/utils/providers/raw";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { getBuildCommand } from "@/server/utils/builders";
|
||||
import { executeCommand } from "@/server/utils/servers/command";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
@@ -97,6 +117,7 @@ export const findComposeById = async (composeId: string) => {
|
||||
github: true,
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
@@ -173,18 +194,55 @@ export const deployCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
if (compose.serverId) {
|
||||
let command = `
|
||||
set -e
|
||||
`;
|
||||
if (compose.sourceType === "github") {
|
||||
command += await getGithubCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
command += await getBitbucketCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose);
|
||||
}
|
||||
command += getBuildComposeCommand(compose, deployment.logPath);
|
||||
await executeCommand(compose.serverId, command);
|
||||
} else {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
|
||||
@@ -100,14 +100,27 @@ export const createDeploymentCompose = async (
|
||||
try {
|
||||
const compose = await findComposeById(deployment.composeId);
|
||||
|
||||
await removeLastTenComposeDeployments(deployment.composeId);
|
||||
// await removeLastTenComposeDeployments(deployment.composeId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${compose.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, compose.appName, fileName);
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
|
||||
if (compose.serverId) {
|
||||
const server = await findServerById(compose.serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${compose.appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await executeCommand(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
|
||||
@@ -136,6 +136,7 @@ export const apiCreateCompose = createSchema.pick({
|
||||
projectId: true,
|
||||
composeType: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
|
||||
@@ -32,43 +32,43 @@ export type DeploymentJob = DeployJob;
|
||||
// export const deploymentWorker = new Worker(
|
||||
// "deployments",
|
||||
// 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,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log("Error", error);
|
||||
// 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,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log("Error", error);
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// autorun: false,
|
||||
|
||||
@@ -3,8 +3,14 @@ import { findServerById, type Server } from "../api/services/server";
|
||||
import type { DeploymentJob } from "./deployments-queue";
|
||||
import {
|
||||
deployApplication,
|
||||
rebuildApplication,
|
||||
updateApplicationStatus,
|
||||
} from "../api/services/application";
|
||||
import {
|
||||
updateCompose,
|
||||
deployCompose,
|
||||
rebuildCompose,
|
||||
} from "../api/services/compose";
|
||||
|
||||
export const redisConfig: ConnectionOptions = {
|
||||
host: "31.220.108.27",
|
||||
@@ -54,15 +60,42 @@ async function setupServerQueueAndWorker(server: Server) {
|
||||
`deployments-${server.serverId}`,
|
||||
async (job: Job<DeploymentJob>) => {
|
||||
// Ejecuta el trabajo de despliegue
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -65,6 +65,30 @@ Compose Type: ${composeType} ✅`;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBuildComposeCommand = (
|
||||
compose: ComposeNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||
const command = createCommand(compose);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
Detected: ${mounts.length} mounts 📂
|
||||
Command: docker ${command}
|
||||
Source Type: docker ${sourceType} ✅
|
||||
Compose Type: ${composeType} ✅`;
|
||||
|
||||
const bashCommand = `
|
||||
echo "${logContent}" >> ${logPath};
|
||||
cd ${projectPath} || exit 1;
|
||||
docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1;
|
||||
echo "Docker Compose Deployed: ✅" >> ${logPath};
|
||||
`;
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
const sanitizeCommand = (command: string) => {
|
||||
const sanitizedCommand = command.trim();
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getCreateComposeFileCommand = (compose: Compose) => {
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
return `echo "${composeFile}" > ${filePath}`;
|
||||
};
|
||||
|
||||
export const createComposeFileRaw = async (compose: Compose) => {
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
|
||||
Reference in New Issue
Block a user