diff --git a/apps/docs/content/docs/core/application/overview.mdx b/apps/docs/content/docs/core/application/overview.mdx index 788daff8..9f881f6b 100644 --- a/apps/docs/content/docs/core/application/overview.mdx +++ b/apps/docs/content/docs/core/application/overview.mdx @@ -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. diff --git a/apps/docs/content/docs/core/databases/overview.mdx b/apps/docs/content/docs/core/databases/overview.mdx index f9702fb0..0fd2f5b0 100644 --- a/apps/docs/content/docs/core/databases/overview.mdx +++ b/apps/docs/content/docs/core/databases/overview.mdx @@ -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. diff --git a/apps/docs/content/docs/core/templates/overview.mdx b/apps/docs/content/docs/core/templates/overview.mdx index 363650f6..6c8a7e8c 100644 --- a/apps/docs/content/docs/core/templates/overview.mdx +++ b/apps/docs/content/docs/core/templates/overview.mdx @@ -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. diff --git a/apps/docs/mdx-components.tsx b/apps/docs/mdx-components.tsx index 96c5b74b..10488dca 100644 --- a/apps/docs/mdx-components.tsx +++ b/apps/docs/mdx-components.tsx @@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { p: ({ children }) => (

{children}

), - li: ({ children }) => ( -
  • {children}
  • + li: ({ children, id }) => ( +
  • + {children} +
  • ), }; } diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx index a068ce18..0ed9d2e2 100644 --- a/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx @@ -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) => { Published Port - { - const value = e.target.value; - if (value === "") { - field.onChange(0); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> + )} /> + { Target Port - { - const value = e.target.value; - if (value === "") { - field.onChange(0); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> + diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx index efe1b9ac..320471ba 100644 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx @@ -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; +// 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 = , + children = , }: 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 ( - + - + Redirects @@ -102,6 +159,24 @@ export const AddRedirect = ({ {isError && {error?.message}} +
    + + +
    + + +
    ( - +
    Permanent diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index 43a3cb69..4b5d4e09 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -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 = ({ {children} - + Domain {dictionary.dialogDescription} @@ -228,19 +228,36 @@ export const AddDomain = ({ Container Port - { - field.onChange(Number.parseInt(e.target.value)); - }} - /> + ); }} /> + + ( + +
    + HTTPS + + Automatically provision SSL Certificate. + + +
    + + + +
    + )} + /> + {form.getValues().https && ( )} - - ( - -
    - HTTPS - - Automatically provision SSL Certificate. - - -
    - - - -
    - )} - />
    diff --git a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx index 056c003a..9f586467 100644 --- a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx @@ -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 = ({ {children} - + Domain {dictionary.dialogDescription} @@ -190,7 +190,7 @@ export const AddDomainCompose = ({ {errorServices?.message} )} -
    +
    Container Port - { - field.onChange(Number.parseInt(e.target.value)); - }} - /> + ); }} /> + + ( + +
    + HTTPS + + Automatically provision SSL Certificate. + + +
    + + + +
    + )} + /> + {https && ( )} - - ( - -
    - HTTPS - - Automatically provision SSL Certificate. - - -
    - - - -
    - )} - />
    diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx index a908de07..c06cacaa 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx @@ -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({ 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 ( <> diff --git a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx index 36d04c9c..7cfab289 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx @@ -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({ 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 ( diff --git a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx index 18c1adaf..009c8c3a 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx @@ -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({ 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 ( <> diff --git a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx index 28a96eb2..e1b4369a 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx @@ -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({ @@ -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 ( diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index 658bf0ae..91dba943 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -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 ( - + { 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({ 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 ( <>
    diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/add-docker-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/add-docker-registry.tsx index 193bf672..0a1ee614 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/add-docker-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/add-docker-registry.tsx @@ -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; @@ -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({ 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 = () => { )} />
    - - + .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 + + + diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/update-docker-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/update-docker-registry.tsx index c84c019a..9c5f281e 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/update-docker-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/update-docker-registry.tsx @@ -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; @@ -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) => { - - {isCloud && ( + +
    + + Select a server to test the registry. If you don't have a server + choose the default one. + + ( + + Server (Optional) + + + + + + + )} + /> - )} +
    diff --git a/apps/dokploy/components/dashboard/settings/servers/add-server.tsx b/apps/dokploy/components/dashboard/settings/servers/add-server.tsx index 6bd44dcf..8cb71167 100644 --- a/apps/dokploy/components/dashboard/settings/servers/add-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/add-server.tsx @@ -212,7 +212,21 @@ export const AddServer = () => { Port - + { + const value = e.target.value; + if (value === "") { + field.onChange(0); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> diff --git a/apps/dokploy/components/dashboard/settings/servers/update-server.tsx b/apps/dokploy/components/dashboard/settings/servers/update-server.tsx index 73eadd30..16eb5ba7 100644 --- a/apps/dokploy/components/dashboard/settings/servers/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/update-server.tsx @@ -228,7 +228,21 @@ export const UpdateServer = ({ serverId }: Props) => { Port - + { + const value = e.target.value; + if (value === "") { + field.onChange(0); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> diff --git a/apps/dokploy/components/ui/input.tsx b/apps/dokploy/components/ui/input.tsx index 55b46e6d..8fe7ab28 100644 --- a/apps/dokploy/components/ui/input.tsx +++ b/apps/dokploy/components/ui/input.tsx @@ -31,4 +31,39 @@ const Input = React.forwardRef( ); Input.displayName = "Input"; -export { Input }; +const NumberInput = React.forwardRef( + ({ className, errorMessage, ...props }, ref) => { + return ( + { + 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, + ); + } + } + }} + /> + ); + }, +); +NumberInput.displayName = "NumberInput"; + +export { Input, NumberInput }; diff --git a/apps/dokploy/drizzle/0038_rapid_landau.sql b/apps/dokploy/drizzle/0038_rapid_landau.sql new file mode 100644 index 00000000..4f756c7c --- /dev/null +++ b/apps/dokploy/drizzle/0038_rapid_landau.sql @@ -0,0 +1 @@ +ALTER TABLE "registry" ALTER COLUMN "registryUrl" SET DEFAULT ''; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0038_snapshot.json b/apps/dokploy/drizzle/meta/0038_snapshot.json index 8e5ba823..4eb2b5ec 100644 --- a/apps/dokploy/drizzle/meta/0038_snapshot.json +++ b/apps/dokploy/drizzle/meta/0038_snapshot.json @@ -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", diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 3ee69b9a..5a84e0b1 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -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 } ] diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index bd1228c5..ab717c3c 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -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", diff --git a/apps/dokploy/public/templates/roundcube.svg b/apps/dokploy/public/templates/roundcube.svg new file mode 100644 index 00000000..04238a06 --- /dev/null +++ b/apps/dokploy/public/templates/roundcube.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index 8c2a8da2..e655570f 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -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 diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index a2bdd66c..547465ef 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -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(" "); +}; diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index 8d06d15e..ac11528e 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -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), { diff --git a/apps/dokploy/templates/roundcube/docker-compose.yml b/apps/dokploy/templates/roundcube/docker-compose.yml new file mode 100644 index 00000000..440f907d --- /dev/null +++ b/apps/dokploy/templates/roundcube/docker-compose.yml @@ -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 diff --git a/apps/dokploy/templates/roundcube/index.ts b/apps/dokploy/templates/roundcube/index.ts new file mode 100644 index 00000000..8df8c743 --- /dev/null +++ b/apps/dokploy/templates/roundcube/index.ts @@ -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 }; +} diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 5603b188..afe9d1b6 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -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), + }, ]; diff --git a/apps/website/public/canary.sh b/apps/website/public/canary.sh index 3a9102b0..32f95bff 100644 --- a/apps/website/public/canary.sh +++ b/apps/website/public/canary.sh @@ -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 \ No newline at end of file diff --git a/apps/website/public/feature.sh b/apps/website/public/feature.sh index 453da012..fca4fccd 100644 --- a/apps/website/public/feature.sh +++ b/apps/website/public/feature.sh @@ -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 \ No newline at end of file diff --git a/apps/website/public/install.sh b/apps/website/public/install.sh index e68a59fd..f5b318f2 100644 --- a/apps/website/public/install.sh +++ b/apps/website/public/install.sh @@ -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 diff --git a/packages/builders/src/auth/token.ts b/packages/builders/src/auth/token.ts index 54162fdc..4c7c2036 100644 --- a/packages/builders/src/auth/token.ts +++ b/packages/builders/src/auth/token.ts @@ -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 && { diff --git a/packages/builders/src/db/schema/registry.ts b/packages/builders/src/db/schema/registry.ts index 832de9b1..ee1bab94 100644 --- a/packages/builders/src/db/schema/registry.ts +++ b/packages/builders/src/db/schema/registry.ts @@ -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 diff --git a/packages/builders/src/services/registry.ts b/packages/builders/src/services/registry.ts index 83dcc2a2..f7c20af6 100644 --- a/packages/builders/src/services/registry.ts +++ b/packages/builders/src/services/registry.ts @@ -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, + registryData: Partial & { 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) { diff --git a/packages/builders/src/utils/builders/nixpacks.ts b/packages/builders/src/utils/builders/nixpacks.ts index 0f408743..2d81a7c0 100644 --- a/packages/builders/src/utils/builders/nixpacks.ts +++ b/packages/builders/src/utils/builders/nixpacks.ts @@ -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; diff --git a/packages/builders/src/utils/providers/bitbucket.ts b/packages/builders/src/utils/providers/bitbucket.ts index 803cfe4e..6a09e9a3 100644 --- a/packages/builders/src/utils/providers/bitbucket.ts +++ b/packages/builders/src/utils/providers/bitbucket.ts @@ -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, { diff --git a/packages/builders/src/utils/providers/github.ts b/packages/builders/src/utils/providers/github.ts index c9e1ee69..fb0c9b68 100644 --- a/packages/builders/src/utils/providers/github.ts +++ b/packages/builders/src/utils/providers/github.ts @@ -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; } diff --git a/packages/builders/src/utils/providers/gitlab.ts b/packages/builders/src/utils/providers/gitlab.ts index 786b82f8..a61e1224 100644 --- a/packages/builders/src/utils/providers/gitlab.ts +++ b/packages/builders/src/utils/providers/gitlab.ts @@ -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; } diff --git a/packages/builders/src/utils/providers/raw.ts b/packages/builders/src/utils/providers/raw.ts index d3e0e7f7..567b28df 100644 --- a/packages/builders/src/utils/providers/raw.ts +++ b/packages/builders/src/utils/providers/raw.ts @@ -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}"; `;