mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acd722678e | ||
|
|
3750977f41 | ||
|
|
9b401059b0 | ||
|
|
6a3ef5c860 | ||
|
|
a5eb4b0a72 | ||
|
|
b5c0876dd4 | ||
|
|
9745d12ac8 | ||
|
|
4aaf04ce74 | ||
|
|
ecfca9419a | ||
|
|
dfd6764320 | ||
|
|
73bf5274f5 | ||
|
|
fc38a42587 | ||
|
|
9b255964fe | ||
|
|
29f55ca1a0 | ||
|
|
6a5fb8faff | ||
|
|
5c225c8d42 | ||
|
|
c1c5fc978b | ||
|
|
18b4b23f79 | ||
|
|
727e50648e | ||
|
|
0b2b20caeb | ||
|
|
6cc64b4454 | ||
|
|
349bc89851 | ||
|
|
7046d05f63 | ||
|
|
cef21ac8b5 | ||
|
|
7027f39c48 | ||
|
|
9f6f872536 | ||
|
|
cb03b153ac | ||
|
|
e5d7a0cb10 | ||
|
|
bf65bc9462 | ||
|
|
b48b9765cd | ||
|
|
7cce02f74d | ||
|
|
3dc3672406 | ||
|
|
bbfe095045 | ||
|
|
65c1001751 | ||
|
|
5212bde021 | ||
|
|
9059f42b03 | ||
|
|
3b9f5d6f5c | ||
|
|
21dee4abac |
@@ -74,7 +74,7 @@ export function generateMetadata({
|
|||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
creator: "@siumauricio",
|
creator: "@getdokploy",
|
||||||
title: page.data.title,
|
title: page.data.title,
|
||||||
description: page.data.description,
|
description: page.data.description,
|
||||||
images: [
|
images: [
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Configure the source of your code, the way your application is built, and also m
|
|||||||
|
|
||||||
If you need to assign environment variables to your application, you can do so here.
|
If you need to assign environment variables to your application, you can do so here.
|
||||||
|
|
||||||
|
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ Actions like deploying, updating, and deleting your database, and stopping it.
|
|||||||
|
|
||||||
If you need to assign environment variables to your application, you can do so here.
|
If you need to assign environment variables to your application, you can do so here.
|
||||||
|
|
||||||
|
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Published Port</FormLabel>
|
<FormLabel>Published Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput placeholder="1-65535" {...field} />
|
||||||
placeholder="1-65535"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(0);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetPort"
|
name="targetPort"
|
||||||
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Target Port</FormLabel>
|
<FormLabel>Target Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="1-65535" {...field} />
|
||||||
placeholder="1-65535"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(0);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -36,6 +45,36 @@ const AddRedirectchema = z.object({
|
|||||||
|
|
||||||
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
||||||
|
|
||||||
|
// Default presets
|
||||||
|
const redirectPresets = [
|
||||||
|
// {
|
||||||
|
// label: "Allow www & non-www.",
|
||||||
|
// redirect: {
|
||||||
|
// regex: "",
|
||||||
|
// permanent: false,
|
||||||
|
// replacement: "",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: "to-www",
|
||||||
|
label: "Redirect to www",
|
||||||
|
redirect: {
|
||||||
|
regex: "^https?://(?:www.)?(.+)",
|
||||||
|
permanent: true,
|
||||||
|
replacement: "https://www.$${1}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "to-non-www",
|
||||||
|
label: "Redirect to non-www",
|
||||||
|
redirect: {
|
||||||
|
regex: "^https?://www.(.+)",
|
||||||
|
permanent: true,
|
||||||
|
replacement: "https://$${1}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -43,9 +82,10 @@ interface Props {
|
|||||||
|
|
||||||
export const AddRedirect = ({
|
export const AddRedirect = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="w-4 h-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [presetSelected, setPresetSelected] = useState("");
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -81,19 +121,36 @@ export const AddRedirect = ({
|
|||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
onDialogToggle(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the redirect");
|
toast.error("Error to create the redirect");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDialogToggle = (open: boolean) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
||||||
|
// setPresetSelected("");
|
||||||
|
// form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPresetSelect = (presetId: string) => {
|
||||||
|
const redirectPreset = redirectPresets.find(
|
||||||
|
(preset) => preset.id === presetId,
|
||||||
|
)?.redirect;
|
||||||
|
if (!redirectPreset) return;
|
||||||
|
const { regex, permanent, replacement } = redirectPreset;
|
||||||
|
form.reset({ regex, permanent, replacement }, { keepDefaultValues: true });
|
||||||
|
setPresetSelected(presetId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Redirects</DialogTitle>
|
<DialogTitle>Redirects</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -102,6 +159,24 @@ export const AddRedirect = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Label>Presets</Label>
|
||||||
|
<Select onValueChange={onPresetSelect} value={presetSelected}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="No preset selected" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{redirectPresets.map((preset) => (
|
||||||
|
<SelectItem key={preset.label} value={preset.id}>
|
||||||
|
{preset.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-add-redirect"
|
id="hook-form-add-redirect"
|
||||||
@@ -142,7 +217,7 @@ export const AddRedirect = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="permanent"
|
name="permanent"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Permanent</FormLabel>
|
<FormLabel>Permanent</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -140,7 +140,7 @@ export const AddDomain = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
@@ -228,19 +228,36 @@ export const AddDomain = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Container Port</FormLabel>
|
<FormLabel>Container Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
placeholder={"3000"}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(Number.parseInt(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<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>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{form.getValues().https && (
|
{form.getValues().https && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -270,28 +287,6 @@ export const AddDomain = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -161,7 +161,7 @@ export const AddDomainCompose = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
@@ -190,7 +190,7 @@ export const AddDomainCompose = ({
|
|||||||
{errorServices?.message}
|
{errorServices?.message}
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row gap-4 w-full items-end">
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serviceName"
|
name="serviceName"
|
||||||
@@ -364,19 +364,36 @@ export const AddDomainCompose = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Container Port</FormLabel>
|
<FormLabel>Container Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
placeholder={"3000"}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(Number.parseInt(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<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>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{https && (
|
{https && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -406,28 +423,6 @@ export const AddDomainCompose = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
form,
|
form,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
ip,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CircuitBoard, HelpCircle } from "lucide-react";
|
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -71,6 +71,7 @@ interface Props {
|
|||||||
|
|
||||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
|
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
|
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
|
|||||||
@@ -17,10 +17,18 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Container } from "lucide-react";
|
import { AlertTriangle, Container } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
|
|||||||
password: z.string().min(1, {
|
password: z.string().min(1, {
|
||||||
message: "Password is required",
|
message: "Password is required",
|
||||||
}),
|
}),
|
||||||
registryUrl: z.string().min(1, {
|
registryUrl: z.string(),
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
imagePrefix: z.string(),
|
imagePrefix: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||||
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync: testRegistry, isLoading } =
|
const { mutateAsync: testRegistry, isLoading } =
|
||||||
api.registry.testRegistry.useMutation();
|
api.registry.testRegistry.useMutation();
|
||||||
const router = useRouter();
|
|
||||||
const form = useForm<AddRegistry>({
|
const form = useForm<AddRegistry>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
|
|||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
imagePrefix: "",
|
imagePrefix: "",
|
||||||
registryName: "",
|
registryName: "",
|
||||||
|
serverId: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRegistrySchema),
|
resolver: zodResolver(AddRegistrySchema),
|
||||||
});
|
});
|
||||||
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
|
|||||||
const registryUrl = form.watch("registryUrl");
|
const registryUrl = form.watch("registryUrl");
|
||||||
const registryName = form.watch("registryName");
|
const registryName = form.watch("registryName");
|
||||||
const imagePrefix = form.watch("imagePrefix");
|
const imagePrefix = form.watch("imagePrefix");
|
||||||
|
const serverId = form.watch("serverId");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
|
|||||||
password: "",
|
password: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
imagePrefix: "",
|
imagePrefix: "",
|
||||||
|
serverId: "",
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
|
|||||||
registryUrl: data.registryUrl,
|
registryUrl: data.registryUrl,
|
||||||
registryType: "cloud",
|
registryType: "cloud",
|
||||||
imagePrefix: data.imagePrefix,
|
imagePrefix: data.imagePrefix,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
await utils.registry.all.invalidate();
|
await utils.registry.all.invalidate();
|
||||||
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
|
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||||
<Button
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
type="button"
|
<span className="text-sm text-muted-foreground">
|
||||||
variant={"secondary"}
|
Select a server to test the registry. If you don't have a
|
||||||
isLoading={isLoading}
|
server choose the default one.
|
||||||
onClick={async () => {
|
</span>
|
||||||
await testRegistry({
|
<FormField
|
||||||
username: username,
|
control={form.control}
|
||||||
password: password,
|
name="serverId"
|
||||||
registryUrl: registryUrl,
|
render={({ field }) => (
|
||||||
registryName: registryName,
|
<FormItem>
|
||||||
registryType: "cloud",
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
imagePrefix: imagePrefix,
|
<FormControl>
|
||||||
})
|
<Select
|
||||||
.then((data) => {
|
onValueChange={field.onChange}
|
||||||
if (data) {
|
defaultValue={field.value}
|
||||||
toast.success("Registry Tested Successfully");
|
>
|
||||||
} else {
|
<SelectTrigger className="w-full">
|
||||||
toast.error("Registry Test Failed");
|
<SelectValue placeholder="Select a server" />
|
||||||
}
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
await testRegistry({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
registryUrl: registryUrl,
|
||||||
|
registryName: registryName,
|
||||||
|
registryType: "cloud",
|
||||||
|
imagePrefix: imagePrefix,
|
||||||
|
serverId: serverId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then((data) => {
|
||||||
toast.error("Error to test the registry");
|
if (data) {
|
||||||
});
|
toast.success("Registry Tested Successfully");
|
||||||
}}
|
} else {
|
||||||
>
|
toast.error("Registry Test Failed");
|
||||||
Test Registry
|
}
|
||||||
</Button>
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to test the registry");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Registry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
|
|||||||
message: "Username is required",
|
message: "Username is required",
|
||||||
}),
|
}),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
registryUrl: z.string().min(1, {
|
registryUrl: z.string(),
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
imagePrefix: z.string(),
|
imagePrefix: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
||||||
@@ -48,6 +56,8 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: testRegistry, isLoading } =
|
const { mutateAsync: testRegistry, isLoading } =
|
||||||
api.registry.testRegistry.useMutation();
|
api.registry.testRegistry.useMutation();
|
||||||
const { data, refetch } = api.registry.one.useQuery(
|
const { data, refetch } = api.registry.one.useQuery(
|
||||||
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
|
serverId: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(updateRegistry),
|
resolver: zodResolver(updateRegistry),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(form.formState.errors);
|
||||||
|
|
||||||
const password = form.watch("password");
|
const password = form.watch("password");
|
||||||
const username = form.watch("username");
|
const username = form.watch("username");
|
||||||
const registryUrl = form.watch("registryUrl");
|
const registryUrl = form.watch("registryUrl");
|
||||||
const registryName = form.watch("registryName");
|
const registryName = form.watch("registryName");
|
||||||
const imagePrefix = form.watch("imagePrefix");
|
const imagePrefix = form.watch("imagePrefix");
|
||||||
|
const serverId = form.watch("serverId");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: data.username || "",
|
username: data.username || "",
|
||||||
password: "",
|
password: "",
|
||||||
registryUrl: data.registryUrl || "",
|
registryUrl: data.registryUrl || "",
|
||||||
|
serverId: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: data.username,
|
username: data.username,
|
||||||
registryUrl: data.registryUrl,
|
registryUrl: data.registryUrl,
|
||||||
imagePrefix: data.imagePrefix,
|
imagePrefix: data.imagePrefix,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
toast.success("Registry Updated");
|
toast.success("Registry Updated");
|
||||||
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter
|
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||||
className={cn(
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
isCloud ? "sm:justify-between " : "",
|
<span className="text-sm text-muted-foreground">
|
||||||
"flex flex-row w-full gap-4 flex-wrap",
|
Select a server to test the registry. If you don't have a server
|
||||||
)}
|
choose the default one.
|
||||||
>
|
</span>
|
||||||
{isCloud && (
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
registryName: registryName,
|
registryName: registryName,
|
||||||
registryType: "cloud",
|
registryType: "cloud",
|
||||||
imagePrefix: imagePrefix,
|
imagePrefix: imagePrefix,
|
||||||
|
serverId: serverId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
>
|
>
|
||||||
Test Registry
|
Test Registry
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
form="hook-form"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
|
form="hook-form"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -212,7 +212,21 @@ export const AddServer = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Port</FormLabel>
|
<FormLabel>Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="22" {...field} />
|
<Input
|
||||||
|
placeholder="22"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(0);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -228,7 +228,21 @@ export const UpdateServer = ({ serverId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Port</FormLabel>
|
<FormLabel>Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="22" {...field} />
|
<Input
|
||||||
|
placeholder="22"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(0);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -31,4 +31,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
);
|
);
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input };
|
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, errorMessage, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className={cn("text-left", className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={props.value === undefined ? undefined : String(props.value)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
props.onChange?.(e);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
value: number,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
props.onChange?.(
|
||||||
|
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
NumberInput.displayName = "NumberInput";
|
||||||
|
|
||||||
|
export { Input, NumberInput };
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0038_rapid_landau.sql
Normal file
1
apps/dokploy/drizzle/0038_rapid_landau.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "registry" ALTER COLUMN "registryUrl" SET DEFAULT '';
|
||||||
3824
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3824
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -267,6 +267,13 @@
|
|||||||
"when": 1726988289562,
|
"when": 1726988289562,
|
||||||
"tag": "0037_legal_namor",
|
"tag": "0037_legal_namor",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 38,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1727942090102,
|
||||||
|
"tag": "0038_rapid_landau",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.9.0",
|
"version": "v0.9.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
const destination = await findDestinationById(input.destinationId);
|
const destination = await findDestinationById(input.destinationId);
|
||||||
return destination;
|
return destination;
|
||||||
}),
|
}),
|
||||||
all: adminProcedure.query(async () => {
|
all: protectedProcedure.query(async () => {
|
||||||
return await db.query.destinations.findMany({});
|
return await db.query.destinations.findMany({});
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: adminProcedure
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
apiUpdateRegistry,
|
apiUpdateRegistry,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||||
import { execAsync } from "@/server/utils/process/execAsync";
|
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||||
import { manageRegistry } from "@/server/utils/traefik/registry";
|
import { manageRegistry } from "@/server/utils/traefik/registry";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import {
|
import {
|
||||||
@@ -58,7 +58,13 @@ export const registryRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
await execAsync(loginCommand);
|
|
||||||
|
if (input.serverId && input.serverId !== "none") {
|
||||||
|
await execAsyncRemote(input.serverId, loginCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(loginCommand);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error Registry:", error);
|
console.log("Error Registry:", error);
|
||||||
@@ -78,6 +84,7 @@ export const registryRouter = createTRPCRouter({
|
|||||||
? input.registryUrl
|
? input.registryUrl
|
||||||
: "dokploy-registry.docker.localhost",
|
: "dokploy-registry.docker.localhost",
|
||||||
imagePrefix: null,
|
imagePrefix: null,
|
||||||
|
serverId: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await manageRegistry(selfHostedRegistry);
|
await manageRegistry(selfHostedRegistry);
|
||||||
@@ -86,3 +93,17 @@ export const registryRouter = createTRPCRouter({
|
|||||||
return selfHostedRegistry;
|
return selfHostedRegistry;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shellEscape = (str: string) => {
|
||||||
|
const ret = [];
|
||||||
|
let s = str;
|
||||||
|
if (/[^A-Za-z0-9_\/:=-]/.test(s)) {
|
||||||
|
s = `'${s.replace(/'/g, "'\\''")}'`;
|
||||||
|
s = s
|
||||||
|
.replace(/^(?:'')+/g, "") // unduplicate single-quote at the beginning
|
||||||
|
.replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
|
||||||
|
}
|
||||||
|
ret.push(s);
|
||||||
|
|
||||||
|
return ret.join(" ");
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { db } from "@/server/db";
|
|||||||
import { type apiCreateRegistry, registry } from "@/server/db/schema";
|
import { type apiCreateRegistry, registry } from "@/server/db/schema";
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||||
import { removeService } from "@/server/utils/docker/utils";
|
import { removeService } from "@/server/utils/docker/utils";
|
||||||
import { execAsync } from "@/server/utils/process/execAsync";
|
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||||
import {
|
import {
|
||||||
manageRegistry,
|
manageRegistry,
|
||||||
removeSelfHostedRegistry,
|
removeSelfHostedRegistry,
|
||||||
@@ -32,9 +32,10 @@ export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
|||||||
message: "Error input: Inserting registry",
|
message: "Error input: Inserting registry",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
if (newRegistry.registryType === "cloud") {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
await execAsyncRemote(input.serverId, loginCommand);
|
||||||
|
} else if (newRegistry.registryType === "cloud") {
|
||||||
await execAsync(loginCommand);
|
await execAsync(loginCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export const removeRegistry = async (registryId: string) => {
|
|||||||
|
|
||||||
export const updateRegistry = async (
|
export const updateRegistry = async (
|
||||||
registryId: string,
|
registryId: string,
|
||||||
registryData: Partial<Registry>,
|
registryData: Partial<Registry> & { serverId?: string | null },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await db
|
const response = await db
|
||||||
@@ -92,6 +93,13 @@ export const updateRegistry = async (
|
|||||||
await manageRegistry(response);
|
await manageRegistry(response);
|
||||||
await initializeRegistry(response.username, response.password);
|
await initializeRegistry(response.username, response.password);
|
||||||
}
|
}
|
||||||
|
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`;
|
||||||
|
|
||||||
|
if (registryData?.serverId && registryData?.serverId !== "none") {
|
||||||
|
await execAsyncRemote(registryData.serverId, loginCommand);
|
||||||
|
} else if (response?.registryType === "cloud") {
|
||||||
|
await execAsync(loginCommand);
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import { TimeSpan } from "lucia";
|
import { TimeSpan } from "lucia";
|
||||||
import { Lucia } from "lucia/dist/core.js";
|
import { Lucia } from "lucia/dist/core.js";
|
||||||
|
import { findAdminByAuthId } from "../api/services/admin";
|
||||||
|
import { findUserByAuthId } from "../api/services/user";
|
||||||
import { type ReturnValidateToken, adapter } from "./auth";
|
import { type ReturnValidateToken, adapter } from "./auth";
|
||||||
|
|
||||||
export const luciaToken = new Lucia(adapter, {
|
export const luciaToken = new Lucia(adapter, {
|
||||||
@@ -31,6 +33,16 @@ export const validateBearerToken = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const result = await luciaToken.validateSession(sessionId);
|
const result = await luciaToken.validateSession(sessionId);
|
||||||
|
|
||||||
|
if (result.user) {
|
||||||
|
if (result.user?.rol === "admin") {
|
||||||
|
const admin = await findAdminByAuthId(result.user.id);
|
||||||
|
result.user.adminId = admin.adminId;
|
||||||
|
} else if (result.user?.rol === "user") {
|
||||||
|
const userResult = await findUserByAuthId(result.user.id);
|
||||||
|
result.user.adminId = userResult.adminId;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
session: result.session,
|
session: result.session,
|
||||||
...((result.user && {
|
...((result.user && {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const registry = pgTable("registry", {
|
|||||||
imagePrefix: text("imagePrefix"),
|
imagePrefix: text("imagePrefix"),
|
||||||
username: text("username").notNull(),
|
username: text("username").notNull(),
|
||||||
password: text("password").notNull(),
|
password: text("password").notNull(),
|
||||||
registryUrl: text("registryUrl").notNull(),
|
registryUrl: text("registryUrl").notNull().default(""),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
@@ -45,7 +45,7 @@ const createSchema = createInsertSchema(registry, {
|
|||||||
registryName: z.string().min(1),
|
registryName: z.string().min(1),
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
registryUrl: z.string().min(1),
|
registryUrl: z.string(),
|
||||||
adminId: z.string().min(1),
|
adminId: z.string().min(1),
|
||||||
registryId: z.string().min(1),
|
registryId: z.string().min(1),
|
||||||
registryType: z.enum(["selfHosted", "cloud"]),
|
registryType: z.enum(["selfHosted", "cloud"]),
|
||||||
@@ -62,7 +62,10 @@ export const apiCreateRegistry = createSchema
|
|||||||
registryType: z.enum(["selfHosted", "cloud"]),
|
registryType: z.enum(["selfHosted", "cloud"]),
|
||||||
imagePrefix: z.string().nullable().optional(),
|
imagePrefix: z.string().nullable().optional(),
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.extend({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiTestRegistry = createSchema.pick({}).extend({
|
export const apiTestRegistry = createSchema.pick({}).extend({
|
||||||
registryName: z.string().min(1),
|
registryName: z.string().min(1),
|
||||||
@@ -71,6 +74,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({
|
|||||||
registryUrl: z.string(),
|
registryUrl: z.string(),
|
||||||
registryType: z.enum(["selfHosted", "cloud"]),
|
registryType: z.enum(["selfHosted", "cloud"]),
|
||||||
imagePrefix: z.string().nullable().optional(),
|
imagePrefix: z.string().nullable().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiRemoveRegistry = createSchema
|
export const apiRemoveRegistry = createSchema
|
||||||
@@ -87,6 +91,7 @@ export const apiFindOneRegistry = createSchema
|
|||||||
|
|
||||||
export const apiUpdateRegistry = createSchema.partial().extend({
|
export const apiUpdateRegistry = createSchema.partial().extend({
|
||||||
registryId: z.string().min(1),
|
registryId: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiEnableSelfHostedRegistry = createSchema
|
export const apiEnableSelfHostedRegistry = createSchema
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
import { type WriteStream, existsSync, mkdirSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { buildStatic, getStaticCommand } from "@/server/utils/builders/static";
|
import { buildStatic, getStaticCommand } from "@/server/utils/builders/static";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@@ -42,7 +42,6 @@ export const buildNixpacks = async (
|
|||||||
and copy the artifacts on the host filesystem.
|
and copy the artifacts on the host filesystem.
|
||||||
Then, remove the container and create a static build.
|
Then, remove the container and create a static build.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (publishDirectory) {
|
if (publishDirectory) {
|
||||||
await spawnAsync(
|
await spawnAsync(
|
||||||
"docker",
|
"docker",
|
||||||
@@ -50,12 +49,22 @@ export const buildNixpacks = async (
|
|||||||
writeToStream,
|
writeToStream,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const localPath = path.join(buildAppDirectory, publishDirectory);
|
||||||
|
|
||||||
|
if (!existsSync(path.dirname(localPath))) {
|
||||||
|
mkdirSync(path.dirname(localPath), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.docker.com/reference/cli/docker/container/cp/
|
||||||
|
const isDirectory =
|
||||||
|
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
||||||
|
|
||||||
await spawnAsync(
|
await spawnAsync(
|
||||||
"docker",
|
"docker",
|
||||||
[
|
[
|
||||||
"cp",
|
"cp",
|
||||||
`${buildContainerId}:/app/${publishDirectory}`,
|
`${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`,
|
||||||
path.join(buildAppDirectory, publishDirectory),
|
localPath,
|
||||||
],
|
],
|
||||||
writeToStream,
|
writeToStream,
|
||||||
);
|
);
|
||||||
@@ -108,9 +117,14 @@ echo "✅ Nixpacks build completed." >> ${logPath};
|
|||||||
Then, remove the container and create a static build.
|
Then, remove the container and create a static build.
|
||||||
*/
|
*/
|
||||||
if (publishDirectory) {
|
if (publishDirectory) {
|
||||||
|
const localPath = path.join(buildAppDirectory, publishDirectory);
|
||||||
|
const isDirectory =
|
||||||
|
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
||||||
|
|
||||||
bashCommand += `
|
bashCommand += `
|
||||||
docker create --name ${buildContainerId} ${appName}
|
docker create --name ${buildContainerId} ${appName}
|
||||||
docker cp ${buildContainerId}:/app/${publishDirectory} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
|
mkdir -p ${localPath}
|
||||||
|
docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
|
||||||
docker rm ${buildContainerId}
|
docker rm ${buildContainerId}
|
||||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
|
|||||||
@@ -147,15 +147,15 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
|||||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||||
const basePath = COMPOSE_PATH;
|
const basePath = COMPOSE_PATH;
|
||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsyncRemote(
|
const command = `
|
||||||
serverId,
|
rm -rf ${outputPath};
|
||||||
`git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}`,
|
git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}
|
||||||
);
|
`;
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,13 +271,13 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
|||||||
const octokit = authGithub(githubProvider);
|
const octokit = authGithub(githubProvider);
|
||||||
const token = await getGithubToken(octokit);
|
const token = await getGithubToken(octokit);
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||||
try {
|
try {
|
||||||
await execAsyncRemote(
|
const command = `
|
||||||
serverId,
|
rm -rf ${outputPath};
|
||||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||||
);
|
`;
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -390,14 +390,14 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
|||||||
await refreshGitlabToken(gitlabId);
|
await refreshGitlabToken(gitlabId);
|
||||||
const basePath = COMPOSE_PATH;
|
const basePath = COMPOSE_PATH;
|
||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||||
try {
|
try {
|
||||||
await execAsyncRemote(
|
const command = `
|
||||||
serverId,
|
rm -rf ${outputPath};
|
||||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||||
);
|
`;
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
|
|||||||
try {
|
try {
|
||||||
const encodedContent = encodeBase64(composeFile);
|
const encodedContent = encodeBase64(composeFile);
|
||||||
const command = `
|
const command = `
|
||||||
|
rm -rf ${outputPath};
|
||||||
mkdir -p ${outputPath};
|
mkdir -p ${outputPath};
|
||||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export const setupTerminalWebSocketServer = (
|
|||||||
"StrictHostKeyChecking=no",
|
"StrictHostKeyChecking=no",
|
||||||
"-i",
|
"-i",
|
||||||
privateKey,
|
privateKey,
|
||||||
|
"-p",
|
||||||
|
`${server.port}`,
|
||||||
`${server.username}@${server.ipAddress}`,
|
`${server.username}@${server.ipAddress}`,
|
||||||
];
|
];
|
||||||
const ptyProcess = spawn("ssh", sshCommand.slice(1), {
|
const ptyProcess = spawn("ssh", sshCommand.slice(1), {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function Footer() {
|
|||||||
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
|
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
|
||||||
<div className="flex gap-x-6">
|
<div className="flex gap-x-6">
|
||||||
<Link
|
<Link
|
||||||
href="https://twitter.com/Siumauricio"
|
href="https://x.com/getdokploy"
|
||||||
className="group"
|
className="group"
|
||||||
aria-label="Dokploy on Twitter"
|
aria-label="Dokploy on Twitter"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,114 +1,133 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
install_dokploy() {
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo "This script must be run as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ]; then
|
# check if is Mac OS
|
||||||
echo "This script must be run as root" >&2
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
exit 1
|
echo "This script must be run on Linux" >&2
|
||||||
fi
|
exit 1
|
||||||
|
fi
|
||||||
# check if is Mac OS
|
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
|
||||||
echo "This script must be run on Linux" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# check if is running inside a container
|
# check if is running inside a container
|
||||||
if [ -f /.dockerenv ]; then
|
if [ -f /.dockerenv ]; then
|
||||||
echo "This script must be run on Linux" >&2
|
echo "This script must be run on Linux" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# check if something is running on port 80
|
# check if something is running on port 80
|
||||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||||
echo "Error: something is already running on port 80" >&2
|
echo "Error: something is already running on port 80" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# check if something is running on port 443
|
# check if something is running on port 443
|
||||||
if ss -tulnp | grep ':443 ' >/dev/null; then
|
if ss -tulnp | grep ':443 ' >/dev/null; then
|
||||||
echo "Error: something is already running on port 443" >&2
|
echo "Error: something is already running on port 443" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
if command_exists docker; then
|
if command_exists docker; then
|
||||||
echo "Docker already installed"
|
echo "Docker already installed"
|
||||||
else
|
|
||||||
curl -sSL https://get.docker.com | sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker swarm leave --force 2>/dev/null
|
|
||||||
|
|
||||||
get_ip() {
|
|
||||||
# Try to get IPv4
|
|
||||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
|
||||||
|
|
||||||
if [ -n "$ipv4" ]; then
|
|
||||||
echo "$ipv4"
|
|
||||||
else
|
else
|
||||||
# Try to get IPv6
|
curl -sSL https://get.docker.com | sh
|
||||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
fi
|
||||||
if [ -n "$ipv6" ]; then
|
|
||||||
echo "$ipv6"
|
docker swarm leave --force 2>/dev/null
|
||||||
|
|
||||||
|
get_ip() {
|
||||||
|
# Try to get IPv4
|
||||||
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$ipv4" ]; then
|
||||||
|
echo "$ipv4"
|
||||||
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
}
|
||||||
|
|
||||||
|
advertise_addr=$(get_ip)
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
echo "Swarm initialized"
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
|
docker pull dokploy/dokploy:canary
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
docker service create \
|
||||||
|
--name dokploy \
|
||||||
|
--replicas 1 \
|
||||||
|
--network dokploy-network \
|
||||||
|
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||||
|
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||||
|
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||||
|
--publish published=3000,target=3000,mode=host \
|
||||||
|
--update-parallelism 1 \
|
||||||
|
--update-order stop-first \
|
||||||
|
--constraint 'node.role == manager' \
|
||||||
|
-e RELEASE_TAG=canary \
|
||||||
|
dokploy/dokploy:canary
|
||||||
|
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
BLUE="\033[0;34m"
|
||||||
|
NC="\033[0m" # No Color
|
||||||
|
|
||||||
|
format_ip_for_url() {
|
||||||
|
local ip="$1"
|
||||||
|
if echo "$ip" | grep -q ':'; then
|
||||||
|
# IPv6
|
||||||
|
echo "[${ip}]"
|
||||||
|
else
|
||||||
|
# IPv4
|
||||||
|
echo "${ip}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_addr=$(format_ip_for_url "$advertise_addr")
|
||||||
|
echo ""
|
||||||
|
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
|
||||||
|
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
|
||||||
|
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_addr=$(get_ip)
|
update_dokploy() {
|
||||||
|
echo "Updating Dokploy..."
|
||||||
|
|
||||||
|
# Pull the latest canary image
|
||||||
|
docker pull dokploy/dokploy:canary
|
||||||
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
# Update the service
|
||||||
|
docker service update --image dokploy/dokploy:canary dokploy
|
||||||
|
|
||||||
echo "Swarm initialized"
|
echo "Dokploy has been updated to the latest canary version."
|
||||||
|
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
|
||||||
|
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
mkdir -p /etc/dokploy
|
|
||||||
|
|
||||||
chmod 777 /etc/dokploy
|
|
||||||
|
|
||||||
docker pull dokploy/dokploy:canary
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
docker service create \
|
|
||||||
--name dokploy \
|
|
||||||
--replicas 1 \
|
|
||||||
--network dokploy-network \
|
|
||||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
|
||||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
|
||||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
|
||||||
--publish published=3000,target=3000,mode=host \
|
|
||||||
--update-parallelism 1 \
|
|
||||||
--update-order stop-first \
|
|
||||||
--constraint 'node.role == manager' \
|
|
||||||
-e RELEASE_TAG=canary \
|
|
||||||
dokploy/dokploy:canary
|
|
||||||
|
|
||||||
GREEN="\033[0;32m"
|
|
||||||
YELLOW="\033[1;33m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
NC="\033[0m" # No Color
|
|
||||||
|
|
||||||
format_ip_for_url() {
|
|
||||||
local ip="$1"
|
|
||||||
if echo "$ip" | grep -q ':'; then
|
|
||||||
# IPv6
|
|
||||||
echo "[${ip}]"
|
|
||||||
else
|
|
||||||
# IPv4
|
|
||||||
echo "${ip}"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted_addr=$(format_ip_for_url "$advertise_addr")
|
# Main script execution
|
||||||
echo ""
|
if [ "$1" = "update" ]; then
|
||||||
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
|
update_dokploy
|
||||||
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
|
else
|
||||||
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
|
install_dokploy
|
||||||
echo ""
|
fi
|
||||||
|
|
||||||
@@ -1,97 +1,117 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
install_dokploy() {
|
||||||
if [ "$(id -u)" != "0" ]; then
|
if [ "$(id -u)" != "0" ]; then
|
||||||
echo "This script must be run as root" >&2
|
echo "This script must be run as root" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
|
||||||
|
|
||||||
# check if is Mac OS
|
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
|
||||||
echo "This script must be run on Linux" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# check if is running inside a container
|
|
||||||
if [ -f /.dockerenv ]; then
|
|
||||||
echo "This script must be run on Linux" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check if something is running on port 80
|
|
||||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
|
||||||
echo "Error: something is already running on port 80" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check if something is running on port 443
|
|
||||||
if ss -tulnp | grep ':443 ' >/dev/null; then
|
|
||||||
echo "Error: something is already running on port 443" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
command_exists() {
|
|
||||||
command -v "$@" > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
if command_exists docker; then
|
|
||||||
echo "Docker already installed"
|
|
||||||
else
|
|
||||||
curl -sSL https://get.docker.com | sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker swarm leave --force 2>/dev/null
|
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
|
||||||
|
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
mkdir -p /etc/dokploy
|
|
||||||
|
|
||||||
chmod 777 /etc/dokploy
|
|
||||||
|
|
||||||
docker pull dokploy/dokploy:feature
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
docker service create \
|
|
||||||
--name dokploy \
|
|
||||||
--replicas 1 \
|
|
||||||
--network dokploy-network \
|
|
||||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
|
||||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
|
||||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
|
||||||
--publish published=3000,target=3000,mode=host \
|
|
||||||
--update-parallelism 1 \
|
|
||||||
--update-order stop-first \
|
|
||||||
--constraint 'node.role == manager' \
|
|
||||||
-e RELEASE_TAG=feature \
|
|
||||||
dokploy/dokploy:feature
|
|
||||||
|
|
||||||
GREEN="\033[0;32m"
|
|
||||||
YELLOW="\033[1;33m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
NC="\033[0m" # No Color
|
|
||||||
|
|
||||||
format_ip_for_url() {
|
|
||||||
local ip="$1"
|
|
||||||
if echo "$ip" | grep -q ':'; then
|
|
||||||
# IPv6
|
|
||||||
echo "[${ip}]"
|
|
||||||
else
|
|
||||||
# IPv4
|
|
||||||
echo "${ip}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# check if is Mac OS
|
||||||
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
|
echo "This script must be run on Linux" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# check if is running inside a container
|
||||||
|
if [ -f /.dockerenv ]; then
|
||||||
|
echo "This script must be run on Linux" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check if something is running on port 80
|
||||||
|
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||||
|
echo "Error: something is already running on port 80" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check if something is running on port 443
|
||||||
|
if ss -tulnp | grep ':443 ' >/dev/null; then
|
||||||
|
echo "Error: something is already running on port 443" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$@" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if command_exists docker; then
|
||||||
|
echo "Docker already installed"
|
||||||
|
else
|
||||||
|
curl -sSL https://get.docker.com | sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker swarm leave --force 2>/dev/null
|
||||||
|
advertise_addr=$(curl -s ifconfig.me)
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
echo "Swarm initialized"
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
|
docker pull dokploy/dokploy:feature
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
docker service create \
|
||||||
|
--name dokploy \
|
||||||
|
--replicas 1 \
|
||||||
|
--network dokploy-network \
|
||||||
|
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||||
|
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||||
|
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||||
|
--publish published=3000,target=3000,mode=host \
|
||||||
|
--update-parallelism 1 \
|
||||||
|
--update-order stop-first \
|
||||||
|
--constraint 'node.role == manager' \
|
||||||
|
-e RELEASE_TAG=feature \
|
||||||
|
dokploy/dokploy:feature
|
||||||
|
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
BLUE="\033[0;34m"
|
||||||
|
NC="\033[0m" # No Color
|
||||||
|
|
||||||
|
format_ip_for_url() {
|
||||||
|
local ip="$1"
|
||||||
|
if echo "$ip" | grep -q ':'; then
|
||||||
|
# IPv6
|
||||||
|
echo "[${ip}]"
|
||||||
|
else
|
||||||
|
# IPv4
|
||||||
|
echo "${ip}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_addr=$(format_ip_for_url "$advertise_addr")
|
||||||
|
echo ""
|
||||||
|
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
|
||||||
|
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
|
||||||
|
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted_addr=$(format_ip_for_url "$advertise_addr")
|
update_dokploy() {
|
||||||
echo ""
|
echo "Updating Dokploy..."
|
||||||
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
|
|
||||||
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
|
# Pull the latest feature image
|
||||||
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
|
docker pull dokploy/dokploy:feature
|
||||||
echo ""
|
|
||||||
|
# Update the service
|
||||||
|
docker service update --image dokploy/dokploy:feature dokploy
|
||||||
|
|
||||||
|
echo "Dokploy has been updated to the latest feature version."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script execution
|
||||||
|
if [ "$1" = "update" ]; then
|
||||||
|
update_dokploy
|
||||||
|
else
|
||||||
|
install_dokploy
|
||||||
|
fi
|
||||||
@@ -1,112 +1,130 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
install_dokploy() {
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
echo "This script must be run as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$(id -u)" != "0" ]; then
|
# check if is Mac OS
|
||||||
echo "This script must be run as root" >&2
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
exit 1
|
echo "This script must be run on Linux" >&2
|
||||||
fi
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# check if is Mac OS
|
# check if is running inside a container
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
if [ -f /.dockerenv ]; then
|
||||||
echo "This script must be run on Linux" >&2
|
echo "This script must be run on Linux" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# check if something is running on port 80
|
||||||
|
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||||
|
echo "Error: something is already running on port 80" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# check if is running inside a container
|
# check if something is running on port 443
|
||||||
if [ -f /.dockerenv ]; then
|
if ss -tulnp | grep ':443 ' >/dev/null; then
|
||||||
echo "This script must be run on Linux" >&2
|
echo "Error: something is already running on port 443" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# check if something is running on port 80
|
command_exists() {
|
||||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
command -v "$@" > /dev/null 2>&1
|
||||||
echo "Error: something is already running on port 80" >&2
|
}
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check if something is running on port 443
|
if command_exists docker; then
|
||||||
if ss -tulnp | grep ':443 ' >/dev/null; then
|
echo "Docker already installed"
|
||||||
echo "Error: something is already running on port 443" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
command_exists() {
|
|
||||||
command -v "$@" > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
if command_exists docker; then
|
|
||||||
echo "Docker already installed"
|
|
||||||
else
|
|
||||||
curl -sSL https://get.docker.com | sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker swarm leave --force 2>/dev/null
|
|
||||||
|
|
||||||
get_ip() {
|
|
||||||
# Try to get IPv4
|
|
||||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
|
||||||
|
|
||||||
if [ -n "$ipv4" ]; then
|
|
||||||
echo "$ipv4"
|
|
||||||
else
|
else
|
||||||
# Try to get IPv6
|
curl -sSL https://get.docker.com | sh
|
||||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
fi
|
||||||
if [ -n "$ipv6" ]; then
|
|
||||||
echo "$ipv6"
|
docker swarm leave --force 2>/dev/null
|
||||||
|
|
||||||
|
get_ip() {
|
||||||
|
# Try to get IPv4
|
||||||
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$ipv4" ]; then
|
||||||
|
echo "$ipv4"
|
||||||
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
}
|
||||||
|
|
||||||
|
advertise_addr=$(get_ip)
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
echo "Swarm initialized"
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
|
docker pull dokploy/dokploy:latest
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
docker service create \
|
||||||
|
--name dokploy \
|
||||||
|
--replicas 1 \
|
||||||
|
--network dokploy-network \
|
||||||
|
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||||
|
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||||
|
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||||
|
--publish published=3000,target=3000,mode=host \
|
||||||
|
--update-parallelism 1 \
|
||||||
|
--update-order stop-first \
|
||||||
|
--constraint 'node.role == manager' \
|
||||||
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
YELLOW="\033[1;33m"
|
||||||
|
BLUE="\033[0;34m"
|
||||||
|
NC="\033[0m" # No Color
|
||||||
|
|
||||||
|
format_ip_for_url() {
|
||||||
|
local ip="$1"
|
||||||
|
if echo "$ip" | grep -q ':'; then
|
||||||
|
# IPv6
|
||||||
|
echo "[${ip}]"
|
||||||
|
else
|
||||||
|
# IPv4
|
||||||
|
echo "${ip}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_addr=$(format_ip_for_url "$advertise_addr")
|
||||||
|
echo ""
|
||||||
|
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
|
||||||
|
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
|
||||||
|
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_addr=$(get_ip)
|
update_dokploy() {
|
||||||
|
echo "Updating Dokploy..."
|
||||||
|
|
||||||
|
# Pull the latest image
|
||||||
|
docker pull dokploy/dokploy:latest
|
||||||
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
# Update the service
|
||||||
|
docker service update --image dokploy/dokploy:latest dokploy
|
||||||
|
|
||||||
echo "Swarm initialized"
|
echo "Dokploy has been updated to the latest version."
|
||||||
|
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
|
||||||
|
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
mkdir -p /etc/dokploy
|
|
||||||
|
|
||||||
chmod 777 /etc/dokploy
|
|
||||||
|
|
||||||
docker pull dokploy/dokploy:latest
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
docker service create \
|
|
||||||
--name dokploy \
|
|
||||||
--replicas 1 \
|
|
||||||
--network dokploy-network \
|
|
||||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
|
||||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
|
||||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
|
||||||
--publish published=3000,target=3000,mode=host \
|
|
||||||
--update-parallelism 1 \
|
|
||||||
--update-order stop-first \
|
|
||||||
--constraint 'node.role == manager' \
|
|
||||||
dokploy/dokploy:latest
|
|
||||||
|
|
||||||
GREEN="\033[0;32m"
|
|
||||||
YELLOW="\033[1;33m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
NC="\033[0m" # No Color
|
|
||||||
|
|
||||||
format_ip_for_url() {
|
|
||||||
local ip="$1"
|
|
||||||
if echo "$ip" | grep -q ':'; then
|
|
||||||
# IPv6
|
|
||||||
echo "[${ip}]"
|
|
||||||
else
|
|
||||||
# IPv4
|
|
||||||
echo "${ip}"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted_addr=$(format_ip_for_url "$advertise_addr")
|
# Main script execution
|
||||||
echo ""
|
if [ "$1" = "update" ]; then
|
||||||
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
|
update_dokploy
|
||||||
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
|
else
|
||||||
printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
|
install_dokploy
|
||||||
echo ""
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user