Merge branch 'canary' into feat/cloud

This commit is contained in:
Mauricio Siu
2024-10-03 13:39:06 -06:00
41 changed files with 901 additions and 506 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -31,8 +31,7 @@ The following templates are available:
- **Wordpress**: Open Source Content Management System - **Wordpress**: Open Source Content Management System
- **Open WebUI**: Free and Open Source ChatGPT Alternative - **Open WebUI**: Free and Open Source ChatGPT Alternative
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres - **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 ## 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: 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) [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.

View File

@@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
p: ({ children }) => ( p: ({ children }) => (
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p> <p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
), ),
li: ({ children }) => ( li: ({ children, id }) => (
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li> <li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
{children}
</li>
), ),
}; };
} }

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 (
<> <>

View File

@@ -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 (

View File

@@ -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 (
<> <>

View File

@@ -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 (

View File

@@ -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"

View File

@@ -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 ">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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 />

View File

@@ -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 };

View File

@@ -0,0 +1 @@
ALTER TABLE "registry" ALTER COLUMN "registryUrl" SET DEFAULT '';

View File

@@ -1,5 +1,5 @@
{ {
"id": "e4ae5e44-8d12-400c-9d7d-3dbbf1e348e5", "id": "8ffdfaff-f166-42dc-ac77-4fd9309d736a",
"prevId": "19a70a39-f719-400b-b61e-6ddf1bcc6ac5", "prevId": "19a70a39-f719-400b-b61e-6ddf1bcc6ac5",
"version": "6", "version": "6",
"dialect": "postgresql", "dialect": "postgresql",
@@ -2923,7 +2923,8 @@
"name": "registryUrl", "name": "registryUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true,
"default": "''"
}, },
"createdAt": { "createdAt": {
"name": "createdAt", "name": "createdAt",
@@ -3252,13 +3253,6 @@
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
"privateKey": {
"name": "privateKey",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"publicKey": { "publicKey": {
"name": "publicKey", "name": "publicKey",
"type": "text", "type": "text",

View File

@@ -271,15 +271,8 @@
{ {
"idx": 38, "idx": 38,
"version": "6", "version": "6",
"when": 1727903587684, "when": 1727942090102,
"tag": "0038_mushy_blindfold", "tag": "0038_rapid_landau",
"breakpoints": true
},
{
"idx": 39,
"version": "6",
"when": 1727937385754,
"tag": "0039_workable_speed_demon",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -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",
@@ -11,7 +11,7 @@
"build-next": "next build", "build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node dist/reset-password.mjs", "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", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --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", "migration:run": "tsx -r dotenv/config migration.ts",

View 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

View File

@@ -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

View File

@@ -8,6 +8,7 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { import {
execAsyncRemote,
initializeRegistry, initializeRegistry,
execAsync, execAsync,
manageRegistry, manageRegistry,
@@ -58,7 +59,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 +85,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 +94,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(" ");
};

View File

@@ -74,6 +74,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), {

View 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

View 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 };
}

View File

@@ -497,4 +497,19 @@ export const templates: TemplateData[] = [
tags: ["self-hosted", "storage"], tags: ["self-hosted", "storage"],
load: () => import("./gitea/index").then((m) => m.generate), 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),
},
]; ];

View File

@@ -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..."
docker swarm init --advertise-addr $advertise_addr # Pull the latest canary image
docker pull dokploy/dokploy:canary
echo "Swarm initialized" # Update the service
docker service update --image dokploy/dokploy:canary dokploy
docker network rm -f dokploy-network 2>/dev/null echo "Dokploy has been updated to the latest canary version."
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

View File

@@ -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

View File

@@ -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..."
docker swarm init --advertise-addr $advertise_addr # Pull the latest image
docker pull dokploy/dokploy:latest
echo "Swarm initialized" # Update the service
docker service update --image dokploy/dokploy:latest dokploy
docker network rm -f dokploy-network 2>/dev/null echo "Dokploy has been updated to the latest version."
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

View File

@@ -2,6 +2,8 @@ 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 { type ReturnValidateToken, adapter } from "./auth"; import { type ReturnValidateToken, adapter } from "./auth";
import { findAdminByAuthId } from "../services/admin";
import { findUserByAuthId } from "../services/user";
export const luciaToken = new Lucia(adapter, { export const luciaToken = new Lucia(adapter, {
sessionCookie: { sessionCookie: {
@@ -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 && {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
} }
@@ -225,7 +225,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
const username = const username =
bitbucketProvider.bitbucketWorkspaceName || bitbucketProvider.bitbucketWorkspaceName ||
bitbucketProvider.bitbucketUsername; 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 { try {
const response = await fetch(url, { const response = await fetch(url, {

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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}";
`; `;