diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index 962666fa9..4b0de9586 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -37,14 +37,7 @@ import { AddSwarmSettings } from "./modify-swarm-settings"; interface Props { id: string; - type: - | "application" - | "libsql" - | "mariadb" - | "mongo" - | "mysql" - | "postgres" - | "redis"; + type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis"; } const AddRedirectchema = z.object({ @@ -58,7 +51,6 @@ export const ShowClusterSettings = ({ id, type }: Props) => { const queryMap = { application: () => api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), - libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }), mariadb: () => api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), @@ -114,7 +106,6 @@ export const ShowClusterSettings = ({ id, type }: Props) => { const onSubmit = async (data: AddCommand) => { await mutateAsync({ applicationId: id || "", - libsqlId: id || "", mariadbId: id || "", mongoId: id || "", mysqlId: id || "", diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx index 4a5d0270b..1ae24fd4f 100644 --- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx @@ -37,15 +37,16 @@ interface Props { export const ShowEnvironment = ({ id, type }: Props) => { const queryMap = { - postgres: () => - api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), - redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), - mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + compose: () => + api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }), mariadb: () => api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), - compose: () => - api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), }; const { data, refetch } = queryMap[type] ? queryMap[type]() @@ -53,12 +54,13 @@ export const ShowEnvironment = ({ id, type }: Props) => { const [isEnvVisible, setIsEnvVisible] = useState(true); const mutationMap = { - postgres: () => api.postgres.update.useMutation(), - redis: () => api.redis.update.useMutation(), - mysql: () => api.mysql.update.useMutation(), + compose: () => api.compose.update.useMutation(), + libsql: () => api.libsql.update.useMutation(), mariadb: () => api.mariadb.update.useMutation(), mongo: () => api.mongo.update.useMutation(), - compose: () => api.compose.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), }; const { mutateAsync, isLoading } = mutationMap[type] ? mutationMap[type]() @@ -85,12 +87,13 @@ export const ShowEnvironment = ({ id, type }: Props) => { const onSubmit = async (formData: EnvironmentSchema) => { mutateAsync({ + composeId: id || "", + libsqlId: id || "", + mariadbId: id || "", mongoId: id || "", + mysqlId: id || "", postgresId: id || "", redisId: id || "", - mysqlId: id || "", - mariadbId: id || "", - composeId: id || "", env: formData.environment, }) .then(async () => { diff --git a/apps/dokploy/components/dashboard/libsql/general/show-external-libsql-credentials.tsx b/apps/dokploy/components/dashboard/libsql/general/show-external-libsql-credentials.tsx index 562f8271e..73f6c2eac 100644 --- a/apps/dokploy/components/dashboard/libsql/general/show-external-libsql-credentials.tsx +++ b/apps/dokploy/components/dashboard/libsql/general/show-external-libsql-credentials.tsx @@ -51,13 +51,29 @@ const createDockerProviderSchema = (sqldNode?: string) => .gte(0, "Range must be 0 - 65535") .lte(65535, "Range must be 0 - 65535") .nullable()), + externalAdminPort: z.preprocess((a) => { + if (a !== null) { + const parsed = Number.parseInt(z.string().parse(a), 10); + return Number.isNaN(parsed) ? null : parsed; + } + return null; + }, z + .number() + .gte(0, "Range must be 0 - 65535") + .lte(65535, "Range must be 0 - 65535") + .nullable()), }) .superRefine((data, ctx) => { - if (data.externalPort === null && data.externalGRPCPort === null) { + if ( + data.externalPort === null && + data.externalGRPCPort === null && + data.externalAdminPort === null + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Either externalPort or externalGRPCPort must be provided.", - path: ["externalPort", "externalGRPCPort"], + message: + "Either externalPort, externalGRPCPort or externalAdminPort must be provided.", + path: ["externalPort", "externalGRPCPort", "externalAdminPort"], }); } if (sqldNode === "replica" && data.externalGRPCPort !== null) { @@ -91,12 +107,16 @@ export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => { useEffect(() => { const fieldsToUpdate: Partial = {}; + if (data?.externalPort !== undefined) { + fieldsToUpdate.externalPort = data.externalPort; + } + if (data?.externalGRPCPort !== undefined) { fieldsToUpdate.externalGRPCPort = data.externalGRPCPort; } - if (data?.externalPort !== undefined) { - fieldsToUpdate.externalPort = data.externalPort; + if (data?.externalAdminPort !== undefined) { + fieldsToUpdate.externalAdminPort = data.externalAdminPort; } if (Object.keys(fieldsToUpdate).length > 0) { @@ -108,6 +128,7 @@ export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => { await mutateAsync({ externalPort: values.externalPort, externalGRPCPort: values.externalGRPCPort, + externalAdminPort: values.externalAdminPort, libsqlId, }) .then(async () => { @@ -205,6 +226,29 @@ export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => { )} +
+ { + return ( + + + External Admin Port (Internet) + + + + + + + ); + }} + /> +
{data?.sqldNode !== "replica" && ( <>
diff --git a/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx b/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx index 9a5612528..0dcf52264 100644 --- a/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx +++ b/apps/dokploy/components/dashboard/libsql/general/show-internal-libsql-credentials.tsx @@ -1,5 +1,7 @@ +import { SelectGroup } from "@radix-ui/react-select"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -63,11 +65,39 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
+
+ + +
+
+ + +
diff --git a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx index 7a6c82e9b..e0bd394a2 100644 --- a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx +++ b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx @@ -131,7 +131,14 @@ export const ShowCustomCommand = ({ id, type }: Props) => { Command - + diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index f4df9ff5e..a67a6bdb1 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -13,6 +13,7 @@ import { RedisIcon, } from "@/components/icons/data-tools-icons"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -97,73 +98,88 @@ const baseDatabaseSchema = z.object({ serverId: z.string().nullable(), }); -const mySchema = z.discriminatedUnion("type", [ - z - .object({ - type: z.literal("libsql"), - dockerImage: z - .string() - .default("ghcr.io/tursodatabase/libsql-server:latest"), - databasePassword: z - .string() - .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { +const mySchema = z + .discriminatedUnion("type", [ + z + .object({ + type: z.literal("libsql"), + dockerImage: z + .string() + .default("ghcr.io/tursodatabase/libsql-server:latest"), + databaseUser: z.string().default("libsql"), + sqldNode: z.enum(["primary", "replica"]).default("primary"), + sqldPrimaryUrl: z.string().optional(), + enableNamespaces: z.boolean().default(false), + }) + .merge(baseDatabaseSchema), + z + .object({ + type: z.literal("mariadb"), + dockerImage: z.string().default("mariadb:4"), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), + databaseUser: z.string().default("mariadb"), + databaseName: z.string().default("mariadb"), + }) + .merge(baseDatabaseSchema), + z + .object({ + type: z.literal("mongo"), + databaseUser: z.string().default("mongo"), + replicaSets: z.boolean().default(false), + }) + .merge(baseDatabaseSchema), + z + .object({ + type: z.literal("mysql"), + databaseRootPassword: z + .string() + .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { + message: + "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", + }) + .optional(), + databaseUser: z.string().default("mysql"), + databaseName: z.string().default("mysql"), + }) + .merge(baseDatabaseSchema), + z + .object({ + type: z.literal("postgres"), + databaseName: z.string().default("postgres"), + databaseUser: z.string().default("postgres"), + }) + .merge(baseDatabaseSchema), + z + .object({ + type: z.literal("redis"), + }) + .merge(baseDatabaseSchema), + ]) + .superRefine((data, ctx) => { + if (data.type === "libsql") { + if (data.sqldNode === "replica" && !data.sqldPrimaryUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["sqldPrimaryUrl"], + message: "sqldPrimaryUrl is required when sqldNode is 'replica'.", + }); + } + if (data.sqldNode !== "replica" && data.sqldPrimaryUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["sqldPrimaryUrl"], message: - "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", - }), - databaseUser: z.string().default("libsql"), - sqldNode: z.enum(["primary", "replica"]).default("primary"), - sqldPrimaryUrl: z.string().optional(), - }) - .merge(baseDatabaseSchema), - z - .object({ - type: z.literal("mariadb"), - dockerImage: z.string().default("mariadb:4"), - databaseRootPassword: z - .string() - .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { - message: - "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", - }) - .optional(), - databaseUser: z.string().default("mariadb"), - databaseName: z.string().default("mariadb"), - }) - .merge(baseDatabaseSchema), - z - .object({ - type: z.literal("mongo"), - databaseUser: z.string().default("mongo"), - replicaSets: z.boolean().default(false), - }) - .merge(baseDatabaseSchema), - z - .object({ - type: z.literal("mysql"), - databaseRootPassword: z - .string() - .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { - message: - "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", - }) - .optional(), - databaseUser: z.string().default("mysql"), - databaseName: z.string().default("mysql"), - }) - .merge(baseDatabaseSchema), - z - .object({ - type: z.literal("postgres"), - databaseName: z.string().default("postgres"), - databaseUser: z.string().default("postgres"), - }) - .merge(baseDatabaseSchema), - z - .object({ - type: z.literal("redis"), - }) - .merge(baseDatabaseSchema), -]); + "sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.", + }); + } + } + }); const databasesMap = { postgres: { @@ -264,7 +280,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => { promise = libsqlMutation.mutateAsync({ ...commonParams, sqldNode: data.sqldNode, - sqldPrimaryUrl: data.sqldPrimaryUrl, + sqldPrimaryUrl: data.sqldPrimaryUrl ?? null, + enableNamespaces: data.enableNamespaces, databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], @@ -574,11 +591,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => { defaultValue={field.value || "primary"} > - + @@ -615,7 +628,46 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => { )} /> )} + {type === "libsql" && ( + { + console.log(field.value); + return ( + + Enable Namespaces + + + + + + ); + }} + /> + )} {(type === "libsql" || type === "mariadb" || type === "mongo" || 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 153ae572a..bf2089498 100644 --- a/apps/dokploy/components/dashboard/shared/show-database-advanced-settings.tsx +++ b/apps/dokploy/components/dashboard/shared/show-database-advanced-settings.tsx @@ -13,7 +13,13 @@ export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => { return (
- + {type === "mariadb" || + type === "mongo" || + type === "mysql" || + type === "postgres" || + type === "redis" ? ( + + ) : null} diff --git a/apps/dokploy/server/api/routers/libsql.ts b/apps/dokploy/server/api/routers/libsql.ts index de5e0c810..7e3f53d4f 100644 --- a/apps/dokploy/server/api/routers/libsql.ts +++ b/apps/dokploy/server/api/routers/libsql.ts @@ -179,6 +179,7 @@ export const libsqlRouter = createTRPCRouter({ await updateLibsqlById(input.libsqlId, { externalPort: input.externalPort, externalGRPCPort: input.externalGRPCPort, + externalAdminPort: input.externalAdminPort, }); await deployLibsql(input.libsqlId); return libsql; diff --git a/packages/server/src/db/schema/libsql.ts b/packages/server/src/db/schema/libsql.ts index c1ad07a29..2f5b74a69 100644 --- a/packages/server/src/db/schema/libsql.ts +++ b/packages/server/src/db/schema/libsql.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm"; -import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; +import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; @@ -41,6 +41,7 @@ export const libsql = pgTable("libsql", { databasePassword: text("databasePassword").notNull(), sqldNode: sqldNode("sqldNode").notNull().default("primary"), sqldPrimaryUrl: text("sqldPrimaryUrl"), + enableNamespaces: boolean("enableNamespaces").notNull().default(false), dockerImage: text("dockerImage").notNull(), command: text("command"), env: text("env"), @@ -52,6 +53,7 @@ export const libsql = pgTable("libsql", { // externalPort: integer("externalPort"), externalGRPCPort: integer("externalGRPCPort"), + externalAdminPort: integer("externalAdminPort"), applicationStatus: applicationStatus("applicationStatus") .notNull() .default("idle"), @@ -102,7 +104,8 @@ const createSchema = createInsertSchema(libsql, { "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", }), sqldNode: z.enum(sqldNode.enumValues), - sqldPrimaryUrl: z.string().optional(), + sqldPrimaryUrl: z.string().nullable(), + enableNamespaces: z.boolean().default(false), dockerImage: z.string().default("ghcr.io/tursodatabase/libsql-server:latest"), command: z.string().optional(), env: z.string().optional(), @@ -114,6 +117,7 @@ const createSchema = createInsertSchema(libsql, { applicationStatus: z.enum(["idle", "running", "done", "error"]), externalPort: z.number(), externalGRPCPort: z.number(), + externalAdminPort: z.number(), description: z.string().optional(), serverId: z.string().optional(), healthCheckSwarm: HealthCheckSwarmSchema.nullable(), @@ -137,6 +141,7 @@ export const apiCreateLibsql = createSchema databasePassword: true, sqldNode: true, sqldPrimaryUrl: true, + enableNamespaces: true, serverId: true, }) .required() @@ -183,14 +188,20 @@ export const apiSaveExternalPortsLibsql = createSchema libsqlId: true, externalPort: true, externalGRPCPort: true, + externalAdminPort: true, }) .required({ libsqlId: true }) .superRefine((data, ctx) => { - if (data.externalPort === null && data.externalGRPCPort === null) { + if ( + data.externalPort === null && + data.externalGRPCPort === null && + data.externalAdminPort === null + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Either externalPort or externalGRPCPort must be provided.", - path: ["externalPort", "externalGRPCPort"], + message: + "Either externalPort, externalGRPCPort or externalAdminPort must be provided.", + path: ["externalPort", "externalGRPCPort", "externalAdminPort"], }); } }); diff --git a/packages/server/src/utils/databases/libsql.ts b/packages/server/src/utils/databases/libsql.ts index c6d8b3292..629c6f223 100644 --- a/packages/server/src/utils/databases/libsql.ts +++ b/packages/server/src/utils/databases/libsql.ts @@ -8,8 +8,27 @@ import { generateVolumeMounts, prepareEnvironmentVariables, } from "../docker/utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRemoteDocker } from "../servers/remote-docker"; +const getServerArchitecture = async ( + serverId?: string | null, +): Promise => { + if (!serverId) { + const { stdout } = await execAsync("uname -m"); + return stdout.trim(); + } + const { stdout } = await execAsyncRemote(serverId, "uname -m"); + return stdout.trim(); +}; + +const getLibsqlImage = (arch: string): string => { + if (arch === "aarch64" || arch === "arm64") { + return "ghcr.io/tursodatabase/libsql-server:latest-arm"; + } + return "ghcr.io/tursodatabase/libsql-server:latest"; +}; + export type LibsqlNested = InferResultType< "libsql", { mounts: true; environment: { with: { project: true } } } @@ -20,6 +39,7 @@ export const buildLibsql = async (libsql: LibsqlNested) => { env, externalPort, externalGRPCPort, + externalAdminPort, dockerImage, memoryLimit, memoryReservation, @@ -31,8 +51,16 @@ export const buildLibsql = async (libsql: LibsqlNested) => { cpuReservation, command, mounts, + serverId, + enableNamespaces, } = libsql; + let finalDockerImage = dockerImage; + if (dockerImage === "ghcr.io/tursodatabase/libsql-server:latest") { + const arch = await getServerArchitecture(serverId); + finalDockerImage = getLibsqlImage(arch); + } + const basicAuth = Buffer.from( `${databaseUser}:${databasePassword}`, "utf-8", @@ -69,18 +97,25 @@ export const buildLibsql = async (libsql: LibsqlNested) => { const docker = await getRemoteDocker(libsql.serverId); + let finalCommand = + command ?? + "sqld --db-path iku.db --http-listen-addr 0.0.0.0:8080 --grpc-listen-addr 0.0.0.0:5001 --admin-listen-addr 0.0.0.0:5000"; + if (enableNamespaces) { + finalCommand += " --enable-namespaces"; + } + const settings: CreateServiceOptions = { Name: appName, TaskTemplate: { ContainerSpec: { HealthCheck, - Image: dockerImage, + Image: finalDockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(command + ...(finalCommand ? { Command: ["/bin/sh"], - Args: ["-c", command], + Args: ["-c", finalCommand], } : {}), Labels, @@ -117,6 +152,16 @@ export const buildLibsql = async (libsql: LibsqlNested) => { } as PortConfig, ] : []), + ...(externalAdminPort + ? [ + { + Protocol: "tcp", + TargetPort: 5000, + PublishedPort: externalAdminPort, + PublishMode: "host", + } as PortConfig, + ] + : []), ], }, UpdateConfig,