Merge remote-tracking branch 'origin/canary' into canary

This commit is contained in:
songtianlun
2024-08-22 13:23:38 +08:00
116 changed files with 8090 additions and 935 deletions

View File

@@ -166,20 +166,26 @@ import {
generateRandomDomain, generateRandomDomain,
type Template, type Template,
type Schema, type Schema,
type DomainSchema,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
// do your stuff here, like create a new domain, generate random passwords, mounts. // do your stuff here, like create a new domain, generate random passwords, mounts.
const mainServiceHash = generateHash(schema.projectName); const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema); const mainDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64); const secretBase = generateBase64(64);
const toptKeyBase = generateBase64(32); const toptKeyBase = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 8000,
serviceName: "plausible",
},
];
const envs = [ const envs = [
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. `BASE_URL=http://${mainDomain}`,
`PLAUSIBLE_HOST=${randomDomain}`,
"PLAUSIBLE_PORT=8000",
`BASE_URL=http://${randomDomain}`,
`SECRET_KEY_BASE=${secretBase}`, `SECRET_KEY_BASE=${secretBase}`,
`TOTP_VAULT_KEY=${toptKeyBase}`, `TOTP_VAULT_KEY=${toptKeyBase}`,
`HASH=${mainServiceHash}`, `HASH=${mainServiceHash}`,
@@ -195,6 +201,7 @@ export function generate(schema: Schema): Template {
return { return {
envs, envs,
mounts, mounts,
domains,
}; };
} }
``` ```

View File

@@ -52,7 +52,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx && pnpm install -g tsx
# Install buildpacks # Install buildpacks
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

26
LICENSE.MD Normal file
View File

@@ -0,0 +1,26 @@
# License
## Core License (Apache License 2.0)
Copyright 2024 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -0,0 +1,89 @@
import type { Domain } from "@/server/api/services/domain";
import { createDomainLabels } from "@/server/utils/docker/domain";
import { describe, expect, it } from "vitest";
describe("createDomainLabels", () => {
const appName = "test-app";
const baseDomain: Domain = {
host: "example.com",
port: 8080,
https: false,
uniqueConfigKey: 1,
certificateType: "none",
applicationId: "",
composeId: "",
domainType: "compose",
serviceName: "test-app",
domainId: "",
path: "/",
createdAt: "",
};
it("should create basic labels for web entrypoint", async () => {
const labels = await createDomainLabels(appName, baseDomain, "web");
expect(labels).toEqual([
"traefik.http.routers.test-app-1-web.rule=Host(`example.com`)",
"traefik.http.routers.test-app-1-web.entrypoints=web",
"traefik.http.services.test-app-1-web.loadbalancer.server.port=8080",
"traefik.http.routers.test-app-1-web.service=test-app-1-web",
]);
});
it("should create labels for websecure entrypoint", async () => {
const labels = await createDomainLabels(appName, baseDomain, "websecure");
expect(labels).toEqual([
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`)",
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
]);
});
it("should add redirect middleware for https on web entrypoint", async () => {
const httpsBaseDomain = { ...baseDomain, https: true };
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file",
);
});
it("should add Let's Encrypt configuration for websecure with letsencrypt certificate", async () => {
const letsencryptDomain = {
...baseDomain,
https: true,
certificateType: "letsencrypt" as const,
};
const labels = await createDomainLabels(
appName,
letsencryptDomain,
"websecure",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
);
});
it("should not add Let's Encrypt configuration for non-letsencrypt certificate", async () => {
const nonLetsencryptDomain = {
...baseDomain,
https: true,
certificateType: "none" as const,
};
const labels = await createDomainLabels(
appName,
nonLetsencryptDomain,
"websecure",
);
expect(labels).not.toContain(
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
);
});
it("should handle different ports correctly", async () => {
const customPortDomain = { ...baseDomain, port: 3000 };
const labels = await createDomainLabels(appName, customPortDomain, "web");
expect(labels).toContain(
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
);
});
});

View File

@@ -0,0 +1,29 @@
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
import { describe, expect, it } from "vitest";
describe("addDokployNetworkToRoot", () => {
it("should create network object if networks is undefined", () => {
const result = addDokployNetworkToRoot(undefined);
expect(result).toEqual({ "dokploy-network": { external: true } });
});
it("should add network to an empty object", () => {
const result = addDokployNetworkToRoot({});
expect(result).toEqual({ "dokploy-network": { external: true } });
});
it("should not modify existing network configuration", () => {
const existing = { "dokploy-network": { external: false } };
const result = addDokployNetworkToRoot(existing);
expect(result).toEqual({ "dokploy-network": { external: true } });
});
it("should add network alongside existing networks", () => {
const existing = { "other-network": { external: true } };
const result = addDokployNetworkToRoot(existing);
expect(result).toEqual({
"other-network": { external: true },
"dokploy-network": { external: true },
});
});
});

View File

@@ -0,0 +1,24 @@
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => {
it("should add network to an empty array", () => {
const result = addDokployNetworkToService([]);
expect(result).toEqual(["dokploy-network"]);
});
it("should not add duplicate network to an array", () => {
const result = addDokployNetworkToService(["dokploy-network"]);
expect(result).toEqual(["dokploy-network"]);
});
it("should add network to an existing array with other networks", () => {
const result = addDokployNetworkToService(["other-network"]);
expect(result).toEqual(["other-network", "dokploy-network"]);
});
it("should add network to an object if networks is an object", () => {
const result = addDokployNetworkToService({ "other-network": {} });
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
});
});

View File

@@ -67,6 +67,9 @@ const baseDomain: Domain = {
https: false, https: false,
path: null, path: null,
port: null, port: null,
serviceName: "",
composeId: "",
domainType: "application",
uniqueConfigKey: 1, uniqueConfigKey: 1,
}; };

View File

@@ -28,7 +28,7 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react"; import { PlusIcon } 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";
@@ -52,6 +52,7 @@ export const AddPort = ({
applicationId, applicationId,
children = <PlusIcon className="h-4 w-4" />, children = <PlusIcon className="h-4 w-4" />,
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
@@ -82,6 +83,7 @@ export const AddPort = ({
await utils.application.one.invalidate({ await utils.application.one.invalidate({
applicationId, applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the port"); toast.error("Error to create the port");
@@ -89,7 +91,7 @@ export const AddPort = ({
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button>{children}</Button> <Button>{children}</Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -28,7 +28,7 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } from "lucide-react"; import { PenBoxIcon, Pencil } 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";
@@ -49,6 +49,7 @@ interface Props {
} }
export const UpdatePort = ({ portId }: Props) => { export const UpdatePort = ({ portId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data } = api.port.one.useQuery( const { data } = api.port.one.useQuery(
{ {
@@ -89,6 +90,7 @@ export const UpdatePort = ({ portId }: Props) => {
await utils.application.one.invalidate({ await utils.application.one.invalidate({
applicationId: response?.applicationId, applicationId: response?.applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the port"); toast.error("Error to update the port");
@@ -96,7 +98,7 @@ export const UpdatePort = ({ portId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" /> <PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -23,7 +23,7 @@ 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";
import { PlusIcon } from "lucide-react"; import { PlusIcon } 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";
@@ -45,6 +45,7 @@ export const AddRedirect = ({
applicationId, applicationId,
children = <PlusIcon className="h-4 w-4" />, children = <PlusIcon className="h-4 w-4" />,
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
@@ -80,6 +81,7 @@ export const AddRedirect = ({
await utils.application.readTraefikConfig.invalidate({ await utils.application.readTraefikConfig.invalidate({
applicationId, applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the redirect"); toast.error("Error to create the redirect");
@@ -87,7 +89,7 @@ export const AddRedirect = ({
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button>{children}</Button> <Button>{children}</Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -23,7 +23,7 @@ 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";
import { PenBoxIcon, Pencil } from "lucide-react"; import { PenBoxIcon, Pencil } 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";
@@ -41,6 +41,7 @@ interface Props {
export const UpdateRedirect = ({ redirectId }: Props) => { export const UpdateRedirect = ({ redirectId }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data } = api.redirects.one.useQuery( const { data } = api.redirects.one.useQuery(
{ {
redirectId, redirectId,
@@ -84,6 +85,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
await utils.application.one.invalidate({ await utils.application.one.invalidate({
applicationId: response?.applicationId, applicationId: response?.applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the redirect"); toast.error("Error to update the redirect");
@@ -91,7 +93,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" /> <PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react"; import { PlusIcon } 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";
@@ -43,7 +43,7 @@ export const AddSecurity = ({
children = <PlusIcon className="h-4 w-4" />, children = <PlusIcon className="h-4 w-4" />,
}: Props) => { }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.security.create.useMutation(); api.security.create.useMutation();
@@ -72,6 +72,7 @@ export const AddSecurity = ({
await utils.application.readTraefikConfig.invalidate({ await utils.application.readTraefikConfig.invalidate({
applicationId, applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the security"); toast.error("Error to create the security");
@@ -79,7 +80,7 @@ export const AddSecurity = ({
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button>{children}</Button> <Button>{children}</Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } from "lucide-react"; import { PenBoxIcon, Pencil } 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";
@@ -38,6 +38,7 @@ interface Props {
} }
export const UpdateSecurity = ({ securityId }: Props) => { export const UpdateSecurity = ({ securityId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data } = api.security.one.useQuery( const { data } = api.security.one.useQuery(
{ {
@@ -79,6 +80,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
await utils.application.one.invalidate({ await utils.application.one.invalidate({
applicationId: response?.applicationId, applicationId: response?.applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the security"); toast.error("Error to update the security");
@@ -86,7 +88,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" /> <PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -24,7 +24,7 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import type React from "react"; import type React from "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";
@@ -77,6 +77,7 @@ export const AddVolumes = ({
refetch, refetch,
children = <PlusIcon className="h-4 w-4" />, children = <PlusIcon className="h-4 w-4" />,
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync } = api.mounts.create.useMutation(); const { mutateAsync } = api.mounts.create.useMutation();
const form = useForm<AddMount>({ const form = useForm<AddMount>({
defaultValues: { defaultValues: {
@@ -103,6 +104,7 @@ export const AddVolumes = ({
}) })
.then(() => { .then(() => {
toast.success("Mount Created"); toast.success("Mount Created");
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the Bind mount"); toast.error("Error to create the Bind mount");
@@ -117,6 +119,7 @@ export const AddVolumes = ({
}) })
.then(() => { .then(() => {
toast.success("Mount Created"); toast.success("Mount Created");
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the Volume mount"); toast.error("Error to create the Volume mount");
@@ -132,6 +135,7 @@ export const AddVolumes = ({
}) })
.then(() => { .then(() => {
toast.success("Mount Created"); toast.success("Mount Created");
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the File mount"); toast.error("Error to create the File mount");
@@ -142,7 +146,7 @@ export const AddVolumes = ({
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<Button>{children}</Button> <Button>{children}</Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
@@ -22,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil } from "lucide-react"; import { Pencil } 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";
@@ -76,6 +77,7 @@ export const UpdateVolume = ({
refetch, refetch,
serviceType, serviceType,
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data } = api.mounts.one.useQuery( const { data } = api.mounts.one.useQuery(
{ {
@@ -135,6 +137,7 @@ export const UpdateVolume = ({
}) })
.then(() => { .then(() => {
toast.success("Mount Update"); toast.success("Mount Update");
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the Bind mount"); toast.error("Error to update the Bind mount");
@@ -148,6 +151,7 @@ export const UpdateVolume = ({
}) })
.then(() => { .then(() => {
toast.success("Mount Update"); toast.success("Mount Update");
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the Volume mount"); toast.error("Error to update the Volume mount");
@@ -162,6 +166,7 @@ export const UpdateVolume = ({
}) })
.then(() => { .then(() => {
toast.success("Mount Update"); toast.success("Mount Update");
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the File mount"); toast.error("Error to update the File mount");
@@ -171,7 +176,7 @@ export const UpdateVolume = ({
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<Pencil className="size-4 text-muted-foreground" /> <Pencil className="size-4 text-muted-foreground" />
@@ -291,13 +296,15 @@ export const UpdateVolume = ({
)} )}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <DialogClose>
isLoading={isLoading} <Button
form="hook-form-update-volume" isLoading={isLoading}
type="submit" form="hook-form-update-volume"
> type="submit"
Update >
</Button> Update
</Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -27,13 +27,20 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
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";
import { domain } from "@/server/db/validations"; import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react";
import type z from "zod"; import type z from "zod";
type Domain = z.infer<typeof domain>; type Domain = z.infer<typeof domain>;
@@ -60,10 +67,22 @@ export const AddDomain = ({
}, },
); );
const { data: application } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
);
const { mutateAsync, isError, error, isLoading } = domainId const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation() ? api.domain.update.useMutation()
: api.domain.create.useMutation(); : api.domain.create.useMutation();
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm<Domain>({ const form = useForm<Domain>({
resolver: zodResolver(domain), resolver: zodResolver(domain),
}); });
@@ -142,9 +161,42 @@ export const AddDomain = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Host</FormLabel> <FormLabel>Host</FormLabel>
<FormControl> <div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<Input placeholder="api.dokploy.com" {...field} /> <FormControl>
</FormControl> <Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: application?.appName || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -44,12 +44,19 @@ export const DeleteDomain = ({ domainId }: Props) => {
domainId, domainId,
}) })
.then((data) => { .then((data) => {
utils.domain.byApplicationId.invalidate({ if (data?.applicationId) {
applicationId: data?.applicationId, utils.domain.byApplicationId.invalidate({
}); applicationId: data?.applicationId,
utils.application.readTraefikConfig.invalidate({ });
applicationId: data?.applicationId, utils.application.readTraefikConfig.invalidate({
}); applicationId: data?.applicationId,
});
} else if (data?.composeId) {
utils.domain.byComposeId.invalidate({
composeId: data?.composeId,
});
}
toast.success("Domain delete succesfully"); toast.success("Domain delete succesfully");
}) })
.catch(() => { .catch(() => {

View File

@@ -1,79 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import Link from "next/link";
import { GenerateTraefikMe } from "./generate-traefikme";
import { GenerateWildCard } from "./generate-wildcard";
interface Props {
applicationId: string;
}
export const GenerateDomain = ({ applicationId }: Props) => {
return (
<Dialog>
<DialogTrigger className="" asChild>
<Button variant="secondary">
Generate Domain
<RefreshCcw className="size-4 text-muted-foreground " />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Generate Domain</DialogTitle>
<DialogDescription>
Generate Domains for your applications
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 w-full">
<ul className="flex flex-col gap-4">
<li className="flex flex-row items-center gap-4">
<div className="flex flex-col gap-2">
<div className="text-base font-bold">
1. Generate TraefikMe Domain
</div>
<div className="text-sm text-muted-foreground">
This option generates a free domain provided by{" "}
<Link
href="https://traefik.me"
className="text-primary"
target="_blank"
>
TraefikMe
</Link>
. We recommend using this for quick domain testing or if you
don't have a domain yet.
</div>
</div>
</li>
{/* <li className="flex flex-row items-center gap-4">
<div className="flex flex-col gap-2">
<div className="text-base font-bold">
2. Use Wildcard Domain
</div>
<div className="text-sm text-muted-foreground">
To use this option, you need to set up an 'A' record in your
domain provider. For example, create a record for
*.yourdomain.com.
</div>
</div>
</li> */}
</ul>
<div className="flex flex-row gap-4 w-full">
<GenerateTraefikMe applicationId={applicationId} />
{/* <GenerateWildCard applicationId={applicationId} /> */}
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,69 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const GenerateTraefikMe = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Generate Domain
<RefreshCcw className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to generate a new domain?
</AlertDialogTitle>
<AlertDialogDescription>
This will generate a new domain and will be used to access to the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: applicationId,
});
toast.success("Generated Domain succesfully");
})
.catch(() => {
toast.error("Error to generate Domain");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -1,69 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { SquareAsterisk } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const GenerateWildCard = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Generate Wildcard Domain
<SquareAsterisk className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to generate a new wildcard domain?
</AlertDialogTitle>
<AlertDialogDescription>
This will generate a new domain and will be used to access to the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: applicationId,
});
toast.success("Generated Domain succesfully");
})
.catch((e) => {
toast.error(`Error to generate Domain: ${e.message}`);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -12,7 +12,6 @@ import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { AddDomain } from "./add-domain"; import { AddDomain } from "./add-domain";
import { DeleteDomain } from "./delete-domain"; import { DeleteDomain } from "./delete-domain";
import { GenerateDomain } from "./generate-domain";
interface Props { interface Props {
applicationId: string; applicationId: string;
@@ -46,9 +45,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
</Button> </Button>
</AddDomain> </AddDomain>
)} )}
{data && data?.length > 0 && (
<GenerateDomain applicationId={applicationId} />
)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-row gap-4"> <CardContent className="flex w-full flex-row gap-4">
@@ -65,8 +61,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
<GlobeIcon className="size-4" /> Add Domain <GlobeIcon className="size-4" /> Add Domain
</Button> </Button>
</AddDomain> </AddDomain>
<GenerateDomain applicationId={applicationId} />
</div> </div>
</div> </div>
) : ( ) : (

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react"; import { AlertTriangle, SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
} }
export const UpdateApplication = ({ applicationId }: Props) => { export const UpdateApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } = const { mutateAsync, error, isError, isLoading } =
api.application.update.useMutation(); api.application.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
utils.application.one.invalidate({ utils.application.one.invalidate({
applicationId: applicationId, applicationId: applicationId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the application"); toast.error("Error to update the application");
@@ -87,7 +89,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost"> <Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" /> <SquarePen className="size-4 text-muted-foreground" />

View File

@@ -0,0 +1,441 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { domainCompose } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import type z from "zod";
type Domain = z.infer<typeof domainCompose>;
export type CacheType = "fetch" | "cache";
interface Props {
composeId: string;
domainId?: string;
children: React.ReactNode;
}
export const AddDomainCompose = ({
composeId,
domainId = "",
children,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
{
domainId,
},
{
enabled: !!domainId,
},
);
const { data: compose } = api.compose.one.useQuery(
{
composeId,
},
{
enabled: !!composeId,
},
);
const {
data: services,
isFetching: isLoadingServices,
error: errorServices,
refetch: refetchServices,
} = api.compose.loadServices.useQuery(
{
composeId,
type: cacheType,
},
{
retry: false,
refetchOnWindowFocus: false,
},
);
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const form = useForm<Domain>({
resolver: zodResolver(domainCompose),
});
const https = form.watch("https");
useEffect(() => {
if (data) {
form.reset({
...data,
/* Convert null to undefined */
path: data?.path || undefined,
port: data?.port || undefined,
serviceName: data?.serviceName || undefined,
});
}
if (!domainId) {
form.reset({});
}
}, [form, form.reset, data, isLoading]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
: "In this section you can add domains",
};
const onSubmit = async (data: Domain) => {
await mutateAsync({
domainId,
composeId,
domainType: "compose",
...data,
})
.then(async () => {
await utils.domain.byComposeId.invalidate({
composeId,
});
toast.success(dictionary.success);
if (domainId) {
refetch();
}
setIsOpen(false);
})
.catch(() => {
toast.error(dictionary.error);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
{errorServices && (
<AlertBlock
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message}
</AlertBlock>
)}
<div className="flex flex-row gap-4 w-full items-end">
<FormField
control={form.control}
name="serviceName"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
</SelectContent>
</Select>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and load the
services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services from the
last deployment/fetch from the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: compose?.appName || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input placeholder={"/"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Container Port</FormLabel>
<FormControl>
<Input
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
{https && (
<FormField
control={form.control}
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
</form>
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
>
{dictionary.submit}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,111 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import Link from "next/link";
import { DeleteDomain } from "../../application/domains/delete-domain";
import { AddDomainCompose } from "./add-domain";
interface Props {
composeId: string;
}
export const ShowDomainsCompose = ({ composeId }: Props) => {
const { data } = api.domain.byComposeId.useQuery(
{
composeId,
},
{
enabled: !!composeId,
},
);
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
<div className="flex flex-col gap-1">
<CardTitle className="text-xl">Domains</CardTitle>
<CardDescription>
Domains are used to access to the application
</CardDescription>
</div>
<div className="flex flex-row gap-4 flex-wrap">
{data && data?.length > 0 && (
<AddDomainCompose composeId={composeId}>
<Button>
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomainCompose>
)}
</div>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
{data?.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3">
<GlobeIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To access to the application is required to set at least 1
domain
</span>
<div className="flex flex-row gap-4 flex-wrap">
<AddDomainCompose composeId={composeId}>
<Button>
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomainCompose>
</div>
</div>
) : (
<div className="flex w-full flex-col gap-4">
{data?.map((item) => {
return (
<div
key={item.domainId}
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
>
<Link target="_blank" href={`http://${item.host}`}>
<ExternalLink className="size-5" />
</Link>
<Button variant="outline" disabled>
{item.serviceName}
</Button>
<Input disabled value={item.host} />
<Button variant="outline" disabled>
{item.path}
</Button>
<Button variant="outline" disabled>
{item.port}
</Button>
<Button variant="outline" disabled>
{item.https ? "HTTPS" : "HTTP"}
</Button>
<div className="flex flex-row gap-1">
<AddDomainCompose
composeId={composeId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</AddDomainCompose>
<DeleteDomain domainId={item.domainId} />
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -72,7 +72,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
.then(async () => { .then(async () => {
toast.success("Compose config Updated"); toast.success("Compose config Updated");
refetch(); refetch();
await utils.compose.allServices.invalidate({ await utils.compose.getConvertedCompose.invalidate({
composeId, composeId,
}); });
}) })

View File

@@ -5,6 +5,7 @@ import { GitBranch, LockIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { ComposeFileEditor } from "../compose-file-editor"; import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveGitProviderCompose } from "./save-git-provider-compose"; import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose";
@@ -29,7 +30,8 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
Select the source of your code Select the source of your code
</p> </p>
</div> </div>
<div className="hidden space-y-1 text-sm font-normal md:block"> <div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
<ShowConvertedCompose composeId={composeId} />
<GitBranch className="size-6 text-muted-foreground" /> <GitBranch className="size-6 text-muted-foreground" />
</div> </div>
</CardTitle> </CardTitle>

View File

@@ -0,0 +1,87 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const ShowConvertedCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const {
data: compose,
error,
isError,
refetch,
} = api.compose.getConvertedCompose.useQuery(
{ composeId },
{
retry: false,
},
);
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="max-lg:w-full" variant="outline">
<Puzzle className="h-4 w-4" />
Preview Compose
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
<DialogHeader>
<DialogTitle>Converted Compose</DialogTitle>
<DialogDescription>
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error to fetch source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</DialogContent>
</Dialog>
);
};

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { SquarePen } from "lucide-react"; import { SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
} }
export const UpdateCompose = ({ composeId }: Props) => { export const UpdateCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } = const { mutateAsync, error, isError, isLoading } =
api.compose.update.useMutation(); api.compose.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
utils.compose.one.invalidate({ utils.compose.one.invalidate({
composeId: composeId, composeId: composeId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the Compose"); toast.error("Error to update the Compose");
@@ -87,7 +89,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost"> <Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" /> <SquarePen className="size-4 text-muted-foreground" />

View File

@@ -36,7 +36,7 @@ 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";
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react"; import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } 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";
@@ -57,6 +57,7 @@ interface Props {
} }
export const UpdateBackup = ({ backupId, refetch }: Props) => { export const UpdateBackup = ({ backupId, refetch }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data, isLoading } = api.destination.all.useQuery(); const { data, isLoading } = api.destination.all.useQuery();
const { data: backup } = api.backup.one.useQuery( const { data: backup } = api.backup.one.useQuery(
{ {
@@ -105,6 +106,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
.then(async () => { .then(async () => {
toast.success("Backup Updated"); toast.success("Backup Updated");
refetch(); refetch();
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the backup"); toast.error("Error to update the backup");
@@ -112,7 +114,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost"> <Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" /> <PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react"; import { AlertTriangle, SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
} }
export const UpdateMongo = ({ mongoId }: Props) => { export const UpdateMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } = const { mutateAsync, error, isError, isLoading } =
api.mongo.update.useMutation(); api.mongo.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
utils.mongo.one.invalidate({ utils.mongo.one.invalidate({
mongoId: mongoId, mongoId: mongoId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update mongo database"); toast.error("Error to update mongo database");
@@ -87,7 +89,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost"> <Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" /> <SquarePen className="size-4 text-muted-foreground" />

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react"; import { AlertTriangle, SquarePen } 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";
@@ -41,6 +41,7 @@ interface Props {
} }
export const UpdatePostgres = ({ postgresId }: Props) => { export const UpdatePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } = const { mutateAsync, error, isError, isLoading } =
api.postgres.update.useMutation(); api.postgres.update.useMutation();
@@ -79,6 +80,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
utils.postgres.one.invalidate({ utils.postgres.one.invalidate({
postgresId: postgresId, postgresId: postgresId,
}); });
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the postgres"); toast.error("Error to update the postgres");
@@ -87,7 +89,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost"> <Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" /> <SquarePen className="size-4 text-muted-foreground" />

View File

@@ -23,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react"; import { AlertTriangle, SquarePen } 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";
@@ -42,6 +42,7 @@ interface Props {
} }
export const UpdateProject = ({ projectId }: Props) => { export const UpdateProject = ({ projectId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError } = api.project.update.useMutation(); const { mutateAsync, error, isError } = api.project.update.useMutation();
const { data } = api.project.one.useQuery( const { data } = api.project.one.useQuery(
@@ -77,6 +78,7 @@ export const UpdateProject = ({ projectId }: Props) => {
.then(() => { .then(() => {
toast.success("Project updated succesfully"); toast.success("Project updated succesfully");
utils.project.all.invalidate(); utils.project.all.invalidate();
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the project"); toast.error("Error to update the project");
@@ -85,7 +87,7 @@ export const UpdateProject = ({ projectId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer space-x-3" className="w-full cursor-pointer space-x-3"

View File

@@ -23,12 +23,14 @@ export const AddManager = () => {
<div className="flex flex-col gap-2.5 text-sm"> <div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span> <span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between"> <span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version 24.0 curl https://get.docker.com | sh -s -- --version {data?.version}
<button <button
type="button" type="button"
className="self-center" className="self-center"
onClick={() => { onClick={() => {
copy("curl https://get.docker.com | sh -s -- --version 24.0"); copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard"); toast.success("Copied to clipboard");
}} }}
> >
@@ -43,12 +45,12 @@ export const AddManager = () => {
cluster cluster
</span> </span>
<span className="bg-muted rounded-lg p-2 flex"> <span className="bg-muted rounded-lg p-2 flex">
{data} {data?.command}
<button <button
type="button" type="button"
className="self-start" className="self-start"
onClick={() => { onClick={() => {
copy(data || ""); copy(data?.command || "");
toast.success("Copied to clipboard"); toast.success("Copied to clipboard");
}} }}
> >

View File

@@ -22,12 +22,14 @@ export const AddWorker = () => {
<div className="flex flex-col gap-2.5 text-sm"> <div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span> <span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between"> <span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version 24.0 curl https://get.docker.com | sh -s -- --version {data?.version}
<button <button
type="button" type="button"
className="self-center" className="self-center"
onClick={() => { onClick={() => {
copy("curl https://get.docker.com | sh -s -- --version 24.0"); copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard"); toast.success("Copied to clipboard");
}} }}
> >
@@ -42,12 +44,12 @@ export const AddWorker = () => {
</span> </span>
<span className="bg-muted rounded-lg p-2 flex"> <span className="bg-muted rounded-lg p-2 flex">
{data} {data?.command}
<button <button
type="button" type="button"
className="self-start" className="self-start"
onClick={() => { onClick={() => {
copy(data || ""); copy(data?.command || "");
toast.success("Copied to clipboard"); toast.success("Copied to clipboard");
}} }}
> >

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react"; import { PenBoxIcon } 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";
@@ -43,6 +43,7 @@ interface Props {
export const UpdateDestination = ({ destinationId }: Props) => { export const UpdateDestination = ({ destinationId }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.destination.one.useQuery( const { data, refetch } = api.destination.one.useQuery(
{ {
destinationId, destinationId,
@@ -93,13 +94,14 @@ export const UpdateDestination = ({ destinationId }: Props) => {
toast.success("Destination Updated"); toast.success("Destination Updated");
await refetch(); await refetch();
await utils.destination.all.invalidate(); await utils.destination.all.invalidate();
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the Destination"); toast.error("Error to update the Destination");
}); });
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<Button variant="ghost"> <Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" /> <PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -27,7 +27,7 @@ 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";
import { Mail, PenBoxIcon } from "lucide-react"; import { Mail, PenBoxIcon } from "lucide-react";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { FieldErrors, useFieldArray, useForm } from "react-hook-form"; import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -41,6 +41,7 @@ interface Props {
export const UpdateNotification = ({ notificationId }: Props) => { export const UpdateNotification = ({ notificationId }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.notification.one.useQuery( const { data, refetch } = api.notification.one.useQuery(
{ {
notificationId, notificationId,
@@ -207,6 +208,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
toast.success("Notification Updated"); toast.success("Notification Updated");
await utils.notification.all.invalidate(); await utils.notification.all.invalidate();
refetch(); refetch();
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to update a notification"); toast.error("Error to update a notification");
@@ -214,7 +216,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
} }
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<Button variant="ghost"> <Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" /> <PenBoxIcon className="size-4 text-muted-foreground" />

View File

@@ -0,0 +1,15 @@
DO $$ BEGIN
CREATE TYPE "public"."domainType" AS ENUM('compose', 'application');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "domain" ALTER COLUMN "applicationId" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "serviceName" text;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "domainType" "domainType" DEFAULT 'application';--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "composeId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "domain" ADD CONSTRAINT "domain_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1 @@
ALTER TABLE "domain" ALTER COLUMN "port" SET DEFAULT 3000;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -218,6 +218,20 @@
"when": 1723608499147, "when": 1723608499147,
"tag": "0030_little_kabuki", "tag": "0030_little_kabuki",
"breakpoints": true "breakpoints": true
},
{
"idx": 31,
"version": "6",
"when": 1723701656243,
"tag": "0031_steep_vulture",
"breakpoints": true
},
{
"idx": 32,
"version": "6",
"when": 1723705257806,
"tag": "0032_flashy_shadow_king",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.6.3", "version": "v0.7.0",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -2,6 +2,7 @@ import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-c
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes"; import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose"; import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose"; import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show"; import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
@@ -34,6 +35,7 @@ type TabState =
| "settings" | "settings"
| "advanced" | "advanced"
| "deployments" | "deployments"
| "domains"
| "monitoring"; | "monitoring";
const Service = ( const Service = (
@@ -117,12 +119,13 @@ const Service = (
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start"> <TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger> <TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger> <TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList> </TabsList>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
@@ -168,6 +171,12 @@ const Service = (
<ShowDeploymentsCompose composeId={composeId} /> <ShowDeploymentsCompose composeId={composeId} />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomainsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced"> <TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} /> <AddCommandCompose composeId={composeId} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -35,14 +35,23 @@ export const clusterRouter = createTRPCRouter({
}), }),
addWorker: protectedProcedure.query(async ({ input }) => { addWorker: protectedProcedure.query(async ({ input }) => {
const result = await docker.swarmInspect(); const result = await docker.swarmInspect();
return `docker swarm join --token ${ const docker_version = await docker.version();
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`; return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}), }),
addManager: protectedProcedure.query(async ({ input }) => { addManager: protectedProcedure.query(async ({ input }) => {
const result = await docker.swarmInspect(); const result = await docker.swarmInspect();
return `docker swarm join --token ${ const docker_version = await docker.version();
result.JoinTokens.Manager return {
} ${await getPublicIpWithFallback()}:2377`; command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}), }),
}); });

View File

@@ -3,6 +3,7 @@ import { db } from "@/server/db";
import { import {
apiCreateCompose, apiCreateCompose,
apiCreateComposeByTemplate, apiCreateComposeByTemplate,
apiFetchServices,
apiFindCompose, apiFindCompose,
apiRandomizeCompose, apiRandomizeCompose,
apiUpdateCompose, apiUpdateCompose,
@@ -15,16 +16,18 @@ import {
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { createCommand } from "@/server/utils/builders/compose"; import { createCommand } from "@/server/utils/builders/compose";
import { randomizeComposeFile } from "@/server/utils/docker/compose"; import { randomizeComposeFile } from "@/server/utils/docker/compose";
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory"; import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { templates } from "@/templates/templates"; import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type"; import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import { import {
generatePassword, generatePassword,
loadTemplateModule, loadTemplateModule,
readComposeFile, readTemplateComposeFile,
} from "@/templates/utils"; } from "@/templates/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import _ from "lodash"; import _ from "lodash";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { findAdmin } from "../services/admin"; import { findAdmin } from "../services/admin";
@@ -38,6 +41,7 @@ import {
updateCompose, updateCompose,
} from "../services/compose"; } from "../services/compose";
import { removeDeploymentsByComposeId } from "../services/deployment"; import { removeDeploymentsByComposeId } from "../services/deployment";
import { createDomain, findDomainsByComposeId } from "../services/domain";
import { createMount } from "../services/mount"; import { createMount } from "../services/mount";
import { findProjectById } from "../services/project"; import { findProjectById } from "../services/project";
import { addNewService, checkServiceAccess } from "../services/user"; import { addNewService, checkServiceAccess } from "../services/user";
@@ -113,10 +117,25 @@ export const composeRouter = createTRPCRouter({
await cleanQueuesByCompose(input.composeId); await cleanQueuesByCompose(input.composeId);
}), }),
allServices: protectedProcedure loadServices: protectedProcedure
.input(apiFindCompose) .input(apiFetchServices)
.query(async ({ input }) => { .query(async ({ input }) => {
return await loadServices(input.composeId); return await loadServices(input.composeId, input.type);
}),
fetchSourceType: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
try {
const compose = await findComposeById(input.composeId);
await cloneCompose(compose);
return compose.sourceType;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to fetch source type",
cause: err,
});
}
}), }),
randomizeCompose: protectedProcedure randomizeCompose: protectedProcedure
@@ -124,6 +143,17 @@ export const composeRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await randomizeComposeFile(input.composeId, input.prefix); return await randomizeComposeFile(input.composeId, input.prefix);
}), }),
getConvertedCompose: protectedProcedure
.input(apiFindCompose)
.query(async ({ input }) => {
const compose = await findComposeById(input.composeId);
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, {
lineWidth: 1000,
});
}),
deploy: protectedProcedure deploy: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
@@ -189,7 +219,7 @@ export const composeRouter = createTRPCRouter({
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create"); await checkServiceAccess(ctx.user.authId, input.projectId, "create");
} }
const composeFile = await readComposeFile(input.id); const composeFile = await readTemplateComposeFile(input.id);
const generate = await loadTemplateModule(input.id as TemplatesKeys); const generate = await loadTemplateModule(input.id as TemplatesKeys);
@@ -206,7 +236,7 @@ export const composeRouter = createTRPCRouter({
const project = await findProjectById(input.projectId); const project = await findProjectById(input.projectId);
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts } = generate({ const { envs, mounts, domains } = generate({
serverIp: admin.serverIp, serverIp: admin.serverIp,
projectName: projectName, projectName: projectName,
}); });
@@ -214,7 +244,7 @@ export const composeRouter = createTRPCRouter({
const compose = await createComposeByTemplate({ const compose = await createComposeByTemplate({
...input, ...input,
composeFile: composeFile, composeFile: composeFile,
env: envs.join("\n"), env: envs?.join("\n"),
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`, appName: `${projectName}-${generatePassword(6)}`,
@@ -237,6 +267,17 @@ export const composeRouter = createTRPCRouter({
} }
} }
if (domains && domains?.length > 0) {
for (const domain of domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
});
}
}
return null; return null;
}), }),

View File

@@ -1,8 +1,12 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { import {
apiCreateDomain, apiCreateDomain,
apiCreateTraefikMeDomain,
apiFindCompose,
apiFindDomain, apiFindDomain,
apiFindDomainByApplication, apiFindDomainByApplication,
apiFindDomainByCompose,
apiFindOneApplication,
apiUpdateDomain, apiUpdateDomain,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain"; import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
@@ -12,8 +16,8 @@ import {
createDomain, createDomain,
findDomainById, findDomainById,
findDomainsByApplicationId, findDomainsByApplicationId,
generateDomain, findDomainsByComposeId,
generateWildcard, generateTraefikMeDomain,
removeDomainById, removeDomainById,
updateDomainById, updateDomainById,
} from "../services/domain"; } from "../services/domain";
@@ -33,27 +37,30 @@ export const domainRouter = createTRPCRouter({
} }
}), }),
byApplicationId: protectedProcedure byApplicationId: protectedProcedure
.input(apiFindDomainByApplication) .input(apiFindOneApplication)
.query(async ({ input }) => { .query(async ({ input }) => {
return await findDomainsByApplicationId(input.applicationId); return await findDomainsByApplicationId(input.applicationId);
}), }),
byComposeId: protectedProcedure
.input(apiFindCompose)
.query(async ({ input }) => {
return await findDomainsByComposeId(input.composeId);
}),
generateDomain: protectedProcedure generateDomain: protectedProcedure
.input(apiFindDomainByApplication) .input(apiCreateTraefikMeDomain)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return generateDomain(input); return generateTraefikMeDomain(input.appName);
}),
generateWildcard: protectedProcedure
.input(apiFindDomainByApplication)
.mutation(async ({ input }) => {
return generateWildcard(input);
}), }),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateDomain) .input(apiUpdateDomain)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const result = await updateDomainById(input.domainId, input); const result = await updateDomainById(input.domainId, input);
const domain = await findDomainById(input.domainId); const domain = await findDomainById(input.domainId);
const application = await findApplicationById(domain.applicationId); if (domain.applicationId) {
await manageDomain(application, domain); const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
}
return result; return result;
}), }),
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => { one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
@@ -64,7 +71,9 @@ export const domainRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const domain = await findDomainById(input.domainId); const domain = await findDomainById(input.domainId);
const result = await removeDomainById(input.domainId); const result = await removeDomainById(input.domainId);
await removeDomain(domain.application.appName, domain.uniqueConfigKey); if (domain.application) {
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
}
return result; return result;
}), }),

View File

@@ -4,6 +4,7 @@ import { db } from "@/server/db";
import { type apiCreateCompose, compose } from "@/server/db/schema"; import { type apiCreateCompose, compose } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema/utils"; import { generateAppName } from "@/server/db/schema/utils";
import { buildCompose } from "@/server/utils/builders/compose"; import { buildCompose } from "@/server/utils/builders/compose";
import { cloneCompose, loadDockerCompose } from "@/server/utils/docker/domain";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error"; import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success"; import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
@@ -14,7 +15,6 @@ import { createComposeFile } from "@/server/utils/providers/raw";
import { generatePassword } from "@/templates/utils"; import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { load } from "js-yaml";
import { findAdmin, getDokployUrl } from "./admin"; import { findAdmin, getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
@@ -91,6 +91,7 @@ export const findComposeById = async (composeId: string) => {
project: true, project: true,
deployments: true, deployments: true,
mounts: true, mounts: true,
domains: true,
}, },
}); });
if (!result) { if (!result) {
@@ -102,20 +103,27 @@ export const findComposeById = async (composeId: string) => {
return result; return result;
}; };
export const loadServices = async (composeId: string) => { export const loadServices = async (
composeId: string,
type: "fetch" | "cache" = "fetch",
) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
// use js-yaml to parse the docker compose file and then extact the services if (type === "fetch") {
const composeFile = compose.composeFile; await cloneCompose(compose);
const composeData = load(composeFile) as ComposeSpecification; }
const composeData = await loadDockerCompose(compose);
if (!composeData?.services) { if (!composeData?.services) {
return ["All Services"]; throw new TRPCError({
code: "NOT_FOUND",
message: "Services not found",
});
} }
const services = Object.keys(composeData.services); const services = Object.keys(composeData.services);
return [...services, "All Services"]; return [...services];
}; };
export const updateCompose = async ( export const updateCompose = async (

View File

@@ -15,8 +15,6 @@ export type Domain = typeof domains.$inferSelect;
export const createDomain = async (input: typeof apiCreateDomain._type) => { export const createDomain = async (input: typeof apiCreateDomain._type) => {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const application = await findApplicationById(input.applicationId);
const domain = await tx const domain = await tx
.insert(domains) .insert(domains)
.values({ .values({
@@ -32,52 +30,19 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
}); });
} }
await manageDomain(application, domain); if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
}
}); });
}; };
export const generateDomain = async ( export const generateTraefikMeDomain = async (appName: string) => {
input: typeof apiFindDomainByApplication._type,
) => {
const application = await findApplicationById(input.applicationId);
const admin = await findAdmin(); const admin = await findAdmin();
const domain = await createDomain({ return generateRandomDomain({
applicationId: application.applicationId, serverIp: admin.serverIp || "",
host: generateRandomDomain({ projectName: appName,
serverIp: admin.serverIp || "",
projectName: application.appName,
}),
port: 3000,
certificateType: "none",
https: false,
path: "/",
}); });
return domain;
};
export const generateWildcard = async (
input: typeof apiFindDomainByApplication._type,
) => {
const application = await findApplicationById(input.applicationId);
const admin = await findAdmin();
if (!admin.host) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "We need a host to generate a wildcard domain",
});
}
const domain = await createDomain({
applicationId: application.applicationId,
host: generateWildcardDomain(application.appName, admin.host || ""),
port: 3000,
certificateType: "none",
https: false,
path: "/",
});
return domain;
}; };
export const generateWildcardDomain = ( export const generateWildcardDomain = (
@@ -114,6 +79,17 @@ export const findDomainsByApplicationId = async (applicationId: string) => {
return domainsArray; return domainsArray;
}; };
export const findDomainsByComposeId = async (composeId: string) => {
const domainsArray = await db.query.domains.findMany({
where: eq(domains.composeId, composeId),
with: {
compose: true,
},
});
return domainsArray;
};
export const updateDomainById = async ( export const updateDomainById = async (
domainId: string, domainId: string,
domainData: Partial<Domain>, domainData: Partial<Domain>,

View File

@@ -1,11 +1,11 @@
import { sshKeys } from "@/server/db/schema/ssh-key"; import { sshKeys } from "@/server/db/schema/ssh-key";
import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import { domains } from "./domain";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { applicationStatus } from "./shared"; import { applicationStatus } from "./shared";
@@ -72,6 +72,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
fields: [compose.customGitSSHKeyId], fields: [compose.customGitSSHKeyId],
references: [sshKeys.sshKeyId], references: [sshKeys.sshKeyId],
}), }),
domains: many(domains),
})); }));
const createSchema = createInsertSchema(compose, { const createSchema = createInsertSchema(compose, {
@@ -106,6 +107,11 @@ export const apiFindCompose = z.object({
composeId: z.string().min(1), composeId: z.string().min(1),
}); });
export const apiFetchServices = z.object({
composeId: z.string().min(1),
type: z.enum(["fetch", "cache"]).optional().default("cache"),
});
export const apiUpdateCompose = createSchema.partial().extend({ export const apiUpdateCompose = createSchema.partial().extend({
composeId: z.string(), composeId: z.string(),
composeFile: z.string().optional(), composeFile: z.string().optional(),

View File

@@ -1,11 +1,22 @@
import { domain } from "@/server/db/validations"; import { domain } from "@/server/db/validations/domain";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core"; import {
boolean,
integer,
pgEnum,
pgTable,
serial,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { compose } from "./compose";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
export const domainType = pgEnum("domainType", ["compose", "application"]);
export const domains = pgTable("domain", { export const domains = pgTable("domain", {
domainId: text("domainId") domainId: text("domainId")
.notNull() .notNull()
@@ -13,15 +24,21 @@ export const domains = pgTable("domain", {
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
host: text("host").notNull(), host: text("host").notNull(),
https: boolean("https").notNull().default(false), https: boolean("https").notNull().default(false),
port: integer("port").default(80), port: integer("port").default(3000),
path: text("path").default("/"), path: text("path").default("/"),
serviceName: text("serviceName"),
domainType: domainType("domainType").default("application"),
uniqueConfigKey: serial("uniqueConfigKey"), uniqueConfigKey: serial("uniqueConfigKey"),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
applicationId: text("applicationId") composeId: text("composeId").references(() => compose.composeId, {
.notNull() onDelete: "cascade",
.references(() => applications.applicationId, { onDelete: "cascade" }), }),
applicationId: text("applicationId").references(
() => applications.applicationId,
{ onDelete: "cascade" },
),
certificateType: certificateType("certificateType").notNull().default("none"), certificateType: certificateType("certificateType").notNull().default("none"),
}); });
@@ -30,6 +47,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({
fields: [domains.applicationId], fields: [domains.applicationId],
references: [applications.applicationId], references: [applications.applicationId],
}), }),
compose: one(compose, {
fields: [domains.composeId],
references: [compose.composeId],
}),
})); }));
const createSchema = createInsertSchema(domains, domain._def.schema.shape); const createSchema = createInsertSchema(domains, domain._def.schema.shape);
@@ -41,6 +62,9 @@ export const apiCreateDomain = createSchema.pick({
https: true, https: true,
applicationId: true, applicationId: true,
certificateType: true, certificateType: true,
composeId: true,
serviceName: true,
domainType: true,
}); });
export const apiFindDomain = createSchema export const apiFindDomain = createSchema
@@ -53,6 +77,14 @@ export const apiFindDomainByApplication = createSchema.pick({
applicationId: true, applicationId: true,
}); });
export const apiCreateTraefikMeDomain = createSchema.pick({}).extend({
appName: z.string().min(1),
});
export const apiFindDomainByCompose = createSchema.pick({
composeId: true,
});
export const apiUpdateDomain = createSchema export const apiUpdateDomain = createSchema
.pick({ .pick({
host: true, host: true,
@@ -60,5 +92,7 @@ export const apiUpdateDomain = createSchema
port: true, port: true,
https: true, https: true,
certificateType: true, certificateType: true,
serviceName: true,
domainType: true,
}) })
.merge(createSchema.pick({ domainId: true }).required()); .merge(createSchema.pick({ domainId: true }).required());

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
export const domain = z
.object({
host: z.string().min(1, { message: "Add a hostname" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});
export const domainCompose = z
.object({
host: z.string().min(1, { message: "Host is required" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
serviceName: z.string().min(1, { message: "Service name is required" }),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});

View File

@@ -35,25 +35,3 @@ export const sshKeyUpdate = sshKeyCreate.pick({
export const sshKeyType = z.object({ export const sshKeyType = z.object({
type: z.enum(["rsa", "ed25519"]).optional(), type: z.enum(["rsa", "ed25519"]).optional(),
}); });
export const domain = z
.object({
host: z.string().min(1, { message: "Add a hostname" }),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});

View File

@@ -8,20 +8,28 @@ import { dirname, join } from "node:path";
import { COMPOSE_PATH } from "@/server/constants"; import { COMPOSE_PATH } from "@/server/constants";
import type { InferResultType } from "@/server/types/with"; import type { InferResultType } from "@/server/types/with";
import boxen from "boxen"; import boxen from "boxen";
import { writeDomainsToCompose } from "../docker/domain";
import { prepareEnvironmentVariables } from "../docker/utils"; import { prepareEnvironmentVariables } from "../docker/utils";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType< export type ComposeNested = InferResultType<
"compose", "compose",
{ project: true; mounts: true } { project: true; mounts: true; domains: true }
>; >;
export const buildCompose = async (compose: ComposeNested, logPath: string) => { export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { sourceType, appName, mounts, composeType, env, composePath } = const {
compose; sourceType,
appName,
mounts,
composeType,
env,
composePath,
domains,
} = compose;
try { try {
const command = createCommand(compose); const command = createCommand(compose);
await writeDomainsToCompose(compose, domains);
createEnvFile(compose); createEnvFile(compose);
const logContent = ` const logContent = `

View File

@@ -0,0 +1,220 @@
import fs, { existsSync, readFileSync, writeSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { Compose } from "@/server/api/services/compose";
import type { Domain } from "@/server/api/services/domain";
import { COMPOSE_PATH } from "@/server/constants";
import { dump, load } from "js-yaml";
import { cloneGitRawRepository } from "../providers/git";
import { cloneRawGithubRepository } from "../providers/github";
import { createComposeFileRaw } from "../providers/raw";
import type {
ComposeSpecification,
DefinitionsService,
PropertiesNetworks,
} from "./types";
export const cloneCompose = async (compose: Compose) => {
if (compose.sourceType === "github") {
await cloneRawGithubRepository(compose);
} else if (compose.sourceType === "git") {
await cloneGitRawRepository(compose);
} else if (compose.sourceType === "raw") {
await createComposeFileRaw(compose);
}
};
export const getComposePath = (compose: Compose) => {
const { appName, sourceType, composePath } = compose;
let path = "";
if (sourceType === "raw") {
path = "docker-compose.yml";
} else {
path = composePath;
}
return join(COMPOSE_PATH, appName, "code", path);
};
export const loadDockerCompose = async (
compose: Compose,
): Promise<ComposeSpecification | null> => {
const path = getComposePath(compose);
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
const parsedConfig = load(yamlStr) as ComposeSpecification;
return parsedConfig;
}
return null;
};
export const readComposeFile = async (compose: Compose) => {
const path = getComposePath(compose);
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
return yamlStr;
}
return null;
};
export const writeDomainsToCompose = async (
compose: Compose,
domains: Domain[],
) => {
if (!domains.length) {
return;
}
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
const composeString = dump(composeConverted, { lineWidth: 1000 });
try {
await writeFile(path, composeString, "utf8");
} catch (error) {
throw error;
}
};
export const addDomainToCompose = async (
compose: Compose,
domains: Domain[],
) => {
const { appName } = compose;
const result = await loadDockerCompose(compose);
if (!result || domains.length === 0) {
return null;
}
for (const domain of domains) {
const { serviceName, https } = domain;
if (!serviceName) {
throw new Error("Service name not found");
}
if (!result?.services?.[serviceName]) {
throw new Error(`The service ${serviceName} not found in the compose`);
}
if (!result.services[serviceName].labels) {
result.services[serviceName].labels = [];
}
const httpLabels = await createDomainLabels(appName, domain, "web");
if (https) {
const httpsLabels = await createDomainLabels(
appName,
domain,
"websecure",
);
httpLabels.push(...httpsLabels);
}
const labels = result.services[serviceName].labels;
if (Array.isArray(labels)) {
if (!labels.includes("traefik.enable=true")) {
labels.push("traefik.enable=true");
}
labels.push(...httpLabels);
}
// Add the dokploy-network to the service
result.services[serviceName].networks = addDokployNetworkToService(
result.services[serviceName].networks,
);
}
// Add dokploy-network to the root of the compose file
result.networks = addDokployNetworkToRoot(result.networks);
return result;
};
export const writeComposeFile = async (
compose: Compose,
composeSpec: ComposeSpecification,
) => {
const path = getComposePath(compose);
try {
const composeFile = dump(composeSpec, {
lineWidth: 1000,
});
fs.writeFileSync(path, composeFile, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const createDomainLabels = async (
appName: string,
domain: Domain,
entrypoint: "web" | "websecure",
) => {
const { host, port, https, uniqueConfigKey, certificateType } = domain;
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
const labels = [
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)`,
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
`traefik.http.routers.${routerName}.service=${routerName}`,
];
if (entrypoint === "web" && https) {
labels.push(
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
);
}
if (entrypoint === "websecure") {
if (certificateType === "letsencrypt") {
labels.push(
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
);
}
}
return labels;
};
export const addDokployNetworkToService = (
networkService: DefinitionsService["networks"],
) => {
let networks = networkService;
const network = "dokploy-network";
if (!networks) {
networks = [];
}
if (Array.isArray(networks)) {
if (!networks.includes(network)) {
networks.push(network);
}
} else if (networks && typeof networks === "object") {
if (!(network in networks)) {
networks[network] = {};
}
}
return networks;
};
export const addDokployNetworkToRoot = (
networkRoot: PropertiesNetworks | undefined,
) => {
let networks = networkRoot;
const network = "dokploy-network";
if (!networks) {
networks = {};
}
if (networks[network] || !networks[network]) {
networks[network] = {
external: true,
};
}
return networks;
};

View File

@@ -139,3 +139,61 @@ const sanitizeRepoPathSSH = (input: string) => {
}, },
}; };
}; };
export const cloneGitRawRepository = async (entity: {
appName: string;
customGitUrl?: string | null;
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
}) => {
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
if (!customGitUrl || !customGitBranch) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: Repository not found",
});
}
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
try {
await addHostToKnownHosts(customGitUrl);
await recreateDirectory(outputPath);
if (customGitSSHKeyId) {
await updateSSHKeyById({
sshKeyId: customGitSSHKeyId,
lastUsedAt: new Date().toISOString(),
});
}
await spawnAsync(
"git",
[
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
customGitUrl,
outputPath,
"--progress",
],
(data) => {},
{
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
},
);
} catch (error) {
throw error;
}
};

View File

@@ -1,6 +1,6 @@
import { createWriteStream } from "node:fs"; import { createWriteStream } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import type { Admin } from "@/server/api/services/admin"; import { type Admin, findAdmin } from "@/server/api/services/admin";
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants"; import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
import { createAppAuth } from "@octokit/auth-app"; import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -128,3 +128,34 @@ export const cloneGithubRepository = async (
writeStream.end(); writeStream.end();
} }
}; };
export const cloneRawGithubRepository = async (entity: {
appName: string;
repository?: string | null;
owner?: string | null;
branch?: string | null;
}) => {
const { appName, repository, owner, branch } = entity;
const admin = await findAdmin();
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(admin);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
await spawnAsync("git", [
"clone",
"--branch",
branch!,
"--depth",
"1",
cloneUrl,
outputPath,
"--progress",
]);
} catch (error) {
throw error;
}
};

View File

@@ -26,3 +26,15 @@ export const createComposeFile = async (compose: Compose, logPath: string) => {
writeStream.end(); writeStream.end();
} }
}; };
export const createComposeFileRaw = async (compose: Compose) => {
const { appName, composeFile } = compose;
const outputPath = join(COMPOSE_PATH, appName, "code");
const filePath = join(outputPath, "docker-compose.yml");
try {
await recreateDirectory(outputPath);
await writeFile(filePath, composeFile);
} catch (error) {
throw error;
}
};

View File

@@ -2,17 +2,5 @@ version: "3.8"
services: services:
appsmith: appsmith:
image: index.docker.io/appsmith/appsmith-ee:v1.29 image: index.docker.io/appsmith/appsmith-ee:v1.29
networks:
- dokploy-network
ports:
- ${APP_SMITH_PORT}
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${APP_SMITH_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${APP_SMITH_PORT}"
volumes: volumes:
- ../files/stacks:/appsmith-stacks - ../files/stacks:/appsmith-stacks
networks:
dokploy-network:
external: true

View File

@@ -1,4 +1,5 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash, generateHash,
@@ -7,14 +8,16 @@ import {
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema);
const envs = [ const domains: DomainSchema[] = [
`APP_SMITH_HOST=${randomDomain}`, {
"APP_SMITH_PORT=80", host: generateRandomDomain(schema),
`HASH=${mainServiceHash}`, port: 80,
serviceName: "appsmith",
},
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -2,21 +2,9 @@ version: "3.8"
services: services:
baserow: baserow:
image: baserow/baserow:1.25.2 image: baserow/baserow:1.25.2
networks:
- dokploy-network
environment: environment:
BASEROW_PUBLIC_URL: "http://${BASEROW_HOST}" BASEROW_PUBLIC_URL: "http://${BASEROW_HOST}"
ports:
- ${BASEROW_PORT}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${BASEROW_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${BASEROW_PORT}
volumes: volumes:
- baserow_data:/baserow/data - baserow_data:/baserow/data
volumes: volumes:
baserow_data: baserow_data:
networks:
dokploy-network:
external: true

View File

@@ -1,20 +1,24 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainHost = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const envs = [ const domains: DomainSchema[] = [
`BASEROW_HOST=${randomDomain}`, {
"BASEROW_PORT=80", host: mainHost,
`HASH=${mainServiceHash}`, port: 80,
serviceName: "baserow",
},
]; ];
const envs = [`BASEROW_HOST=${mainHost}`];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -21,16 +21,6 @@ services:
- DATABASE_URL=postgres://postgres:password@postgres:5432/db - DATABASE_URL=postgres://postgres:password@postgres:5432/db
- NEXT_PUBLIC_WEBAPP_URL=http://${CALCOM_HOST} - NEXT_PUBLIC_WEBAPP_URL=http://${CALCOM_HOST}
- NEXTAUTH_URL=http://${CALCOM_HOST}/api/auth - NEXTAUTH_URL=http://${CALCOM_HOST}/api/auth
networks:
- dokploy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${CALCOM_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${CALCOM_PORT}"
networks:
dokploy-network:
external: true
volumes: volumes:
calcom-data: calcom-data:

View File

@@ -1,27 +1,32 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
// https://cal.com/
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const calcomEncryptionKey = generateBase64(32); const calcomEncryptionKey = generateBase64(32);
const nextAuthSecret = generateBase64(32); const nextAuthSecret = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 3000,
serviceName: "calcom",
},
];
const envs = [ const envs = [
`CALCOM_HOST=${randomDomain}`, `CALCOM_HOST=${mainDomain}`,
"CALCOM_PORT=3000",
`HASH=${mainServiceHash}`,
`NEXTAUTH_SECRET=${nextAuthSecret}`, `NEXTAUTH_SECRET=${nextAuthSecret}`,
`CALENDSO_ENCRYPTION_KEY=${calcomEncryptionKey}`, `CALENDSO_ENCRYPTION_KEY=${calcomEncryptionKey}`,
]; ];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -18,8 +18,6 @@ services:
directus: directus:
image: directus/directus:10.12.1 image: directus/directus:10.12.1
networks:
- dokploy-network
ports: ports:
- 8055 - 8055
volumes: volumes:
@@ -28,10 +26,6 @@ services:
depends_on: depends_on:
- cache - cache
- database - database
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${DIRECTUS_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${DIRECTUS_PORT}
environment: environment:
SECRET: "replace-with-secure-random-value" SECRET: "replace-with-secure-random-value"
@@ -49,8 +43,5 @@ services:
ADMIN_EMAIL: "admin@example.com" ADMIN_EMAIL: "admin@example.com"
ADMIN_PASSWORD: "d1r3ctu5" ADMIN_PASSWORD: "d1r3ctu5"
networks:
dokploy-network:
external: true
volumes: volumes:
directus: directus:

View File

@@ -1,20 +1,20 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const domains: DomainSchema[] = [
const randomDomain = generateRandomDomain(schema); {
const envs = [ host: generateRandomDomain(schema),
`DIRECTUS_HOST=${randomDomain}`, port: 8055,
"DIRECTUS_PORT=8055", serviceName: "directus",
`HASH=${mainServiceHash}`, },
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -19,8 +19,6 @@ services:
documenso: documenso:
image: documenso/documenso:1.5.6-rc.2 image: documenso/documenso:1.5.6-rc.2
networks:
- dokploy-network
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -38,16 +36,8 @@ services:
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
ports: ports:
- ${DOCUMENSO_PORT} - ${DOCUMENSO_PORT}
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${DOCUMENSO_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${DOCUMENSO_PORT}"
volumes: volumes:
- /opt/documenso/cert.p12:/opt/documenso/cert.p12 - /opt/documenso/cert.p12:/opt/documenso/cert.p12
networks:
dokploy-network:
external: true
volumes: volumes:
documenso-data: documenso-data:

View File

@@ -1,24 +1,29 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generatePassword, generatePassword,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const nextAuthSecret = generateBase64(32); const nextAuthSecret = generateBase64(32);
const documensoEncryptionKey = generatePassword(32); const documensoEncryptionKey = generatePassword(32);
const documensoSecondaryEncryptionKey = generatePassword(64); const documensoSecondaryEncryptionKey = generatePassword(64);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 3000,
serviceName: "documenso",
},
];
const envs = [ const envs = [
`DOCUMENSO_HOST=${randomDomain}`, `DOCUMENSO_HOST=${mainDomain}`,
"DOCUMENSO_PORT=3000", "DOCUMENSO_PORT=3000",
`HASH=${mainServiceHash}`,
`NEXTAUTH_SECRET=${nextAuthSecret}`, `NEXTAUTH_SECRET=${nextAuthSecret}`,
`NEXT_PRIVATE_ENCRYPTION_KEY=${documensoEncryptionKey}`, `NEXT_PRIVATE_ENCRYPTION_KEY=${documensoEncryptionKey}`,
`NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${documensoSecondaryEncryptionKey}`, `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${documensoSecondaryEncryptionKey}`,
@@ -26,5 +31,6 @@ export function generate(schema: Schema): Template {
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -2,10 +2,6 @@ services:
doublezero: doublezero:
restart: always restart: always
image: liltechnomancer/double-zero:0.2.1 image: liltechnomancer/double-zero:0.2.1
ports:
- ${DOUBLEZERO_PORT}
networks:
- dokploy-network
volumes: volumes:
- db-data:/var/lib/doublezero/data - db-data:/var/lib/doublezero/data
environment: environment:
@@ -17,15 +13,7 @@ services:
SECRET_KEY_BASE: ${SECRET_KEY_BASE} SECRET_KEY_BASE: ${SECRET_KEY_BASE}
PHX_HOST: ${DOUBLEZERO_HOST} PHX_HOST: ${DOUBLEZERO_HOST}
DATABASE_PATH: ./00.db DATABASE_PATH: ./00.db
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${DOUBLEZERO_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${DOUBLEZERO_PORT}"
volumes: volumes:
db-data: db-data:
driver: local driver: local
networks:
dokploy-network:
external: true

View File

@@ -1,20 +1,26 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const secretKeyBase = generateBase64(64); const secretKeyBase = generateBase64(64);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 4000,
serviceName: "doublezero",
},
];
const envs = [ const envs = [
`DOUBLEZERO_HOST=${randomDomain}`, `DOUBLEZERO_HOST=${mainDomain}`,
"DOUBLEZERO_PORT=4000", "DOUBLEZERO_PORT=4000",
`HASH=${mainServiceHash}`,
`SECRET_KEY_BASE=${secretKeyBase}`, `SECRET_KEY_BASE=${secretKeyBase}`,
"AWS_ACCESS_KEY_ID=your-aws-access-key", "AWS_ACCESS_KEY_ID=your-aws-access-key",
"AWS_SECRET_ACCESS_KEY=your-aws-secret-key", "AWS_SECRET_ACCESS_KEY=your-aws-secret-key",
@@ -25,5 +31,6 @@ export function generate(schema: Schema): Template {
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -1,17 +1,7 @@
version: '3.8' version: "3.8"
services: services:
excalidraw: excalidraw:
networks: networks:
- dokploy-network - dokploy-network
image: excalidraw/excalidraw:latest image: excalidraw/excalidraw:latest
ports:
- ${EXCALIDRAW_PORT}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${EXCALIDRAW_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${EXCALIDRAW_PORT}
networks:
dokploy-network:
external: true

View File

@@ -1,4 +1,5 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash, generateHash,
@@ -6,15 +7,17 @@ import {
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const envs = [ const domains: DomainSchema[] = [
`EXCALIDRAW_HOST=${randomDomain}`, {
"EXCALIDRAW_PORT=80", host: mainDomain,
`HASH=${mainServiceHash}`, port: 80,
serviceName: "excalidraw",
},
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -1,13 +1,8 @@
version: "3.8" version: "3.8"
services: services:
ghost: ghost:
image: ghost:5-alpine image: ghost:5-alpine
restart: always restart: always
networks:
- dokploy-network
ports:
- ${GHOST_PORT}
environment: environment:
database__client: mysql database__client: mysql
database__connection__host: db database__connection__host: db
@@ -15,10 +10,7 @@ services:
database__connection__password: example database__connection__password: example
database__connection__database: ghost database__connection__database: ghost
url: http://${GHOST_HOST} url: http://${GHOST_HOST}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${GHOST_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${GHOST_PORT}
volumes: volumes:
- ghost:/var/lib/ghost/content - ghost:/var/lib/ghost/content
@@ -35,7 +27,3 @@ services:
volumes: volumes:
ghost: ghost:
db: db:
networks:
dokploy-network:
external: true

View File

@@ -1,4 +1,5 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash, generateHash,
@@ -6,15 +7,19 @@ import {
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const envs = [ const domains: DomainSchema[] = [
`GHOST_HOST=${randomDomain}`, {
"GHOST_PORT=2368", host: mainDomain,
`HASH=${mainServiceHash}`, port: 2368,
serviceName: "ghost",
},
]; ];
const envs = [`GHOST_HOST=${mainDomain}`];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -1,5 +1,4 @@
x-environment: x-environment: &default-environment
&default-environment
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
SECRET_KEY: ${SECRET_KEY} SECRET_KEY: ${SECRET_KEY}
PORT: ${GLITCHTIP_PORT} PORT: ${GLITCHTIP_PORT}
@@ -9,8 +8,7 @@ x-environment:
CELERY_WORKER_AUTOSCALE: "1,3" CELERY_WORKER_AUTOSCALE: "1,3"
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
x-depends_on: x-depends_on: &default-depends_on
&default-depends_on
- postgres - postgres
- redis - redis
@@ -38,12 +36,6 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- uploads:/code/uploads - uploads:/code/uploads
networks:
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${GLITCHTIP_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${GLITCHTIP_PORT}
worker: worker:
image: glitchtip/glitchtip:v4.0 image: glitchtip/glitchtip:v4.0
command: ./bin/run-celery-with-beat.sh command: ./bin/run-celery-with-beat.sh
@@ -65,7 +57,3 @@ services:
volumes: volumes:
pg-data: pg-data:
uploads: uploads:
networks:
dokploy-network:
external: true

View File

@@ -1,23 +1,30 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const secretKey = generateBase64(32); const secretKey = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 8000,
serviceName: "web",
},
];
const envs = [ const envs = [
`GLITCHTIP_HOST=${randomDomain}`, `GLITCHTIP_HOST=${mainDomain}`,
"GLITCHTIP_PORT=8000", "GLITCHTIP_PORT=8000",
`SECRET_KEY=${secretKey}`, `SECRET_KEY=${secretKey}`,
`HASH=${mainServiceHash}`,
]; ];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -1,20 +1,9 @@
version: "3.8" version: "3.8"
services: services:
grafana: grafana:
networks:
- dokploy-network
image: grafana/grafana-enterprise:9.5.20 image: grafana/grafana-enterprise:9.5.20
restart: unless-stopped restart: unless-stopped
ports:
- ${GRAFANA_PORT}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${GRAFANA_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${GRAFANA_PORT}
volumes: volumes:
- grafana-storage:/var/lib/grafana - grafana-storage:/var/lib/grafana
networks:
dokploy-network:
external: true
volumes: volumes:
grafana-storage: {} grafana-storage: {}

View File

@@ -1,20 +1,19 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const domains: DomainSchema[] = [
const randomDomain = generateRandomDomain(schema); {
const envs = [ host: generateRandomDomain(schema),
`GRAFANA_HOST=${randomDomain}`, port: 3000,
"GRAFANA_PORT=3000", serviceName: "grafana",
`HASH=${mainServiceHash}`, },
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -1,30 +1,19 @@
version: '3.8' version: "3.8"
services: services:
jellyfin: jellyfin:
image: jellyfin/jellyfin:10 image: jellyfin/jellyfin:10
networks:
- dokploy-network
ports:
- ${JELLYFIN_PORT}
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${JELLYFIN_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${JELLYFIN_PORT}"
volumes: volumes:
- config:/config - config:/config
- cache:/cache - cache:/cache
- media:/media - media:/media
restart: 'unless-stopped' restart: "unless-stopped"
# Optional - alternative address used for autodiscovery # Optional - alternative address used for autodiscovery
environment: environment:
- JELLYFIN_PublishedServerUrl=http://${JELLYFIN_HOST} - JELLYFIN_PublishedServerUrl=http://${JELLYFIN_HOST}
# Optional - may be necessary for docker healthcheck to pass if running in host network mode # Optional - may be necessary for docker healthcheck to pass if running in host network mode
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - "host.docker.internal:host-gateway"
volumes: volumes:
config: config:
cache: cache:
media: media:
networks:
dokploy-network:
external: true

View File

@@ -1,22 +1,25 @@
// EXAMPLE // EXAMPLE
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const domain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema); const domains: DomainSchema[] = [
const port = 8096; {
const envs = [ host: domain,
`JELLYFIN_HOST=${randomDomain}`, port: 8096,
`HASH=${mainServiceHash}`, serviceName: "jellyfin",
`JELLYFIN_PORT=${port}`, },
]; ];
const envs = [`JELLYFIN_HOST=${domain}`];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -36,10 +36,6 @@ services:
app: app:
restart: unless-stopped restart: unless-stopped
image: listmonk/listmonk:v3.0.0 image: listmonk/listmonk:v3.0.0
ports:
- "${LISTMONK_PORT}"
networks:
- dokploy-network
environment: environment:
- TZ=Etc/UTC - TZ=Etc/UTC
depends_on: depends_on:
@@ -47,15 +43,7 @@ services:
- setup - setup
volumes: volumes:
- ../files/config.toml:/listmonk/config.toml - ../files/config.toml:/listmonk/config.toml
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${LISTMONK_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${LISTMONK_PORT}"
volumes: volumes:
listmonk-data: listmonk-data:
driver: local driver: local
networks:
dokploy-network:
external: true

View File

@@ -1,20 +1,24 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generatePassword, generatePassword,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema); const randomDomain = generateRandomDomain(schema);
const adminPassword = generatePassword(32); const adminPassword = generatePassword(32);
const domains: DomainSchema[] = [
{
host: randomDomain,
port: 9000,
serviceName: "app",
},
];
const envs = [ const envs = [
`LISTMONK_HOST=${randomDomain}`,
"LISTMONK_PORT=9000",
`HASH=${mainServiceHash}`,
`# login with admin:${adminPassword}`, `# login with admin:${adminPassword}`,
"# check config.toml in Advanced / Volumes for more options", "# check config.toml in Advanced / Volumes for more options",
]; ];
@@ -48,5 +52,6 @@ params = ""
return { return {
envs, envs,
mounts, mounts,
domains,
}; };
} }

View File

@@ -1,25 +1,14 @@
version: '3.8' version: "3.8"
services: services:
meilisearch: meilisearch:
networks:
- dokploy-network
image: getmeili/meilisearch:v1.8.3 image: getmeili/meilisearch:v1.8.3
ports:
- ${MEILISEARCH_PORT}
volumes: volumes:
- meili_data:/meili_data - meili_data:/meili_data
environment: environment:
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY} MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
MEILI_ENV: ${MEILI_ENV} MEILI_ENV: ${MEILI_ENV}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${MEILISEARCH_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${MEILISEARCH_PORT}
volumes: volumes:
meili_data: meili_data:
driver: local driver: local
networks:
dokploy-network:
external: true

View File

@@ -1,24 +1,26 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const masterKey = generateBase64(32); const masterKey = generateBase64(32);
const envs = [
`MEILISEARCH_HOST=${randomDomain}`, const domains: DomainSchema[] = [
"MEILISEARCH_PORT=7700", {
"MEILI_ENV=development", host: mainDomain,
`MEILI_MASTER_KEY=${masterKey}`, port: 7700,
`HASH=${mainServiceHash}`, serviceName: "meilisearch",
},
]; ];
const envs = ["MEILI_ENV=development", `MEILI_MASTER_KEY=${masterKey}`];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -4,8 +4,6 @@ services:
image: metabase/metabase:v0.50.8 image: metabase/metabase:v0.50.8
volumes: volumes:
- /dev/urandom:/dev/random:ro - /dev/urandom:/dev/random:ro
ports:
- ${METABASE_PORT}
environment: environment:
MB_DB_TYPE: postgres MB_DB_TYPE: postgres
MB_DB_DBNAME: metabaseappdb MB_DB_DBNAME: metabaseappdb
@@ -13,17 +11,11 @@ services:
MB_DB_USER: metabase MB_DB_USER: metabase
MB_DB_PASS: mysecretpassword MB_DB_PASS: mysecretpassword
MB_DB_HOST: postgres MB_DB_HOST: postgres
networks:
- dokploy-network
healthcheck: healthcheck:
test: curl --fail -I http://localhost:3000/api/health || exit 1 test: curl --fail -I http://localhost:3000/api/health || exit 1
interval: 15s interval: 15s
timeout: 5s timeout: 5s
retries: 5 retries: 5
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${METABASE_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${METABASE_PORT}
postgres: postgres:
image: postgres:14 image: postgres:14
environment: environment:
@@ -32,7 +24,3 @@ services:
POSTGRES_PASSWORD: mysecretpassword POSTGRES_PASSWORD: mysecretpassword
networks: networks:
- dokploy-network - dokploy-network
networks:
dokploy-network:
external: true

View File

@@ -1,20 +1,22 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema); const randomDomain = generateRandomDomain(schema);
const envs = [
`METABASE_HOST=${randomDomain}`, const domains: DomainSchema[] = [
"METABASE_PORT=3000", {
`HASH=${mainServiceHash}`, host: randomDomain,
port: 3000,
serviceName: "metabase",
},
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -1,31 +1,13 @@
version: '3.8' version: "3.8"
services: services:
minio: minio:
image: minio/minio image: minio/minio
ports:
- ${MINIO_API_PORT}
- ${MINIO_DASHBOARD_PORT}
volumes: volumes:
- minio-data:/data - minio-data:/data
environment: environment:
- MINIO_ROOT_USER=minioadmin - MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin123 - MINIO_ROOT_PASSWORD=minioadmin123
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
networks:
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.service=${HASH}
- traefik.http.routers.${HASH}.rule=Host(`${MINIO_DASHBOARD_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${MINIO_DASHBOARD_PORT}
# API router and service
- traefik.http.routers.${HASH}-api.service=${HASH}-api
- traefik.http.routers.${HASH}-api.rule=Host(`${MINIO_API_HOST}`)
- traefik.http.services.${HASH}-api.loadbalancer.server.port=${MINIO_API_PORT}
volumes: volumes:
minio-data: minio-data:
networks:
dokploy-network:
external: true

View File

@@ -1,23 +1,28 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const apiDomain = generateRandomDomain(schema); const apiDomain = generateRandomDomain(schema);
const envs = [
`MINIO_DASHBOARD_HOST=${randomDomain}`, const domains: DomainSchema[] = [
"MINIO_DASHBOARD_PORT=9001", {
`MINIO_API_HOST=${apiDomain}`, host: mainDomain,
"MINIO_API_PORT=9000", port: 9001,
`HASH=${mainServiceHash}`, serviceName: "minio",
},
{
host: apiDomain,
port: 9000,
serviceName: "minio",
},
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -3,17 +3,9 @@ services:
n8n: n8n:
image: docker.n8n.io/n8nio/n8n:1.48.1 image: docker.n8n.io/n8nio/n8n:1.48.1
restart: always restart: always
networks:
- dokploy-network
ports:
- ${N8N_PORT}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${N8N_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${N8N_PORT}
environment: environment:
- N8N_HOST=${N8N_HOST} - N8N_HOST=${N8N_HOST}
- N8N_PORT=5678 - N8N_PORT=${N8N_PORT}
- N8N_PROTOCOL=http - N8N_PROTOCOL=http
- NODE_ENV=production - NODE_ENV=production
- WEBHOOK_URL=https://${N8N_HOST}/ - WEBHOOK_URL=https://${N8N_HOST}/
@@ -24,6 +16,3 @@ services:
volumes: volumes:
n8n_data: n8n_data:
networks:
dokploy-network:
external: true

View File

@@ -1,21 +1,28 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 5678,
serviceName: "n8n",
},
];
const envs = [ const envs = [
`N8N_HOST=${randomDomain}`, `N8N_HOST=${mainDomain}`,
"N8N_PORT=5678", "N8N_PORT=5678",
`HASH=${mainServiceHash}`,
"GENERIC_TIMEZONE=Europe/Berlin", "GENERIC_TIMEZONE=Europe/Berlin",
]; ];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -3,18 +3,10 @@ services:
nocodb: nocodb:
image: nocodb/nocodb:0.251.1 image: nocodb/nocodb:0.251.1
restart: always restart: always
networks:
- dokploy-network
ports:
- ${NOCODB_PORT}
environment: environment:
NC_DB : "pg://root_db?u=postgres&p=password&d=root_db" NC_DB: "pg://root_db?u=postgres&p=password&d=root_db"
PORT : ${NOCODB_PORT} PORT: ${NOCODB_PORT}
NC_REDIS_URL: ${NC_REDIS_URL} NC_REDIS_URL: ${NC_REDIS_URL}
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${NOCODB_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${NOCODB_PORT}
volumes: volumes:
- nc_data:/usr/app/data - nc_data:/usr/app/data
@@ -30,15 +22,11 @@ services:
healthcheck: healthcheck:
interval: 10s interval: 10s
retries: 10 retries: 10
test: "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\"" test: 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'
timeout: 2s timeout: 2s
volumes: volumes:
- "db_data:/var/lib/postgresql/data" - "db_data:/var/lib/postgresql/data"
networks:
dokploy-network:
external: true
volumes: volumes:
db_data: {} db_data: {}
nc_data: {} nc_data: {}

View File

@@ -1,26 +1,28 @@
// EXAMPLE // EXAMPLE
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema); const randomDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64); const secretBase = generateBase64(64);
const toptKeyBase = generateBase64(32);
const envs = [ const domains: DomainSchema[] = [
`NOCODB_HOST=${randomDomain}`, {
"NOCODB_PORT=8000", host: randomDomain,
`NC_AUTH_JWT_SECRET=${secretBase}`, port: 8000,
`HASH=${mainServiceHash}`, serviceName: "nocodb",
},
]; ];
const envs = ["NOCODB_PORT=8000", `NC_AUTH_JWT_SECRET=${secretBase}`];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -2,20 +2,12 @@ version: "3.8"
services: services:
web: web:
image: odoo:16.0 image: odoo:16.0
networks:
- dokploy-network
depends_on: depends_on:
- db - db
ports:
- ${ODOO_PORT}
environment: environment:
- HOST=db - HOST=db
- USER=odoo - USER=odoo
- PASSWORD=odoo - PASSWORD=odoo
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${ODOO_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${ODOO_PORT}"
volumes: volumes:
- odoo-web-data:/var/lib/odoo - odoo-web-data:/var/lib/odoo
- ../files/config:/etc/odoo - ../files/config:/etc/odoo
@@ -35,7 +27,3 @@ services:
volumes: volumes:
odoo-web-data: odoo-web-data:
odoo-db-data: odoo-db-data:
networks:
dokploy-network:
external: true

View File

@@ -1,20 +1,22 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema); const randomDomain = generateRandomDomain(schema);
const envs = [
`ODOO_HOST=${randomDomain}`, const domains: DomainSchema[] = [
"ODOO_PORT=8069", {
`HASH=${mainServiceHash}`, host: randomDomain,
port: 8069,
serviceName: "web",
},
]; ];
return { return {
envs, domains,
}; };
} }

View File

@@ -1,6 +1,5 @@
version: '3.8' version: "3.8"
services: services:
ollama: ollama:
volumes: volumes:
- ollama:/root/.ollama - ollama:/root/.ollama
@@ -13,24 +12,14 @@ services:
open-webui: open-webui:
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main} image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
networks:
- dokploy-network
volumes: volumes:
- open-webui:/app/backend/data - open-webui:/app/backend/data
depends_on: depends_on:
- ollama - ollama
environment: environment:
- 'OLLAMA_BASE_URL=http://ollama:11434' - "OLLAMA_BASE_URL=http://ollama:11434"
- 'WEBUI_SECRET_KEY=' - "WEBUI_SECRET_KEY="
restart: unless-stopped restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${OPEN_WEBUI_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${OPEN_WEBUI_PORT}"
networks:
dokploy-network:
external: true
volumes: volumes:
ollama: {} ollama: {}

View File

@@ -1,22 +1,24 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema); const randomDomain = generateRandomDomain(schema);
const envs = [
`OPEN_WEBUI_HOST=${randomDomain}`, const domains: DomainSchema[] = [
"OPEN_WEBUI_PORT=8080", {
`HASH=${mainServiceHash}`, host: randomDomain,
"OLLAMA_DOCKER_TAG=0.1.47", port: 8080,
"WEBUI_DOCKER_TAG=0.3.7", serviceName: "open-webui",
},
]; ];
const envs = ["OLLAMA_DOCKER_TAG=0.1.47", "WEBUI_DOCKER_TAG=0.3.7"];
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -1,4 +1,4 @@
version: '3.8' version: "3.8"
services: services:
db: db:
@@ -20,21 +20,9 @@ services:
PMA_USER: ${MYSQL_USER} PMA_USER: ${MYSQL_USER}
PMA_PASSWORD: ${MYSQL_PASSWORD} PMA_PASSWORD: ${MYSQL_PASSWORD}
PMA_ARBITRARY: 1 PMA_ARBITRARY: 1
ports:
- ${PHPMYADMIN_PORT}
depends_on: depends_on:
- db - db
labels:
- traefik.enable=true
- traefik.http.routers.${HASH}.rule=Host(`${PHPMYADMIN_HOST}`)
- traefik.http.services.${HASH}.loadbalancer.server.port=${PHPMYADMIN_PORT}
networks:
- dokploy-network
volumes: volumes:
db_data: db_data:
driver: local driver: local
networks:
dokploy-network:
external: true

View File

@@ -1,20 +1,24 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateHash,
generatePassword, generatePassword,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const rootPassword = generatePassword(32); const rootPassword = generatePassword(32);
const password = generatePassword(32); const password = generatePassword(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 80,
serviceName: "phpmyadmin",
},
];
const envs = [ const envs = [
`PHPMYADMIN_HOST=${randomDomain}`,
"PHPMYADMIN_PORT=80",
`HASH=${mainServiceHash}`,
`MYSQL_ROOT_PASSWORD=${rootPassword}`, `MYSQL_ROOT_PASSWORD=${rootPassword}`,
"MYSQL_DATABASE=mysql", "MYSQL_DATABASE=mysql",
"MYSQL_USER=phpmyadmin", "MYSQL_USER=phpmyadmin",
@@ -23,5 +27,6 @@ export function generate(schema: Schema): Template {
return { return {
envs, envs,
domains,
}; };
} }

View File

@@ -32,17 +32,9 @@ services:
depends_on: depends_on:
- plausible_db - plausible_db
- plausible_events_db - plausible_events_db
ports:
- ${PLAUSIBLE_PORT}
networks:
- dokploy-network
env_file: env_file:
- .env - .env
labels:
- "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${PLAUSIBLE_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${PLAUSIBLE_PORT}"
volumes: volumes:
db-data: db-data:
driver: local driver: local
@@ -50,7 +42,3 @@ volumes:
driver: local driver: local
event-logs: event-logs:
driver: local driver: local
networks:
dokploy-network:
external: true

View File

@@ -1,24 +1,28 @@
import { import {
type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateBase64, generateBase64,
generateHash,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName); const mainDomain = generateRandomDomain(schema);
const randomDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64); const secretBase = generateBase64(64);
const toptKeyBase = generateBase64(32); const toptKeyBase = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 8000,
serviceName: "plausible",
},
];
const envs = [ const envs = [
`PLAUSIBLE_HOST=${randomDomain}`, `BASE_URL=http://${mainDomain}`,
"PLAUSIBLE_PORT=8000",
`BASE_URL=http://${randomDomain}`,
`SECRET_KEY_BASE=${secretBase}`, `SECRET_KEY_BASE=${secretBase}`,
`TOTP_VAULT_KEY=${toptKeyBase}`, `TOTP_VAULT_KEY=${toptKeyBase}`,
`HASH=${mainServiceHash}`,
]; ];
const mounts: Template["mounts"] = [ const mounts: Template["mounts"] = [
@@ -62,5 +66,6 @@ export function generate(schema: Schema): Template {
return { return {
envs, envs,
mounts, mounts,
domains,
}; };
} }

Some files were not shown because too many files have changed in this diff Show More