mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
3 Commits
v0.16.0
...
804-servic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159584c5df | ||
|
|
bffd8c3e9c | ||
|
|
5e7486859f |
@@ -99,14 +99,14 @@ workflows:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- 804-services-deployed-via-dokploy-inaccessible-after-customizing-traefik-ports-via-environment-variables
|
||||
- build-arm64:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- 804-services-deployed-via-dokploy-inaccessible-after-customizing-traefik-ports-via-environment-variables
|
||||
- combine-manifests:
|
||||
requires:
|
||||
- build-amd64
|
||||
@@ -116,4 +116,4 @@ workflows:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- 804-services-deployed-via-dokploy-inaccessible-after-customizing-traefik-ports-via-environment-variables
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -31,67 +29,28 @@ export const DockerLogs = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
export const badgeStateColor = (state: string) => {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "green";
|
||||
case "exited":
|
||||
case "shutdown":
|
||||
return "red";
|
||||
case "accepted":
|
||||
case "created":
|
||||
return "blue";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
},
|
||||
);
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
|
||||
const { data: services, isLoading: servicesLoading } =
|
||||
api.docker.getServiceContainersByAppName.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "swarm",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: containers, isLoading: containersLoading } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "native",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (option === "native") {
|
||||
if (containers && containers?.length > 0) {
|
||||
setContainerId(containers[0]?.containerId);
|
||||
}
|
||||
} else {
|
||||
if (services && services?.length > 0) {
|
||||
setContainerId(services[0]?.containerId);
|
||||
}
|
||||
if (data && data?.length > 0) {
|
||||
setContainerId(data[0]?.containerId);
|
||||
}
|
||||
}, [option, services, containers]);
|
||||
|
||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||
const containersLenght =
|
||||
option === "native" ? containers?.length : services?.length;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
@@ -103,21 +62,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<Label>Select a container to view logs</Label>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{option === "native" ? "Native" : "Swarm"}
|
||||
</span>
|
||||
<Switch
|
||||
checked={option === "native"}
|
||||
onCheckedChange={(checked) => {
|
||||
setOption(checked ? "native" : "swarm");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
{isLoading ? (
|
||||
@@ -131,45 +76,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{option === "native" ? (
|
||||
<div>
|
||||
{containers?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{services?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}@{container.node}
|
||||
)
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||
{data?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
(e) => e.DockerLogsId,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
badgeStateColor;
|
||||
|
||||
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
const { data: services, isLoading: servicesLoading } =
|
||||
api.docker.getStackContainersByAppName.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "swarm",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: containers, isLoading: containersLoading } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
appType: "stack",
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "native",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (option === "native") {
|
||||
if (containers && containers?.length > 0) {
|
||||
setContainerId(containers[0]?.containerId);
|
||||
}
|
||||
} else {
|
||||
if (services && services?.length > 0) {
|
||||
setContainerId(services[0]?.containerId);
|
||||
}
|
||||
}
|
||||
}, [option, services, containers]);
|
||||
|
||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||
const containersLenght =
|
||||
option === "native" ? containers?.length : services?.length;
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Logs</CardTitle>
|
||||
<CardDescription>
|
||||
Watch the logs of the application in real time
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<Label>Select a container to view logs</Label>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{option === "native" ? "Native" : "Swarm"}
|
||||
</span>
|
||||
<Switch
|
||||
checked={option === "native"}
|
||||
onCheckedChange={(checked) => {
|
||||
setOption(checked ? "native" : "swarm");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{option === "native" ? (
|
||||
<div>
|
||||
{containers?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{services?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}@{container.node}
|
||||
)
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,3 @@
|
||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -89,10 +87,7 @@ export const ShowDockerLogsCompose = ({
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
@@ -102,7 +97,6 @@ export const ShowDockerLogsCompose = ({
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType="native"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
runType: "swarm" | "native";
|
||||
}
|
||||
|
||||
export const priorities = [
|
||||
@@ -38,11 +37,7 @@ export const priorities = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DockerLogsId: React.FC<Props> = ({
|
||||
containerId,
|
||||
serverId,
|
||||
runType,
|
||||
}) => {
|
||||
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
@@ -109,7 +104,6 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
tail: lines.toString(),
|
||||
since,
|
||||
search,
|
||||
runType,
|
||||
});
|
||||
|
||||
if (serverId) {
|
||||
|
||||
@@ -46,11 +46,7 @@ export const ShowDockerModalLogs = ({
|
||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
runType="native"
|
||||
/>
|
||||
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
export const DockerLogsId = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
(e) => e.DockerLogsId,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
children?: React.ReactNode;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export const ShowDockerModalStackLogs = ({
|
||||
containerId,
|
||||
children,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>View Logs</DialogTitle>
|
||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
runType="swarm"
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -36,7 +35,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -97,7 +95,6 @@ const mySchema = z.discriminatedUnion("type", [
|
||||
.object({
|
||||
type: z.literal("mongo"),
|
||||
databaseUser: z.string().default("mongo"),
|
||||
replicaSets: z.boolean().default(false),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
z
|
||||
@@ -219,7 +216,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
replicaSets: data.replicaSets,
|
||||
});
|
||||
} else if (data.type === "redis") {
|
||||
promise = redisMutation.mutateAsync({
|
||||
@@ -546,30 +542,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{type === "mongo" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="replicaSets"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Use Replica Sets</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -91,11 +91,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogsId
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
runType="native"
|
||||
/>
|
||||
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ShowDockerModalStackLogs } from "../../docker/logs/show-docker-modal-stack-logs";
|
||||
import { ShowNodeConfig } from "../details/show-node-config";
|
||||
// import { ShowContainerConfig } from "../config/show-container-config";
|
||||
// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
||||
// import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
||||
// import type { Container } from "./show-containers";
|
||||
|
||||
export interface ApplicationList {
|
||||
ID: string;
|
||||
@@ -24,7 +28,6 @@ export interface ApplicationList {
|
||||
DesiredState: string;
|
||||
Error: string;
|
||||
Node: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<ApplicationList>[] = [
|
||||
@@ -209,37 +212,7 @@ export const columns: ColumnDef<ApplicationList>[] = [
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <div className="w-[10rem]">{row.getValue("Errors")}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "Logs",
|
||||
accessorFn: (row) => row.Error,
|
||||
header: ({ column }) => {
|
||||
return <span>Logs</span>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="w-[10rem]">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<ShowDockerModalStackLogs
|
||||
containerId={row.original.ID}
|
||||
serverId={row.original.serverId}
|
||||
>
|
||||
View Logs
|
||||
</ShowDockerModalStackLogs>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
);
|
||||
return <div>{row.getValue("Errors")}</div>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,13 +10,26 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { Layers, Loader2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { type ApplicationList, columns } from "./columns";
|
||||
import { columns } from "./columns";
|
||||
import { DataTable } from "./data-table";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
interface ApplicationList {
|
||||
ID: string;
|
||||
Image: string;
|
||||
Mode: string;
|
||||
Name: string;
|
||||
Ports: string;
|
||||
Replicas: string;
|
||||
CurrentState: string;
|
||||
DesiredState: string;
|
||||
Error: string;
|
||||
Node: string;
|
||||
}
|
||||
|
||||
export const ShowNodeApplications = ({ serverId }: Props) => {
|
||||
const { data: NodeApps, isLoading: NodeAppsLoading } =
|
||||
api.swarm.getNodeApps.useQuery({ serverId });
|
||||
@@ -76,7 +89,6 @@ export const ShowNodeApplications = ({ serverId }: Props) => {
|
||||
Error: detail.Error,
|
||||
Node: detail.Node,
|
||||
Ports: detail.Ports || app.Ports,
|
||||
serverId: serverId || "",
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "mongo" ADD COLUMN "replicaSets" boolean DEFAULT false;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -372,13 +372,6 @@
|
||||
"when": 1734809337308,
|
||||
"tag": "0052_bumpy_luckman",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 53,
|
||||
"version": "6",
|
||||
"when": 1735118844878,
|
||||
"tag": "0053_broken_kulan_gath",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,8 +13,6 @@ export enum Languages {
|
||||
Portuguese = "pt-br",
|
||||
Italian = "it",
|
||||
Japanese = "ja",
|
||||
Spanish = "es",
|
||||
Norwegian = "no",
|
||||
}
|
||||
|
||||
export type Language = keyof typeof Languages;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.16.0",
|
||||
"version": "v0.15.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-
|
||||
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
|
||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
||||
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
|
||||
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
@@ -252,18 +251,11 @@ const Service = (
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
{data?.composeType === "docker-compose" ? (
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
) : (
|
||||
<ShowDockerLogsStack
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
)}
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"settings.common.save": "Guardar",
|
||||
"settings.server.domain.title": "Dominio del Servidor",
|
||||
"settings.server.domain.description": "Añade un dominio a tu aplicación de servidor.",
|
||||
"settings.server.domain.form.domain": "Dominio",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Correo de Let's Encrypt",
|
||||
"settings.server.domain.form.certificate.label": "Proveedor de Certificado",
|
||||
"settings.server.domain.form.certificate.placeholder": "Selecciona un certificado",
|
||||
"settings.server.domain.form.certificateOptions.none": "Ninguno",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
|
||||
|
||||
"settings.server.webServer.title": "Servidor Web",
|
||||
"settings.server.webServer.description": "Recarga o limpia el servidor web.",
|
||||
"settings.server.webServer.actions": "Acciones",
|
||||
"settings.server.webServer.reload": "Recargar",
|
||||
"settings.server.webServer.watchLogs": "Ver registros",
|
||||
"settings.server.webServer.updateServerIp": "Actualizar IP del Servidor",
|
||||
"settings.server.webServer.server.label": "Servidor",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Modificar Entorno",
|
||||
"settings.server.webServer.traefik.managePorts": "Asignación Adicional de Puertos",
|
||||
"settings.server.webServer.traefik.managePortsDescription": "Añadir o eliminar puertos adicionales para Traefik",
|
||||
"settings.server.webServer.traefik.targetPort": "Puerto de Destino",
|
||||
"settings.server.webServer.traefik.publishedPort": "Puerto Publicado",
|
||||
"settings.server.webServer.traefik.addPort": "Añadir Puerto",
|
||||
"settings.server.webServer.traefik.portsUpdated": "Puertos actualizados correctamente",
|
||||
"settings.server.webServer.traefik.portsUpdateError": "Error al actualizar los puertos",
|
||||
"settings.server.webServer.traefik.publishMode": "Modo de Publicación",
|
||||
"settings.server.webServer.storage.label": "Espacio",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Limpiar imágenes no utilizadas",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Limpiar volúmenes no utilizados",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "Limpiar contenedores detenidos",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "Limpiar Constructor de Docker y Sistema",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "Limpiar Monitoreo",
|
||||
"settings.server.webServer.storage.cleanAll": "Limpiar todo",
|
||||
|
||||
"settings.profile.title": "Cuenta",
|
||||
"settings.profile.description": "Cambia los detalles de tu perfil aquí.",
|
||||
"settings.profile.email": "Correo electrónico",
|
||||
"settings.profile.password": "Contraseña",
|
||||
"settings.profile.avatar": "Avatar",
|
||||
|
||||
"settings.appearance.title": "Apariencia",
|
||||
"settings.appearance.description": "Personaliza el tema de tu panel.",
|
||||
"settings.appearance.theme": "Tema",
|
||||
"settings.appearance.themeDescription": "Selecciona un tema para tu panel",
|
||||
"settings.appearance.themes.light": "Claro",
|
||||
"settings.appearance.themes.dark": "Oscuro",
|
||||
"settings.appearance.themes.system": "Sistema",
|
||||
"settings.appearance.language": "Idioma",
|
||||
"settings.appearance.languageDescription": "Selecciona un idioma para tu panel"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"settings.common.save": "Lagre",
|
||||
"settings.server.domain.title": "Serverdomene",
|
||||
"settings.server.domain.description": "Legg til et domene i serverapplikasjonen din.",
|
||||
"settings.server.domain.form.domain": "Domene",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Epost",
|
||||
"settings.server.domain.form.certificate.label": "Sertifikatleverandør",
|
||||
"settings.server.domain.form.certificate.placeholder": "Velg et sertifikat",
|
||||
"settings.server.domain.form.certificateOptions.none": "Ingen",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
|
||||
|
||||
"settings.server.webServer.title": "Webserver",
|
||||
"settings.server.webServer.description": "Last på nytt eller rens webserveren.",
|
||||
"settings.server.webServer.actions": "Handlinger",
|
||||
"settings.server.webServer.reload": "Last på nytt",
|
||||
"settings.server.webServer.watchLogs": "Se logger",
|
||||
"settings.server.webServer.updateServerIp": "Oppdater server-IP",
|
||||
"settings.server.webServer.server.label": "Server",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Endre miljø",
|
||||
"settings.server.webServer.traefik.managePorts": "Ytterligere portkartlegginger",
|
||||
"settings.server.webServer.traefik.managePortsDescription": "Legg til eller fjern flere porter for Traefik",
|
||||
"settings.server.webServer.traefik.targetPort": "Målport",
|
||||
"settings.server.webServer.traefik.publishedPort": "Publisert port",
|
||||
"settings.server.webServer.traefik.addPort": "Legg til port",
|
||||
"settings.server.webServer.traefik.portsUpdated": "Portene ble oppdatert",
|
||||
"settings.server.webServer.traefik.portsUpdateError": "Kunne ikke oppdatere portene",
|
||||
"settings.server.webServer.traefik.publishMode": "Publiseringsmodus",
|
||||
"settings.server.webServer.storage.label": "Lagring",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Rens ubrukte bilder",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Rens ubrukte volumer",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "Rens stoppete containere",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "Rens Docker Builder og System",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "Rens overvåking",
|
||||
"settings.server.webServer.storage.cleanAll": "Rens alt",
|
||||
|
||||
"settings.profile.title": "Konto",
|
||||
"settings.profile.description": "Endre detaljene for profilen din her.",
|
||||
"settings.profile.email": "Epost",
|
||||
"settings.profile.password": "Passord",
|
||||
"settings.profile.avatar": "Avatar",
|
||||
|
||||
"settings.appearance.title": "Utseende",
|
||||
"settings.appearance.description": "Tilpass temaet for dashbordet ditt.",
|
||||
"settings.appearance.theme": "Tema",
|
||||
"settings.appearance.themeDescription": "Velg et tema for dashbordet ditt",
|
||||
"settings.appearance.themes.light": "Lys",
|
||||
"settings.appearance.themes.dark": "Mørk",
|
||||
"settings.appearance.themes.system": "System",
|
||||
"settings.appearance.language": "Språk",
|
||||
"settings.appearance.languageDescription": "Velg et språk for dashbordet ditt"
|
||||
}
|
||||
@@ -555,9 +555,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await updateApplication(input.applicationId as string, {
|
||||
updateApplication(input.applicationId as string, {
|
||||
sourceType: "drop",
|
||||
dropBuildPath: input.dropBuildPath || "",
|
||||
dropBuildPath: input.dropBuildPath,
|
||||
});
|
||||
|
||||
await unzipDrop(zipFile, app);
|
||||
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
getContainers,
|
||||
getContainersByAppLabel,
|
||||
getContainersByAppNameMatch,
|
||||
getServiceContainersByAppName,
|
||||
getStackContainersByAppName,
|
||||
} from "@dokploy/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
@@ -70,26 +68,4 @@ export const dockerRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
return await getContainersByAppLabel(input.appName, input.serverId);
|
||||
}),
|
||||
|
||||
getStackContainersByAppName: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getStackContainersByAppName(input.appName, input.serverId);
|
||||
}),
|
||||
|
||||
getServiceContainersByAppName: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getServiceContainersByAppName(input.appName, input.serverId);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
const search = url.searchParams.get("search");
|
||||
const since = url.searchParams.get("since");
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const runType = url.searchParams.get("runType");
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
|
||||
if (!containerId) {
|
||||
@@ -54,9 +53,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
const client = new Client();
|
||||
client
|
||||
.once("ready", () => {
|
||||
const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${
|
||||
runType === "swarm" ? "--raw" : ""
|
||||
} --tail ${tail} ${
|
||||
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
|
||||
since === "all" ? "" : `--since ${since}`
|
||||
} --follow ${containerId}`;
|
||||
const escapedSearch = search ? search.replace(/'/g, "'\\''") : "";
|
||||
@@ -100,9 +97,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
});
|
||||
} else {
|
||||
const shell = getShell();
|
||||
const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${
|
||||
runType === "swarm" ? "--raw" : ""
|
||||
} --tail ${tail} ${
|
||||
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
|
||||
since === "all" ? "" : `--since ${since}`
|
||||
} --follow ${containerId}`;
|
||||
const command = search
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -43,7 +43,6 @@ export const mongo = pgTable("mongo", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
replicaSets: boolean("replicaSets").default(false),
|
||||
});
|
||||
|
||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
@@ -78,7 +77,6 @@ const createSchema = createInsertSchema(mongo, {
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
replicaSets: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const apiCreateMongo = createSchema
|
||||
@@ -91,7 +89,6 @@ export const apiCreateMongo = createSchema
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
replicaSets: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
@@ -157,124 +157,6 @@ export const getContainersByAppNameMatch = async (
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getStackContainersByAppName = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
result = stdout.trim().split("\n");
|
||||
} else {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
result = stdout.trim().split("\n");
|
||||
}
|
||||
|
||||
const containers = result.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||
: "No state";
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
node,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getServiceContainersByAppName = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
result = stdout.trim().split("\n");
|
||||
} else {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
result = stdout.trim().split("\n");
|
||||
}
|
||||
|
||||
const containers = result.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||
: "No state";
|
||||
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
node,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getContainersByAppLabel = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
@@ -364,6 +246,8 @@ export const getSwarmNodes = async (serverId?: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = JSON.parse(stdout);
|
||||
|
||||
const nodesArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
@@ -423,8 +307,7 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line))
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch (error) {}
|
||||
@@ -437,7 +320,7 @@ export const getApplicationInfo = async (
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`;
|
||||
const command = `docker service ps ${appName} --format '{{json .}}'`;
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
|
||||
@@ -9,325 +9,325 @@ import type { FileConfig } from "../utils/traefik/file-types";
|
||||
import type { MainTraefikConfig } from "../utils/traefik/types";
|
||||
|
||||
export const TRAEFIK_SSL_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
|
||||
export const TRAEFIK_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
|
||||
|
||||
interface TraefikOptions {
|
||||
enableDashboard?: boolean;
|
||||
env?: string[];
|
||||
serverId?: string;
|
||||
additionalPorts?: {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
publishMode?: "ingress" | "host";
|
||||
}[];
|
||||
enableDashboard?: boolean;
|
||||
env?: string[];
|
||||
serverId?: string;
|
||||
additionalPorts?: {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
publishMode?: "ingress" | "host";
|
||||
}[];
|
||||
}
|
||||
|
||||
export const initializeTraefik = async ({
|
||||
enableDashboard = false,
|
||||
env,
|
||||
serverId,
|
||||
additionalPorts = [],
|
||||
enableDashboard = false,
|
||||
env,
|
||||
serverId,
|
||||
additionalPorts = [],
|
||||
}: TraefikOptions = {}) => {
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
||||
const containerName = "dokploy-traefik";
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Env: env,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
|
||||
Target: "/etc/traefik/traefik.yml",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: DYNAMIC_TRAEFIK_PATH,
|
||||
Target: "/etc/dokploy/traefik/dynamic",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
Labels: {
|
||||
"traefik.enable": "true",
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 443,
|
||||
PublishedPort: TRAEFIK_SSL_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: TRAEFIK_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
...(enableDashboard
|
||||
? [
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 8080,
|
||||
PublishMode: "host" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...additionalPorts.map((port) => ({
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
PublishMode: port.publishMode || ("host" as const),
|
||||
})),
|
||||
],
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
if (serverId) {
|
||||
await pullRemoteImage(imageName, serverId);
|
||||
} else {
|
||||
await pullImage(imageName);
|
||||
}
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
||||
const containerName = "dokploy-traefik";
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Env: env,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
|
||||
Target: "/etc/traefik/traefik.yml",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: DYNAMIC_TRAEFIK_PATH,
|
||||
Target: "/etc/dokploy/traefik/dynamic",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
// Labels: {
|
||||
// "traefik.enable": "true",
|
||||
// },
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 443,
|
||||
PublishedPort: TRAEFIK_SSL_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: TRAEFIK_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
...(enableDashboard
|
||||
? [
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 8080,
|
||||
PublishMode: "host" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...additionalPorts.map((port) => ({
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
PublishMode: port.publishMode || ("host" as const),
|
||||
})),
|
||||
],
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
if (serverId) {
|
||||
await pullRemoteImage(imageName, serverId);
|
||||
} else {
|
||||
await pullImage(imageName);
|
||||
}
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || [];
|
||||
const updatedEnv = !env ? existingEnv : env;
|
||||
const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || [];
|
||||
const updatedEnv = !env ? existingEnv : env;
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ContainerSpec: {
|
||||
...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec,
|
||||
Env: updatedEnv,
|
||||
},
|
||||
},
|
||||
};
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...updatedSettings,
|
||||
});
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ContainerSpec: {
|
||||
...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec,
|
||||
Env: updatedEnv,
|
||||
},
|
||||
},
|
||||
};
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...updatedSettings,
|
||||
});
|
||||
|
||||
console.log("Traefik Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
console.log("Traefik Not Found: Starting ✅");
|
||||
}
|
||||
console.log("Traefik Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
console.log("Traefik Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
|
||||
export const createDefaultServerTraefikConfig = () => {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
|
||||
|
||||
if (existsSync(configFilePath)) {
|
||||
console.log("Default traefik config already exists");
|
||||
return;
|
||||
}
|
||||
if (existsSync(configFilePath)) {
|
||||
console.log("Default traefik config already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = "dokploy";
|
||||
const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`;
|
||||
const config: FileConfig = {
|
||||
http: {
|
||||
routers: {
|
||||
[`${appName}-router-app`]: {
|
||||
rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`,
|
||||
service: `${appName}-service-app`,
|
||||
entryPoints: ["web"],
|
||||
},
|
||||
},
|
||||
services: {
|
||||
[`${appName}-service-app`]: {
|
||||
loadBalancer: {
|
||||
servers: [{ url: serviceURLDefault }],
|
||||
passHostHeader: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const appName = "dokploy";
|
||||
const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`;
|
||||
const config: FileConfig = {
|
||||
http: {
|
||||
routers: {
|
||||
[`${appName}-router-app`]: {
|
||||
rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`,
|
||||
service: `${appName}-service-app`,
|
||||
entryPoints: ["web"],
|
||||
},
|
||||
},
|
||||
services: {
|
||||
[`${appName}-service-app`]: {
|
||||
loadBalancer: {
|
||||
servers: [{ url: serviceURLDefault }],
|
||||
passHostHeader: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const yamlStr = dump(config);
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
|
||||
yamlStr,
|
||||
"utf8",
|
||||
);
|
||||
const yamlStr = dump(config);
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
|
||||
yamlStr,
|
||||
"utf8"
|
||||
);
|
||||
};
|
||||
|
||||
export const getDefaultTraefikConfig = () => {
|
||||
const configObject: MainTraefikConfig = {
|
||||
providers: {
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? {
|
||||
docker: {
|
||||
defaultRule:
|
||||
"Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
|
||||
},
|
||||
}
|
||||
: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
},
|
||||
}),
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
watch: true,
|
||||
},
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: `:${TRAEFIK_PORT}`,
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
api: {
|
||||
insecure: true,
|
||||
},
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
certificatesResolvers: {
|
||||
letsencrypt: {
|
||||
acme: {
|
||||
email: "test@localhost.com",
|
||||
storage: "/etc/dokploy/traefik/dynamic/acme.json",
|
||||
httpChallenge: {
|
||||
entryPoint: "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
const configObject: MainTraefikConfig = {
|
||||
providers: {
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? {
|
||||
docker: {
|
||||
defaultRule:
|
||||
"Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
|
||||
},
|
||||
}
|
||||
: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
},
|
||||
}),
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
watch: true,
|
||||
},
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: `:${TRAEFIK_PORT}`,
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
api: {
|
||||
insecure: true,
|
||||
},
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
certificatesResolvers: {
|
||||
letsencrypt: {
|
||||
acme: {
|
||||
email: "test@localhost.com",
|
||||
storage: "/etc/dokploy/traefik/dynamic/acme.json",
|
||||
httpChallenge: {
|
||||
entryPoint: "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const yamlStr = dump(configObject);
|
||||
const yamlStr = dump(configObject);
|
||||
|
||||
return yamlStr;
|
||||
return yamlStr;
|
||||
};
|
||||
|
||||
export const getDefaultServerTraefikConfig = () => {
|
||||
const configObject: MainTraefikConfig = {
|
||||
providers: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
},
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
watch: true,
|
||||
},
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: `:${TRAEFIK_PORT}`,
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
api: {
|
||||
insecure: true,
|
||||
},
|
||||
certificatesResolvers: {
|
||||
letsencrypt: {
|
||||
acme: {
|
||||
email: "test@localhost.com",
|
||||
storage: "/etc/dokploy/traefik/dynamic/acme.json",
|
||||
httpChallenge: {
|
||||
entryPoint: "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const configObject: MainTraefikConfig = {
|
||||
providers: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
},
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
watch: true,
|
||||
},
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: `:${TRAEFIK_PORT}`,
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
api: {
|
||||
insecure: true,
|
||||
},
|
||||
certificatesResolvers: {
|
||||
letsencrypt: {
|
||||
acme: {
|
||||
email: "test@localhost.com",
|
||||
storage: "/etc/dokploy/traefik/dynamic/acme.json",
|
||||
httpChallenge: {
|
||||
entryPoint: "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const yamlStr = dump(configObject);
|
||||
const yamlStr = dump(configObject);
|
||||
|
||||
return yamlStr;
|
||||
return yamlStr;
|
||||
};
|
||||
|
||||
export const createDefaultTraefikConfig = () => {
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
|
||||
const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
|
||||
const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");
|
||||
|
||||
if (existsSync(acmeJsonPath)) {
|
||||
chmodSync(acmeJsonPath, "600");
|
||||
}
|
||||
if (existsSync(mainConfig)) {
|
||||
console.log("Main config already exists");
|
||||
return;
|
||||
}
|
||||
const yamlStr = getDefaultTraefikConfig();
|
||||
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(mainConfig, yamlStr, "utf8");
|
||||
if (existsSync(acmeJsonPath)) {
|
||||
chmodSync(acmeJsonPath, "600");
|
||||
}
|
||||
if (existsSync(mainConfig)) {
|
||||
console.log("Main config already exists");
|
||||
return;
|
||||
}
|
||||
const yamlStr = getDefaultTraefikConfig();
|
||||
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(mainConfig, yamlStr, "utf8");
|
||||
};
|
||||
|
||||
export const getDefaultMiddlewares = () => {
|
||||
const defaultMiddlewares = {
|
||||
http: {
|
||||
middlewares: {
|
||||
"redirect-to-https": {
|
||||
redirectScheme: {
|
||||
scheme: "https",
|
||||
permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const yamlStr = dump(defaultMiddlewares);
|
||||
return yamlStr;
|
||||
const defaultMiddlewares = {
|
||||
http: {
|
||||
middlewares: {
|
||||
"redirect-to-https": {
|
||||
redirectScheme: {
|
||||
scheme: "https",
|
||||
permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const yamlStr = dump(defaultMiddlewares);
|
||||
return yamlStr;
|
||||
};
|
||||
export const createDefaultMiddlewares = () => {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const middlewaresPath = path.join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
||||
if (existsSync(middlewaresPath)) {
|
||||
console.log("Default middlewares already exists");
|
||||
return;
|
||||
}
|
||||
const yamlStr = getDefaultMiddlewares();
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(middlewaresPath, yamlStr, "utf8");
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const middlewaresPath = path.join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
||||
if (existsSync(middlewaresPath)) {
|
||||
console.log("Default middlewares already exists");
|
||||
return;
|
||||
}
|
||||
const yamlStr = getDefaultMiddlewares();
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(middlewaresPath, yamlStr, "utf8");
|
||||
};
|
||||
|
||||
@@ -27,9 +27,7 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const zip = new AdmZip(buffer);
|
||||
const zipEntries = zip
|
||||
.getEntries()
|
||||
.filter((entry) => !entry.entryName.startsWith("__MACOSX"));
|
||||
const zipEntries = zip.getEntries();
|
||||
|
||||
const rootEntries = zipEntries.filter(
|
||||
(entry) =>
|
||||
@@ -61,22 +59,14 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
|
||||
if (!filePath) continue;
|
||||
|
||||
const fullPath = path.join(outputPath, filePath).replace(/\\/g, "/");
|
||||
const fullPath = path.join(outputPath, filePath);
|
||||
|
||||
if (application.serverId) {
|
||||
if (!entry.isDirectory) {
|
||||
if (entry.isDirectory) {
|
||||
await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`);
|
||||
} else {
|
||||
if (sftp === null) throw new Error("No SFTP connection available");
|
||||
try {
|
||||
const dirPath = path.dirname(fullPath);
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`mkdir -p "${dirPath}"`,
|
||||
);
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
} catch (err) {
|
||||
console.error(`Error uploading file ${fullPath}:`, err);
|
||||
throw err;
|
||||
}
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
}
|
||||
} else {
|
||||
if (entry.isDirectory) {
|
||||
@@ -113,6 +103,7 @@ const getSFTPConnection = async (serverId: string): Promise<SFTPWrapper> => {
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -124,10 +115,7 @@ const uploadFileToServer = (
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.writeFile(remotePath, data, (err) => {
|
||||
if (err) {
|
||||
console.error(`SFTP write error for ${remotePath}:`, err);
|
||||
return reject(err);
|
||||
}
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,66 +28,17 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
databasePassword,
|
||||
command,
|
||||
mounts,
|
||||
replicaSets,
|
||||
} = mongo;
|
||||
|
||||
const startupScript = `
|
||||
#!/bin/bash
|
||||
${
|
||||
replicaSets
|
||||
? `
|
||||
mongod --port 27017 --replSet rs0 --bind_ip_all &
|
||||
MONGOD_PID=$!
|
||||
|
||||
# Wait for MongoDB to be ready
|
||||
while ! mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Check if replica set is already initialized
|
||||
REPLICA_STATUS=$(mongosh --quiet --eval "rs.status().ok || 0")
|
||||
|
||||
if [ "$REPLICA_STATUS" != "1" ]; then
|
||||
echo "Initializing replica set..."
|
||||
mongosh --eval '
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "localhost:27017", priority: 1 }]
|
||||
});
|
||||
|
||||
// Wait for the replica set to initialize
|
||||
while (!rs.isMaster().ismaster) {
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
// Create root user after replica set is initialized and we are primary
|
||||
db.getSiblingDB("admin").createUser({
|
||||
user: "${databaseUser}",
|
||||
pwd: "${databasePassword}",
|
||||
roles: ["root"]
|
||||
});
|
||||
'
|
||||
|
||||
else
|
||||
echo "Replica set already initialized."
|
||||
fi
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${command ?? "wait $MONGOD_PID"}`;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMongoEnv,
|
||||
mongo.project.env,
|
||||
@@ -105,17 +56,12 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(replicaSets
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", startupScript],
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {
|
||||
...(command && {
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", command],
|
||||
}),
|
||||
}),
|
||||
: {}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
@@ -144,7 +90,6 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
: [],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
Reference in New Issue
Block a user