diff --git a/README.md b/README.md index cfec6692..668f40bb 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
Lightspeed.run +Lightspeed.run
### Community Backers 🤝 diff --git a/apps/dokploy/components/dashboard/settings/web-server.tsx b/apps/dokploy/components/dashboard/settings/web-server.tsx index 732570cc..b1d99388 100644 --- a/apps/dokploy/components/dashboard/settings/web-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server.tsx @@ -22,6 +22,7 @@ import { import { api } from "@/utils/api"; import { toast } from "sonner"; import { DockerTerminalModal } from "./web-server/docker-terminal-modal"; +import { EditTraefikEnv } from "./web-server/edit-traefik-env"; import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config"; import { ShowModalLogs } from "./web-server/show-modal-logs"; import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config"; @@ -67,6 +68,9 @@ export const WebServer = () => { const { mutateAsync: updateDockerCleanup } = api.settings.updateDockerCleanup.useMutation(); + const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } = + api.settings.haveTraefikDashboardPortEnabled.useQuery(); + return ( @@ -167,37 +171,38 @@ export const WebServer = () => { View Traefik config + + e.preventDefault()} + className="w-full cursor-pointer space-x-3" + > + Modify Env + + + { await toggleDashboard({ - enableDashboard: true, + enableDashboard: !haveTraefikDashboardPortEnabled, }) .then(async () => { - toast.success("Dashboard Enabled"); + toast.success( + `${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`, + ); + refetchDashboard(); }) .catch(() => { - toast.error("Error to enable Dashboard"); + toast.error( + `${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`, + ); }); }} className="w-full cursor-pointer space-x-3" > - Enable Dashboard - - { - await toggleDashboard({ - enableDashboard: false, - }) - .then(async () => { - toast.success("Dashboard Disabled"); - }) - .catch(() => { - toast.error("Error to disable Dashboard"); - }); - }} - className="w-full cursor-pointer space-x-3" - > - Disable Dashboard + + {haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "} + Dashboard + diff --git a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx new file mode 100644 index 00000000..c0504ab4 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx @@ -0,0 +1,146 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const schema = z.object({ + env: z.string(), +}); + +type Schema = z.infer; + +interface Props { + children?: React.ReactNode; +} + +export const EditTraefikEnv = ({ children }: Props) => { + const [canEdit, setCanEdit] = useState(true); + + const { data } = api.settings.readTraefikEnv.useQuery(); + + const { mutateAsync, isLoading, error, isError } = + api.settings.writeTraefikEnv.useMutation(); + + const form = useForm({ + defaultValues: { + env: data || "", + }, + disabled: canEdit, + resolver: zodResolver(schema), + }); + + useEffect(() => { + if (data) { + form.reset({ + env: data || "", + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: Schema) => { + await mutateAsync(data.env) + .then(async () => { + toast.success("Traefik Env Updated"); + }) + .catch(() => { + toast.error("Error to update the traefik env"); + }); + }; + + return ( + + {children} + + + Update Traefik Env + Update the traefik env + + {isError && {error?.message}} + +
+ +
+ ( + + Env + + + + +
+											
+										
+
+ +
+
+ )} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index ecc0ff81..1b88677a 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.7.0", + "version": "v0.7.1", "private": true, "license": "Apache-2.0", "type": "module", diff --git a/apps/dokploy/public/templates/aptabase.svg b/apps/dokploy/public/templates/aptabase.svg new file mode 100644 index 00000000..3cb71ecf --- /dev/null +++ b/apps/dokploy/public/templates/aptabase.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index a26de6e3..5c3a4f01 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -15,6 +15,7 @@ import { cleanUpSystemPrune, cleanUpUnusedImages, cleanUpUnusedVolumes, + prepareEnvironmentVariables, startService, stopService, } from "@/server/utils/docker/utils"; @@ -37,6 +38,7 @@ import { import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { TRPCError } from "@trpc/server"; import { scheduleJob, scheduledJobs } from "node-schedule"; +import { z } from "zod"; import { appRouter } from "../root"; import { findAdmin, updateAdmin } from "../services/admin"; import { @@ -69,7 +71,9 @@ export const settingsRouter = createTRPCRouter({ toggleDashboard: adminProcedure .input(apiEnableDashboard) .mutation(async ({ input }) => { - await initializeTraefik(input.enableDashboard); + await initializeTraefik({ + enableDashboard: input.enableDashboard, + }); return true; }), @@ -309,4 +313,37 @@ export const settingsRouter = createTRPCRouter({ return openApiDocument; }, ), + readTraefikEnv: adminProcedure.query(async () => { + const { stdout } = await execAsync( + "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik", + ); + + return stdout.trim(); + }), + + writeTraefikEnv: adminProcedure + .input(z.string()) + .mutation(async ({ input }) => { + const envs = prepareEnvironmentVariables(input); + await initializeTraefik({ + env: envs, + }); + + return true; + }), + haveTraefikDashboardPortEnabled: adminProcedure.query(async () => { + const { stdout } = await execAsync( + "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik", + ); + + const parsed: any[] = JSON.parse(stdout.trim()); + + for (const port of parsed) { + if (port.PublishedPort === 8080) { + return true; + } + } + + return false; + }), }); diff --git a/apps/dokploy/server/setup/traefik-setup.ts b/apps/dokploy/server/setup/traefik-setup.ts index 2b44cf40..afebf07e 100644 --- a/apps/dokploy/server/setup/traefik-setup.ts +++ b/apps/dokploy/server/setup/traefik-setup.ts @@ -11,7 +11,15 @@ const TRAEFIK_SSL_PORT = Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443; const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80; -export const initializeTraefik = async (enableDashboard = false) => { +interface TraefikOptions { + enableDashboard?: boolean; + env?: string[]; +} + +export const initializeTraefik = async ({ + enableDashboard = false, + env = [], +}: TraefikOptions = {}) => { const imageName = "traefik:v2.5"; const containerName = "dokploy-traefik"; const settings: CreateServiceOptions = { @@ -19,6 +27,7 @@ export const initializeTraefik = async (enableDashboard = false) => { TaskTemplate: { ContainerSpec: { Image: imageName, + Env: env, Mounts: [ { Type: "bind", diff --git a/apps/dokploy/templates/aptabase/docker-compose.yml b/apps/dokploy/templates/aptabase/docker-compose.yml new file mode 100644 index 00000000..934fd1ee --- /dev/null +++ b/apps/dokploy/templates/aptabase/docker-compose.yml @@ -0,0 +1,51 @@ +services: + aptabase_db: + image: postgres:15-alpine + restart: always + volumes: + - db-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: aptabase + POSTGRES_PASSWORD: sTr0NGp4ssw0rd + networks: + - dokploy-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U aptabase"] + interval: 10s + timeout: 5s + retries: 5 + + aptabase_events_db: + image: clickhouse/clickhouse-server:23.8.16.16-alpine + restart: always + volumes: + - events-db-data:/var/lib/clickhouse + environment: + CLICKHOUSE_USER: aptabase + CLICKHOUSE_PASSWORD: sTr0NGp4ssw0rd + ulimits: + nofile: + soft: 262144 + hard: 262144 + networks: + - dokploy-network + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8123 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + + aptabase: + image: ghcr.io/aptabase/aptabase:main + restart: always + environment: + BASE_URL: http://${APTABASE_HOST} + AUTH_SECRET: ${AUTH_SECRET} + DATABASE_URL: Server=aptabase_db;Port=5432;User Id=aptabase;Password=sTr0NGp4ssw0rd;Database=aptabase + CLICKHOUSE_URL: Host=aptabase_events_db;Port=8123;Username=aptabase;Password=sTr0NGp4ssw0rd + +volumes: + db-data: + driver: local + events-db-data: + driver: local diff --git a/apps/dokploy/templates/aptabase/index.ts b/apps/dokploy/templates/aptabase/index.ts new file mode 100644 index 00000000..38b077ae --- /dev/null +++ b/apps/dokploy/templates/aptabase/index.ts @@ -0,0 +1,27 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const authSecret = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "aptabase", + }, + ]; + + const envs = [`APTABASE_HOST=${mainDomain}`, `AUTH_SECRET=${authSecret}`]; + + return { + envs, + domains, + }; +} diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 04234158..100802e5 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -438,4 +438,19 @@ export const templates: TemplateData[] = [ tags: ["chat"], load: () => import("./soketi/index").then((m) => m.generate), }, + { + id: "aptabase", + name: "Aptabase", + version: "v1.0.0", + description: + "Aptabase is a self-hosted web analytics platform that lets you track website traffic and user behavior.", + logo: "aptabase.svg", + links: { + github: "https://github.com/aptabase/aptabase", + website: "https://aptabase.com/", + docs: "https://github.com/aptabase/aptabase/blob/main/README.md", + }, + tags: ["analytics", "self-hosted"], + load: () => import("./aptabase/index").then((m) => m.generate), + }, ];