From 69598821ed33da85e0fab18c930dc6c638fa88cc Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 21 Feb 2026 14:53:27 -0600 Subject: [PATCH 01/22] feat: implement Docker network management functionality - Added components for handling and displaying Docker networks, including creation, editing, and listing of networks. - Introduced a new API router for network operations, integrating with the database schema for network management. - Updated the sidebar layout to include a link to the networks dashboard, ensuring user access to network features. - Created necessary database migrations for the network table and its associated types. - Enhanced the dashboard layout to support the new network management interface. --- .../dashboard/networks/handle-network.tsx | 566 ++ .../dashboard/networks/show-networks.tsx | 116 + apps/dokploy/components/layouts/side.tsx | 15 + .../drizzle/0146_stormy_ender_wiggin.sql | 20 + apps/dokploy/drizzle/meta/0146_snapshot.json | 7604 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/pages/dashboard/networks.tsx | 84 + apps/dokploy/server/api/root.ts | 2 + apps/dokploy/server/api/routers/network.ts | 71 + packages/server/src/db/schema/account.ts | 2 + packages/server/src/db/schema/index.ts | 1 + packages/server/src/db/schema/network.ts | 137 + packages/server/src/db/schema/server.ts | 2 + packages/server/src/index.ts | 1 + packages/server/src/services/network.ts | 114 + 15 files changed, 8742 insertions(+) create mode 100644 apps/dokploy/components/dashboard/networks/handle-network.tsx create mode 100644 apps/dokploy/components/dashboard/networks/show-networks.tsx create mode 100644 apps/dokploy/drizzle/0146_stormy_ender_wiggin.sql create mode 100644 apps/dokploy/drizzle/meta/0146_snapshot.json create mode 100644 apps/dokploy/pages/dashboard/networks.tsx create mode 100644 apps/dokploy/server/api/routers/network.ts create mode 100644 packages/server/src/db/schema/network.ts create mode 100644 packages/server/src/services/network.ts diff --git a/apps/dokploy/components/dashboard/networks/handle-network.tsx b/apps/dokploy/components/dashboard/networks/handle-network.tsx new file mode 100644 index 000000000..8af7db795 --- /dev/null +++ b/apps/dokploy/components/dashboard/networks/handle-network.tsx @@ -0,0 +1,566 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Network, Pencil, Plus } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +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"; + +const networkDriverEnum = [ + "bridge", + "host", + "overlay", + "macvlan", + "none", + "ipvlan", +] as const; + +/** Sentinel for "no scope" */ +const SCOPE_EMPTY = "__scope_none__"; + +/** Value for "Dokploy server" (local / no specific server). Not used in cloud. */ +const DOKPLOY_SERVER_VALUE = "__dokploy_server__"; + +const ipamConfigEntrySchema = z.object({ + subnet: z.string().optional(), + ipRange: z.string().optional(), + gateway: z.string().optional(), +}); + +const networkFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + driver: z.enum(networkDriverEnum).default("bridge"), + scope: z.string().optional(), + serverId: z.string().optional(), + internal: z.boolean().default(false), + attachable: z.boolean().default(false), + ingress: z.boolean().default(false), + configOnly: z.boolean().default(false), + enableIPv4: z.boolean().default(true), + enableIPv6: z.boolean().default(false), + ipamDriver: z.string().optional(), + ipamConfig: z.array(ipamConfigEntrySchema).default([]), +}); + +type NetworkFormValues = z.infer; + +const defaultValues: NetworkFormValues = { + name: "", + driver: "bridge", + scope: SCOPE_EMPTY, + serverId: DOKPLOY_SERVER_VALUE, + internal: false, + attachable: false, + ingress: false, + configOnly: false, + enableIPv4: true, + enableIPv6: false, + ipamDriver: "", + ipamConfig: [], +}; + +interface HandleNetworkProps { + networkId?: string; + children?: React.ReactNode; +} + +export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { + const [isOpen, setIsOpen] = useState(false); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const utils = api.useUtils(); + const isEdit = !!networkId; + + const { data: servers } = api.server.all.useQuery(); + const { data: network, isLoading: isLoadingNetwork } = + api.network.one.useQuery( + { networkId: networkId! }, + { enabled: isEdit && !!networkId }, + ); + + const { mutateAsync, isLoading: isPending } = networkId + ? api.network.update.useMutation() + : api.network.create.useMutation(); + + const form = useForm({ + resolver: zodResolver(networkFormSchema), + defaultValues, + }); + + const ipamConfigFieldArray = useFieldArray({ + control: form.control, + name: "ipamConfig", + }); + + useEffect(() => { + if (isEdit && network && isOpen) { + const ipam = network.ipam ?? {}; + const ipamConfigArr = (ipam.config ?? []).map((c) => ({ + subnet: c.subnet ?? "", + ipRange: c.ipRange ?? "", + gateway: c.gateway ?? "", + })); + form.reset({ + ...defaultValues, + name: network.name, + driver: network.driver, + scope: network.scope ?? SCOPE_EMPTY, + serverId: network.serverId ?? DOKPLOY_SERVER_VALUE, + internal: network.internal, + attachable: network.attachable, + enableIPv4: network.enableIPv4, + enableIPv6: network.enableIPv6, + ipamDriver: ipam.driver ?? "", + ipamConfig: ipamConfigArr, + ingress: network.ingress, + configOnly: network.configOnly, + }); + } + }, [isEdit, isOpen, network, form]); + + const onSubmit = async (data: NetworkFormValues) => { + const scope = + data.scope && data.scope !== SCOPE_EMPTY ? data.scope : undefined; + + try { + await mutateAsync({ + networkId: networkId ?? "", + name: data.name, + driver: data.driver, + scope, + serverId: data.serverId ?? undefined, + internal: data.internal, + attachable: data.attachable, + ingress: data.ingress, + configOnly: data.configOnly, + enableIPv4: data.enableIPv4, + enableIPv6: data.enableIPv6, + ipam: { + driver: data.ipamDriver, + config: data.ipamConfig, + }, + }); + + await utils.network.all.invalidate(); + if (networkId) await utils.network.one.invalidate({ networkId }); + setIsOpen(false); + form.reset(defaultValues); + } catch { + toast.error(isEdit ? "Error updating network" : "Error creating network"); + } + }; + + const trigger = + children ?? + (isEdit ? ( + + ) : ( + + )); + + return ( + + {trigger} + + + + + {isEdit ? "Edit network" : "Add network"} + + + {isEdit + ? "Update this Docker network. Changes apply to name, driver, and server assignment." + : "Create a new Docker network for your organization. You can optionally assign it to a server."} + + + {isEdit && isLoadingNetwork ? ( +
+ Loading network… +
+ ) : ( +
+ +
+ ( + + Name + + + + + + )} + /> + ( + + Driver + + + + )} + /> +
+ ( + + Server + + + {isCloud + ? "Server where this network will be created." + : "Dokploy server is the default local server; or choose a specific server."} + + + + )} + /> + ( + + Scope (optional) + + + + )} + /> +
+ ( + +
+ Internal + + Restrict external access; containers on this network + cannot reach external networks. + +
+ + + +
+ )} + /> + ( + +
+ Attachable + + Allow standalone containers to attach to this network + (e.g. in Swarm, not only services). + +
+ + + +
+ )} + /> + ( + +
+ Enable IPv4 + + Enable IPv4 addressing on the network. + +
+ + + +
+ )} + /> + ( + +
+ Enable IPv6 + + Enable IPv6 addressing on the network. + +
+ + + +
+ )} + /> + ( + +
+ Ingress + + Use as the routing-mesh network in Swarm mode (load + balancing between nodes). + +
+ + + +
+ )} + /> + ( + +
+ Config only + + Create a placeholder network whose config is reused by + other networks; cannot run containers on it. + +
+ + + +
+ )} + /> +
+
+ IPAM + ( + + + Driver (optional) + + + + + + + )} + /> +
+ + Config (subnet / gateway / IP range) + + {ipamConfigFieldArray.fields.map((field, index) => ( +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ ))} + +
+
+ + + + + + + )} +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/networks/show-networks.tsx b/apps/dokploy/components/dashboard/networks/show-networks.tsx new file mode 100644 index 000000000..6319ebe39 --- /dev/null +++ b/apps/dokploy/components/dashboard/networks/show-networks.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { Loader2, Network } from "lucide-react"; +import { HandleNetwork } from "@/components/dashboard/networks/handle-network"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { api } from "@/utils/api"; + +export const ShowNetworks = () => { + const { data: networks, isLoading } = api.network.all.useQuery(); + + return ( +
+ +
+
+ + + + Networks + + + Manage Docker networks for your organization. Networks can be + scoped to a server (optional). + + + {networks && networks?.length > 0 && } +
+ + +
+
+
+ {isLoading ? ( +
+ Loading... + +
+ ) : !networks?.length ? ( +
+
+ +
+
+

No networks yet

+

+ Create Docker networks for your organization and + optionally attach them to a server. Add your first + network to get started. +

+
+ +
+ ) : ( + + + + Name + Driver + Scope + Internal + Attachable + Server + Created + Actions + + + + {networks.map((n) => ( + + + {n.name} + + {n.driver} + {n.scope ?? "—"} + {n.internal ? "Yes" : "No"} + {n.attachable ? "Yes" : "No"} + {n.serverId ?? "Dokploy server"} + + {new Date(n.createdAt).toLocaleDateString()} + + + + + + + + ))} + +
+ )} +
+
+
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 4b3354ed8..3fbf8b313 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -23,6 +23,7 @@ import { Loader2, LogIn, type LucideIcon, + Network, Package, PieChart, Server, @@ -204,6 +205,20 @@ const MENU: Menu = { !isCloud ), }, + { + isSingle: true, + title: "Networks", + url: "/dashboard/networks", + icon: Network, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!( + (auth?.role === "owner" || + auth?.role === "admin" || + auth?.canAccessToDocker) && + !isCloud + ), + }, { isSingle: true, title: "Requests", diff --git a/apps/dokploy/drizzle/0146_stormy_ender_wiggin.sql b/apps/dokploy/drizzle/0146_stormy_ender_wiggin.sql new file mode 100644 index 000000000..405e9323e --- /dev/null +++ b/apps/dokploy/drizzle/0146_stormy_ender_wiggin.sql @@ -0,0 +1,20 @@ +CREATE TYPE "public"."networkDriver" AS ENUM('bridge', 'host', 'overlay', 'macvlan', 'none', 'ipvlan');--> statement-breakpoint +CREATE TABLE "network" ( + "networkId" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "driver" "networkDriver" DEFAULT 'bridge' NOT NULL, + "scope" text, + "internal" boolean DEFAULT false NOT NULL, + "attachable" boolean DEFAULT false NOT NULL, + "ingress" boolean DEFAULT false NOT NULL, + "configOnly" boolean DEFAULT false NOT NULL, + "enableIPv4" boolean DEFAULT true NOT NULL, + "enableIPv6" boolean DEFAULT false NOT NULL, + "ipam" jsonb DEFAULT '{}'::jsonb, + "createdAt" text NOT NULL, + "organizationId" text NOT NULL, + "serverId" text +); +--> statement-breakpoint +ALTER TABLE "network" ADD CONSTRAINT "network_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "network" ADD CONSTRAINT "network_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0146_snapshot.json b/apps/dokploy/drizzle/meta/0146_snapshot.json new file mode 100644 index 000000000..4489bee53 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0146_snapshot.json @@ -0,0 +1,7604 @@ +{ + "id": "95c21059-b9f0-420d-a452-a2926bc3bd34", + "prevId": "ec86a5ad-8776-483e-a003-346d32f65089", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "giteaInternalUrl": { + "name": "giteaInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "gitlabInternalUrl": { + "name": "gitlabInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network": { + "name": "network", + "schema": "", + "columns": { + "networkId": { + "name": "networkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "driver": { + "name": "driver", + "type": "networkDriver", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bridge'" + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal": { + "name": "internal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attachable": { + "name": "attachable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ingress": { + "name": "ingress", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "configOnly": { + "name": "configOnly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableIPv4": { + "name": "enableIPv4", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableIPv6": { + "name": "enableIPv6", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipam": { + "name": "ipam", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "network_organizationId_organization_id_fk": { + "name": "network_organizationId_organization_id_fk", + "tableFrom": "network", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_serverId_server_serverId_fk": { + "name": "network_serverId_server_serverId_fk", + "tableFrom": "network", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_teamsId_teams_teamsId_fk": { + "name": "notification_teamsId_teams_teamsId_fk", + "tableFrom": "notification", + "tableTo": "teams", + "columnsFrom": [ + "teamsId" + ], + "columnsTo": [ + "teamsId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patch": { + "name": "patch", + "schema": "", + "columns": { + "patchId": { + "name": "patchId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "patchType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'update'" + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "patch_applicationId_application_applicationId_fk": { + "name": "patch_applicationId_application_applicationId_fk", + "tableFrom": "patch", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patch_composeId_compose_composeId_fk": { + "name": "patch_composeId_compose_composeId_fk", + "tableFrom": "patch", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "patch_filepath_application_unique": { + "name": "patch_filepath_application_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "applicationId" + ] + }, + "patch_filepath_compose_unique": { + "name": "patch_filepath_compose_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "composeId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_userId_user_id_fk": { + "name": "schedule_userId_user_id_fk", + "tableFrom": "schedule", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_temp": { + "name": "session_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_temp_user_id_user_id_fk": { + "name": "session_temp_user_id_user_id_fk", + "tableFrom": "session_temp", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableEnterpriseFeatures": { + "name": "enableEnterpriseFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "licenseKey": { + "name": "licenseKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isValidEnterpriseLicense": { + "name": "isValidEnterpriseLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "trustedOrigins": { + "name": "trustedOrigins", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.networkDriver": { + "name": "networkDriver", + "schema": "public", + "values": [ + "bridge", + "host", + "overlay", + "macvlan", + "none", + "ipvlan" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "pushover", + "custom", + "lark", + "teams" + ] + }, + "public.patchType": { + "name": "patchType", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index b09199e7f..0c893edbb 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1023,6 +1023,13 @@ "when": 1771447229358, "tag": "0145_remarkable_titania", "breakpoints": true + }, + { + "idx": 146, + "version": "7", + "when": 1771621786767, + "tag": "0146_stormy_ender_wiggin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/pages/dashboard/networks.tsx b/apps/dokploy/pages/dashboard/networks.tsx new file mode 100644 index 000000000..d9d65cd8e --- /dev/null +++ b/apps/dokploy/pages/dashboard/networks.tsx @@ -0,0 +1,84 @@ +import { IS_CLOUD } from "@dokploy/server/constants"; +import { validateRequest } from "@dokploy/server/lib/auth"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import type { GetServerSidePropsContext } from "next"; +import type { ReactElement } from "react"; +import superjson from "superjson"; +import { ShowNetworks } from "@/components/dashboard/networks/show-networks"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { appRouter } from "@/server/api/root"; + +const Dashboard = () => { + return ; +}; + +export default Dashboard; + +Dashboard.getLayout = (page: ReactElement) => { + return {page}; +}; + +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }>, +) { + if (IS_CLOUD) { + return { + redirect: { + permanent: true, + destination: "/dashboard/projects", + }, + }; + } + const { user, session } = await validateRequest(ctx.req); + if (!user) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + const { req } = ctx; + + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: ctx.res as any, + db: null as any, + session: session as any, + user: user as any, + }, + transformer: superjson, + }); + try { + await helpers.project.all.prefetch(); + + if (user.role === "member") { + const userR = await helpers.user.one.fetch({ + userId: user.id, + }); + + if (!userR?.canAccessToDocker) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + } + + await helpers.network.all.prefetch(); + + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch { + return { + props: {}, + }; + } +} diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index f123fde85..c4deb3593 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -20,6 +20,7 @@ import { mariadbRouter } from "./routers/mariadb"; import { mongoRouter } from "./routers/mongo"; import { mountRouter } from "./routers/mount"; import { mysqlRouter } from "./routers/mysql"; +import { networkRouter } from "./routers/network"; import { notificationRouter } from "./routers/notification"; import { organizationRouter } from "./routers/organization"; import { patchRouter } from "./routers/patch"; @@ -66,6 +67,7 @@ export const appRouter = createTRPCRouter({ deployment: deploymentRouter, previewDeployment: previewDeploymentRouter, mounts: mountRouter, + network: networkRouter, certificates: certificateRouter, settings: settingsRouter, security: securityRouter, diff --git a/apps/dokploy/server/api/routers/network.ts b/apps/dokploy/server/api/routers/network.ts new file mode 100644 index 000000000..46da4d79d --- /dev/null +++ b/apps/dokploy/server/api/routers/network.ts @@ -0,0 +1,71 @@ +import { + createNetwork, + findNetworkById, + removeNetwork, + updateNetwork, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { desc, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateNetwork, + apiFindOneNetwork, + apiRemoveNetwork, + apiUpdateNetwork, + network as networkTable, +} from "@/server/db/schema"; + +export const networkRouter = createTRPCRouter({ + all: protectedProcedure.query(async ({ ctx }) => { + const rows = await db + .select() + .from(networkTable) + .where(eq(networkTable.organizationId, ctx.session.activeOrganizationId)) + .orderBy(desc(networkTable.createdAt)); + return rows; + }), + + one: protectedProcedure + .input(apiFindOneNetwork) + .query(async ({ ctx, input }) => { + const row = await findNetworkById(input.networkId); + if (row.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Network not found", + }); + } + return row; + }), + create: protectedProcedure + .input(apiCreateNetwork) + .mutation(async ({ ctx, input }) => { + return createNetwork(input, ctx.session.activeOrganizationId); + }), + update: protectedProcedure + .input(apiUpdateNetwork) + .mutation(async ({ ctx, input }) => { + const network = await findNetworkById(input.networkId); + if (network.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Not authorized to update this network", + }); + } + return updateNetwork(input); + }), + + remove: protectedProcedure + .input(apiRemoveNetwork) + .mutation(async ({ ctx, input }) => { + const network = await findNetworkById(input.networkId); + if (network.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Not authorized to delete this network", + }); + } + return removeNetwork(input.networkId); + }), +}); diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts index 6814613e5..0680c76ff 100644 --- a/packages/server/src/db/schema/account.ts +++ b/packages/server/src/db/schema/account.ts @@ -7,6 +7,7 @@ import { timestamp, } from "drizzle-orm/pg-core"; import { nanoid } from "nanoid"; +import { network } from "./network"; import { projects } from "./project"; import { server } from "./server"; import { ssoProvider } from "./sso"; @@ -77,6 +78,7 @@ export const organizationRelations = relations( references: [user.id], }), servers: many(server), + networks: many(network), projects: many(projects), members: many(member), ssoProviders: many(ssoProvider), diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index fece17f02..42330d308 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -16,6 +16,7 @@ export * from "./gitlab"; export * from "./mariadb"; export * from "./mongo"; export * from "./mount"; +export * from "./network"; export * from "./mysql"; export * from "./notification"; export * from "./patch"; diff --git a/packages/server/src/db/schema/network.ts b/packages/server/src/db/schema/network.ts new file mode 100644 index 000000000..2341f3cdb --- /dev/null +++ b/packages/server/src/db/schema/network.ts @@ -0,0 +1,137 @@ +import { relations } from "drizzle-orm"; +import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-zod"; +import { nanoid } from "nanoid"; +import { z } from "zod"; +import { organization } from "./account"; +import { server } from "./server"; + +/** Docker network driver types */ +export const networkDriver = pgEnum("networkDriver", [ + "bridge", + "host", + "overlay", + "macvlan", + "none", + "ipvlan", +]); + +export const network = pgTable("network", { + networkId: text("networkId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + driver: networkDriver("driver").notNull().default("bridge"), + scope: text("scope"), // e.g. "local", "swarm" + internal: boolean("internal").notNull().default(false), + attachable: boolean("attachable").notNull().default(false), + ingress: boolean("ingress").notNull().default(false), + configOnly: boolean("configOnly").notNull().default(false), + enableIPv4: boolean("enableIPv4").notNull().default(true), + enableIPv6: boolean("enableIPv6").notNull().default(false), + ipam: jsonb("ipam") + .$type<{ + driver?: string; + config?: Array<{ subnet?: string; gateway?: string; ipRange?: string }>; + }>() + .default({}), + createdAt: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + organizationId: text("organizationId") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + serverId: text("serverId").references(() => server.serverId, { + onDelete: "cascade", + }), +}); + +export const networkRelations = relations(network, ({ one }) => ({ + organization: one(organization, { + fields: [network.organizationId], + references: [organization.id], + }), + server: one(server, { + fields: [network.serverId], + references: [server.serverId], + }), +})); + +const createSchema = createInsertSchema(network, { + networkId: z.string().min(1), + name: z.string().min(1), + driver: z + .enum(["bridge", "host", "overlay", "macvlan", "none", "ipvlan"]) + .optional(), + scope: z.string().optional(), + internal: z.boolean().optional(), + attachable: z.boolean().optional(), + ingress: z.boolean().optional(), + configOnly: z.boolean().optional(), + enableIPv4: z.boolean().optional(), + enableIPv6: z.boolean().optional(), + ipam: z + .object({ + driver: z.string().optional(), + config: z + .array( + z.object({ + subnet: z.string().optional(), + gateway: z.string().optional(), + ipRange: z.string().optional(), + }), + ) + .optional(), + }) + .optional(), + organizationId: z.string().min(1), + serverId: z.string().optional().nullable(), +}); + +export const apiCreateNetwork = createSchema + .pick({ + name: true, + driver: true, + scope: true, + internal: true, + attachable: true, + ingress: true, + configOnly: true, + enableIPv4: true, + enableIPv6: true, + ipam: true, + serverId: true, + }) + .partial() + .required({ name: true }); + +export const apiFindOneNetwork = createSchema + .pick({ + networkId: true, + }) + .required(); + +export const apiRemoveNetwork = createSchema + .pick({ + networkId: true, + }) + .required(); + +export const apiUpdateNetwork = createSchema + .pick({ + networkId: true, + name: true, + driver: true, + scope: true, + internal: true, + attachable: true, + ingress: true, + configOnly: true, + enableIPv4: true, + enableIPv6: true, + ipam: true, + serverId: true, + }) + .partial() + .required({ networkId: true }); diff --git a/packages/server/src/db/schema/server.ts b/packages/server/src/db/schema/server.ts index 176f35948..be95e74f7 100644 --- a/packages/server/src/db/schema/server.ts +++ b/packages/server/src/db/schema/server.ts @@ -18,6 +18,7 @@ import { deployments } from "./deployment"; import { mariadb } from "./mariadb"; import { mongo } from "./mongo"; import { mysql } from "./mysql"; +import { network } from "./network"; import { postgres } from "./postgres"; import { redis } from "./redis"; import { schedules } from "./schedule"; @@ -122,6 +123,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({ mysql: many(mysql), postgres: many(postgres), certificates: many(certificates), + networks: many(network), organization: one(organization, { fields: [server.organizationId], references: [organization.id], diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9adc9081e..1f2abbc98 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -26,6 +26,7 @@ export * from "./services/mariadb"; export * from "./services/mongo"; export * from "./services/mount"; export * from "./services/mysql"; +export * from "./services/network"; export * from "./services/notification"; export * from "./services/patch"; export * from "./services/patch-repo"; diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts new file mode 100644 index 000000000..a6879bed9 --- /dev/null +++ b/packages/server/src/services/network.ts @@ -0,0 +1,114 @@ +import { db } from "@dokploy/server/db"; +import { + type apiCreateNetwork, + type apiUpdateNetwork, + network, +} from "@dokploy/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { getRemoteDocker } from "../utils/servers/remote-docker"; + +export const findNetworkById = async (networkId: string) => { + const [row] = await db + .select() + .from(network) + .where(eq(network.networkId, networkId)) + .limit(1); + + if (!row) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Network not found", + }); + } + + return row; +}; + +export const createNetwork = async ( + input: typeof apiCreateNetwork._type, + organizationId: string, +) => { + const created = await db.transaction(async (tx) => { + const [row] = await tx + .insert(network) + .values({ + ...input, + organizationId, + }) + .returning(); + + if (!row) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create network", + }); + } + + const ipam = row.ipam ?? {}; + const ipamConfig = (ipam.config ?? []) + .map((c) => { + const entry: Record = {}; + if (c.subnet) entry.Subnet = c.subnet; + if (c.gateway) entry.Gateway = c.gateway; + if (c.ipRange) entry.IPRange = c.ipRange; + return entry; + }) + .filter((e) => Object.keys(e).length > 0); + + const docker = await getRemoteDocker(input.serverId ?? null); + await docker.createNetwork({ + Name: row.name, + Driver: row.driver, + Internal: row.internal, + Attachable: row.attachable, + Ingress: row.ingress, + EnableIPv6: row.enableIPv6, + IPAM: + ipamConfig.length > 0 || ipam.driver + ? { + Driver: ipam.driver ?? "default", + Config: ipamConfig.length > 0 ? ipamConfig : undefined, + } + : undefined, + }); + + return row; + }); + + return created; +}; + +export const updateNetwork = async (input: typeof apiUpdateNetwork._type) => { + const { networkId, ...rest } = input; + const [updated] = await db + .update(network) + .set(rest) + .where(eq(network.networkId, networkId)) + .returning(); + + if (!updated) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Network not found", + }); + } + + return updated; +}; + +export const removeNetwork = async (networkId: string) => { + const [deleted] = await db + .delete(network) + .where(eq(network.networkId, networkId)) + .returning(); + + if (!deleted) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Network not found", + }); + } + + return deleted; +}; From a0566cdbd777f0d3e14052b18d1800196bbd6687 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 21 Feb 2026 14:59:50 -0600 Subject: [PATCH 02/22] refactor: remove unused server value and update network handling logic - Eliminated the unused DOKPLOY_SERVER_VALUE constant from the network handling component. - Updated the default serverId to undefined in the network form values and adjusted related logic to ensure compatibility with cloud environments. - Enhanced server validation in the createNetwork function to require a serverId when in cloud mode, improving error handling for network creation. --- .../dashboard/networks/handle-network.tsx | 13 +++++-------- packages/server/src/services/network.ts | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/networks/handle-network.tsx b/apps/dokploy/components/dashboard/networks/handle-network.tsx index 8af7db795..86d328b8e 100644 --- a/apps/dokploy/components/dashboard/networks/handle-network.tsx +++ b/apps/dokploy/components/dashboard/networks/handle-network.tsx @@ -48,9 +48,6 @@ const networkDriverEnum = [ /** Sentinel for "no scope" */ const SCOPE_EMPTY = "__scope_none__"; -/** Value for "Dokploy server" (local / no specific server). Not used in cloud. */ -const DOKPLOY_SERVER_VALUE = "__dokploy_server__"; - const ipamConfigEntrySchema = z.object({ subnet: z.string().optional(), ipRange: z.string().optional(), @@ -78,7 +75,7 @@ const defaultValues: NetworkFormValues = { name: "", driver: "bridge", scope: SCOPE_EMPTY, - serverId: DOKPLOY_SERVER_VALUE, + serverId: undefined, internal: false, attachable: false, ingress: false, @@ -134,7 +131,7 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { name: network.name, driver: network.driver, scope: network.scope ?? SCOPE_EMPTY, - serverId: network.serverId ?? DOKPLOY_SERVER_VALUE, + serverId: network.serverId || undefined, internal: network.internal, attachable: network.attachable, enableIPv4: network.enableIPv4, @@ -157,7 +154,7 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { name: data.name, driver: data.driver, scope, - serverId: data.serverId ?? undefined, + serverId: data.serverId || undefined, internal: data.internal, attachable: data.attachable, ingress: data.ingress, @@ -268,7 +265,7 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { Server - - - - - - - {!isCloud && ( - - Dokploy server - - )} - {servers?.map((server) => ( - - {server.name} - - ))} - - - - {isCloud - ? "Server where this network will be created." - : "Dokploy server is the default local server; or choose a specific server."} - - - - )} - /> - ( - - Scope (optional) - - - - )} - />
( - -
- Internal - - Restrict external access; containers on this network - cannot reach external networks. - -
- - - + + Server + + + {isCloud + ? "Server where this network will be created." + : "Dokploy server is the default local server."} + + )} /> ( - -
- Attachable - - Allow standalone containers to attach to this network - (e.g. in Swarm, not only services). - -
- - - -
- )} - /> - ( - -
- Enable IPv4 - - Enable IPv4 addressing on the network. - -
- - - -
- )} - /> - ( - -
- Enable IPv6 - - Enable IPv6 addressing on the network. - -
- - - -
- )} - /> - ( - -
- Ingress - - Use as the routing-mesh network in Swarm mode (load - balancing between nodes). - -
- - - -
- )} - /> - ( - -
- Config only - - Create a placeholder network whose config is reused by - other networks; cannot run containers on it. - -
- - - + + Scope (optional) + + )} />
-
- IPAM +
+ {toggleOptions.map((option) => ( + ( + +
+ {option.label} + + {option.description} + +
+ + + +
+ )} + /> + ))} +
+
+
+ IPAM +

+ IP address management settings for this network. +

+
{ type="button" variant="outline" size="icon" + aria-label="Remove IPAM config" onClick={() => ipamConfigFieldArray.remove(index)} > - − +
))} @@ -532,6 +486,7 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { }) } > + Add IPAM config
@@ -544,14 +499,8 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { > Cancel - diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index 2fe764ab6..7934e2ebc 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -6,6 +6,7 @@ import { } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import type { z } from "zod"; import { IS_CLOUD } from "../constants"; import { getRemoteDocker } from "../utils/servers/remote-docker"; @@ -27,7 +28,7 @@ export const findNetworkById = async (networkId: string) => { }; export const createNetwork = async ( - input: typeof apiCreateNetwork._type, + input: z.infer, organizationId: string, ) => { if (IS_CLOUD) { @@ -86,7 +87,9 @@ export const createNetwork = async ( return created; }; -export const updateNetwork = async (input: typeof apiUpdateNetwork._type) => { +export const updateNetwork = async ( + input: z.infer, +) => { const { networkId, ...rest } = input; const [updated] = await db .update(network) From 61c2b73f08a30bc1f5018b157beb88a260481e90 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:47:06 +0000 Subject: [PATCH 14/22] [autofix.ci] apply automated fixes --- apps/dokploy/components/dashboard/networks/handle-network.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/networks/handle-network.tsx b/apps/dokploy/components/dashboard/networks/handle-network.tsx index 8e1dc5087..c30591088 100644 --- a/apps/dokploy/components/dashboard/networks/handle-network.tsx +++ b/apps/dokploy/components/dashboard/networks/handle-network.tsx @@ -141,7 +141,9 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { const createMutation = api.network.create.useMutation(); const updateMutation = api.network.update.useMutation(); - const isPending = isEdit ? updateMutation.isPending : createMutation.isPending; + const isPending = isEdit + ? updateMutation.isPending + : createMutation.isPending; const form = useForm({ resolver: zodResolver(networkFormSchema), From 94ddbf8ba5474c7d2be986739b8a811a2cb33922 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 21 Jul 2026 22:19:22 -0600 Subject: [PATCH 15/22] refactor(networks): streamline network management and enhance validation - Simplified the network form schema by removing unnecessary fields and enforcing validation rules for IP settings. - Updated the HandleNetwork component to eliminate the edit functionality, focusing on network creation only. - Improved the ShowNetworks component by adding a delete action for network management, enhancing user experience. - Introduced a new SQL migration for the network schema, establishing necessary constraints and types. - Cleaned up the API by removing the update network functionality, reflecting the immutable nature of Docker networks. --- .../dashboard/networks/handle-network.tsx | 658 ++++++++---------- .../dashboard/networks/show-networks.tsx | 35 +- ...e.sql => 0175_gray_dreaming_celestial.sql} | 5 +- apps/dokploy/drizzle/meta/0175_snapshot.json | 28 +- apps/dokploy/drizzle/meta/_journal.json | 4 +- apps/dokploy/server/api/routers/network.ts | 21 +- packages/server/src/db/schema/network.ts | 79 ++- packages/server/src/services/network.ts | 81 ++- 8 files changed, 402 insertions(+), 509 deletions(-) rename apps/dokploy/drizzle/{0175_wet_grey_gargoyle.sql => 0175_gray_dreaming_celestial.sql} (86%) diff --git a/apps/dokploy/components/dashboard/networks/handle-network.tsx b/apps/dokploy/components/dashboard/networks/handle-network.tsx index c30591088..8c006cfdb 100644 --- a/apps/dokploy/components/dashboard/networks/handle-network.tsx +++ b/apps/dokploy/components/dashboard/networks/handle-network.tsx @@ -1,8 +1,8 @@ "use client"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { Network, Pencil, Plus, Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { Network, Plus, Trash2 } from "lucide-react"; +import { useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -36,17 +36,10 @@ import { import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -const networkDriverEnum = [ - "bridge", - "host", - "overlay", - "macvlan", - "none", - "ipvlan", -] as const; +// Only bridge and overlay can be created: "host"/"none" are Docker +// singletons and macvlan/ipvlan need driver options we don't expose. +const networkDriverEnum = ["bridge", "overlay"] as const; -/** Sentinel for "no scope" */ -const SCOPE_EMPTY = "__scope_none__"; /** Sentinel for the local Dokploy server (no serverId) */ const SERVER_LOCAL = "__server_local__"; @@ -56,32 +49,45 @@ const ipamConfigEntrySchema = z.object({ gateway: z.string().optional(), }); -const networkFormSchema = z.object({ - name: z.string().min(1, "Name is required"), - driver: z.enum(networkDriverEnum), - scope: z.string().optional(), - serverId: z.string().optional(), - internal: z.boolean(), - attachable: z.boolean(), - ingress: z.boolean(), - configOnly: z.boolean(), - enableIPv4: z.boolean(), - enableIPv6: z.boolean(), - ipamDriver: z.string().optional(), - ipamConfig: z.array(ipamConfigEntrySchema), -}); +const networkFormSchema = z + .object({ + name: z.string().min(1, "Name is required"), + driver: z.enum(networkDriverEnum), + serverId: z.string().optional(), + internal: z.boolean(), + attachable: z.boolean(), + enableIPv4: z.boolean(), + enableIPv6: z.boolean(), + ipamDriver: z.string().optional(), + ipamConfig: z.array(ipamConfigEntrySchema), + }) + .superRefine((input, ctx) => { + if (!input.enableIPv4 && !input.enableIPv6) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["enableIPv4"], + message: "IPv4 or IPv6 must be enabled", + }); + } + for (const [index, entry] of input.ipamConfig.entries()) { + if (!entry.subnet && (entry.gateway || entry.ipRange)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["ipamConfig", index, "subnet"], + message: "Gateway and IP range require a subnet", + }); + } + } + }); type NetworkFormValues = z.infer; const defaultValues: NetworkFormValues = { name: "", driver: "bridge", - scope: SCOPE_EMPTY, serverId: undefined, internal: false, attachable: false, - ingress: false, - configOnly: false, enableIPv4: true, enableIPv6: false, ipamDriver: "", @@ -97,7 +103,8 @@ const toggleOptions = [ { name: "attachable", label: "Attachable", - description: "Allow standalone containers to attach to this network.", + description: + "Allow standalone containers to attach (overlay networks only).", }, { name: "enableIPv4", @@ -109,41 +116,21 @@ const toggleOptions = [ label: "Enable IPv6", description: "Enable IPv6 addressing on the network.", }, - { - name: "ingress", - label: "Ingress", - description: "Use as the routing-mesh network in Swarm mode.", - }, - { - name: "configOnly", - label: "Config only", - description: "Placeholder network whose config is reused by others.", - }, ] as const; interface HandleNetworkProps { - networkId?: string; children?: React.ReactNode; } -export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { +// Docker networks are immutable, so this dialog only creates them; +// changing a network means deleting and recreating it. +export const HandleNetwork = ({ children }: HandleNetworkProps) => { const [isOpen, setIsOpen] = useState(false); const { data: isCloud } = api.settings.isCloud.useQuery(); const utils = api.useUtils(); - const isEdit = !!networkId; const { data: servers } = api.server.all.useQuery(); - const { data: network, isLoading: isLoadingNetwork } = - api.network.one.useQuery( - { networkId: networkId! }, - { enabled: isEdit && !!networkId }, - ); - - const createMutation = api.network.create.useMutation(); - const updateMutation = api.network.update.useMutation(); - const isPending = isEdit - ? updateMutation.isPending - : createMutation.isPending; + const { mutateAsync, isPending } = api.network.create.useMutation(); const form = useForm({ resolver: zodResolver(networkFormSchema), @@ -155,83 +142,39 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { name: "ipamConfig", }); - useEffect(() => { - if (isEdit && network && isOpen) { - const ipam = network.ipam ?? {}; - const ipamConfigArr = (ipam.config ?? []).map((c) => ({ - subnet: c.subnet ?? "", - ipRange: c.ipRange ?? "", - gateway: c.gateway ?? "", - })); - form.reset({ - ...defaultValues, - name: network.name, - driver: network.driver, - scope: network.scope ?? SCOPE_EMPTY, - serverId: network.serverId || undefined, - internal: network.internal, - attachable: network.attachable, - enableIPv4: network.enableIPv4, - enableIPv6: network.enableIPv6, - ipamDriver: ipam.driver ?? "", - ipamConfig: ipamConfigArr, - ingress: network.ingress, - configOnly: network.configOnly, - }); - } - }, [isEdit, isOpen, network, form]); - const onSubmit = async (data: NetworkFormValues) => { - const scope = - data.scope && data.scope !== SCOPE_EMPTY ? data.scope : undefined; - - const payload = { - name: data.name, - driver: data.driver, - scope, - serverId: data.serverId || undefined, - internal: data.internal, - attachable: data.attachable, - ingress: data.ingress, - configOnly: data.configOnly, - enableIPv4: data.enableIPv4, - enableIPv6: data.enableIPv6, - ipam: { - driver: data.ipamDriver, - config: data.ipamConfig, - }, - }; - try { - if (isEdit && networkId) { - await updateMutation.mutateAsync({ networkId, ...payload }); - } else { - await createMutation.mutateAsync(payload); - } + await mutateAsync({ + name: data.name, + driver: data.driver, + serverId: data.serverId || undefined, + internal: data.internal, + attachable: data.attachable, + enableIPv4: data.enableIPv4, + enableIPv6: data.enableIPv6, + ipam: { + driver: data.ipamDriver || undefined, + config: data.ipamConfig, + }, + }); - toast.success(isEdit ? "Network updated" : "Network created"); + toast.success("Network created"); await utils.network.all.invalidate(); - if (networkId) await utils.network.one.invalidate({ networkId }); setIsOpen(false); form.reset(defaultValues); - } catch { - toast.error(isEdit ? "Error updating network" : "Error creating network"); + } catch (error) { + toast.error("Error creating network", { + description: error instanceof Error ? error.message : "Unknown error", + }); } }; - const trigger = - children ?? - (isEdit ? ( - - ) : ( - - )); + const trigger = children ?? ( + + ); return ( @@ -240,274 +183,237 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { - {isEdit ? "Edit network" : "Add network"} + Add network - {isEdit - ? "Update this Docker network. Changes apply to name, driver, and server assignment." - : "Create a new Docker network for your organization. You can optionally assign it to a server."} + Create a new Docker network for your organization. Networks are + immutable: to change one, delete it and create it again. - {isEdit && isLoadingNetwork ? ( -
- Loading network… -
- ) : ( -
- -
- ( - - Name + + +
+ ( + + Name + + + + + + )} + /> + ( + + Driver + + + + - - - )} - /> - ( - - Driver - - - - )} - /> -
-
- ( - - Server - - - {isCloud - ? "Server where this network will be created." - : "Dokploy server is the default local server."} - - - - )} - /> - ( - - Scope (optional) - - - - )} - /> -
-
- {toggleOptions.map((option) => ( - ( - -
- {option.label} - - {option.description} - -
- - - -
- )} - /> - ))} -
-
-
- IPAM -

- IP address management settings for this network. -

-
- ( - - - Driver (optional) - - - - - - - )} - /> -
- - Config (subnet / gateway / IP range) - - {ipamConfigFieldArray.fields.map((field, index) => ( -
- ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - -
- ))} -
+ ( + + Server + + + {isCloud + ? "Server where this network will be created." + : "Dokploy server is the default local server."} + + + + )} + /> +
+ {toggleOptions.map((option) => ( + ( + +
+ {option.label} + + {option.description} + +
+ + + +
+ )} + /> + ))} +
+
+
+ IPAM +

+ IP address management settings for this network. +

- + ( + + + Driver (optional) + + + + + + + )} + /> +
+ + Config (subnet / gateway / IP range) + + {ipamConfigFieldArray.fields.map((field, index) => ( +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ ))} - - - - - )} +
+
+ + + + + +
); diff --git a/apps/dokploy/components/dashboard/networks/show-networks.tsx b/apps/dokploy/components/dashboard/networks/show-networks.tsx index 0d8f642b5..e26e60846 100644 --- a/apps/dokploy/components/dashboard/networks/show-networks.tsx +++ b/apps/dokploy/components/dashboard/networks/show-networks.tsx @@ -1,7 +1,9 @@ "use client"; -import { Loader2, Network, Pencil } from "lucide-react"; +import { Loader2, Network, Trash2 } from "lucide-react"; +import { toast } from "sonner"; import { HandleNetwork } from "@/components/dashboard/networks/handle-network"; +import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -23,7 +25,9 @@ import { import { api } from "@/utils/api"; export const ShowNetworks = () => { + const utils = api.useUtils(); const { data: networks, isLoading } = api.network.all.useQuery(); + const { mutateAsync: removeNetwork } = api.network.remove.useMutation(); return (
@@ -91,7 +95,7 @@ export const ShowNetworks = () => { {n.driver} - {n.scope ?? "—"} + {n.driver === "overlay" ? "swarm" : "local"} {n.internal ? "Yes" : "No"} @@ -106,15 +110,34 @@ export const ShowNetworks = () => { {new Date(n.createdAt).toLocaleDateString()} - + { + try { + await removeNetwork({ + networkId: n.networkId, + }); + toast.success("Network deleted"); + await utils.network.all.invalidate(); + } catch (error) { + toast.error("Error deleting network", { + description: + error instanceof Error + ? error.message + : "Unknown error", + }); + } + }} + > - + ))} diff --git a/apps/dokploy/drizzle/0175_wet_grey_gargoyle.sql b/apps/dokploy/drizzle/0175_gray_dreaming_celestial.sql similarity index 86% rename from apps/dokploy/drizzle/0175_wet_grey_gargoyle.sql rename to apps/dokploy/drizzle/0175_gray_dreaming_celestial.sql index 314ea2052..9ace63e8e 100644 --- a/apps/dokploy/drizzle/0175_wet_grey_gargoyle.sql +++ b/apps/dokploy/drizzle/0175_gray_dreaming_celestial.sql @@ -1,13 +1,10 @@ -CREATE TYPE "public"."networkDriver" AS ENUM('bridge', 'host', 'overlay', 'macvlan', 'none', 'ipvlan');--> statement-breakpoint +CREATE TYPE "public"."networkDriver" AS ENUM('bridge', 'overlay');--> statement-breakpoint CREATE TABLE "network" ( "networkId" text PRIMARY KEY NOT NULL, "name" text NOT NULL, "driver" "networkDriver" DEFAULT 'bridge' NOT NULL, - "scope" text, "internal" boolean DEFAULT false NOT NULL, "attachable" boolean DEFAULT false NOT NULL, - "ingress" boolean DEFAULT false NOT NULL, - "configOnly" boolean DEFAULT false NOT NULL, "enableIPv4" boolean DEFAULT true NOT NULL, "enableIPv6" boolean DEFAULT false NOT NULL, "ipam" jsonb DEFAULT '{}'::jsonb, diff --git a/apps/dokploy/drizzle/meta/0175_snapshot.json b/apps/dokploy/drizzle/meta/0175_snapshot.json index e698338e3..0b4c609c9 100644 --- a/apps/dokploy/drizzle/meta/0175_snapshot.json +++ b/apps/dokploy/drizzle/meta/0175_snapshot.json @@ -1,5 +1,5 @@ { - "id": "36b4b7bf-31bb-4d60-8ef8-0a33b328d623", + "id": "957b942e-9aeb-4551-8d5d-1174eec70fd6", "prevId": "daaa9837-09f3-452c-b5ba-232c3d6eeca2", "version": "7", "dialect": "postgresql", @@ -4788,12 +4788,6 @@ "notNull": true, "default": "'bridge'" }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, "internal": { "name": "internal", "type": "boolean", @@ -4808,20 +4802,6 @@ "notNull": true, "default": false }, - "ingress": { - "name": "ingress", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "configOnly": { - "name": "configOnly", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, "enableIPv4": { "name": "enableIPv4", "type": "boolean", @@ -8604,11 +8584,7 @@ "schema": "public", "values": [ "bridge", - "host", - "overlay", - "macvlan", - "none", - "ipvlan" + "overlay" ] }, "public.notificationType": { diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 26ba1d3c6..4c7100b60 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1230,8 +1230,8 @@ { "idx": 175, "version": "7", - "when": 1784682163836, - "tag": "0175_wet_grey_gargoyle", + "when": 1784692797270, + "tag": "0175_gray_dreaming_celestial", "breakpoints": true } ] diff --git a/apps/dokploy/server/api/routers/network.ts b/apps/dokploy/server/api/routers/network.ts index cf6503375..a58a68b92 100644 --- a/apps/dokploy/server/api/routers/network.ts +++ b/apps/dokploy/server/api/routers/network.ts @@ -1,9 +1,4 @@ -import { - createNetwork, - findNetworkById, - removeNetwork, - updateNetwork, -} from "@dokploy/server"; +import { createNetwork, findNetworkById, removeNetwork } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; @@ -12,7 +7,6 @@ import { apiCreateNetwork, apiFindOneNetwork, apiRemoveNetwork, - apiUpdateNetwork, network as networkTable, } from "@/server/db/schema"; @@ -50,19 +44,6 @@ export const networkRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { return createNetwork(input, ctx.session.activeOrganizationId); }), - update: protectedProcedure - .input(apiUpdateNetwork) - .mutation(async ({ ctx, input }) => { - const network = await findNetworkById(input.networkId); - if (network.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Not authorized to update this network", - }); - } - return updateNetwork(input); - }), - remove: protectedProcedure .input(apiRemoveNetwork) .mutation(async ({ ctx, input }) => { diff --git a/packages/server/src/db/schema/network.ts b/packages/server/src/db/schema/network.ts index 2341f3cdb..add0ef249 100644 --- a/packages/server/src/db/schema/network.ts +++ b/packages/server/src/db/schema/network.ts @@ -6,15 +6,14 @@ import { z } from "zod"; import { organization } from "./account"; import { server } from "./server"; -/** Docker network driver types */ -export const networkDriver = pgEnum("networkDriver", [ - "bridge", - "host", - "overlay", - "macvlan", - "none", - "ipvlan", -]); +/** + * Docker network driver types. Only bridge and overlay are supported: + * "host"/"none" are Docker singletons that cannot be created, and + * macvlan/ipvlan require driver options (parent interface) we don't expose. + * Scope is derived from the driver (bridge = local, overlay = swarm), and + * ingress/config-only networks are not manageable from Dokploy. + */ +export const networkDriver = pgEnum("networkDriver", ["bridge", "overlay"]); export const network = pgTable("network", { networkId: text("networkId") @@ -23,11 +22,8 @@ export const network = pgTable("network", { .$defaultFn(() => nanoid()), name: text("name").notNull(), driver: networkDriver("driver").notNull().default("bridge"), - scope: text("scope"), // e.g. "local", "swarm" internal: boolean("internal").notNull().default(false), attachable: boolean("attachable").notNull().default(false), - ingress: boolean("ingress").notNull().default(false), - configOnly: boolean("configOnly").notNull().default(false), enableIPv4: boolean("enableIPv4").notNull().default(true), enableIPv6: boolean("enableIPv6").notNull().default(false), ipam: jsonb("ipam") @@ -61,14 +57,9 @@ export const networkRelations = relations(network, ({ one }) => ({ const createSchema = createInsertSchema(network, { networkId: z.string().min(1), name: z.string().min(1), - driver: z - .enum(["bridge", "host", "overlay", "macvlan", "none", "ipvlan"]) - .optional(), - scope: z.string().optional(), + driver: z.enum(["bridge", "overlay"]).optional(), internal: z.boolean().optional(), attachable: z.boolean().optional(), - ingress: z.boolean().optional(), - configOnly: z.boolean().optional(), enableIPv4: z.boolean().optional(), enableIPv6: z.boolean().optional(), ipam: z @@ -89,22 +80,48 @@ const createSchema = createInsertSchema(network, { serverId: z.string().optional().nullable(), }); +const validateNetworkInput = ( + input: { + enableIPv4?: boolean; + enableIPv6?: boolean; + ipam?: { + config?: Array<{ subnet?: string; gateway?: string; ipRange?: string }>; + } | null; + }, + ctx: z.RefinementCtx, +) => { + if (input.enableIPv4 === false && input.enableIPv6 !== true) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["enableIPv4"], + message: "IPv4 or IPv6 must be enabled", + }); + } + for (const [index, entry] of (input.ipam?.config ?? []).entries()) { + if (!entry.subnet && (entry.gateway || entry.ipRange)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["ipam", "config", index, "subnet"], + message: "Gateway and IP range require a subnet", + }); + } + } +}; + export const apiCreateNetwork = createSchema .pick({ name: true, driver: true, - scope: true, internal: true, attachable: true, - ingress: true, - configOnly: true, enableIPv4: true, enableIPv6: true, ipam: true, serverId: true, }) .partial() - .required({ name: true }); + .required({ name: true }) + .superRefine(validateNetworkInput); export const apiFindOneNetwork = createSchema .pick({ @@ -117,21 +134,3 @@ export const apiRemoveNetwork = createSchema networkId: true, }) .required(); - -export const apiUpdateNetwork = createSchema - .pick({ - networkId: true, - name: true, - driver: true, - scope: true, - internal: true, - attachable: true, - ingress: true, - configOnly: true, - enableIPv4: true, - enableIPv6: true, - ipam: true, - serverId: true, - }) - .partial() - .required({ networkId: true }); diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index 7934e2ebc..fbca5c776 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -1,9 +1,5 @@ import { db } from "@dokploy/server/db"; -import { - type apiCreateNetwork, - type apiUpdateNetwork, - network, -} from "@dokploy/server/db/schema"; +import { type apiCreateNetwork, network } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import type { z } from "zod"; @@ -68,18 +64,32 @@ export const createNetwork = async ( .filter((e) => Object.keys(e).length > 0); const docker = await getRemoteDocker(input.serverId ?? null); - await docker.createNetwork({ - Name: row.name, - Driver: row.driver, - Internal: row.internal, - Attachable: row.attachable, - Ingress: row.ingress, - EnableIPv6: row.enableIPv6, - IPAM: { - Driver: ipam.driver ?? "default", - Config: ipamConfig.length > 0 ? ipamConfig : undefined, - }, - }); + try { + await docker.createNetwork({ + Name: row.name, + Driver: row.driver, + CheckDuplicate: true, + Internal: row.internal, + Attachable: row.attachable, + // EnableIPv4 is missing from dockerode's types but supported by + // the daemon (API >= 1.47); the body is sent as-is + EnableIPv4: row.enableIPv4, + EnableIPv6: row.enableIPv6, + IPAM: { + Driver: ipam.driver || "default", + Config: ipamConfig.length > 0 ? ipamConfig : undefined, + }, + } as Parameters[0]); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Failed to create Docker network", + cause: error, + }); + } return row; }); @@ -87,27 +97,28 @@ export const createNetwork = async ( return created; }; -export const updateNetwork = async ( - input: z.infer, -) => { - const { networkId, ...rest } = input; - const [updated] = await db - .update(network) - .set(rest) - .where(eq(network.networkId, networkId)) - .returning(); +// Docker networks are immutable: there is no update, only create and remove. +export const removeNetwork = async (networkId: string) => { + const row = await findNetworkById(networkId); - if (!updated) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Network not found", - }); + const docker = await getRemoteDocker(row.serverId ?? null); + try { + await docker.getNetwork(row.name).remove(); + } catch (error) { + // If the network is already gone from Docker, still clean up the DB row + const statusCode = (error as { statusCode?: number })?.statusCode; + if (statusCode !== 404) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Failed to remove Docker network", + cause: error, + }); + } } - return updated; -}; - -export const removeNetwork = async (networkId: string) => { const [deleted] = await db .delete(network) .where(eq(network.networkId, networkId)) From c578c185006aa68b06efc9f596acb4586234c8e7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 21 Jul 2026 22:28:00 -0600 Subject: [PATCH 16/22] feat(networks): add network configuration viewer and enhance network actions - Introduced a new ShowNetworkConfig component for displaying detailed Docker network configuration. - Integrated ShowNetworkConfig into the ShowNetworks component, allowing users to view network details directly. - Updated the layout of network actions for improved user experience and accessibility. - Enhanced the API to support network inspection functionality, providing necessary data for the new component. --- .../networks/show-network-config.tsx | 69 +++++++++++++++++ .../dashboard/networks/show-networks.tsx | 76 ++++++++++--------- apps/dokploy/components/ui/button.tsx | 2 +- apps/dokploy/server/api/routers/network.ts | 20 ++++- packages/server/src/services/network.ts | 18 +++++ 5 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 apps/dokploy/components/dashboard/networks/show-network-config.tsx diff --git a/apps/dokploy/components/dashboard/networks/show-network-config.tsx b/apps/dokploy/components/dashboard/networks/show-network-config.tsx new file mode 100644 index 000000000..c7f9a158c --- /dev/null +++ b/apps/dokploy/components/dashboard/networks/show-network-config.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Eye, Loader2 } from "lucide-react"; +import { useState } from "react"; +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"; + +interface Props { + networkId: string; + networkName: string; +} + +export const ShowNetworkConfig = ({ networkId, networkName }: Props) => { + const [open, setOpen] = useState(false); + const { data, isLoading, error } = api.network.inspect.useQuery( + { networkId }, + { enabled: open }, + ); + + return ( + + + + + + + Network Config + + docker network inspect output for "{networkName}" + + + {error ? ( + {error.message} + ) : isLoading ? ( +
+ Loading... + +
+ ) : ( +
+ +
+								
+							
+
+
+ )} +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/networks/show-networks.tsx b/apps/dokploy/components/dashboard/networks/show-networks.tsx index e26e60846..25ca3fd02 100644 --- a/apps/dokploy/components/dashboard/networks/show-networks.tsx +++ b/apps/dokploy/components/dashboard/networks/show-networks.tsx @@ -3,6 +3,7 @@ import { Loader2, Network, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { HandleNetwork } from "@/components/dashboard/networks/handle-network"; +import { ShowNetworkConfig } from "@/components/dashboard/networks/show-network-config"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -77,12 +78,11 @@ export const ShowNetworks = () => { Name Driver - Scope Internal Attachable Server Created - + Actions @@ -92,10 +92,12 @@ export const ShowNetworks = () => { {n.name} - {n.driver} - - - {n.driver === "overlay" ? "swarm" : "local"} +
+ {n.driver} + + {n.driver === "overlay" ? "swarm" : "local"} + +
{n.internal ? "Yes" : "No"} @@ -109,35 +111,41 @@ export const ShowNetworks = () => { {new Date(n.createdAt).toLocaleDateString()} - - { - try { - await removeNetwork({ - networkId: n.networkId, - }); - toast.success("Network deleted"); - await utils.network.all.invalidate(); - } catch (error) { - toast.error("Error deleting network", { - description: - error instanceof Error - ? error.message - : "Unknown error", - }); - } - }} - > - - + + +
))} diff --git a/apps/dokploy/components/ui/button.tsx b/apps/dokploy/components/ui/button.tsx index 67f4405b0..f31410ab4 100644 --- a/apps/dokploy/components/ui/button.tsx +++ b/apps/dokploy/components/ui/button.tsx @@ -6,7 +6,7 @@ import type * as React from "react"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { diff --git a/apps/dokploy/server/api/routers/network.ts b/apps/dokploy/server/api/routers/network.ts index a58a68b92..ac8a71310 100644 --- a/apps/dokploy/server/api/routers/network.ts +++ b/apps/dokploy/server/api/routers/network.ts @@ -1,4 +1,9 @@ -import { createNetwork, findNetworkById, removeNetwork } from "@dokploy/server"; +import { + createNetwork, + findNetworkById, + inspectNetwork, + removeNetwork, +} from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; @@ -44,6 +49,19 @@ export const networkRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { return createNetwork(input, ctx.session.activeOrganizationId); }), + inspect: protectedProcedure + .input(apiFindOneNetwork) + .query(async ({ ctx, input }) => { + const network = await findNetworkById(input.networkId); + if (network.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Network not found", + }); + } + return inspectNetwork(input.networkId); + }), + remove: protectedProcedure .input(apiRemoveNetwork) .mutation(async ({ ctx, input }) => { diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index fbca5c776..f01a3044f 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -97,6 +97,24 @@ export const createNetwork = async ( return created; }; +export const inspectNetwork = async (networkId: string) => { + const row = await findNetworkById(networkId); + + const docker = await getRemoteDocker(row.serverId ?? null); + try { + return await docker.getNetwork(row.name).inspect(); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Failed to inspect Docker network", + cause: error, + }); + } +}; + // Docker networks are immutable: there is no update, only create and remove. export const removeNetwork = async (networkId: string) => { const row = await findNetworkById(networkId); From adcbd6fd8e6507e3f2f46f5f7d04e9c77e1bf98c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 21 Jul 2026 22:55:10 -0600 Subject: [PATCH 17/22] feat(networks): enhance network management with server filtering and synchronization - Added server filtering capability to the dashboard, allowing users to manage networks specific to selected servers. - Introduced SyncNetworks component for synchronizing Docker networks with Dokploy, enabling import of new networks and cleanup of stale records. - Updated HandleNetwork and ShowNetworks components to support server-specific operations, improving user experience and functionality. - Enhanced API to support network synchronization and importing, ensuring accurate data handling for network management. --- .../dashboard/networks/handle-network.tsx | 55 +---- .../dashboard/networks/show-networks.tsx | 182 ++++++++------ .../dashboard/networks/sync-networks.tsx | 225 ++++++++++++++++++ apps/dokploy/pages/dashboard/networks.tsx | 9 +- apps/dokploy/server/api/routers/network.ts | 58 +++-- packages/server/src/services/network.ts | 174 +++++++++++++- 6 files changed, 562 insertions(+), 141 deletions(-) create mode 100644 apps/dokploy/components/dashboard/networks/sync-networks.tsx diff --git a/apps/dokploy/components/dashboard/networks/handle-network.tsx b/apps/dokploy/components/dashboard/networks/handle-network.tsx index 8c006cfdb..8f522c40d 100644 --- a/apps/dokploy/components/dashboard/networks/handle-network.tsx +++ b/apps/dokploy/components/dashboard/networks/handle-network.tsx @@ -40,9 +40,6 @@ import { api } from "@/utils/api"; // singletons and macvlan/ipvlan need driver options we don't expose. const networkDriverEnum = ["bridge", "overlay"] as const; -/** Sentinel for the local Dokploy server (no serverId) */ -const SERVER_LOCAL = "__server_local__"; - const ipamConfigEntrySchema = z.object({ subnet: z.string().optional(), ipRange: z.string().optional(), @@ -53,7 +50,6 @@ const networkFormSchema = z .object({ name: z.string().min(1, "Name is required"), driver: z.enum(networkDriverEnum), - serverId: z.string().optional(), internal: z.boolean(), attachable: z.boolean(), enableIPv4: z.boolean(), @@ -85,7 +81,6 @@ type NetworkFormValues = z.infer; const defaultValues: NetworkFormValues = { name: "", driver: "bridge", - serverId: undefined, internal: false, attachable: false, enableIPv4: true, @@ -119,17 +114,17 @@ const toggleOptions = [ ] as const; interface HandleNetworkProps { + /** Target server; undefined creates on the local Dokploy server */ + serverId?: string; children?: React.ReactNode; } // Docker networks are immutable, so this dialog only creates them; // changing a network means deleting and recreating it. -export const HandleNetwork = ({ children }: HandleNetworkProps) => { +export const HandleNetwork = ({ serverId, children }: HandleNetworkProps) => { const [isOpen, setIsOpen] = useState(false); - const { data: isCloud } = api.settings.isCloud.useQuery(); const utils = api.useUtils(); - const { data: servers } = api.server.all.useQuery(); const { mutateAsync, isPending } = api.network.create.useMutation(); const form = useForm({ @@ -147,7 +142,7 @@ export const HandleNetwork = ({ children }: HandleNetworkProps) => { await mutateAsync({ name: data.name, driver: data.driver, - serverId: data.serverId || undefined, + serverId, internal: data.internal, attachable: data.attachable, enableIPv4: data.enableIPv4, @@ -238,48 +233,6 @@ export const HandleNetwork = ({ children }: HandleNetworkProps) => { )} /> - ( - - Server - - - {isCloud - ? "Server where this network will be created." - : "Dokploy server is the default local server."} - - - - )} - />
{toggleOptions.map((option) => ( { +interface Props { + /** Selected server; undefined shows the local Dokploy server */ + serverId?: string; +} + +export const ShowNetworks = ({ serverId }: Props) => { const utils = api.useUtils(); - const { data: networks, isLoading } = api.network.all.useQuery(); + const { data: networks, isLoading } = api.network.all.useQuery({ serverId }); const { mutateAsync: removeNetwork } = api.network.remove.useMutation(); return ( @@ -40,14 +46,16 @@ export const ShowNetworks = () => { Networks - Manage Docker networks for your organization. Networks can be - scoped to a server (optional). + Manage the Docker networks of the selected server. - {networks && networks.length > 0 && ( - - - - )} + +
+ + {networks && networks.length > 0 && ( + + )} +
+
@@ -69,7 +77,7 @@ export const ShowNetworks = () => { started.

- + ) : (
@@ -78,9 +86,9 @@ export const ShowNetworks = () => { Name Driver + Subnet Internal Attachable - Server Created Actions @@ -88,67 +96,99 @@ export const ShowNetworks = () => { - {networks.map((n) => ( - - {n.name} - -
- {n.driver} - - {n.driver === "overlay" ? "swarm" : "local"} - -
-
- - {n.internal ? "Yes" : "No"} - - - {n.attachable ? "Yes" : "No"} - - - {n.server?.name ?? "Dokploy Server"} - - - {new Date(n.createdAt).toLocaleDateString()} - - -
- - { - try { - await removeNetwork({ - networkId: n.networkId, - }); - toast.success("Network deleted"); - await utils.network.all.invalidate(); - } catch (error) { - toast.error("Error deleting network", { - description: - error instanceof Error - ? error.message - : "Unknown error", - }); - } - }} - > - - -
-
-
- ))} + + +
+ + + ); + })} diff --git a/apps/dokploy/components/dashboard/networks/sync-networks.tsx b/apps/dokploy/components/dashboard/networks/sync-networks.tsx new file mode 100644 index 000000000..4b031d70b --- /dev/null +++ b/apps/dokploy/components/dashboard/networks/sync-networks.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { Loader2, RefreshCw, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import { api } from "@/utils/api"; + +interface Props { + /** Target server; undefined syncs the local Dokploy server */ + serverId?: string; +} + +// Compares the Docker daemon's networks against Dokploy's records: +// networks that only exist in Docker can be imported, and records whose +// network no longer exists in Docker can be cleaned up. +export const SyncNetworks = ({ serverId }: Props) => { + const [open, setOpen] = useState(false); + const [selected, setSelected] = useState>(new Set()); + const utils = api.useUtils(); + + const { data, isLoading, error, refetch } = + api.network.networksToSync.useQuery({ serverId }, { enabled: open }); + + const importMutation = api.network.import.useMutation(); + const removeMutation = api.network.remove.useMutation(); + + const toggleSelected = (name: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + return next; + }); + }; + + const onImport = async () => { + try { + const result = await importMutation.mutateAsync({ + serverId, + names: Array.from(selected), + }); + + if (result.imported.length > 0) { + toast.success(`Imported ${result.imported.length} network(s)`); + } + for (const failure of result.errors) { + toast.error(`Could not import "${failure.name}"`, { + description: failure.error, + }); + } + + setSelected(new Set()); + await utils.network.all.invalidate(); + + setOpen(false); + await refetch(); + } catch (error) { + toast.error("Error importing networks", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + const onRemoveStale = async (networkId: string, name: string) => { + try { + await removeMutation.mutateAsync({ networkId }); + toast.success(`Removed stale record "${name}"`); + await utils.network.all.invalidate(); + await refetch(); + } catch (error) { + toast.error("Error removing record", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + return ( + { + setOpen(value); + if (!value) setSelected(new Set()); + }} + > + + + + + + + + Sync networks + + + Import networks that exist in Docker but not in Dokploy, and clean + up records whose network no longer exists. + + + + {error ? ( + {error.message} + ) : isLoading ? ( +
+ Scanning Docker networks... + +
+ ) : ( +
+
+ + Found in Docker ({data?.importable.length ?? 0}) + + {data?.importable.length ? ( + data.importable.map((dockerNetwork) => ( + + )) + ) : ( + + Nothing to import — everything is in sync. + + )} +
+ + {!!data?.missing.length && ( + <> + +
+ + Missing in Docker ({data.missing.length}) + + + These records exist in Dokploy but their network is gone + from Docker. + + {data.missing.map((stale) => ( +
+ {stale.name} + +
+ ))} +
+ + )} +
+ )} + + + + + +
+
+ ); +}; diff --git a/apps/dokploy/pages/dashboard/networks.tsx b/apps/dokploy/pages/dashboard/networks.tsx index d9d65cd8e..cf11fcd3d 100644 --- a/apps/dokploy/pages/dashboard/networks.tsx +++ b/apps/dokploy/pages/dashboard/networks.tsx @@ -6,10 +6,15 @@ import type { ReactElement } from "react"; import superjson from "superjson"; import { ShowNetworks } from "@/components/dashboard/networks/show-networks"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { ServerFilter } from "@/components/shared/server-filter"; import { appRouter } from "@/server/api/root"; const Dashboard = () => { - return ; + return ( + + {(serverId) => } + + ); }; export default Dashboard; @@ -69,7 +74,7 @@ export async function getServerSideProps( } } - await helpers.network.all.prefetch(); + await helpers.network.all.prefetch({}); return { props: { diff --git a/apps/dokploy/server/api/routers/network.ts b/apps/dokploy/server/api/routers/network.ts index ac8a71310..ca1260265 100644 --- a/apps/dokploy/server/api/routers/network.ts +++ b/apps/dokploy/server/api/routers/network.ts @@ -1,11 +1,14 @@ import { createNetwork, findNetworkById, + findNetworksToSync, + importDockerNetworks, inspectNetwork, removeNetwork, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; -import { desc, eq } from "drizzle-orm"; +import { and, desc, eq, isNull } from "drizzle-orm"; +import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { db } from "@/server/db"; import { @@ -16,21 +19,20 @@ import { } from "@/server/db/schema"; export const networkRouter = createTRPCRouter({ - all: protectedProcedure.query(async ({ ctx }) => { - const rows = await db.query.network.findMany({ - where: eq(networkTable.organizationId, ctx.session.activeOrganizationId), - with: { - server: { - columns: { - serverId: true, - name: true, - }, - }, - }, - orderBy: desc(networkTable.createdAt), - }); - return rows; - }), + all: protectedProcedure + .input(z.object({ serverId: z.string().optional() })) + .query(async ({ ctx, input }) => { + const rows = await db.query.network.findMany({ + where: and( + eq(networkTable.organizationId, ctx.session.activeOrganizationId), + input.serverId + ? eq(networkTable.serverId, input.serverId) + : isNull(networkTable.serverId), + ), + orderBy: desc(networkTable.createdAt), + }); + return rows; + }), one: protectedProcedure .input(apiFindOneNetwork) @@ -49,6 +51,30 @@ export const networkRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { return createNetwork(input, ctx.session.activeOrganizationId); }), + networksToSync: protectedProcedure + .input(z.object({ serverId: z.string().optional() })) + .query(async ({ ctx, input }) => { + return findNetworksToSync( + ctx.session.activeOrganizationId, + input.serverId ?? null, + ); + }), + + import: protectedProcedure + .input( + z.object({ + serverId: z.string().optional(), + names: z.array(z.string().min(1)).min(1), + }), + ) + .mutation(async ({ ctx, input }) => { + return importDockerNetworks( + ctx.session.activeOrganizationId, + input.serverId ?? null, + input.names, + ); + }), + inspect: protectedProcedure .input(apiFindOneNetwork) .query(async ({ ctx, input }) => { diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index f01a3044f..1c21598ad 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -1,11 +1,183 @@ import { db } from "@dokploy/server/db"; import { type apiCreateNetwork, network } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; -import { eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import type { z } from "zod"; import { IS_CLOUD } from "../constants"; import { getRemoteDocker } from "../utils/servers/remote-docker"; +// Networks managed by Docker/Dokploy itself that must never be imported +// or deleted through the networks UI +const RESERVED_NETWORKS = [ + "bridge", + "host", + "none", + "ingress", + "docker_gwbridge", + "dokploy-network", +]; + +type DockerNetworkInfo = { + Name: string; + Driver: string; + Internal?: boolean; + Attachable?: boolean; + Ingress?: boolean; + EnableIPv4?: boolean; + EnableIPv6?: boolean; + IPAM?: { + Driver?: string; + Config?: Array<{ + Subnet?: string; + Gateway?: string; + IPRange?: string; + }> | null; + }; +}; + +const isImportableDockerNetwork = (dockerNetwork: DockerNetworkInfo) => + !RESERVED_NETWORKS.includes(dockerNetwork.Name) && + !dockerNetwork.Ingress && + (dockerNetwork.Driver === "bridge" || dockerNetwork.Driver === "overlay"); + +const mapDockerNetworkToRow = ( + dockerNetwork: DockerNetworkInfo, + organizationId: string, + serverId: string | null, +) => ({ + name: dockerNetwork.Name, + driver: dockerNetwork.Driver as "bridge" | "overlay", + internal: dockerNetwork.Internal ?? false, + attachable: dockerNetwork.Attachable ?? false, + // Older daemons don't report EnableIPv4; IPv4 is always on there + enableIPv4: dockerNetwork.EnableIPv4 ?? true, + enableIPv6: dockerNetwork.EnableIPv6 ?? false, + ipam: { + driver: dockerNetwork.IPAM?.Driver, + config: (dockerNetwork.IPAM?.Config ?? []).map((c) => ({ + subnet: c.Subnet, + gateway: c.Gateway, + ipRange: c.IPRange, + })), + }, + organizationId, + serverId, +}); + +const findNetworksByServer = async ( + organizationId: string, + serverId: string | null, +) => { + return await db.query.network.findMany({ + where: and( + eq(network.organizationId, organizationId), + serverId ? eq(network.serverId, serverId) : isNull(network.serverId), + ), + }); +}; + +export const findNetworksToSync = async ( + organizationId: string, + serverId: string | null, +) => { + if (IS_CLOUD && !serverId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Server is required", + }); + } + + const docker = await getRemoteDocker(serverId); + let dockerNetworks: DockerNetworkInfo[] = []; + try { + dockerNetworks = (await docker.listNetworks()) as DockerNetworkInfo[]; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Failed to list Docker networks", + cause: error, + }); + } + + const existing = await findNetworksByServer(organizationId, serverId); + const existingNames = new Set(existing.map((row) => row.name)); + const dockerNames = new Set(dockerNetworks.map((d) => d.Name)); + + const importable = dockerNetworks + .filter( + (dockerNetwork) => + isImportableDockerNetwork(dockerNetwork) && + !existingNames.has(dockerNetwork.Name), + ) + .map((dockerNetwork) => ({ + name: dockerNetwork.Name, + driver: dockerNetwork.Driver, + internal: dockerNetwork.Internal ?? false, + attachable: dockerNetwork.Attachable ?? false, + subnets: (dockerNetwork.IPAM?.Config ?? []) + .map((c) => c.Subnet) + .filter((s): s is string => !!s), + })); + + // Rows in Dokploy whose network no longer exists in Docker + const missing = existing + .filter((row) => !dockerNames.has(row.name)) + .map((row) => ({ networkId: row.networkId, name: row.name })); + + return { importable, missing }; +}; + +export const importDockerNetworks = async ( + organizationId: string, + serverId: string | null, + names: string[], +) => { + if (IS_CLOUD && !serverId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Server is required", + }); + } + + const docker = await getRemoteDocker(serverId); + const existing = await findNetworksByServer(organizationId, serverId); + const existingNames = new Set(existing.map((row) => row.name)); + + const imported: string[] = []; + const errors: { name: string; error: string }[] = []; + + for (const name of names) { + if (existingNames.has(name)) { + errors.push({ name, error: "Already imported" }); + continue; + } + try { + const info = (await docker + .getNetwork(name) + .inspect()) as DockerNetworkInfo; + if (!isImportableDockerNetwork(info)) { + errors.push({ name, error: "Network is reserved or not supported" }); + continue; + } + await db + .insert(network) + .values(mapDockerNetworkToRow(info, organizationId, serverId)); + imported.push(name); + } catch (error) { + errors.push({ + name, + error: + error instanceof Error ? error.message : "Failed to inspect network", + }); + } + } + + return { imported, errors }; +}; + export const findNetworkById = async (networkId: string) => { const [row] = await db .select() From 8a376266369dcdf2e8473429a5464213ac6c7f08 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 21 Jul 2026 23:10:33 -0600 Subject: [PATCH 18/22] feat(networks): implement network recreation functionality and enhance synchronization - Added a new API endpoint for recreating Docker networks that have been removed, allowing users to restore networks directly from the Dokploy interface. - Updated the ShowNetworks and SyncNetworks components to include options for recreating networks, improving user management capabilities. - Enhanced the network synchronization process to better handle stale records, providing users with clear actions for network maintenance. - Refactored network creation logic to streamline the process and ensure consistency across network management operations. --- .../dashboard/networks/show-networks.tsx | 517 ++++++++++++++---- .../dashboard/networks/sync-networks.tsx | 56 +- apps/dokploy/server/api/routers/network.ts | 14 + packages/server/src/services/network.ts | 88 +-- 4 files changed, 509 insertions(+), 166 deletions(-) diff --git a/apps/dokploy/components/dashboard/networks/show-networks.tsx b/apps/dokploy/components/dashboard/networks/show-networks.tsx index 3059076bc..d87278b1e 100644 --- a/apps/dokploy/components/dashboard/networks/show-networks.tsx +++ b/apps/dokploy/components/dashboard/networks/show-networks.tsx @@ -1,6 +1,25 @@ "use client"; -import { Loader2, Network, Trash2 } from "lucide-react"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + type PaginationState, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; +import type { inferRouterOutputs } from "@trpc/server"; +import { + ArrowUpDown, + Loader2, + Network, + RotateCcw, + ShieldCheck, + Trash2, +} from "lucide-react"; +import { useMemo, useState } from "react"; import { toast } from "sonner"; import { HandleNetwork } from "@/components/dashboard/networks/handle-network"; import { ShowNetworkConfig } from "@/components/dashboard/networks/show-network-config"; @@ -16,6 +35,14 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -24,17 +51,294 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import type { AppRouter } from "@/server/api/root"; import { api } from "@/utils/api"; +type NetworkRow = inferRouterOutputs["network"]["all"][number]; + interface Props { /** Selected server; undefined shows the local Dokploy server */ serverId?: string; } +const getIpamEntries = (row: NetworkRow) => + (row.ipam?.config ?? []).filter((c) => c.subnet || c.gateway || c.ipRange); + +const SortableHeader = ({ + column, + title, +}: { + column: { + getIsSorted: () => false | "asc" | "desc"; + toggleSorting: (asc: boolean) => void; + }; + title: string; +}) => ( + +); + export const ShowNetworks = ({ serverId }: Props) => { const utils = api.useUtils(); + const [verified, setVerified] = useState(false); + const [sorting, setSorting] = useState([ + { id: "createdAt", desc: true }, + ]); + const [globalFilter, setGlobalFilter] = useState(""); + const [driverFilter, setDriverFilter] = useState("all"); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const { data: networks, isLoading } = api.network.all.useQuery({ serverId }); const { mutateAsync: removeNetwork } = api.network.remove.useMutation(); + const recreateMutation = api.network.recreate.useMutation(); + + // Same query the Sync dialog uses; "missing" tells us which records + // no longer have a real network in Docker + const { + data: syncStatus, + isFetching: isVerifying, + refetch: refetchVerify, + } = api.network.networksToSync.useQuery({ serverId }, { enabled: verified }); + + const missingIds = useMemo( + () => new Set(syncStatus?.missing.map((m) => m.networkId) ?? []), + [syncStatus], + ); + + const onVerify = async () => { + setVerified(true); + const { data: result, error } = await refetchVerify(); + if (error) { + toast.error("Error verifying networks", { + description: error.message, + }); + return; + } + if (!result) return; + if (result.missing.length === 0) { + toast.success("All networks exist in Docker"); + } else { + toast.warning( + `${result.missing.length} network(s) no longer exist in Docker`, + ); + } + }; + + const filteredData = useMemo(() => { + let list = networks ?? []; + if (driverFilter !== "all") { + list = list.filter((n) => n.driver === driverFilter); + } + if (globalFilter.trim()) { + const query = globalFilter.toLowerCase(); + list = list.filter( + (n) => + n.name.toLowerCase().includes(query) || + (n.ipam?.config ?? []).some( + (c) => + c.subnet?.toLowerCase().includes(query) || + c.gateway?.toLowerCase().includes(query) || + c.ipRange?.toLowerCase().includes(query), + ), + ); + } + return list; + }, [networks, driverFilter, globalFilter]); + + const columns = useMemo[]>( + () => [ + { + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => ( +
+ {row.original.name} + {verified && + syncStatus && + (missingIds.has(row.original.networkId) ? ( + <> + Missing in Docker + + + ) : ( + In sync + ))} +
+ ), + }, + { + accessorKey: "driver", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.driver} + + {row.original.driver === "overlay" ? "swarm" : "local"} + +
+ ), + }, + { + id: "subnet", + accessorFn: (row) => getIpamEntries(row)[0]?.subnet ?? "", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const ipamEntries = getIpamEntries(row.original); + if (ipamEntries.length === 0) { + return Auto; + } + return ( +
+ {ipamEntries.map((c, index) => ( +
+ {c.subnet ?? "—"} + {(c.gateway || c.ipRange) && ( + + {[ + c.gateway && `gw ${c.gateway}`, + c.ipRange && `range ${c.ipRange}`, + ] + .filter(Boolean) + .join(" · ")} + + )} +
+ ))} +
+ ); + }, + }, + { + accessorKey: "internal", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.internal ? "Yes" : "No"} + + ), + }, + { + accessorKey: "attachable", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.attachable ? "Yes" : "No"} + + ), + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {new Date(row.original.createdAt).toLocaleDateString()} + + ), + }, + { + id: "actions", + enableSorting: false, + header: () =>
Actions
, + cell: ({ row }) => ( +
+ + { + try { + await removeNetwork({ + networkId: row.original.networkId, + }); + toast.success("Network deleted"); + await utils.network.all.invalidate(); + await utils.network.networksToSync.invalidate(); + } catch (error) { + toast.error("Error deleting network", { + description: + error instanceof Error ? error.message : "Unknown error", + }); + } + }} + > + + +
+ ), + }, + ], + [verified, syncStatus, missingIds, removeNetwork, recreateMutation, utils], + ); + + const table = useReactTable({ + data: filteredData, + columns, + state: { + sorting, + pagination, + }, + onSortingChange: setSorting, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); return (
@@ -50,6 +354,16 @@ export const ShowNetworks = ({ serverId }: Props) => {
+ {networks && networks.length > 0 && ( + + )} {networks && networks.length > 0 && ( @@ -58,7 +372,7 @@ export const ShowNetworks = ({ serverId }: Props) => { - + {isLoading ? (
Loading... @@ -80,118 +394,97 @@ export const ShowNetworks = ({ serverId }: Props) => {
) : ( -
- - - - Name - Driver - Subnet - Internal - Attachable - Created - - Actions - - - - - {networks.map((n) => { - const ipamEntries = (n.ipam?.config ?? []).filter( - (c) => c.subnet || c.gateway || c.ipRange, - ); - return ( - - - {n.name} - - -
- {n.driver} - - {n.driver === "overlay" ? "swarm" : "local"} - -
-
- - {ipamEntries.length > 0 ? ( -
- {ipamEntries.map((c, index) => ( -
- {c.subnet ?? "—"} - {(c.gateway || c.ipRange) && ( - - {[ - c.gateway && `gw ${c.gateway}`, - c.ipRange && `range ${c.ipRange}`, - ] - .filter(Boolean) - .join(" · ")} - - )} -
- ))} -
- ) : ( - - Auto - - )} -
- - {n.internal ? "Yes" : "No"} - - - {n.attachable ? "Yes" : "No"} - - - {new Date(n.createdAt).toLocaleDateString()} - - -
- - { - try { - await removeNetwork({ - networkId: n.networkId, - }); - toast.success("Network deleted"); - await utils.network.all.invalidate(); - } catch (error) { - toast.error("Error deleting network", { - description: - error instanceof Error - ? error.message - : "Unknown error", - }); - } - }} - > - - -
+ <> +
+ setGlobalFilter(e.target.value)} + className="max-w-xs" + /> + +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No networks match your filters. - ); - })} - -
-
+ )} + + +
+ {table.getPageCount() > 1 && ( +
+ + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + +
+ + +
+
+ )} + )}
diff --git a/apps/dokploy/components/dashboard/networks/sync-networks.tsx b/apps/dokploy/components/dashboard/networks/sync-networks.tsx index 4b031d70b..2a0403048 100644 --- a/apps/dokploy/components/dashboard/networks/sync-networks.tsx +++ b/apps/dokploy/components/dashboard/networks/sync-networks.tsx @@ -1,6 +1,6 @@ "use client"; -import { Loader2, RefreshCw, Trash2 } from "lucide-react"; +import { Loader2, RefreshCw, RotateCcw, Trash2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; @@ -20,13 +20,9 @@ import { Separator } from "@/components/ui/separator"; import { api } from "@/utils/api"; interface Props { - /** Target server; undefined syncs the local Dokploy server */ serverId?: string; } -// Compares the Docker daemon's networks against Dokploy's records: -// networks that only exist in Docker can be imported, and records whose -// network no longer exists in Docker can be cleaned up. export const SyncNetworks = ({ serverId }: Props) => { const [open, setOpen] = useState(false); const [selected, setSelected] = useState>(new Set()); @@ -37,6 +33,7 @@ export const SyncNetworks = ({ serverId }: Props) => { const importMutation = api.network.import.useMutation(); const removeMutation = api.network.remove.useMutation(); + const recreateMutation = api.network.recreate.useMutation(); const toggleSelected = (name: string) => { setSelected((prev) => { @@ -91,6 +88,20 @@ export const SyncNetworks = ({ serverId }: Props) => { } }; + const onRecreate = async (networkId: string, name: string) => { + try { + await recreateMutation.mutateAsync({ networkId }); + toast.success(`Network "${name}" recreated in Docker`); + await utils.network.all.invalidate(); + await utils.network.networksToSync.invalidate(); + await refetch(); + } catch (error) { + toast.error("Error recreating network", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + return ( { className="flex items-center justify-between gap-3 rounded-lg border border-dashed p-3" > {stale.name} - +
+ + +
))} diff --git a/apps/dokploy/server/api/routers/network.ts b/apps/dokploy/server/api/routers/network.ts index ca1260265..68372b4d8 100644 --- a/apps/dokploy/server/api/routers/network.ts +++ b/apps/dokploy/server/api/routers/network.ts @@ -4,6 +4,7 @@ import { findNetworksToSync, importDockerNetworks, inspectNetwork, + recreateNetwork, removeNetwork, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; @@ -88,6 +89,19 @@ export const networkRouter = createTRPCRouter({ return inspectNetwork(input.networkId); }), + recreate: protectedProcedure + .input(apiFindOneNetwork) + .mutation(async ({ ctx, input }) => { + const network = await findNetworkById(input.networkId); + if (network.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Network not found", + }); + } + return recreateNetwork(input.networkId); + }), + remove: protectedProcedure .input(apiRemoveNetwork) .mutation(async ({ ctx, input }) => { diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index 1c21598ad..c63e73c8c 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -224,44 +224,7 @@ export const createNetwork = async ( }); } - const ipam = row.ipam ?? {}; - const ipamConfig = (ipam.config ?? []) - .map((c) => { - const entry: Record = {}; - if (c.subnet) entry.Subnet = c.subnet; - if (c.gateway) entry.Gateway = c.gateway; - if (c.ipRange) entry.IPRange = c.ipRange; - return entry; - }) - .filter((e) => Object.keys(e).length > 0); - - const docker = await getRemoteDocker(input.serverId ?? null); - try { - await docker.createNetwork({ - Name: row.name, - Driver: row.driver, - CheckDuplicate: true, - Internal: row.internal, - Attachable: row.attachable, - // EnableIPv4 is missing from dockerode's types but supported by - // the daemon (API >= 1.47); the body is sent as-is - EnableIPv4: row.enableIPv4, - EnableIPv6: row.enableIPv6, - IPAM: { - Driver: ipam.driver || "default", - Config: ipamConfig.length > 0 ? ipamConfig : undefined, - }, - } as Parameters[0]); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - error instanceof Error - ? error.message - : "Failed to create Docker network", - cause: error, - }); - } + await createDockerNetworkFromRow(row); return row; }); @@ -269,6 +232,55 @@ export const createNetwork = async ( return created; }; +const createDockerNetworkFromRow = async (row: typeof network.$inferSelect) => { + const ipam = row.ipam ?? {}; + const ipamConfig = (ipam.config ?? []) + .map((c) => { + const entry: Record = {}; + if (c.subnet) entry.Subnet = c.subnet; + if (c.gateway) entry.Gateway = c.gateway; + if (c.ipRange) entry.IPRange = c.ipRange; + return entry; + }) + .filter((e) => Object.keys(e).length > 0); + + const docker = await getRemoteDocker(row.serverId ?? null); + try { + await docker.createNetwork({ + Name: row.name, + Driver: row.driver, + CheckDuplicate: true, + Internal: row.internal, + Attachable: row.attachable, + // EnableIPv4 is missing from dockerode's types but supported by + // the daemon (API >= 1.47); the body is sent as-is + EnableIPv4: row.enableIPv4, + EnableIPv6: row.enableIPv6, + IPAM: { + Driver: ipam.driver || "default", + Config: ipamConfig.length > 0 ? ipamConfig : undefined, + }, + } as Parameters[0]); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Failed to create Docker network", + cause: error, + }); + } +}; + +// Re-creates the Docker network from the stored record, for records whose +// network was removed from Docker outside of Dokploy +export const recreateNetwork = async (networkId: string) => { + const row = await findNetworkById(networkId); + await createDockerNetworkFromRow(row); + return row; +}; + export const inspectNetwork = async (networkId: string) => { const row = await findNetworkById(networkId); From 97885ee44a80ddda8f635d57534b02b34c070c85 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 25 Jul 2026 03:09:54 -0600 Subject: [PATCH 19/22] feat(networks): enhance network management and UI components - Added functionality for assigning Docker networks to services, allowing users to manage network associations directly from the UI. - Introduced new components for assigning networks to both individual services and compose files, improving user experience and flexibility. - Updated existing components to integrate network assignment features, ensuring seamless interaction within the dashboard. - Enhanced the database schema to support new network-related fields, including `networkIds` and `detachDokployNetwork`, for better service configuration. - Improved the layout and responsiveness of various UI elements to accommodate new features and enhance usability. --- .gitignore | 3 +- apps/dokploy/__test__/drop/drop.test.ts | 2 + apps/dokploy/__test__/traefik/traefik.test.ts | 2 + .../advanced/import/show-import.tsx | 18 +- .../compose/advanced/add-isolation.tsx | 11 +- .../general/show-converted-compose.tsx | 15 +- .../networks/assign-compose-networks.tsx | 347 +++++++++++++++++ .../dashboard/networks/assign-networks.tsx | 355 ++++++++++++++++++ .../dashboard/requests/requests-table.tsx | 2 +- .../show-database-advanced-settings.tsx | 2 + .../dokploy/components/shared/drawer-logs.tsx | 2 +- apps/dokploy/components/ui/textarea.tsx | 2 +- ...ial.sql => 0175_fantastic_peter_quill.sql} | 10 +- apps/dokploy/drizzle/meta/0175_snapshot.json | 292 ++++++++------ apps/dokploy/drizzle/meta/_journal.json | 4 +- .../services/application/[applicationId].tsx | 2 + .../services/compose/[composeId].tsx | 2 + .../services/libsql/[libsqlId].tsx | 2 +- .../services/mariadb/[mariadbId].tsx | 2 +- .../services/mongo/[mongoId].tsx | 2 +- .../services/mysql/[mysqlId].tsx | 2 +- .../services/postgres/[postgresId].tsx | 2 +- .../services/redis/[redisId].tsx | 2 +- apps/dokploy/server/api/routers/ai.ts | 1 - apps/dokploy/server/api/routers/compose.ts | 2 - .../dokploy/server/api/routers/environment.ts | 1 + apps/dokploy/server/api/routers/schedule.ts | 8 +- .../server/api/routers/volume-backups.ts | 18 +- packages/server/src/db/schema/application.ts | 4 + packages/server/src/db/schema/compose.ts | 28 +- packages/server/src/db/schema/index.ts | 2 +- packages/server/src/db/schema/libsql.ts | 6 + packages/server/src/db/schema/mariadb.ts | 14 +- packages/server/src/db/schema/mongo.ts | 5 + packages/server/src/db/schema/mysql.ts | 14 +- packages/server/src/db/schema/postgres.ts | 14 +- packages/server/src/db/schema/redis.ts | 14 +- packages/server/src/services/deployment.ts | 13 +- packages/server/src/services/domain.ts | 12 +- packages/server/src/services/environment.ts | 49 ++- packages/server/src/services/mount.ts | 92 +---- packages/server/src/services/network.ts | 32 +- packages/server/src/services/project.ts | 79 +++- packages/server/src/services/rollbacks.ts | 8 +- packages/server/src/services/server.ts | 17 +- packages/server/src/templates/index.ts | 5 +- packages/server/src/utils/builders/index.ts | 6 +- packages/server/src/utils/databases/libsql.ts | 6 +- .../server/src/utils/databases/mariadb.ts | 6 +- packages/server/src/utils/databases/mongo.ts | 6 +- packages/server/src/utils/databases/mysql.ts | 6 +- .../server/src/utils/databases/postgres.ts | 6 +- packages/server/src/utils/databases/redis.ts | 6 +- packages/server/src/utils/docker/domain.ts | 106 +++++- packages/server/src/utils/docker/utils.ts | 8 - packages/server/src/utils/schedules/index.ts | 8 +- .../server/src/utils/volume-backups/index.ts | 4 +- 57 files changed, 1382 insertions(+), 307 deletions(-) create mode 100644 apps/dokploy/components/dashboard/networks/assign-compose-networks.tsx create mode 100644 apps/dokploy/components/dashboard/networks/assign-networks.tsx rename apps/dokploy/drizzle/{0175_gray_dreaming_celestial.sql => 0175_fantastic_peter_quill.sql} (60%) diff --git a/.gitignore b/.gitignore index d39d787d4..602556df8 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ yarn-error.log* .db -.playwright-* \ No newline at end of file +.playwright-* +.credentials \ No newline at end of file diff --git a/apps/dokploy/__test__/drop/drop.test.ts b/apps/dokploy/__test__/drop/drop.test.ts index a524e8da0..eda3b9f0d 100644 --- a/apps/dokploy/__test__/drop/drop.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.ts @@ -32,6 +32,8 @@ const baseApp: ApplicationNested = { railpackVersion: "0.15.4", applicationId: "", previewLabels: [], + networkIds: [], + detachDokployNetwork: false, createEnvFile: true, bitbucketRepositorySlug: "", herokuVersion: "", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 68758ca2d..b7b0f5645 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -7,6 +7,8 @@ const baseApp: ApplicationNested = { rollbackActive: false, applicationId: "", previewLabels: [], + networkIds: [], + detachDokployNetwork: false, createEnvFile: true, bitbucketRepositorySlug: "", herokuVersion: "", diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx index f06b173cf..db39db2e8 100644 --- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => { - + Template Information @@ -199,7 +199,7 @@ export const ShowImport = ({ composeId }: Props) => { -
+
@@ -207,12 +207,14 @@ export const ShowImport = ({ composeId }: Props) => { Docker Compose
- +
+ +
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx index 352571909..730fe7a7b 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, @@ -111,7 +112,10 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => { return ( - Enable Isolated Deployment + + Enable Isolated Deployment + Deprecated + Configure isolated deployment to the compose file.
@@ -138,6 +142,11 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
+ + Isolated deployment is deprecated. Use the Networks section above to + attach networks per service and detach them from dokploy-network — + it is declarative and does not break on restarts. + {isError && {error?.message}}
{ Preview Compose - + Converted Compose @@ -62,10 +62,6 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { {isError && {error?.message}} - - Preview your docker-compose file with added domains. Note: At least - one domain must be specified for this conversion to take effect. - {isPending ? (
@@ -79,7 +75,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
) : ( <> -
+
-
+						
-
+
)}
diff --git a/apps/dokploy/components/dashboard/networks/assign-compose-networks.tsx b/apps/dokploy/components/dashboard/networks/assign-compose-networks.tsx new file mode 100644 index 000000000..dfde4907c --- /dev/null +++ b/apps/dokploy/components/dashboard/networks/assign-compose-networks.tsx @@ -0,0 +1,347 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Check, ChevronsUpDown, Loader2, RefreshCw } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Command, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; + +interface Props { + composeId: string; +} + +const serviceSchema = z.object({ + serviceName: z.string(), + networkIds: z.array(z.string()), + detachDokployNetwork: z.boolean(), +}); + +const formSchema = z.object({ + services: z.array(serviceSchema), +}); + +type FormValues = z.infer; + +export const AssignComposeNetworks = ({ composeId }: Props) => { + const [cacheType, setCacheType] = useState<"cache" | "fetch">("cache"); + + const { data: compose } = api.compose.one.useQuery({ composeId }); + const { + data: services, + isLoading: isLoadingServices, + error: servicesError, + refetch: refetchServices, + isRefetching: isRefetchingServices, + } = api.compose.loadServices.useQuery( + { composeId, type: cacheType }, + { retry: false }, + ); + + const onRetry = () => { + setCacheType("fetch"); + setTimeout(() => refetchServices(), 0); + }; + + const { data: networks } = api.network.all.useQuery( + { serverId: compose?.serverId ?? undefined }, + { enabled: compose !== undefined }, + ); + const { mutateAsync, isPending } = api.compose.update.useMutation(); + const utils = api.useUtils(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { services: [] }, + }); + const { fields } = useFieldArray({ + control: form.control, + name: "services", + }); + + useEffect(() => { + if (!services) return; + const serviceNetworks = compose?.serviceNetworks ?? []; + form.reset({ + services: services.map((serviceName) => { + const config = serviceNetworks.find( + (s) => s.serviceName === serviceName, + ); + return { + serviceName, + networkIds: config?.networkIds ?? [], + detachDokployNetwork: config?.detachDokployNetwork ?? false, + }; + }), + }); + }, [services, compose?.serviceNetworks, form]); + + const allowsBridge = compose?.composeType === "docker-compose"; + const availableNetworks = (networks ?? []).filter( + (n) => allowsBridge || n.driver === "overlay", + ); + + const onSubmit = async (values: FormValues) => { + try { + const serviceNetworks = values.services.filter( + (s) => s.networkIds.length > 0 || s.detachDokployNetwork, + ); + await mutateAsync({ + composeId, + serviceNetworks, + }); + toast.success("Networks updated. Redeploy the compose to apply them."); + await utils.compose.one.invalidate({ composeId }); + } catch (error) { + toast.error("Error updating networks", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + return ( + + +
+ Networks + + Attach Docker networks per service and detach it from + dokploy-network. Takes effect on the next deploy. + +
+ +
+ + {servicesError ? ( +
+ + Could not load the compose services. If this compose was just + created from a template, it hasn't been cloned yet — click Reload + to clone the repository and read its services. + +
+ +
+
+ ) : isLoadingServices ? ( +
+ Loading services... + +
+ ) : !services?.length ? ( + + No services found in this compose. + + ) : ( + + + {fields.map((fieldItem, index) => ( + + ))} +
+ +
+ + + )} +
+
+ ); +}; + +type NetworkOption = { networkId: string; name: string; driver: string }; + +const ServiceRow = ({ + control, + index, + service, + availableNetworks, +}: { + control: ReturnType>["control"]; + index: number; + service: string; + availableNetworks: NetworkOption[]; +}) => { + const [open, setOpen] = useState(false); + + return ( +
+ ( + + {service} +
+ + Detach dokploy-network + + + + +
+
+ )} + /> + + { + const selectedNetworks = availableNetworks.filter((n) => + field.value.includes(n.networkId), + ); + const toggle = (networkId: string) => { + field.onChange( + field.value.includes(networkId) + ? field.value.filter((id) => id !== networkId) + : [...field.value, networkId], + ); + }; + return ( + + + + + + + + + + {availableNetworks.length === 0 ? ( +
+ No networks available on this server. +
+ ) : ( + + {availableNetworks.map((n) => { + const isSelected = field.value.includes( + n.networkId, + ); + return ( + toggle(n.networkId)} + className="cursor-pointer" + > + toggle(n.networkId)} + /> + {n.name} + + {n.driver} + + + + ); + })} + + )} +
+
+
+
+ + {selectedNetworks.length > 0 && ( +
+ {selectedNetworks.map((n) => ( + + {n.name} + + ))} +
+ )} +
+ ); + }} + /> +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/networks/assign-networks.tsx b/apps/dokploy/components/dashboard/networks/assign-networks.tsx new file mode 100644 index 000000000..1ae7ec79a --- /dev/null +++ b/apps/dokploy/components/dashboard/networks/assign-networks.tsx @@ -0,0 +1,355 @@ +import { Check, ChevronsUpDown, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; + +type ServiceType = + | "application" + | "postgres" + | "mysql" + | "mariadb" + | "mongo" + | "redis" + | "libsql"; + +interface Props { + id: string; + type: ServiceType; +} + +export const AssignNetworks = ({ id, type }: Props) => { + const [open, setOpen] = useState(false); + const [selected, setSelected] = useState([]); + const [detached, setDetached] = useState(false); + + const { + service, + serverId, + networkIds, + detachDokployNetwork, + updateAsync, + isUpdating, + refetch, + } = useServiceNetworks(id, type); + + const { data: networks } = api.network.all.useQuery( + { serverId: serverId ?? undefined }, + { enabled: service !== undefined }, + ); + + const { data: applicationDomains } = api.domain.byApplicationId.useQuery( + { applicationId: id }, + { enabled: type === "application" }, + ); + const hasDomains = (applicationDomains?.length ?? 0) > 0; + + useEffect(() => { + setSelected(networkIds ?? []); + setDetached(detachDokployNetwork ?? false); + }, [networkIds, detachDokployNetwork]); + + const availableNetworks = (networks ?? []).filter( + (n) => n.driver === "overlay", + ); + const selectedNetworks = availableNetworks.filter((n) => + selected.includes(n.networkId), + ); + const isDirty = + selected.length !== (networkIds?.length ?? 0) || + selected.some((networkId) => !networkIds?.includes(networkId)) || + detached !== detachDokployNetwork; + + const toggle = (networkId: string) => { + setSelected((prev) => + prev.includes(networkId) + ? prev.filter((id) => id !== networkId) + : [...prev, networkId], + ); + }; + + const onSave = async () => { + try { + await updateAsync({ + networkIds: selected, + detachDokployNetwork: detached, + }); + toast.success("Networks updated. Redeploy the service to apply them."); + await refetch(); + } catch (error) { + toast.error("Error updating networks", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + return ( + + +
+ Networks + + Attach additional Docker networks to this service so it can reach + services on those networks. Takes effect on the next deploy. + +
+
+ +
+
+
+ + Detach from dokploy-network + + dokploy-network +
+

+ By default the service joins the shared dokploy-network. Detach it + to keep it reachable only through the networks you attach below. +

+
+ +
+ + {detached && hasDomains && ( + + Warning: this service has domains. Detaching it from dokploy-network + will break Traefik routing, and its domains will stop working. + + )} + + {detached && !hasDomains && selected.length === 0 && ( + + This service is detached from dokploy-network but has no other + network attached. It would be unreachable, so dokploy-network will + be kept until you attach a network below. + + )} + + + + + + + + + + {availableNetworks.length === 0 ? ( +
+ No overlay networks on this server. +
+ ) : ( + <> + + No networks found. + + + {availableNetworks.map((n) => { + const isSelected = selected.includes(n.networkId); + return ( + toggle(n.networkId)} + className="cursor-pointer" + > + toggle(n.networkId)} + /> + {n.name} + + {n.driver} + + + + ); + })} + + + )} +
+
+
+
+ + {selectedNetworks.length > 0 && ( +
+ {selectedNetworks.map((n) => ( + + {n.name} + + + ))} +
+ )} + +
+ +
+
+
+ ); +}; + +// Maps a service type to its one-query and update-mutation, normalizing the +// per-type id field name and the networkIds field. +const useServiceNetworks = (id: string, type: ServiceType) => { + const application = api.application.one.useQuery( + { applicationId: id }, + { enabled: type === "application" }, + ); + const postgres = api.postgres.one.useQuery( + { postgresId: id }, + { enabled: type === "postgres" }, + ); + const mysql = api.mysql.one.useQuery( + { mysqlId: id }, + { enabled: type === "mysql" }, + ); + const mariadb = api.mariadb.one.useQuery( + { mariadbId: id }, + { enabled: type === "mariadb" }, + ); + const mongo = api.mongo.one.useQuery( + { mongoId: id }, + { enabled: type === "mongo" }, + ); + const redis = api.redis.one.useQuery( + { redisId: id }, + { enabled: type === "redis" }, + ); + const libsql = api.libsql.one.useQuery( + { libsqlId: id }, + { enabled: type === "libsql" }, + ); + const applicationUpdate = api.application.update.useMutation(); + const postgresUpdate = api.postgres.update.useMutation(); + const mysqlUpdate = api.mysql.update.useMutation(); + const mariadbUpdate = api.mariadb.update.useMutation(); + const mongoUpdate = api.mongo.update.useMutation(); + const redisUpdate = api.redis.update.useMutation(); + const libsqlUpdate = api.libsql.update.useMutation(); + + const map = { + application: { + query: application, + mutation: applicationUpdate, + save: (payload: SavePayload) => + applicationUpdate.mutateAsync({ applicationId: id, ...payload }), + }, + postgres: { + query: postgres, + mutation: postgresUpdate, + save: (payload: SavePayload) => + postgresUpdate.mutateAsync({ postgresId: id, ...payload }), + }, + mysql: { + query: mysql, + mutation: mysqlUpdate, + save: (payload: SavePayload) => + mysqlUpdate.mutateAsync({ mysqlId: id, ...payload }), + }, + mariadb: { + query: mariadb, + mutation: mariadbUpdate, + save: (payload: SavePayload) => + mariadbUpdate.mutateAsync({ mariadbId: id, ...payload }), + }, + mongo: { + query: mongo, + mutation: mongoUpdate, + save: (payload: SavePayload) => + mongoUpdate.mutateAsync({ mongoId: id, ...payload }), + }, + redis: { + query: redis, + mutation: redisUpdate, + save: (payload: SavePayload) => + redisUpdate.mutateAsync({ redisId: id, ...payload }), + }, + libsql: { + query: libsql, + mutation: libsqlUpdate, + save: (payload: SavePayload) => + libsqlUpdate.mutateAsync({ libsqlId: id, ...payload }), + }, + }[type]; + + const service = map.query.data; + + return { + service, + serverId: service?.serverId ?? null, + networkIds: service?.networkIds ?? [], + detachDokployNetwork: service?.detachDokployNetwork ?? false, + updateAsync: map.save, + isUpdating: map.mutation.isPending, + refetch: map.query.refetch, + }; +}; + +type SavePayload = { + networkIds: string[]; + detachDokployNetwork: boolean; +}; diff --git a/apps/dokploy/components/dashboard/requests/requests-table.tsx b/apps/dokploy/components/dashboard/requests/requests-table.tsx index d7e836a61..802b60e46 100644 --- a/apps/dokploy/components/dashboard/requests/requests-table.tsx +++ b/apps/dokploy/components/dashboard/requests/requests-table.tsx @@ -328,7 +328,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => { open={!!selectedRow} onOpenChange={(_open) => setSelectedRow(undefined)} > - + Request log diff --git a/apps/dokploy/components/dashboard/shared/show-database-advanced-settings.tsx b/apps/dokploy/components/dashboard/shared/show-database-advanced-settings.tsx index bf2089498..0534c9178 100644 --- a/apps/dokploy/components/dashboard/shared/show-database-advanced-settings.tsx +++ b/apps/dokploy/components/dashboard/shared/show-database-advanced-settings.tsx @@ -1,5 +1,6 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-resources"; import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; +import { AssignNetworks } from "@/components/dashboard/networks/assign-networks"; import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command"; import { ShowClusterSettings } from "../application/advanced/cluster/show-cluster-settings"; import { RebuildDatabase } from "./rebuild-database"; @@ -21,6 +22,7 @@ export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => { ) : null} +
diff --git a/apps/dokploy/components/shared/drawer-logs.tsx b/apps/dokploy/components/shared/drawer-logs.tsx index 38f8b5db4..301a0fe0d 100644 --- a/apps/dokploy/components/shared/drawer-logs.tsx +++ b/apps/dokploy/components/shared/drawer-logs.tsx @@ -47,7 +47,7 @@ export const DrawerLogs = ({ isOpen, onClose, filteredLogs }: Props) => { onClose(); }} > - + Deployment Logs Details of the request log entry. diff --git a/apps/dokploy/components/ui/textarea.tsx b/apps/dokploy/components/ui/textarea.tsx index 959d789e2..4a8168211 100644 --- a/apps/dokploy/components/ui/textarea.tsx +++ b/apps/dokploy/components/ui/textarea.tsx @@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {