mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/cloud
This commit is contained in:
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -31,8 +31,7 @@ The following templates are available:
|
||||
- **Wordpress**: Open Source Content Management System
|
||||
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
||||
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
||||
|
||||
|
||||
- **Roundcube**: Free and open source webmail software for the masses, written in PHP, uses SMTP[^1].
|
||||
|
||||
## Create your own template
|
||||
|
||||
@@ -41,3 +40,5 @@ We accept contributions to upload new templates to the dokploy repository.
|
||||
Make sure to follow the guidelines for creating a template:
|
||||
|
||||
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
||||
|
||||
[^1]: Please note that if you're self-hosting a mail server you need port 25 to be open for SMTP (Mail Transmission Protocol that allows you to send and receive) to work properly. Some VPS providers like [Hetzner](https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server) block this port by default for new clients.
|
||||
|
||||
@@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
p: ({ children }) => (
|
||||
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li>
|
||||
li: ({ children, id }) => (
|
||||
<li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Published Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="targetPort"
|
||||
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Target Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -19,6 +19,15 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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 { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -36,6 +45,36 @@ const AddRedirectchema = z.object({
|
||||
|
||||
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 {
|
||||
applicationId: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -43,9 +82,10 @@ interface Props {
|
||||
|
||||
export const AddRedirect = ({
|
||||
applicationId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
children = <PlusIcon className="w-4 h-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [presetSelected, setPresetSelected] = useState("");
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
@@ -81,19 +121,36 @@ export const AddRedirect = ({
|
||||
await utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
onDialogToggle(false);
|
||||
})
|
||||
.catch(() => {
|
||||
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 (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
</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>
|
||||
<DialogTitle>Redirects</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -102,6 +159,24 @@ export const AddRedirect = ({
|
||||
</DialogHeader>
|
||||
{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
|
||||
id="hook-form-add-redirect"
|
||||
@@ -142,7 +217,7 @@ export const AddRedirect = ({
|
||||
control={form.control}
|
||||
name="permanent"
|
||||
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">
|
||||
<FormLabel>Permanent</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -140,7 +140,7 @@ export const AddDomain = ({
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</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>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
@@ -228,19 +228,36 @@ export const AddDomain = ({
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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 && (
|
||||
<FormField
|
||||
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>
|
||||
</form>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -161,7 +161,7 @@ export const AddDomainCompose = ({
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</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>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
@@ -190,7 +190,7 @@ export const AddDomainCompose = ({
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
<div className="flex flex-row gap-4 w-full items-end">
|
||||
<div className="flex flex-row items-end w-full gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serviceName"
|
||||
@@ -364,19 +364,36 @@ export const AddDomainCompose = ({
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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 && (
|
||||
<FormField
|
||||
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>
|
||||
</form>
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
const buildConnectionUrl = () => {
|
||||
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());
|
||||
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
form,
|
||||
data?.databaseName,
|
||||
data?.databaseUser,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
const buildConnectionUrl = () => {
|
||||
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());
|
||||
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
data?.databasePassword,
|
||||
form,
|
||||
data?.databaseUser,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
const buildConnectionUrl = () => {
|
||||
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());
|
||||
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
data?.databaseName,
|
||||
data?.databaseUser,
|
||||
form,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||
const { mutateAsync, isLoading } =
|
||||
api.postgres.saveExternalPort.useMutation();
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
const buildConnectionUrl = () => {
|
||||
const hostname = window.location.hostname;
|
||||
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());
|
||||
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
data?.databasePassword,
|
||||
form,
|
||||
data?.databaseName,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -71,6 +71,7 @@ interface Props {
|
||||
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
setVisible(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
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 { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
const hostname = window.location.hostname;
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
|
||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
|
||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
|
||||
@@ -17,10 +17,18 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Container } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
registryUrl: z.string(),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { mutateAsync: testRegistry, isLoading } =
|
||||
api.registry.testRegistry.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<AddRegistry>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
registryName: "",
|
||||
serverId: "",
|
||||
},
|
||||
resolver: zodResolver(AddRegistrySchema),
|
||||
});
|
||||
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
|
||||
const registryUrl = form.watch("registryUrl");
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
const serverId = form.watch("serverId");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
serverId: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
|
||||
registryUrl: data.registryUrl,
|
||||
registryType: "cloud",
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.registry.all.invalidate();
|
||||
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Select a server to test the registry. If you don't have a
|
||||
server choose the default one.
|
||||
</span>
|
||||
<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
|
||||
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(() => {
|
||||
toast.error("Error to test the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to test the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||
Create
|
||||
</Button>
|
||||
|
||||
@@ -17,6 +17,15 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string(),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
registryUrl: z.string(),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
||||
@@ -48,6 +56,8 @@ interface Props {
|
||||
|
||||
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const { mutateAsync: testRegistry, isLoading } =
|
||||
api.registry.testRegistry.useMutation();
|
||||
const { data, refetch } = api.registry.one.useQuery(
|
||||
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
serverId: "",
|
||||
},
|
||||
resolver: zodResolver(updateRegistry),
|
||||
});
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
const password = form.watch("password");
|
||||
const username = form.watch("username");
|
||||
const registryUrl = form.watch("registryUrl");
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
const serverId = form.watch("serverId");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
username: data.username || "",
|
||||
password: "",
|
||||
registryUrl: data.registryUrl || "",
|
||||
serverId: "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl,
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async (data) => {
|
||||
toast.success("Registry Updated");
|
||||
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "sm:justify-between " : "",
|
||||
"flex flex-row w-full gap-4 flex-wrap",
|
||||
)}
|
||||
>
|
||||
{isCloud && (
|
||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Select a server to test the registry. If you don't have a server
|
||||
choose the default one.
|
||||
</span>
|
||||
<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
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
serverId: serverId,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
form="hook-form"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
@@ -212,7 +212,21 @@ export const AddServer = () => {
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<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>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -228,7 +228,21 @@ export const UpdateServer = ({ serverId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<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>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -31,4 +31,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
);
|
||||
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 '';
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "e4ae5e44-8d12-400c-9d7d-3dbbf1e348e5",
|
||||
"id": "8ffdfaff-f166-42dc-ac77-4fd9309d736a",
|
||||
"prevId": "19a70a39-f719-400b-b61e-6ddf1bcc6ac5",
|
||||
"version": "6",
|
||||
"dialect": "postgresql",
|
||||
@@ -2923,7 +2923,8 @@
|
||||
"name": "registryUrl",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
@@ -3252,13 +3253,6 @@
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"privateKey": {
|
||||
"name": "privateKey",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"publicKey": {
|
||||
"name": "publicKey",
|
||||
"type": "text",
|
||||
|
||||
@@ -271,15 +271,8 @@
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "6",
|
||||
"when": 1727903587684,
|
||||
"tag": "0038_mushy_blindfold",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 39,
|
||||
"version": "6",
|
||||
"when": 1727937385754,
|
||||
"tag": "0039_workable_speed_demon",
|
||||
"when": 1727942090102,
|
||||
"tag": "0038_rapid_landau",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.9.0",
|
||||
"version": "v0.9.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build-next": "next build",
|
||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||
"reset-password": "node dist/reset-password.mjs",
|
||||
"dev": "tsx watch -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||
|
||||
15
apps/dokploy/public/templates/roundcube.svg
Normal file
15
apps/dokploy/public/templates/roundcube.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="9.14 141.8 573.65 573.65">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#404F54;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#E5E5E5;}
|
||||
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#CCCCCC;}
|
||||
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#37BEFF;}
|
||||
</style>
|
||||
<polygon class="st3" points="582.79,549.77 295.96,384.1 295.96,207.27 582.79,372.95 "/>
|
||||
<polygon class="st0" points="9.14,549.77 295.96,384.1 295.96,207.27 9.14,372.95 "/>
|
||||
<path class="st2" d="M295.96,141.8c109.56,0,198.41,88.85,198.41,198.41c0,109.56-88.85,198.41-198.41,198.41 c-109.56,0-198.41-88.85-198.41-198.41C97.55,230.65,186.4,141.8,295.96,141.8"/>
|
||||
<path class="st1" d="M295.96,141.8c109.6,0,198.48,88.85,198.48,198.41c0,109.56-88.88,198.41-198.48,198.41 c-62.91-42.34-88.94-127.64-88.94-198.3S233.05,184.22,295.96,141.8"/>
|
||||
<polygon class="st3" points="582.79,372.95 295.96,538.62 295.96,715.45 582.79,549.77 "/>
|
||||
<polygon class="st0" points="9.14,372.95 295.96,538.62 295.96,715.45 9.14,549.77 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -68,7 +68,7 @@ export const destinationRouter = createTRPCRouter({
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
return destination;
|
||||
}),
|
||||
all: adminProcedure.query(async () => {
|
||||
all: protectedProcedure.query(async () => {
|
||||
return await db.query.destinations.findMany({});
|
||||
}),
|
||||
remove: adminProcedure
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
execAsyncRemote,
|
||||
initializeRegistry,
|
||||
execAsync,
|
||||
manageRegistry,
|
||||
@@ -58,7 +59,13 @@ export const registryRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
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;
|
||||
} catch (error) {
|
||||
console.log("Error Registry:", error);
|
||||
@@ -78,6 +85,7 @@ export const registryRouter = createTRPCRouter({
|
||||
? input.registryUrl
|
||||
: "dokploy-registry.docker.localhost",
|
||||
imagePrefix: null,
|
||||
serverId: undefined,
|
||||
});
|
||||
|
||||
await manageRegistry(selfHostedRegistry);
|
||||
@@ -86,3 +94,17 @@ export const registryRouter = createTRPCRouter({
|
||||
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(" ");
|
||||
};
|
||||
|
||||
@@ -74,6 +74,8 @@ export const setupTerminalWebSocketServer = (
|
||||
"StrictHostKeyChecking=no",
|
||||
"-i",
|
||||
privateKey,
|
||||
"-p",
|
||||
`${server.port}`,
|
||||
`${server.username}@${server.ipAddress}`,
|
||||
];
|
||||
const ptyProcess = spawn("ssh", sshCommand.slice(1), {
|
||||
|
||||
17
apps/dokploy/templates/roundcube/docker-compose.yml
Normal file
17
apps/dokploy/templates/roundcube/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
roundcubemail:
|
||||
image: roundcube/roundcubemail:1.6.9-apache
|
||||
volumes:
|
||||
- ./www:/var/www/html
|
||||
- ./db/sqlite:/var/roundcube/db
|
||||
environment:
|
||||
- ROUNDCUBEMAIL_DB_TYPE=sqlite
|
||||
- ROUNDCUBEMAIL_SKIN=elastic
|
||||
- ROUNDCUBEMAIL_DEFAULT_HOST=${DEFAULT_HOST}
|
||||
- ROUNDCUBEMAIL_SMTP_SERVER=${SMTP_SERVER}
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
24
apps/dokploy/templates/roundcube/index.ts
Normal file
24
apps/dokploy/templates/roundcube/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const envs = [
|
||||
"DEFAULT_HOST=tls://mail.example.com",
|
||||
"SMTP_SERVER=tls://mail.example.com",
|
||||
];
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: randomDomain,
|
||||
port: 80,
|
||||
serviceName: "roundcubemail",
|
||||
},
|
||||
];
|
||||
|
||||
return { envs, domains };
|
||||
}
|
||||
@@ -497,4 +497,19 @@ export const templates: TemplateData[] = [
|
||||
tags: ["self-hosted", "storage"],
|
||||
load: () => import("./gitea/index").then((m) => m.generate),
|
||||
},
|
||||
{
|
||||
id: "roundcube",
|
||||
name: "Roundcube",
|
||||
version: "1.6.9",
|
||||
description:
|
||||
"Free and open source webmail software for the masses, written in PHP.",
|
||||
logo: "roundcube.svg",
|
||||
links: {
|
||||
github: "https://github.com/roundcube/roundcubemail",
|
||||
website: "https://roundcube.net/",
|
||||
docs: "https://roundcube.net/about/",
|
||||
},
|
||||
tags: ["self-hosted", "email", "webmail"],
|
||||
load: () => import("./roundcube/index").then((m) => m.generate),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,114 +1,133 @@
|
||||
#!/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
|
||||
echo "This script must be run as root" >&2
|
||||
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 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 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 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
|
||||
# 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
|
||||
}
|
||||
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"
|
||||
if command_exists docker; then
|
||||
echo "Docker already installed"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
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
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
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"
|
||||
|
||||
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
|
||||
echo "Dokploy has been updated to the latest canary version."
|
||||
}
|
||||
|
||||
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 ""
|
||||
|
||||
# Main script execution
|
||||
if [ "$1" = "update" ]; then
|
||||
update_dokploy
|
||||
else
|
||||
install_dokploy
|
||||
fi
|
||||
@@ -1,97 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "This script must be run as root" >&2
|
||||
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}"
|
||||
install_dokploy() {
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "This script must be run as root" >&2
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
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 ""
|
||||
update_dokploy() {
|
||||
echo "Updating Dokploy..."
|
||||
|
||||
# Pull the latest feature image
|
||||
docker pull dokploy/dokploy:feature
|
||||
|
||||
# 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
|
||||
install_dokploy() {
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "This script must be run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "This script must be run as root" >&2
|
||||
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 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 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 443
|
||||
if ss -tulnp | grep ':443 ' >/dev/null; then
|
||||
echo "Error: something is already running on port 443" >&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
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
|
||||
if [ -n "$ipv4" ]; then
|
||||
echo "$ipv4"
|
||||
if command_exists docker; then
|
||||
echo "Docker already installed"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
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
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
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"
|
||||
|
||||
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
|
||||
echo "Dokploy has been updated to the latest version."
|
||||
}
|
||||
|
||||
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 ""
|
||||
# Main script execution
|
||||
if [ "$1" = "update" ]; then
|
||||
update_dokploy
|
||||
else
|
||||
install_dokploy
|
||||
fi
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { IncomingMessage } from "node:http";
|
||||
import { TimeSpan } from "lucia";
|
||||
import { Lucia } from "lucia/dist/core.js";
|
||||
import { type ReturnValidateToken, adapter } from "./auth";
|
||||
import { findAdminByAuthId } from "../services/admin";
|
||||
import { findUserByAuthId } from "../services/user";
|
||||
|
||||
export const luciaToken = new Lucia(adapter, {
|
||||
sessionCookie: {
|
||||
@@ -31,6 +33,16 @@ export const validateBearerToken = async (
|
||||
};
|
||||
}
|
||||
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 {
|
||||
session: result.session,
|
||||
...((result.user && {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const registry = pgTable("registry", {
|
||||
imagePrefix: text("imagePrefix"),
|
||||
username: text("username").notNull(),
|
||||
password: text("password").notNull(),
|
||||
registryUrl: text("registryUrl").notNull(),
|
||||
registryUrl: text("registryUrl").notNull().default(""),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -45,7 +45,7 @@ const createSchema = createInsertSchema(registry, {
|
||||
registryName: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
adminId: z.string().min(1),
|
||||
registryId: z.string().min(1),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
@@ -62,7 +62,10 @@ export const apiCreateRegistry = createSchema
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
registryName: z.string().min(1),
|
||||
@@ -71,6 +74,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
registryUrl: z.string(),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiRemoveRegistry = createSchema
|
||||
@@ -87,6 +91,7 @@ export const apiFindOneRegistry = createSchema
|
||||
|
||||
export const apiUpdateRegistry = createSchema.partial().extend({
|
||||
registryId: z.string().min(1),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiEnableSelfHostedRegistry = createSchema
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@/server/db";
|
||||
import { type apiCreateRegistry, registry } from "@/server/db/schema";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { removeService } from "@/server/utils/docker/utils";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||
import {
|
||||
manageRegistry,
|
||||
removeSelfHostedRegistry,
|
||||
@@ -32,9 +32,10 @@ export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||
message: "Error input: Inserting registry",
|
||||
});
|
||||
}
|
||||
|
||||
if (newRegistry.registryType === "cloud") {
|
||||
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`;
|
||||
if (input.serverId && input.serverId !== "none") {
|
||||
await execAsyncRemote(input.serverId, loginCommand);
|
||||
} else if (newRegistry.registryType === "cloud") {
|
||||
await execAsync(loginCommand);
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ export const removeRegistry = async (registryId: string) => {
|
||||
|
||||
export const updateRegistry = async (
|
||||
registryId: string,
|
||||
registryData: Partial<Registry>,
|
||||
registryData: Partial<Registry> & { serverId?: string | null },
|
||||
) => {
|
||||
try {
|
||||
const response = await db
|
||||
@@ -92,6 +93,13 @@ export const updateRegistry = async (
|
||||
await manageRegistry(response);
|
||||
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;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { type WriteStream, existsSync, mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildStatic, getStaticCommand } from "@/server/utils/builders/static";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -42,7 +42,6 @@ export const buildNixpacks = async (
|
||||
and copy the artifacts on the host filesystem.
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
|
||||
if (publishDirectory) {
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
@@ -50,12 +49,22 @@ export const buildNixpacks = async (
|
||||
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(
|
||||
"docker",
|
||||
[
|
||||
"cp",
|
||||
`${buildContainerId}:/app/${publishDirectory}`,
|
||||
path.join(buildAppDirectory, publishDirectory),
|
||||
`${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`,
|
||||
localPath,
|
||||
],
|
||||
writeToStream,
|
||||
);
|
||||
@@ -108,9 +117,14 @@ echo "✅ Nixpacks build completed." >> ${logPath};
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
if (publishDirectory) {
|
||||
const localPath = path.join(buildAppDirectory, publishDirectory);
|
||||
const isDirectory =
|
||||
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
||||
|
||||
bashCommand += `
|
||||
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}
|
||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
||||
exit 1;
|
||||
|
||||
@@ -147,15 +147,15 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}`,
|
||||
);
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
const username =
|
||||
bitbucketProvider.bitbucketWorkspaceName ||
|
||||
bitbucketProvider.bitbucketUsername;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}?pagelen=100`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -271,13 +271,13 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
||||
);
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -390,14 +390,14 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
||||
);
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
|
||||
try {
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user