Merge branch 'Dokploy:canary' into 4053-fix-slack-notifications-content

This commit is contained in:
vincent-tarrit
2026-03-24 07:10:24 +01:00
committed by GitHub
85 changed files with 11713 additions and 411 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies th
Dokploy includes multiple features to make your life easier.
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, libsql, and Redis.
- **Backups**: Automate backups for databases to an external storage destination.
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
@@ -35,6 +35,7 @@ const ENTERPRISE_RESOURCES = [
"domain",
"destination",
"notification",
"tag",
"logs",
"monitoring",
"auditLog",
@@ -110,16 +110,16 @@ const menuItems: MenuItem[] = [
},
];
const hasStopGracePeriodSwarm = (
value: unknown,
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
typeof value === "object" &&
value !== null &&
"stopGracePeriodSwarm" in value;
interface Props {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "application"
| "libsql"
| "mariadb"
| "mongo"
| "mysql"
| "postgres"
| "redis";
}
export const AddSwarmSettings = ({ id, type }: Props) => {
@@ -37,7 +37,7 @@ import { AddSwarmSettings } from "./modify-swarm-settings";
interface Props {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
}
const AddRedirectchema = z.object({
@@ -49,15 +49,15 @@ type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ id, type }: Props) => {
const queryMap = {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: 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 }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -65,12 +65,13 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
const { data: registries } = api.registry.all.useQuery();
const mutationMap = {
application: () => api.application.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isPending } = mutationMap[type]
@@ -105,11 +106,11 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
const onSubmit = async (data: AddCommand) => {
await mutateAsync({
applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
mysqlId: id || "",
postgresId: id || "",
redisId: id || "",
...(type === "application"
? {
registryId:
@@ -28,7 +28,14 @@ export const endpointSpecFormSchema = z.object({
interface EndpointSpecFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
@@ -44,6 +51,7 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -56,6 +64,7 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -94,6 +103,7 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
endpointSpecSwarm: hasAnyValue ? formData : null,
});
@@ -26,7 +26,14 @@ export const healthCheckFormSchema = z.object({
interface HealthCheckFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
@@ -42,6 +49,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -54,6 +62,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -104,6 +113,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
healthCheckSwarm: hasAnyValue ? formData : null,
});
@@ -29,7 +29,14 @@ export const labelsFormSchema = z.object({
interface LabelsFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
@@ -45,6 +52,7 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -57,6 +65,7 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -112,6 +121,7 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
labelsSwarm: labelsToSend,
});
@@ -23,7 +23,14 @@ import { api } from "@/utils/api";
interface ModeFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const ModeForm = ({ id, type }: ModeFormProps) => {
@@ -39,6 +46,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -51,6 +59,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -95,6 +104,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
modeSwarm: null,
});
toast.success("Mode updated successfully");
@@ -122,6 +132,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
modeSwarm: modeData,
});
@@ -35,7 +35,14 @@ export const networkFormSchema = z.object({
interface NetworkFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
@@ -51,6 +58,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -63,6 +71,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -132,6 +141,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
networkSwarm: networksToSend,
});
@@ -34,7 +34,14 @@ export const placementFormSchema = z.object({
interface PlacementFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
@@ -50,6 +57,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -62,6 +70,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -114,6 +123,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
placementSwarm: hasAnyValue
? {
...formData,
@@ -32,7 +32,14 @@ export const restartPolicyFormSchema = z.object({
interface RestartPolicyFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
@@ -48,6 +55,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -60,6 +68,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -104,6 +113,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
restartPolicySwarm: hasAnyValue ? formData : null,
});
@@ -34,7 +34,14 @@ export const rollbackConfigFormSchema = z.object({
interface RollbackConfigFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
@@ -50,6 +57,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -62,6 +70,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -103,6 +112,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
rollbackConfigSwarm: (hasAnyValue ? formData : null) as any,
});
@@ -23,7 +23,14 @@ const hasStopGracePeriodSwarm = (
interface StopGracePeriodFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
@@ -39,6 +46,7 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -51,6 +59,7 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -88,6 +97,7 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
stopGracePeriodSwarm: formData.value,
});
@@ -34,7 +34,14 @@ export const updateConfigFormSchema = z.object({
interface UpdateConfigFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
type:
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis"
| "application"
| "libsql";
}
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
@@ -50,6 +57,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -62,6 +70,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
@@ -109,6 +118,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
libsqlId: id || "",
updateConfigSwarm: (hasAnyValue ? formData : null) as any,
});
@@ -89,12 +89,13 @@ const ULIMIT_PRESETS = [
];
export type ServiceType =
| "postgres"
| "mongo"
| "redis"
| "mysql"
| "application"
| "libsql"
| "mariadb"
| "application";
| "mongo"
| "mysql"
| "postgres"
| "redis";
interface Props {
id: string;
@@ -105,27 +106,29 @@ type AddResources = z.infer<typeof addResourcesSchema>;
export const ShowResources = ({ 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 }),
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 }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
application: () => api.application.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isPending } = mutationMap[type]
@@ -155,19 +158,20 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
ulimitsSwarm: data?.ulimitsSwarm || [],
ulimitsSwarm: (data as any)?.ulimitsSwarm || [],
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddResources) => {
await mutateAsync({
applicationId: id || "",
libsqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
mysqlId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
applicationId: id || "",
cpuLimit: formData.cpuLimit || null,
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
@@ -34,13 +34,13 @@ interface Props {
serviceId: string;
serviceType:
| "application"
| "postgres"
| "redis"
| "mongo"
| "redis"
| "mysql"
| "compose"
| "libsql"
| "mariadb"
| "compose";
| "mongo"
| "mysql"
| "postgres"
| "redis";
refetch: () => void;
children?: React.ReactNode;
}
@@ -29,23 +29,25 @@ export const ShowVolumes = ({ id, type }: Props) => {
if (!canRead) return null;
const queryMap = {
application: () =>
api.application.one.useQuery({ applicationId: 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 }),
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 }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync: deleteVolume, isPending: isRemoving } =
api.mounts.remove.useMutation();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
@@ -67,13 +67,13 @@ interface Props {
refetch: () => void;
serviceType:
| "application"
| "postgres"
| "redis"
| "mongo"
| "redis"
| "mysql"
| "compose"
| "libsql"
| "mariadb"
| "compose";
| "mongo"
| "mysql"
| "postgres"
| "redis";
}
export const UpdateVolume = ({
@@ -253,7 +253,7 @@ export const UpdateVolume = ({
control={form.control}
name="content"
render={({ field }) => (
<FormItem className="max-w-full max-w-[45rem]">
<FormItem className="w-full max-w-[45rem]">
<FormLabel>Content</FormLabel>
<FormControl>
<FormControl>
@@ -39,15 +39,16 @@ export const ShowEnvironment = ({ id, type }: Props) => {
const { data: permissions } = api.user.getPermissions.useQuery();
const canWrite = permissions?.envVars.write ?? false;
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]()
@@ -55,12 +56,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, isPending } = mutationMap[type]
? mutationMap[type]()
@@ -87,12 +89,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 () => {
@@ -71,6 +71,7 @@ const formSchema = z
"mongo",
"mysql",
"redis",
"libsql",
]),
serviceName: z.string(),
destinationId: z.string().min(1, "Destination required"),
@@ -57,6 +57,7 @@ export const DeleteService = ({ id, type }: Props) => {
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
@@ -72,6 +73,7 @@ export const DeleteService = ({ id, type }: Props) => {
redis: () => api.redis.remove.useMutation(),
mysql: () => api.mysql.remove.useMutation(),
mariadb: () => api.mariadb.remove.useMutation(),
libsql: () => api.libsql.remove.useMutation(),
application: () => api.application.delete.useMutation(),
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
@@ -98,6 +100,7 @@ export const DeleteService = ({ id, type }: Props) => {
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
libsqlId: id || "",
applicationId: id || "",
composeId: id || "",
deleteVolumes,
@@ -65,7 +65,13 @@ import { ScheduleFormField } from "../../application/schedules/handle-schedules"
type CacheType = "cache" | "fetch";
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
type DatabaseType =
| "postgres"
| "mariadb"
| "mysql"
| "mongo"
| "web-server"
| "libsql";
const Schema = z
.object({
@@ -77,7 +83,7 @@ const Schema = z
keepLatestCount: z.coerce.number().optional(),
serviceName: z.string().nullable(),
databaseType: z
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
.optional(),
backupType: z.enum(["database", "compose"]),
metadata: z
@@ -209,7 +215,12 @@ export const HandleBackup = ({
const form = useForm({
defaultValues: {
database: databaseType === "web-server" ? "dokploy" : "",
database:
databaseType === "web-server"
? "dokploy"
: databaseType === "libsql"
? "iku.db"
: "",
destinationId: "",
enabled: true,
prefix: "/",
@@ -246,7 +257,9 @@ export const HandleBackup = ({
? backup?.database
: databaseType === "web-server"
? "dokploy"
: "",
: databaseType === "libsql"
? "iku.db"
: "",
destinationId: backup?.destinationId ?? "",
enabled: backup?.enabled ?? true,
prefix: backup?.prefix ?? "/",
@@ -281,11 +294,15 @@ export const HandleBackup = ({
? {
mongoId: id,
}
: databaseType === "web-server"
: databaseType === "libsql"
? {
userId: id,
libsqlId: id,
}
: undefined;
: databaseType === "web-server"
? {
userId: id,
}
: undefined;
await createBackup({
destinationId: data.destinationId,
@@ -568,7 +585,10 @@ export const HandleBackup = ({
<FormLabel>Database</FormLabel>
<FormControl>
<Input
disabled={databaseType === "web-server"}
disabled={
databaseType === "web-server" ||
databaseType === "libsql"
}
placeholder={"dokploy"}
{...field}
/>
@@ -88,7 +88,7 @@ const RestoreBackupSchema = z
message: "Database name is required",
}),
databaseType: z
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
.optional(),
backupType: z.enum(["database", "compose"]).default("database"),
metadata: z
@@ -211,7 +211,12 @@ export const RestoreBackup = ({
defaultValues: {
destinationId: "",
backupFile: "",
databaseName: databaseType === "web-server" ? "dokploy" : "",
databaseName:
databaseType === "web-server"
? "dokploy"
: databaseType === "libsql"
? "iku.db"
: "",
databaseType:
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
backupType: backupType,
@@ -523,7 +528,10 @@ export const RestoreBackup = ({
<Input
placeholder="Enter database name"
{...field}
disabled={databaseType === "web-server"}
disabled={
databaseType === "web-server" ||
databaseType === "libsql"
}
/>
</FormControl>
<FormMessage />
@@ -53,14 +53,16 @@ export const ShowBackups = ({
const queryMap =
backupType === "database"
? {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
mysql: () =>
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () =>
api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
mysql: () =>
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
libsql: () =>
api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
"web-server": () => api.user.getBackups.useQuery(),
}
: {
@@ -77,10 +79,11 @@ export const ShowBackups = ({
const mutationMap =
backupType === "database"
? {
postgres: api.backup.manualBackupPostgres.useMutation(),
mysql: api.backup.manualBackupMySql.useMutation(),
mariadb: api.backup.manualBackupMariadb.useMutation(),
mongo: api.backup.manualBackupMongo.useMutation(),
mysql: api.backup.manualBackupMySql.useMutation(),
postgres: api.backup.manualBackupPostgres.useMutation(),
libsql: api.backup.manualBackupLibsql.useMutation(),
"web-server": api.backup.manualBackupWebServer.useMutation(),
}
: {
@@ -0,0 +1,251 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a === null || a === undefined || a === "") return null;
const parsed = Number.parseInt(String(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalGRPCPort: z.preprocess((a) => {
if (a === null || a === undefined || a === "") return null;
const parsed = Number.parseInt(String(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalAdminPort: z.preprocess((a) => {
if (a === null || a === undefined || a === "") return null;
const parsed = Number.parseInt(String(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
libsqlId: string;
}
export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.libsql.one.useQuery({ libsqlId });
const { mutateAsync, isPending } = api.libsql.saveExternalPorts.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const [connectionGRPCUrl, setGRPCConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data) {
form.reset({
externalPort: data.externalPort,
externalGRPCPort: data.externalGRPCPort,
externalAdminPort: data.externalAdminPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
externalGRPCPort: values.externalGRPCPort,
externalAdminPort: values.externalAdminPort,
libsqlId,
})
.then(async () => {
toast.success("External port/ports updated");
await refetch();
})
.catch((error: Error) => {
toast.error(error?.message || "Error saving the external port/ports");
});
};
useEffect(() => {
const port = form.watch("externalPort") || data?.externalPort;
setConnectionUrl(
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`,
);
if (data?.sqldNode !== "replica") {
const grpcPort = form.watch("externalGRPCPort") || data?.externalGRPCPort;
setGRPCConnectionUrl(
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${grpcPort}`,
);
}
}, [
data?.externalGRPCPort,
data?.databasePassword,
form,
data?.databaseUser,
getIp,
]);
return (
<div className="flex w-full flex-col gap-5">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable through the internet, you
must set a port and ensure that the port is not being used by
another application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings/server" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="8080"
{...field}
value={field.value as string}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalAdminPort"
render={({ field }) => (
<FormItem>
<FormLabel>External Admin Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="5000"
{...field}
value={field.value as string}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{data?.sqldNode !== "replica" && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalGRPCPort"
render={({ field }) => (
<FormItem>
<FormLabel>External GRPC Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="5001"
{...field}
value={field.value as string}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{!!data?.externalGRPCPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External GRPC Host</Label>
<ToggleVisibilityInput
value={connectionGRPCUrl}
disabled
/>
</div>
</div>
)}
</>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isPending}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,268 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props {
libsqlId: string;
}
export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
const { data, refetch } = api.libsql.one.useQuery(
{
libsqlId,
},
{ enabled: !!libsqlId },
);
const { mutateAsync: reload, isPending: isReloading } =
api.libsql.reload.useMutation();
const { mutateAsync: start, isPending: isStarting } =
api.libsql.start.useMutation();
const { mutateAsync: stop, isPending: isStopping } =
api.libsql.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
api.libsql.deployWithLogs.useSubscription(
{
libsqlId: libsqlId,
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
if (log === "Deployment completed successfully!") {
setIsDeploying(false);
}
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Deployment logs error:", error);
setIsDeploying(false);
},
},
);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Libsql"
description="Are you sure you want to deploy this Libsql?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the Libsql database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<DialogAction
title="Reload Libsql"
description="Are you sure you want to reload this libsql?"
type="default"
onClick={async () => {
await reload({
libsqlId: libsqlId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Libsql reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Libsql");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the Libsql service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
</TooltipProvider>
{data?.applicationStatus === "idle" ? (
<TooltipProvider delayDuration={0}>
<DialogAction
title="Start Libsql"
description="Are you sure you want to start this Libsql?"
type="default"
onClick={async () => {
await start({
libsqlId: libsqlId,
})
.then(() => {
toast.success("Libsql started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Libsql");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the Libsql database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
</TooltipProvider>
) : (
<TooltipProvider delayDuration={0}>
<DialogAction
title="Stop Libsql"
description="Are you sure you want to stop this Libsql?"
onClick={async () => {
await stop({
libsqlId: libsqlId,
})
.then(() => {
toast.success("Libsql stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Libsql");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running Libsql database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
</TooltipProvider>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the Libsql container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>
</Card>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
</>
);
};
@@ -0,0 +1,121 @@
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
interface Props {
libsqlId: string;
}
export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
const { data } = api.libsql.one.useQuery({ libsqlId });
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Internal Credentials</CardTitle>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2">
<Label>User</Label>
<Input disabled value={data?.databaseUser} />
</div>
<div className="flex flex-col gap-2">
<Label>Sqld Node</Label>
<Select value={data?.sqldNode} disabled>
<SelectTrigger>
<SelectValue placeholder="Select Node type" />
</SelectTrigger>
<SelectContent>
{["primary", "replica"].map((node) => (
<SelectItem key={node} value={node}>
{node.charAt(0).toUpperCase() + node.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
/>
</div>
</div>
<div className="flex flex-row gap-2">
<div className="w-full flex flex-col gap-2">
<Label>Internal Port (Container)</Label>
<Input disabled value="8080" />
</div>
<div className="w-full flex flex-col gap-2">
<Label>Internal GRPC Port (Container)</Label>
<Input disabled value="5001" />
</div>
<div className="w-full flex flex-col gap-2">
<Label>Internal Admin Port (Container)</Label>
<Input disabled value="5000" />
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Internal Host</Label>
<Input disabled value={data?.appName} />
</div>
<div className="flex flex-col gap-2">
<Label>Enable Namespaces</Label>
<Select
disabled
defaultValue={
data?.enableNamespaces
? String(data?.enableNamespaces)
: "false"
}
>
<SelectTrigger>
<SelectValue placeholder={"false"} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["false", "true"].map((node) => (
<SelectItem key={node} value={node}>
{node.charAt(0).toUpperCase() + node.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Connection URL </Label>
<ToggleVisibilityInput
disabled
value={`http://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:8080`}
/>
</div>
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Replication Connection URL </Label>
<ToggleVisibilityInput
disabled
value={`http://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5001`}
/>
</div>
</div>
</CardContent>
</Card>
</div>
</>
);
};
@@ -0,0 +1,163 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
const updateLibsqlSchema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
});
type UpdateLibsql = z.infer<typeof updateLibsqlSchema>;
interface Props {
libsqlId: string;
}
export const UpdateLibsql = ({ libsqlId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, error, isError, isPending } =
api.libsql.update.useMutation();
const { data } = api.libsql.one.useQuery(
{
libsqlId,
},
{
enabled: !!libsqlId,
},
);
const form = useForm<UpdateLibsql>({
defaultValues: {
description: data?.description ?? "",
name: data?.name ?? "",
},
resolver: zodResolver(updateLibsqlSchema),
});
useEffect(() => {
if (data) {
form.reset({
description: data.description ?? "",
name: data.name,
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: UpdateLibsql) => {
await mutateAsync({
name: formData.name,
libsqlId: libsqlId,
description: formData.description || "",
})
.then(() => {
toast.success("Libsql updated successfully");
utils.libsql.one.invalidate({
libsqlId: libsqlId,
});
})
.catch(() => {
toast.error("Error updating the Libsql");
})
.finally(() => {});
};
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Libsql</DialogTitle>
<DialogDescription>Update the Libsql data</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid gap-4">
<div className="grid items-center gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-update-libsql"
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Vandelay Industries" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description about your project..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isPending}
form="hook-form-update-libsql"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -42,6 +42,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
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 }),
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
@@ -56,6 +57,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
libsql: () => api.libsql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
@@ -84,7 +86,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
form.reset({
dockerImage: data.dockerImage,
command: data.command || "",
args: data.args?.map((arg) => ({ value: arg })) || [],
args: (data as any).args?.map((arg: string) => ({ value: arg })) || [],
});
}
}, [data, form]);
@@ -95,6 +97,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
libsqlId: id || "",
mariadbId: id || "",
dockerImage: formData?.dockerImage,
command: formData?.command,
@@ -144,7 +147,14 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Input placeholder="/bin/sh" {...field} />
<Input
placeholder={
type === "libsql"
? "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"
: "Custom command"
}
{...field}
/>
</FormControl>
<FormMessage />
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import {
LibsqlIcon,
MariadbIcon,
MongodbIcon,
MysqlIcon,
@@ -55,6 +56,7 @@ import { api } from "@/utils/api";
type DbType = z.infer<typeof mySchema>["type"];
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
libsql: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
mongo: "mongo:7",
mariadb: "mariadb:11",
mysql: "mysql:8",
@@ -66,8 +68,9 @@ const databasesUserDefaultPlaceholder: Record<
Exclude<DbType, "redis">,
string
> = {
mongo: "mongo",
libsql: "libsql",
mariadb: "mariadb",
mongo: "mongo",
mysql: "mysql",
postgres: "postgres",
};
@@ -94,56 +97,88 @@ const baseDatabaseSchema = z.object({
serverId: z.string().nullable(),
});
const mySchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("postgres"),
databaseName: z.string().default("postgres"),
databaseUser: z.string().default("postgres"),
})
.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("redis"),
})
.merge(baseDatabaseSchema),
z
.object({
type: z.literal("mysql"),
databaseRootPassword: 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:v0.24.32"),
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",
})
.optional(),
databaseUser: z.string().default("mysql"),
databaseName: z.string().default("mysql"),
})
.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),
]);
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
});
}
}
});
const databasesMap = {
postgres: {
@@ -166,6 +201,10 @@ const databasesMap = {
icon: <RedisIcon />,
label: "Redis",
},
libsql: {
icon: <LibsqlIcon className="size-10" />,
label: "libSQL",
},
};
type AddDatabase = z.infer<typeof mySchema>;
@@ -181,11 +220,12 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const redisMutation = api.redis.create.useMutation();
const libsqlMutation = api.libsql.create.useMutation();
const mariadbMutation = api.mariadb.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation();
const postgresMutation = api.postgres.create.useMutation();
const redisMutation = api.redis.create.useMutation();
// Get environment data to extract projectId
const { data: environment } = api.environment.one.useQuery({ environmentId });
@@ -210,13 +250,15 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
},
resolver: zodResolver(mySchema),
});
const sqldNode = form.watch("sqldNode");
const type = form.watch("type");
const activeMutation = {
postgres: postgresMutation,
mongo: mongoMutation,
redis: redisMutation,
libsql: libsqlMutation,
mariadb: mariadbMutation,
mongo: mongoMutation,
mysql: mysqlMutation,
postgres: postgresMutation,
redis: redisMutation,
};
const onSubmit = async (data: AddDatabase) => {
@@ -233,12 +275,23 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
description: data.description,
};
if (data.type === "postgres") {
promise = postgresMutation.mutateAsync({
if (data.type === "libsql") {
promise = libsqlMutation.mutateAsync({
...commonParams,
sqldNode: data.sqldNode,
sqldPrimaryUrl: data.sqldPrimaryUrl ?? null,
enableNamespaces: data.enableNamespaces,
databasePassword: data.databasePassword,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "mariadb") {
promise = mariadbMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName || "postgres",
databaseRootPassword: data.databaseRootPassword || "",
databaseName: data.databaseName || "mariadb",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId === "dokploy" ? null : data.serverId,
@@ -252,22 +305,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
serverId: data.serverId === "dokploy" ? null : data.serverId,
replicaSets: data.replicaSets,
});
} else if (data.type === "redis") {
promise = redisMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "mariadb") {
promise = mariadbMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseRootPassword: data.databaseRootPassword || "",
databaseName: data.databaseName || "mariadb",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "mysql") {
promise = mysqlMutation.mutateAsync({
...commonParams,
@@ -278,6 +315,21 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
serverId: data.serverId === "dokploy" ? null : data.serverId,
databaseRootPassword: data.databaseRootPassword || "",
});
} else if (data.type === "postgres") {
promise = postgresMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName || "postgres",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
} else if (data.type === "redis") {
promise = redisMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
serverId: data.serverId === "dokploy" ? null : data.serverId,
});
}
if (promise) {
@@ -305,6 +357,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
});
}
};
return (
<Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full">
@@ -506,8 +559,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
</FormItem>
)}
/>
{(type === "mysql" ||
type === "mariadb" ||
{(type === "mariadb" ||
type === "mysql" ||
type === "postgres") && (
<FormField
control={form.control}
@@ -524,10 +577,101 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
)}
/>
)}
{(type === "mysql" ||
{type === "libsql" && (
<FormField
control={form.control}
name="sqldNode"
render={({ field }) => (
<FormItem>
<FormLabel>Sqld Node</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || "primary"}
>
<SelectTrigger>
<SelectValue placeholder={"primary"} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["primary", "replica"].map((node) => (
<SelectItem key={node} value={node}>
{node.charAt(0).toUpperCase() + node.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{type === "libsql" && sqldNode === "replica" && (
<FormField
control={form.control}
name="sqldPrimaryUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Sqld Primary URL</FormLabel>
<FormControl>
<Input
placeholder={"https://<host>:<port>"}
autoComplete="off"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{type === "libsql" && (
<FormField
control={form.control}
name="enableNamespaces"
render={({ field }) => {
console.log(field.value);
return (
<FormItem>
<FormLabel>Enable Namespaces</FormLabel>
<FormControl>
<Select
onValueChange={(value) =>
field.onChange(Boolean(value))
}
defaultValue={
field.value ? String(field.value) : "false"
}
>
<SelectTrigger>
<SelectValue placeholder={"false"} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["false", "true"].map((node) => (
<SelectItem key={node} value={node}>
{node.charAt(0).toUpperCase() +
node.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
)}
{(type === "libsql" ||
type === "mariadb" ||
type === "postgres" ||
type === "mongo") && (
type === "mongo" ||
type === "mysql" ||
type === "postgres") && (
<FormField
control={form.control}
name="databaseUser"
@@ -568,7 +712,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
</FormItem>
)}
/>
{(type === "mysql" || type === "mariadb") && (
{(type === "mariadb" || type === "mysql") && (
<FormField
control={form.control}
name="databaseRootPassword"
@@ -28,13 +28,14 @@ export type Services = {
serverId?: string | null;
name: string;
type:
| "mariadb"
| "application"
| "postgres"
| "mysql"
| "compose"
| "libsql"
| "mariadb"
| "mongo"
| "redis"
| "compose";
| "mysql"
| "postgres"
| "redis";
description?: string | null;
id: string;
createdAt: string;
@@ -51,7 +51,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TimeBadge } from "@/components/ui/time-badge";
import { api } from "@/utils/api";
import { useDebounce } from "@/utils/hooks/use-debounce";
import { HandleProject } from "./handle-project";
@@ -199,11 +198,6 @@ export const ShowProjects = () => {
return (
<>
{!isCloud && (
<div className="absolute top-4 right-4">
<TimeBadge />
</div>
)}
<BreadcrumbSidebar
list={[{ name: "Projects", href: "/dashboard/projects" }]}
/>
@@ -300,26 +294,27 @@ export const ShowProjects = () => {
.map(
(env) =>
env.applications.length === 0 &&
env.compose.length === 0 &&
env.libsql.length === 0 &&
env.mariadb.length === 0 &&
env.mongo.length === 0 &&
env.mysql.length === 0 &&
env.postgres.length === 0 &&
env.redis.length === 0 &&
env.applications.length === 0 &&
env.compose.length === 0,
env.redis.length === 0,
)
.every(Boolean);
const totalServices = project?.environments
.map(
(env) =>
env.applications.length +
env.compose.length +
env.libsql.length +
env.mariadb.length +
env.mongo.length +
env.mysql.length +
env.postgres.length +
env.redis.length +
env.applications.length +
env.compose.length,
env.redis.length,
)
.reduce((acc, curr) => acc + curr, 0);
@@ -46,7 +46,8 @@ export type Services = {
| "mysql"
| "mongo"
| "redis"
| "compose";
| "compose"
| "libsql";
description?: string | null;
id: string;
createdAt: string;
@@ -136,6 +137,18 @@ export const extractServices = (data: Environment | undefined) => {
serverId: item.serverId,
})) ?? []) as Services[];
const libsql: Services[] =
data?.libsql?.map((item) => ({
appName: item.appName,
name: item.name,
type: "libsql" as const,
id: item.libsqlId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
applications.push(
...mysql,
...redis,
@@ -143,6 +156,7 @@ export const extractServices = (data: Environment | undefined) => {
...postgres,
...mariadb,
...compose,
...libsql,
);
applications.sort((a, b) => {
@@ -17,17 +17,18 @@ import { api } from "@/utils/api";
interface Props {
id: string;
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
}
export const RebuildDatabase = ({ id, type }: Props) => {
const utils = api.useUtils();
const mutationMap = {
postgres: () => api.postgres.rebuild.useMutation(),
mysql: () => api.mysql.rebuild.useMutation(),
libsql: () => api.libsql.rebuild.useMutation(),
mariadb: () => api.mariadb.rebuild.useMutation(),
mongo: () => api.mongo.rebuild.useMutation(),
mysql: () => api.mysql.rebuild.useMutation(),
postgres: () => api.postgres.rebuild.useMutation(),
redis: () => api.redis.rebuild.useMutation(),
};
@@ -36,10 +37,11 @@ export const RebuildDatabase = ({ id, type }: Props) => {
const handleRebuild = async () => {
try {
await mutateAsync({
postgresId: type === "postgres" ? id : "",
mysqlId: type === "mysql" ? id : "",
libsqlId: type === "libsql" ? id : "",
mariadbId: type === "mariadb" ? id : "",
mongoId: type === "mongo" ? id : "",
mysqlId: type === "mysql" ? id : "",
postgresId: type === "postgres" ? id : "",
redisId: type === "redis" ? id : "",
});
toast.success("Database rebuilt successfully");
@@ -6,14 +6,20 @@ import { RebuildDatabase } from "./rebuild-database";
interface Props {
id: string;
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
}
export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
return (
<div className="flex w-full flex-col gap-5">
<ShowCustomCommand id={id} type={type} />
<ShowClusterSettings id={id} type={type} />
{type === "mariadb" ||
type === "mongo" ||
type === "mysql" ||
type === "postgres" ||
type === "redis" ? (
<ShowClusterSettings id={id} type={type} />
) : null}
<ShowVolumes id={id} type={type} />
<ShowResources id={id} type={type} />
<RebuildDatabase id={id} type={type} />
@@ -156,6 +156,61 @@ export const RedisIcon = ({ className }: Props) => {
);
};
export const LibsqlIcon = ({ className }: Props) => {
return (
<svg
aria-label="libsql"
height="1em"
width="1em"
viewBox="0 0 217.2 217.2"
className={className}
>
<path
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
d="M118,87.2c.7,0,1.3,0,1.9.3.4.1.8.3,1.2.5.2.1.4.2.6.3.4.3.7.5,1.1.9l5.2,5.2c2.7,2.7,2.7,7,0,9.7l-95.1,95.1c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l85.1-85.1,10-10c.3-.3.7-.6,1.1-.9.2-.1.4-.2.6-.3.4-.2.8-.4,1.2-.5.6-.2,1.3-.3,1.9-.3M118,71.2c-2.2,0-4.4.3-6.5.9-1.4.4-2.8,1-4.1,1.7-.7.4-1.3.7-2,1.2-1.3.8-2.5,1.8-3.6,2.9l-10,10L6.7,173c-8.9,8.9-8.9,23.4,0,32.3l5.2,5.2c4.3,4.3,10.1,6.7,16.2,6.7s11.8-2.4,16.2-6.7l95.1-95.1c4.3-4.3,6.7-10.1,6.7-16.2s-2.4-11.8-6.7-16.2l-5.2-5.2c-1.1-1.1-2.3-2.1-3.6-2.9-.6-.4-1.3-.8-1.9-1.2-1.3-.7-2.7-1.3-4.1-1.7-2.1-.6-4.3-.9-6.5-.9h0Z"
/>
<g>
<path
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,0c-8.4,0-16.3,3.3-22.3,9.2L17.4,88.9c-8.5,8.5-11.4,20.8-7.6,32.1l16.5,49.9c3.1,9.4,10.6,16.9,20,20l49.9,16.5c3.2,1.1,6.5,1.6,9.9,1.6,8.4,0,16.3-3.3,22.3-9.2l79.7-79.7c6.3-6.3,9.7-15.1,9.2-24.1l-3.8-66.5c-.8-13.9-11.9-24.9-25.7-25.7L121.2,0c-.6,0-1.2,0-1.8,0h0Z"
/>
<path
style={{ fill: "#a8f7d9", strokeWidth: "0px" }}
d="M24.9,116.1l16.5,49.9c1.5,4.7,5.2,8.3,9.8,9.8l49.9,16.5c5.6,1.8,11.7.4,15.8-3.8l79.7-79.7c3.1-3.1,4.8-7.4,4.5-11.8l-3.8-66.5c-.3-5.8-4.9-10.3-10.7-10.7l-66.5-3.8c-4.4-.3-8.7,1.4-11.8,4.5L28.7,100.3c-4.1,4.1-5.6,10.3-3.8,15.8Z"
/>
<path
style={{ fill: "#141b1f", strokeWidth: "0px" }}
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,6c-6.8,0-13.2,2.7-18,7.5L21.6,93.2c-6.8,6.8-9.2,16.8-6.2,26l16.5,49.9c2.5,7.6,8.6,13.7,16.2,16.2l49.9,16.5c2.6.9,5.3,1.3,8,1.3,6.8,0,13.2-2.7,18-7.5l79.7-79.7c5.1-5.1,7.8-12.2,7.4-19.5l-3.8-66.5c-.6-10.8-9.3-19.5-20.1-20.1l-66.5-3.8c-.5,0-1,0-1.5,0h0Z"
/>
<path
style={{ fill: "#141b1f", strokeWidth: "0px" }}
d="M136.7,173.7l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
/>
<path
style={{ fill: "#141b1f", strokeWidth: "0px" }}
d="M166.5,143.9l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
/>
<path
style={{ fill: "#141b1f", strokeWidth: "0px" }}
d="M43.5,80.5l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
/>
<path
style={{ fill: "#141b1f", strokeWidth: "0px" }}
d="M73.3,50.7l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
/>
</g>
<path
style={{ fill: "#79ac91", strokeWidth: "0px" }}
d="M130.6,101.5l-97.7,97.7c-2.7,2.7-7,2.7-9.7,0l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c2.8-.3,5.1,2.1,4.9,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5Z"
/>
<path
style={{ fill: "#141b1f", strokeWidth: "0px" }}
d="M129.5,83.3c2.6,0,4.7,2.2,4.4,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5l-97.7,97.7c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c.1,0,.3,0,.4,0M129.5,73.3h0c-.5,0-.9,0-1.4,0l-7.9.8c-4.4.4-8.5,2.4-11.5,5.5L10.9,177.3c-6.6,6.6-6.6,17.3,0,23.8l5.2,5.2c3.2,3.2,7.4,4.9,11.9,4.9s8.7-1.8,11.9-4.9l97.7-97.7c3.1-3.1,5-7.2,5.5-11.5l.8-7.9c.4-4-.9-8.1-3.7-11.1-2.7-3-6.6-4.7-10.7-4.7h0Z"
/>
</svg>
);
};
export const GitlabIcon = ({ className }: Props) => {
return (
<svg
@@ -11,6 +11,7 @@ import {
import { useRouter } from "next/router";
import { type ComponentType, useEffect, useMemo, useState } from "react";
import {
LibsqlIcon,
MariadbIcon,
MongodbIcon,
MysqlIcon,
@@ -58,6 +59,7 @@ type EnvironmentServiceCollections = {
mariadb: (NamedService & { mariadbId: string })[];
redis: (NamedService & { redisId: string })[];
mongo: (NamedService & { mongoId: string })[];
libsql: (NamedService & { libsqlId: string })[];
};
type ServiceCollections = Pick<
@@ -69,6 +71,7 @@ type ServiceCollections = Pick<
| "mariadb"
| "redis"
| "mongo"
| "libsql"
>;
const SERVICE_COLLECTION_KEYS = [
@@ -79,6 +82,7 @@ const SERVICE_COLLECTION_KEYS = [
"mariadb",
"redis",
"mongo",
"libsql",
] as const satisfies ReadonlyArray<keyof ServiceCollections>;
const SERVICE_QUERY_KEYS = [
@@ -89,6 +93,7 @@ const SERVICE_QUERY_KEYS = [
"mariadbId",
"redisId",
"mongoId",
"libsqlId",
] as const;
const SERVICE_ICONS: Record<
@@ -102,6 +107,7 @@ const SERVICE_ICONS: Record<
mariadb: MariadbIcon,
redis: RedisIcon,
mongo: MongodbIcon,
libsql: LibsqlIcon,
};
const getStringQueryParam = (value: string | string[] | undefined) =>
@@ -156,6 +162,7 @@ const extractServicesFromEnvironment = (
...mapServices(servicesByType.mariadb, (item) => item.mariadbId, "mariadb"),
...mapServices(servicesByType.redis, (item) => item.redisId, "redis"),
...mapServices(servicesByType.mongo, (item) => item.mongoId, "mongo"),
...mapServices(servicesByType.libsql, (item) => item.libsqlId, "libsql"),
];
};
+1 -1
View File
@@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-inset",
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -0,0 +1,49 @@
CREATE TYPE "public"."sqldNode" AS ENUM('primary', 'replica');--> statement-breakpoint
ALTER TYPE "public"."databaseType" ADD VALUE 'libsql';--> statement-breakpoint
ALTER TYPE "public"."serviceType" ADD VALUE 'libsql';--> statement-breakpoint
CREATE TABLE "libsql" (
"libsqlId" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"appName" text NOT NULL,
"description" text,
"databaseUser" text NOT NULL,
"databasePassword" text NOT NULL,
"sqldNode" "sqldNode" DEFAULT 'primary' NOT NULL,
"sqldPrimaryUrl" text,
"enableNamespaces" boolean DEFAULT false NOT NULL,
"dockerImage" text NOT NULL,
"command" text,
"env" text,
"memoryReservation" text,
"memoryLimit" text,
"cpuReservation" text,
"cpuLimit" text,
"externalPort" integer,
"externalGRPCPort" integer,
"externalAdminPort" integer,
"applicationStatus" "applicationStatus" DEFAULT 'idle' NOT NULL,
"healthCheckSwarm" json,
"restartPolicySwarm" json,
"placementSwarm" json,
"updateConfigSwarm" json,
"rollbackConfigSwarm" json,
"modeSwarm" json,
"labelsSwarm" json,
"networkSwarm" json,
"stopGracePeriodSwarm" bigint,
"endpointSpecSwarm" json,
"replicas" integer DEFAULT 1 NOT NULL,
"createdAt" text NOT NULL,
"environmentId" text NOT NULL,
"serverId" text,
CONSTRAINT "libsql_appName_unique" UNIQUE("appName")
);
--> statement-breakpoint
ALTER TABLE "backup" ADD COLUMN "libsqlId" text;--> statement-breakpoint
ALTER TABLE "mount" ADD COLUMN "libsqlId" text;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD COLUMN "libsqlId" text;--> statement-breakpoint
ALTER TABLE "libsql" ADD CONSTRAINT "libsql_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "libsql" ADD CONSTRAINT "libsql_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "backup" ADD CONSTRAINT "backup_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mount" ADD CONSTRAINT "mount_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1072,6 +1072,13 @@
"when": 1773903778014,
"tag": "0152_odd_firelord",
"breakpoints": true
},
{
"idx": 153,
"version": "7",
"when": 1774322599182,
"tag": "0153_motionless_mastermind",
"breakpoints": true
}
]
}
+8 -6
View File
@@ -6,11 +6,12 @@ import { useCallback, useEffect, useState } from "react";
const PAGES = [
"compose",
"application",
"postgres",
"redis",
"mysql",
"libsql",
"mariadb",
"mongodb",
"mysql",
"postgres",
"redis",
] as const;
type Page = (typeof PAGES)[number];
@@ -63,11 +64,12 @@ const REDIS_SHORTCUTS: Shortcuts = {
const SHORTCUTS: ShortcutsDictionary = {
application: APPLICATION_SHORTCUTS,
compose: COMPOSE_SHORTCUTS,
postgres: POSTGRES_SHORTCUTS,
redis: REDIS_SHORTCUTS,
mysql: POSTGRES_SHORTCUTS,
libsql: POSTGRES_SHORTCUTS,
mariadb: POSTGRES_SHORTCUTS,
mongodb: POSTGRES_SHORTCUTS,
mysql: POSTGRES_SHORTCUTS,
postgres: POSTGRES_SHORTCUTS,
redis: REDIS_SHORTCUTS,
};
/**
@@ -37,6 +37,7 @@ import { DuplicateProject } from "@/components/dashboard/project/duplicate-proje
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment";
import {
LibsqlIcon,
MariadbIcon,
MongodbIcon,
MysqlIcon,
@@ -111,7 +112,8 @@ export type Services = {
| "mysql"
| "mongo"
| "redis"
| "compose";
| "compose"
| "libsql";
description?: string | null;
id: string;
createdAt: string;
@@ -248,14 +250,27 @@ export const extractServicesFromEnvironment = (
};
}) || [];
const libsql: Services[] =
environment.libsql?.map((item) => ({
name: item.name,
type: "libsql",
id: item.libsqlId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
serverName: item?.server?.name || null,
})) || [];
allServices.push(
...applications,
...compose,
...libsql,
...mysql,
...redis,
...mongo,
...postgres,
...mariadb,
...compose,
);
allServices.sort((a, b) => {
@@ -383,7 +398,8 @@ const EnvironmentPage = (
(currentEnvironment.postgres?.length || 0) === 0 &&
(currentEnvironment.redis?.length || 0) === 0 &&
(currentEnvironment.applications?.length || 0) === 0 &&
(currentEnvironment.compose?.length || 0) === 0);
(currentEnvironment.compose?.length || 0) === 0 &&
(currentEnvironment.libsql?.length || 0) === 0);
const applications = extractServicesFromEnvironment(currentEnvironment);
@@ -396,6 +412,7 @@ const EnvironmentPage = (
{ value: "mysql", label: "MySQL", icon: MysqlIcon },
{ value: "redis", label: "Redis", icon: RedisIcon },
{ value: "compose", label: "Compose", icon: CircuitBoard },
{ value: "libsql", label: "Libsql", icon: LibsqlIcon },
];
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
@@ -1525,6 +1542,9 @@ const EnvironmentPage = (
{service.type === "compose" && (
<CircuitBoard className="h-6 w-6" />
)}
{service.type === "libsql" && (
<LibsqlIcon className="h-6 w-6" />
)}
</span>
</div>
</CardTitle>
@@ -0,0 +1,358 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-external-libsql-credentials";
import { ShowGeneralLibsql } from "@/components/dashboard/libsql/general/show-general-libsql";
import { ShowInternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-internal-libsql-credentials";
import { UpdateLibsql } from "@/components/dashboard/libsql/update-libsql";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { LibsqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Libsql = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { libsqlId, activeTab } = props;
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.libsql.one.useQuery({ libsqlId });
const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="pb-10">
<UseKeyboardNav forPage="libsql" />
<AdvanceBreadcrumb />
<div className="flex flex-col gap-4">
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |
Dokploy
</title>
</Head>
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<LibsqlIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
<UpdateLibsql libsqlId={libsqlId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={libsqlId} type="libsql" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/libsql/${libsqlId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralLibsql libsqlId={libsqlId} />
<ShowInternalLibsqlCredentials libsqlId={libsqlId} />
<ShowExternalLibsqlCredentials libsqlId={libsqlId} />
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={libsqlId} type="libsql" />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups
id={libsqlId}
databaseType="libsql"
backupType="database"
/>
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={libsqlId}
type="libsql"
/>
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export default Libsql;
Libsql.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{
libsqlId: string;
activeTab: TabState;
environmentId: string;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
// Fetch data from external API
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
if (typeof params?.libsqlId === "string") {
try {
await helpers.libsql.one.fetch({
libsqlId: params?.libsqlId,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
libsqlId: params?.libsqlId,
activeTab: (activeTab || "general") as TabState,
environmentId: params?.environmentId,
},
};
} catch {
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
},
};
}
}
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
+28 -26
View File
@@ -16,6 +16,7 @@ import { gitProviderRouter } from "./routers/git-provider";
import { giteaRouter } from "./routers/gitea";
import { githubRouter } from "./routers/github";
import { gitlabRouter } from "./routers/gitlab";
import { libsqlRouter } from "./routers/libsql";
import { mariadbRouter } from "./routers/mariadb";
import { mongoRouter } from "./routers/mongo";
import { mountRouter } from "./routers/mount";
@@ -54,39 +55,40 @@ import { volumeBackupsRouter } from "./routers/volume-backups";
export const appRouter = createTRPCRouter({
admin: adminRouter,
docker: dockerRouter,
project: projectRouter,
application: applicationRouter,
mysql: mysqlRouter,
postgres: postgresRouter,
redis: redisRouter,
mongo: mongoRouter,
mariadb: mariadbRouter,
compose: composeRouter,
user: userRouter,
domain: domainRouter,
destination: destinationRouter,
backup: backupRouter,
deployment: deploymentRouter,
previewDeployment: previewDeploymentRouter,
mounts: mountRouter,
certificates: certificateRouter,
settings: settingsRouter,
security: securityRouter,
redirects: redirectsRouter,
port: portRouter,
registry: registryRouter,
cluster: clusterRouter,
notification: notificationRouter,
sshKey: sshRouter,
gitProvider: gitProviderRouter,
gitea: giteaRouter,
bitbucket: bitbucketRouter,
gitlab: gitlabRouter,
certificates: certificateRouter,
cluster: clusterRouter,
compose: composeRouter,
deployment: deploymentRouter,
destination: destinationRouter,
docker: dockerRouter,
domain: domainRouter,
gitea: giteaRouter,
gitProvider: gitProviderRouter,
github: githubRouter,
gitlab: gitlabRouter,
libsql: libsqlRouter,
mariadb: mariadbRouter,
mongo: mongoRouter,
mounts: mountRouter,
mysql: mysqlRouter,
notification: notificationRouter,
port: portRouter,
postgres: postgresRouter,
previewDeployment: previewDeploymentRouter,
project: projectRouter,
redirects: redirectsRouter,
redis: redisRouter,
registry: registryRouter,
security: securityRouter,
server: serverRouter,
settings: settingsRouter,
sshKey: sshRouter,
stripe: stripeRouter,
swarm: swarmRouter,
user: userRouter,
ai: aiRouter,
organization: organizationRouter,
licenseKey: licenseKeyRouter,
+43
View File
@@ -3,6 +3,8 @@ import {
findBackupById,
findComposeByBackupId,
findComposeById,
findLibsqlByBackupId,
findLibsqlById,
findMariadbByBackupId,
findMariadbById,
findMongoByBackupId,
@@ -16,6 +18,7 @@ import {
keepLatestNBackups,
removeBackupById,
removeScheduleBackup,
runLibsqlBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
@@ -36,6 +39,7 @@ import {
} from "@dokploy/server/utils/process/execAsync";
import {
restoreComposeBackup,
restoreLibsqlBackup,
restoreMariadbBackup,
restoreMongoBackup,
restoreMySqlBackup,
@@ -82,6 +86,7 @@ export const backupRouter = createTRPCRouter({
input.mysqlId ||
input.mariadbId ||
input.mongoId ||
input.libsqlId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -103,6 +108,8 @@ export const backupRouter = createTRPCRouter({
serverId = backup.mongo.serverId;
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
serverId = backup.mariadb.serverId;
} else if (databaseType === "libsql" && backup.libsql?.serverId) {
serverId = backup.libsql.serverId;
} else if (
backup.backupType === "compose" &&
backup.compose?.serverId
@@ -154,6 +161,7 @@ export const backupRouter = createTRPCRouter({
backup.mysqlId ||
backup.mariadbId ||
backup.mongoId ||
backup.libsqlId ||
backup.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -173,6 +181,7 @@ export const backupRouter = createTRPCRouter({
existing.mysqlId ||
existing.mariadbId ||
existing.mongoId ||
existing.libsqlId ||
existing.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -229,6 +238,7 @@ export const backupRouter = createTRPCRouter({
backup.mysqlId ||
backup.mariadbId ||
backup.mongoId ||
backup.libsqlId ||
backup.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -400,6 +410,33 @@ export const backupRouter = createTRPCRouter({
});
}
}),
manualBackupLibsql: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.libsqlId) {
await checkServicePermissionAndAccess(ctx, backup.libsqlId, {
backup: ["create"],
});
}
const libsql = await findLibsqlByBackupId(backup.backupId);
await runLibsqlBackup(libsql, backup);
await keepLatestNBackups(backup, libsql?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error running manual Libsql backup ",
cause: error,
});
}
}),
manualBackupWebServer: withPermission("backup", "create")
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
@@ -536,6 +573,12 @@ export const backupRouter = createTRPCRouter({
queue.push(log);
});
}
if (input.databaseType === "libsql") {
const libsql = await findLibsqlById(input.databaseId);
restoreLibsqlBackup(libsql, destination, input, (log) => {
queue.push(log);
});
}
if (input.databaseType === "web-server") {
restoreWebServerBackup(destination, input.backupFile, (log) => {
queue.push(log);
@@ -38,6 +38,12 @@ const filterEnvironmentServices = (
applications: environment.applications.filter((app: any) =>
accessedServices.includes(app.applicationId),
),
compose: environment.compose.filter((comp: any) =>
accessedServices.includes(comp.composeId),
),
libsql: environment.libsql.filter((db: any) =>
accessedServices.includes(db.libsqlId),
),
mariadb: environment.mariadb.filter((db: any) =>
accessedServices.includes(db.mariadbId),
),
@@ -53,9 +59,6 @@ const filterEnvironmentServices = (
redis: environment.redis.filter((db: any) =>
accessedServices.includes(db.redisId),
),
compose: environment.compose.filter((comp: any) =>
accessedServices.includes(comp.composeId),
),
});
export const environmentRouter = createTRPCRouter({
+453
View File
@@ -0,0 +1,453 @@
import {
checkPortInUse,
createLibsql,
createMount,
deployLibsql,
findEnvironmentById,
findLibsqlById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
removeLibsqlById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateLibsqlById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { db } from "@/server/db";
import {
apiChangeLibsqlStatus,
apiCreateLibsql,
apiDeployLibsql,
apiFindOneLibsql,
apiRebuildLibsql,
apiResetLibsql,
apiSaveEnvironmentVariablesLibsql,
apiSaveExternalPortsLibsql,
apiUpdateLibsql,
libsql as libsqlTable,
} from "@/server/db/schema";
export const libsqlRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateLibsql)
.mutation(async ({ input, ctx }) => {
try {
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a Libsql",
});
}
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newLibsql = await createLibsql({
...input,
});
await addNewService(ctx, newLibsql.libsqlId);
await createMount({
serviceId: newLibsql.libsqlId,
serviceType: "libsql",
volumeName: `${newLibsql.appName}-data`,
mountPath: "/var/lib/sqld",
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newLibsql.libsqlId,
resourceName: newLibsql.appName,
});
return true;
} catch (error) {
throw error;
}
}),
one: protectedProcedure
.input(apiFindOneLibsql)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "read");
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this Libsql",
});
}
return libsql;
}),
start: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await startServiceRemote(libsql.serverId, libsql.appName);
} else {
await startService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
stop: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await stopServiceRemote(libsql.serverId, libsql.appName);
} else {
await stopService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
saveExternalPorts: protectedProcedure
.input(apiSaveExternalPortsLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
service: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.sqldNode === "replica" && input.externalGRPCPort !== null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "externalGRPCPort cannot be set when sqldNode is 'replica'",
});
}
const portsToCheck = [
{
port: input.externalPort,
name: "externalPort",
current: libsql.externalPort,
},
{
port: input.externalGRPCPort,
name: "externalGRPCPort",
current: libsql.externalGRPCPort,
},
{
port: input.externalAdminPort,
name: "externalAdminPort",
current: libsql.externalAdminPort,
},
];
for (const { port, name, current } of portsToCheck) {
if (port && port !== current) {
const portCheck = await checkPortInUse(
port,
libsql.serverId || undefined,
);
if (portCheck.isInUse) {
throw new TRPCError({
code: "CONFLICT",
message: `Port ${port} (${name}) is already in use by ${portCheck.conflictingContainer}`,
});
}
}
}
await updateLibsqlById(input.libsqlId, {
externalPort: input.externalPort,
externalGRPCPort: input.externalGRPCPort,
externalAdminPort: input.externalAdminPort,
});
await deployLibsql(input.libsqlId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
deploy: protectedProcedure
.input(apiDeployLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return deployLibsql(input.libsqlId);
}),
deployWithLogs: protectedProcedure
.meta({
openapi: {
path: "/deploy/libsql-with-logs",
method: "POST",
override: true,
enabled: false,
},
})
.input(apiDeployLibsql)
.subscription(async function* ({ input, ctx, signal }) {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
deployLibsql(input.libsqlId, (log) => {
queue.push(log);
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
yield queue.shift()!;
} else {
await new Promise((r) => setTimeout(r, 50));
}
if (signal?.aborted) {
return;
}
}
}),
changeStatus: protectedProcedure
.input(apiChangeLibsqlStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
await updateLibsqlById(input.libsqlId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
remove: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "delete");
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this Libsql",
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
const cleanupOperations = [
async () => await removeService(libsql?.appName, libsql.serverId),
async () => await removeLibsqlById(input.libsqlId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (_) {}
}
return libsql;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
envVars: ["write"],
});
const service = await updateLibsqlById(input.libsqlId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error adding environment variables",
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.libsqlId,
});
return true;
}),
reload: protectedProcedure
.input(apiResetLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await stopServiceRemote(libsql.serverId, libsql.appName);
} else {
await stopService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "idle",
});
if (libsql.serverId) {
await startServiceRemote(libsql.serverId, libsql.appName);
} else {
await startService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateLibsql)
.mutation(async ({ input, ctx }) => {
const { libsqlId, ...rest } = input;
await checkServicePermissionAndAccess(ctx, libsqlId, {
service: ["create"],
});
const libsql = await updateLibsqlById(libsqlId, {
...rest,
});
if (!libsql) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error updating Libsql",
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsqlId,
resourceName: libsql.appName,
});
return true;
}),
move: protectedProcedure
.input(
z.object({
libsqlId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
service: ["create"],
});
const updatedLibsql = await db
.update(libsqlTable)
.set({
environmentId: input.targetEnvironmentId,
})
.where(eq(libsqlTable.libsqlId, input.libsqlId))
.returning()
.then((res) => res[0]);
if (!updatedLibsql) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move libsql",
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedLibsql.libsqlId,
resourceName: updatedLibsql.appName,
});
return updatedLibsql;
}),
rebuild: protectedProcedure
.input(apiRebuildLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
await rebuildDatabase(input.libsqlId, "libsql");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.libsqlId,
});
return true;
}),
});
+10 -3
View File
@@ -3,6 +3,7 @@ import {
deleteMount,
findApplicationById,
findComposeById,
findLibsqlById,
findMariadbById,
findMongoById,
findMountById,
@@ -13,13 +14,14 @@ import {
getServiceContainer,
updateMount,
} from "@dokploy/server";
import type { ServiceType } from "@dokploy/server/db/schema/mount";
import {
checkServiceAccess,
checkServicePermissionAndAccess,
} from "@dokploy/server/services/permission";
import type { ServiceType } from "@dokploy/server/db/schema/mount";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateMount,
apiFindMountByApplicationId,
@@ -28,7 +30,6 @@ import {
apiUpdateMount,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { audit } from "@/server/api/utils/audit";
async function getServiceOrganizationId(
serviceId: string,
@@ -63,6 +64,10 @@ async function getServiceOrganizationId(
const compose = await findComposeById(serviceId);
return compose?.environment?.project?.organizationId ?? null;
}
case "libsql": {
const libsql = await findLibsqlById(serviceId);
return libsql?.environment?.project?.organizationId ?? null;
}
default:
return null;
}
@@ -95,6 +100,7 @@ export const mountRouter = createTRPCRouter({
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.libsqlId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -120,6 +126,7 @@ export const mountRouter = createTRPCRouter({
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.libsqlId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -139,6 +146,7 @@ export const mountRouter = createTRPCRouter({
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.libsqlId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -169,7 +177,6 @@ export const mountRouter = createTRPCRouter({
listByServiceId: protectedProcedure
.input(apiFindMountByApplicationId)
.query(async ({ input, ctx }) => {
console.log("input", input);
await checkServiceAccess(ctx, input.serviceId, "read");
const organizationId = await getServiceOrganizationId(
input.serviceId,
+121 -61
View File
@@ -3,6 +3,7 @@ import {
createBackup,
createCompose,
createDomain,
createLibsql,
createMariadb,
createMongo,
createMount,
@@ -18,6 +19,7 @@ import {
findApplicationById,
findComposeById,
findEnvironmentById,
findLibsqlById,
findMariadbById,
findMongoById,
findMySqlById,
@@ -28,6 +30,7 @@ import {
IS_CLOUD,
updateProjectById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewEnvironment,
addNewProject,
@@ -35,17 +38,16 @@ import {
checkProjectAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateProject,
apiFindOneProject,
@@ -54,6 +56,7 @@ import {
applications,
compose,
environments,
libsql,
mariadb,
mongo,
mysql,
@@ -138,6 +141,9 @@ export const projectRouter = createTRPCRouter({
accessedServices,
),
},
libsql: {
where: buildServiceFilter(libsql.libsqlId, accessedServices),
},
mariadb: {
where: buildServiceFilter(
mariadb.mariadbId,
@@ -227,6 +233,14 @@ export const projectRouter = createTRPCRouter({
applicationStatus: true,
},
},
libsql: {
where: buildServiceFilter(libsql.libsqlId, accessedServices),
columns: {
libsqlId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
columns: {
@@ -338,6 +352,11 @@ export const projectRouter = createTRPCRouter({
composeStatus: true,
},
},
libsql: {
columns: {
libsqlId: true,
},
},
},
columns: {
name: true,
@@ -450,6 +469,17 @@ export const projectRouter = createTRPCRouter({
serverId: true,
},
},
libsql: {
columns: {
libsqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
},
},
@@ -622,12 +652,13 @@ export const projectRouter = createTRPCRouter({
id: z.string(),
type: z.enum([
"application",
"postgres",
"compose",
"libsql",
"mariadb",
"mongo",
"mysql",
"postgres",
"redis",
"compose",
]),
}),
)
@@ -771,21 +802,27 @@ export const projectRouter = createTRPCRouter({
break;
}
case "postgres": {
const { postgresId, mounts, backups, appName, ...postgres } =
await findPostgresById(id);
case "compose": {
const {
composeId,
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newPostgres = await createPostgres({
...postgres,
const newCompose = await createCompose({
...compose,
appName: newAppName,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
? `${compose.name} (copy)`
: compose.name,
environmentId: targetProject?.environmentId || "",
});
@@ -793,18 +830,49 @@ export const projectRouter = createTRPCRouter({
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newPostgres.postgresId,
serviceType: "postgres",
serviceId: newCompose.composeId,
serviceType: "compose",
});
}
for (const backup of backups) {
const { backupId, appName: _appName, ...rest } = backup;
await createBackup({
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
postgresId: newPostgres.postgresId,
composeId: newCompose.composeId,
domainType: "compose",
});
}
break;
}
case "libsql": {
const { libsqlId, mounts, appName, ...libsql } =
await findLibsqlById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newLibsql = await createLibsql({
...libsql,
appName: newAppName,
name: input.duplicateInSameProject
? `${libsql.name} (copy)`
: libsql.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newLibsql.libsqlId,
serviceType: "libsql",
});
}
break;
}
case "mariadb": {
@@ -915,6 +983,42 @@ export const projectRouter = createTRPCRouter({
}
break;
}
case "postgres": {
const { postgresId, mounts, backups, appName, ...postgres } =
await findPostgresById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newPostgres = await createPostgres({
...postgres,
appName: newAppName,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newPostgres.postgresId,
serviceType: "postgres",
});
}
for (const backup of backups) {
const { backupId, ...rest } = backup;
await createBackup({
...rest,
postgresId: newPostgres.postgresId,
});
}
break;
}
case "redis": {
const { redisId, mounts, appName, ...redis } =
await findRedisById(id);
@@ -942,50 +1046,6 @@ export const projectRouter = createTRPCRouter({
});
}
break;
}
case "compose": {
const {
composeId,
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newCompose = await createCompose({
...compose,
appName: newAppName,
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newCompose.composeId,
serviceType: "compose",
});
}
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
composeId: newCompose.composeId,
domainType: "compose",
});
}
break;
}
}
@@ -648,6 +648,7 @@ export const settingsRouter = createTRPCRouter({
"postgres",
"redis",
"mongo",
"libsql",
"mariadb",
"sshRouter",
"gitProvider",
@@ -15,6 +15,7 @@ import {
updateVolumeBackupSchema,
volumeBackups,
} from "@dokploy/server/db/schema";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import {
execAsyncRemote,
execAsyncStream,
@@ -25,7 +26,6 @@ import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({
@@ -41,6 +41,7 @@ export const volumeBackupsRouter = createTRPCRouter({
"mongo",
"redis",
"compose",
"libsql",
]),
}),
)
@@ -58,6 +59,7 @@ export const volumeBackupsRouter = createTRPCRouter({
mongo: true,
redis: true,
compose: true,
libsql: true,
},
orderBy: [desc(volumeBackups.createdAt)],
});
@@ -72,6 +74,7 @@ export const volumeBackupsRouter = createTRPCRouter({
input.mariadbId ||
input.mongoId ||
input.redisId ||
input.libsqlId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -113,6 +116,7 @@ export const volumeBackupsRouter = createTRPCRouter({
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -136,6 +140,7 @@ export const volumeBackupsRouter = createTRPCRouter({
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -161,6 +166,7 @@ export const volumeBackupsRouter = createTRPCRouter({
existingVb.mariadbId ||
existingVb.mongoId ||
existingVb.redisId ||
existingVb.libsqlId ||
existingVb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -220,6 +226,7 @@ export const volumeBackupsRouter = createTRPCRouter({
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
+11
View File
@@ -8,6 +8,7 @@ import {
keepLatestNBackups,
runCommand,
runComposeBackup,
runLibsqlBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
@@ -38,6 +39,7 @@ export const runJobs = async (job: QueueJob) => {
mysql,
mongo,
mariadb,
libsql,
compose,
backupType,
} = backup;
@@ -75,6 +77,14 @@ export const runJobs = async (job: QueueJob) => {
}
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, server.serverId);
} else if (databaseType === "libsql" && libsql) {
const server = await findServerById(libsql.serverId as string);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await runLibsqlBackup(libsql, backup);
await keepLatestNBackups(backup, server.serverId);
}
} else if (backupType === "compose" && compose) {
const server = await findServerById(compose.serverId as string);
@@ -141,6 +151,7 @@ export const initializeJobs = async () => {
mysql: true,
postgres: true,
mongo: true,
libsql: true,
compose: true,
},
});
+73 -12
View File
@@ -1,7 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const DOKPLOY_DOCKER_API_VERSION =
process.env.DOKPLOY_DOCKER_API_VERSION;
export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
@@ -10,20 +12,79 @@ export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
: undefined;
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const docker = new Docker({
...(DOKPLOY_DOCKER_API_VERSION && {
version: DOKPLOY_DOCKER_API_VERSION,
}),
...(DOKPLOY_DOCKER_HOST && {
host: DOKPLOY_DOCKER_HOST,
}),
...(DOKPLOY_DOCKER_PORT && {
port: DOKPLOY_DOCKER_PORT,
}),
});
type DockerSocketCandidate = {
label: string;
path: string;
};
const getDockerConfig = (): Docker => {
const versionOption = DOKPLOY_DOCKER_API_VERSION
? { version: DOKPLOY_DOCKER_API_VERSION }
: {};
// Explicit remote Docker host configuration
if (DOKPLOY_DOCKER_HOST) {
console.info(
`Using remote Docker host: ${DOKPLOY_DOCKER_HOST}${DOKPLOY_DOCKER_PORT ? `:${DOKPLOY_DOCKER_PORT}` : ""}`,
);
return new Docker({
host: DOKPLOY_DOCKER_HOST,
...(DOKPLOY_DOCKER_PORT && { port: DOKPLOY_DOCKER_PORT }),
...versionOption,
});
}
// Local socket auto-detection (Rancher Desktop, Colima, standard Docker)
const dockerSocketCandidates: Array<DockerSocketCandidate> = [];
if (process.env.DOCKER_HOST) {
dockerSocketCandidates.push({
label: "DOCKER_HOST environment variable",
path: process.env.DOCKER_HOST.replace("unix://", ""),
});
}
if (process.env.HOME) {
dockerSocketCandidates.push({
label: "Rancher Desktop socket",
path: `${process.env.HOME}/.rd/docker.sock`,
});
}
dockerSocketCandidates.push({
label: "Standard Docker socket",
path: "/var/run/docker.sock",
});
for (const candidate of dockerSocketCandidates) {
try {
if (candidate.path && fs.existsSync(candidate.path)) {
console.info(
`Using Docker socket (${candidate.label}): ${candidate.path}`,
);
return new Docker({
socketPath: candidate.path,
...versionOption,
});
}
} catch (e) {
console.info(
`Docker socket initialization failed for ${candidate.label} (${candidate.path}): ${e instanceof Error ? e.message : "Unknown error"}`,
);
}
}
console.info(
"Using default Docker configuration. You can set the DOCKER_HOST environment variable to specify a custom Docker socket path.",
);
return new Docker({ ...versionOption });
};
export const docker = getDockerConfig();
// When not set, use the legacy default so 2FA remains working for users who
// enabled it before BETTER_AUTH_SECRET was introduced .
// enabled it before BETTER_AUTH_SECRET was introduced.
export const BETTER_AUTH_SECRET =
process.env.BETTER_AUTH_SECRET || "better-auth-secret-123456789";
+27 -2
View File
@@ -15,6 +15,7 @@ import { generateAppName } from ".";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -26,6 +27,7 @@ export const databaseType = pgEnum("databaseType", [
"mysql",
"mongo",
"web-server",
"libsql",
]);
export const backupType = pgEnum("backupType", ["database", "compose"]);
@@ -74,6 +76,9 @@ export const backups = pgTable("backup", {
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
onDelete: "cascade",
}),
libsqlId: text("libsqlId").references((): AnyPgColumn => libsql.libsqlId, {
onDelete: "cascade",
}),
userId: text("userId").references(() => user.id),
// Only for compose backups
metadata: jsonb("metadata").$type<
@@ -118,6 +123,10 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
fields: [backups.mongoId],
references: [mongo.mongoId],
}),
libsql: one(libsql, {
fields: [backups.libsqlId],
references: [libsql.libsqlId],
}),
user: one(user, {
fields: [backups.userId],
references: [user.id],
@@ -137,11 +146,19 @@ const createSchema = createInsertSchema(backups, {
database: z.string().min(1),
schedule: z.string(),
keepLatestCount: z.number().optional(),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
databaseType: z.enum([
"postgres",
"mariadb",
"mysql",
"mongo",
"web-server",
"libsql",
]),
postgresId: z.string().optional(),
mariadbId: z.string().optional(),
mysqlId: z.string().optional(),
mongoId: z.string().optional(),
libsqlId: z.string().optional(),
userId: z.string().optional(),
metadata: z.any().optional(),
});
@@ -157,6 +174,7 @@ export const apiCreateBackup = createSchema.pick({
mysqlId: true,
postgresId: true,
mongoId: true,
libsqlId: true,
databaseType: true,
userId: true,
backupType: true,
@@ -192,7 +210,14 @@ export const apiUpdateBackup = createSchema
export const apiRestoreBackup = z.object({
databaseId: z.string(),
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
databaseType: z.enum([
"postgres",
"mysql",
"mariadb",
"mongo",
"web-server",
"libsql",
]),
backupType: z.enum(["database", "compose"]),
databaseName: z.string().min(1),
backupFile: z.string().min(1),
+7 -5
View File
@@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -36,12 +37,13 @@ export const environmentRelations = relations(
references: [projects.projectId],
}),
applications: many(applications),
mariadb: many(mariadb),
postgres: many(postgres),
mysql: many(mysql),
redis: many(redis),
mongo: many(mongo),
compose: many(compose),
libsql: many(libsql),
mariadb: many(mariadb),
mongo: many(mongo),
mysql: many(mysql),
postgres: many(postgres),
redis: many(redis),
}),
);
+1
View File
@@ -14,6 +14,7 @@ export * from "./git-provider";
export * from "./gitea";
export * from "./github";
export * from "./gitlab";
export * from "./libsql";
export * from "./mariadb";
export * from "./mongo";
export * from "./mount";
+249
View File
@@ -0,0 +1,249 @@
import { relations } from "drizzle-orm";
import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { server } from "./server";
import {
applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
sqldNode,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils";
export const libsql = pgTable("libsql", {
libsqlId: text("libsqlId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
appName: text("appName")
.notNull()
.$defaultFn(() => generateAppName("libsql"))
.unique(),
description: text("description"),
databaseUser: text("databaseUser").notNull(),
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"),
// RESOURCES
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
//
externalPort: integer("externalPort"),
externalGRPCPort: integer("externalGRPCPort"),
externalAdminPort: integer("externalAdminPort"),
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
environmentId: text("environmentId")
.notNull()
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const libsqlRelations = relations(libsql, ({ one, many }) => ({
environment: one(environments, {
fields: [libsql.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
server: one(server, {
fields: [libsql.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(libsql, {
libsqlId: z.string(),
name: z.string().min(1),
appName: z.string().min(1),
createdAt: z.string(),
databaseUser: z.string().min(1),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
sqldNode: z.enum(sqldNode.enumValues),
sqldPrimaryUrl: z.string().nullable(),
enableNamespaces: z.boolean().default(false),
dockerImage: z
.string()
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
environmentId: z.string(),
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(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
});
export const apiCreateLibsql = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
environmentId: true,
description: true,
databaseUser: true,
databasePassword: true,
sqldNode: true,
sqldPrimaryUrl: true,
enableNamespaces: true,
serverId: true,
})
.required()
.superRefine((data, ctx) => {
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:
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
});
}
});
export const apiFindOneLibsql = createSchema
.pick({
libsqlId: true,
})
.required();
export const apiChangeLibsqlStatus = createSchema
.pick({
libsqlId: true,
applicationStatus: true,
})
.required();
export const apiSaveEnvironmentVariablesLibsql = createSchema
.pick({
libsqlId: true,
env: true,
})
.required();
export const apiSaveExternalPortsLibsql = createSchema
.pick({
libsqlId: true,
externalPort: true,
externalGRPCPort: true,
externalAdminPort: true,
})
.required({ libsqlId: true })
.superRefine((data, ctx) => {
if (
data.externalPort === null &&
data.externalGRPCPort === null &&
data.externalAdminPort === null
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Either externalPort, externalGRPCPort or externalAdminPort must be provided.",
path: ["externalPort", "externalGRPCPort", "externalAdminPort"],
});
}
});
export const apiDeployLibsql = createSchema
.pick({
libsqlId: true,
})
.required();
export const apiResetLibsql = createSchema
.pick({
libsqlId: true,
appName: true,
})
.required();
export const apiUpdateLibsql = createSchema
.partial()
.extend({
libsqlId: z.string().min(1),
})
.omit({ serverId: true });
export const apiRebuildLibsql = createSchema
.pick({
libsqlId: true,
})
.required();
+27 -28
View File
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -19,8 +20,11 @@ export const serviceType = pgEnum("serviceType", [
"mongo",
"redis",
"compose",
"libsql",
]);
export type ServiceType = (typeof serviceType.enumValues)[number];
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
export const mounts = pgTable("mount", {
@@ -39,7 +43,10 @@ export const mounts = pgTable("mount", {
() => applications.applicationId,
{ onDelete: "cascade" },
),
postgresId: text("postgresId").references(() => postgres.postgresId, {
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
onDelete: "cascade",
}),
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
@@ -51,10 +58,10 @@ export const mounts = pgTable("mount", {
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
onDelete: "cascade",
}),
redisId: text("redisId").references(() => redis.redisId, {
postgresId: text("postgresId").references(() => postgres.postgresId, {
onDelete: "cascade",
}),
composeId: text("composeId").references(() => compose.composeId, {
redisId: text("redisId").references(() => redis.redisId, {
onDelete: "cascade",
}),
});
@@ -64,9 +71,13 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
fields: [mounts.applicationId],
references: [applications.applicationId],
}),
postgres: one(postgres, {
fields: [mounts.postgresId],
references: [postgres.postgresId],
compose: one(compose, {
fields: [mounts.composeId],
references: [compose.composeId],
}),
libsql: one(libsql, {
fields: [mounts.libsqlId],
references: [libsql.libsqlId],
}),
mariadb: one(mariadb, {
fields: [mounts.mariadbId],
@@ -80,14 +91,14 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
fields: [mounts.mysqlId],
references: [mysql.mysqlId],
}),
postgres: one(postgres, {
fields: [mounts.postgresId],
references: [postgres.postgresId],
}),
redis: one(redis, {
fields: [mounts.redisId],
references: [redis.redisId],
}),
compose: one(compose, {
fields: [mounts.composeId],
references: [compose.composeId],
}),
}));
const createSchema = createInsertSchema(mounts, {
@@ -107,13 +118,10 @@ const createSchema = createInsertSchema(mounts, {
"mongo",
"redis",
"compose",
"libsql",
]),
});
export type ServiceType = NonNullable<
z.infer<typeof createSchema>["serviceType"]
>;
export const apiCreateMount = createSchema
.pick({
type: true,
@@ -121,8 +129,8 @@ export const apiCreateMount = createSchema
volumeName: true,
content: true,
mountPath: true,
serviceType: true,
filePath: true,
serviceType: true,
})
.extend({
serviceId: z.string().min(1),
@@ -142,21 +150,12 @@ export const apiRemoveMount = createSchema
.required();
export const apiFindMountByApplicationId = createSchema
.pick({
serviceType: true,
})
.required()
.extend({
serviceId: z.string().min(1),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
})
.pick({
serviceId: true,
serviceType: true,
});
export const apiUpdateMount = createSchema.partial().extend({
+2
View File
@@ -15,6 +15,7 @@ import { applications } from "./application";
import { certificates } from "./certificate";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -116,6 +117,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
relationName: "applicationBuildServer",
}),
compose: many(compose),
libsql: many(libsql),
redis: many(redis),
mariadb: many(mariadb),
mongo: many(mongo),
+2
View File
@@ -16,6 +16,8 @@ export const certificateType = pgEnum("certificateType", [
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
export const sqldNode = pgEnum("sqldNode", ["primary", "replica"]);
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { serviceType } from "./mount";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -7,9 +8,9 @@ import { applications } from "./application";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { serviceType } from "./mount";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
@@ -53,6 +54,9 @@ export const volumeBackups = pgTable("volume_backup", {
redisId: text("redisId").references(() => redis.redisId, {
onDelete: "cascade",
}),
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
onDelete: "cascade",
}),
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
@@ -93,6 +97,10 @@ export const volumeBackupsRelations = relations(
fields: [volumeBackups.redisId],
references: [redis.redisId],
}),
libsql: one(libsql, {
fields: [volumeBackups.libsqlId],
references: [libsql.libsqlId],
}),
compose: one(compose, {
fields: [volumeBackups.composeId],
references: [compose.composeId],
@@ -14,7 +14,7 @@ import {
export type TemplateProps = {
projectName: string;
applicationName: string;
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
type: "error" | "success";
errorMessage?: string;
date: string;
@@ -22,7 +22,8 @@ export type TemplateProps = {
| "mongodb"
| "mariadb"
| "redis"
| "compose";
| "compose"
| "libsql";
type: "error" | "success";
errorMessage?: string;
backupSize?: string;
+2
View File
@@ -22,6 +22,7 @@ export * from "./services/git-provider";
export * from "./services/gitea";
export * from "./services/github";
export * from "./services/gitlab";
export * from "./services/libsql";
export * from "./services/mariadb";
export * from "./services/mongo";
export * from "./services/mount";
@@ -67,6 +68,7 @@ export * from "./utils/access-log/types";
export * from "./utils/access-log/utils";
export * from "./utils/backups/compose";
export * from "./utils/backups/index";
export * from "./utils/backups/libsql";
export * from "./utils/backups/mariadb";
export * from "./utils/backups/mongo";
export * from "./utils/backups/mysql";
+3 -1
View File
@@ -33,6 +33,7 @@ export const findBackupById = async (backupId: string) => {
mysql: true,
mariadb: true,
mongo: true,
libsql: true,
destination: true,
compose: true,
},
@@ -72,7 +73,7 @@ export const removeBackupById = async (backupId: string) => {
export const findBackupsByDbId = async (
id: string,
type: "postgres" | "mysql" | "mariadb" | "mongo",
type: "postgres" | "mysql" | "mariadb" | "mongo" | "libsql",
) => {
const result = await db.query.backups.findMany({
where: eq(backups[`${type}Id`], id),
@@ -81,6 +82,7 @@ export const findBackupsByDbId = async (
mysql: true,
mariadb: true,
mongo: true,
libsql: true,
destination: true,
},
});
@@ -169,6 +169,24 @@ export const findEnvironmentById = async (environmentId: string) => {
serverId: true,
},
},
libsql: {
with: {
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
libsqlId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
project: true,
},
});
@@ -193,6 +211,7 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
postgres: true,
redis: true,
compose: true,
libsql: true,
project: true,
},
columns: {
@@ -211,6 +230,7 @@ const environmentHasServices = (
return (
(env.applications?.length ?? 0) > 0 ||
(env.compose?.length ?? 0) > 0 ||
(env.libsql?.length ?? 0) > 0 ||
(env.mariadb?.length ?? 0) > 0 ||
(env.mongo?.length ?? 0) > 0 ||
(env.mysql?.length ?? 0) > 0 ||
+162
View File
@@ -0,0 +1,162 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateLibsql,
backups,
buildAppName,
libsql,
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildLibsql } from "@dokploy/server/utils/databases/libsql";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import type { z } from "zod";
import { validUniqueServerAppName } from "./project";
export type Libsql = typeof libsql.$inferSelect;
export const createLibsql = async (input: z.infer<typeof apiCreateLibsql>) => {
const appName = buildAppName("libsql", input.appName);
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newLibsql = await db
.insert(libsql)
.values({
...input,
databasePassword: input.databasePassword
? input.databasePassword
: generatePassword(),
appName,
})
.returning()
.then((value) => value[0]);
if (!newLibsql) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting libsql database",
});
}
return newLibsql;
};
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const findLibsqlById = async (libsqlId: string) => {
const result = await db.query.libsql.findFirst({
where: eq(libsql.libsqlId, libsqlId),
with: {
environment: {
with: {
project: true,
},
},
mounts: true,
server: true,
backups: {
with: {
destination: true,
deployments: true,
},
},
},
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Libsql not found",
});
}
return result;
};
export const updateLibsqlById = async (
libsqlId: string,
libsqlData: Partial<Libsql>,
) => {
const { appName, ...rest } = libsqlData;
const result = await db
.update(libsql)
.set({
...rest,
})
.where(eq(libsql.libsqlId, libsqlId))
.returning();
return result[0];
};
export const removeLibsqlById = async (libsqlId: string) => {
const result = await db
.delete(libsql)
.where(eq(libsql.libsqlId, libsqlId))
.returning();
return result[0];
};
export const findLibsqlByBackupId = async (backupId: string) => {
const result = await db
.select({
...getTableColumns(libsql),
})
.from(libsql)
.innerJoin(backups, eq(libsql.libsqlId, backups.libsqlId))
.where(eq(backups.backupId, backupId))
.limit(1);
if (!result || !result[0]) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Libsql not found",
});
}
return result[0];
};
export const deployLibsql = async (
libsqlId: string,
onData?: (data: any) => void,
) => {
const libsql = await findLibsqlById(libsqlId);
try {
await updateLibsqlById(libsqlId, {
applicationStatus: "running",
});
onData?.("Starting libsql deployment...");
if (libsql.serverId) {
await execAsyncRemote(
libsql.serverId,
`docker pull ${libsql.dockerImage}`,
onData,
);
} else {
await pullImage(libsql.dockerImage, onData);
}
await buildLibsql(libsql);
await updateLibsqlById(libsqlId, {
applicationStatus: "done",
});
onData?.("Deployment completed successfully!");
} catch (error) {
onData?.(`Error: ${error}`);
await updateLibsqlById(libsqlId, {
applicationStatus: "error",
});
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error on deploy libsql${error}`,
});
}
return libsql;
};
+40 -15
View File
@@ -32,8 +32,11 @@ export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
...(input.serviceType === "application" && {
applicationId: serviceId,
}),
...(input.serviceType === "postgres" && {
postgresId: serviceId,
...(input.serviceType === "compose" && {
composeId: serviceId,
}),
...(input.serviceType === "libsql" && {
libsqlId: serviceId,
}),
...(input.serviceType === "mariadb" && {
mariadbId: serviceId,
@@ -44,12 +47,12 @@ export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
...(input.serviceType === "mysql" && {
mysqlId: serviceId,
}),
...(input.serviceType === "postgres" && {
postgresId: serviceId,
}),
...(input.serviceType === "redis" && {
redisId: serviceId,
}),
...(input.serviceType === "compose" && {
composeId: serviceId,
}),
})
.returning()
.then((value) => value[0]);
@@ -115,7 +118,16 @@ export const findMountById = async (mountId: string) => {
},
},
},
postgres: {
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
libsql: {
with: {
environment: {
with: {
@@ -151,7 +163,7 @@ export const findMountById = async (mountId: string) => {
},
},
},
redis: {
postgres: {
with: {
environment: {
with: {
@@ -160,7 +172,7 @@ export const findMountById = async (mountId: string) => {
},
},
},
compose: {
redis: {
with: {
environment: {
with: {
@@ -186,8 +198,11 @@ export const findMountOrganizationId = async (mountId: string) => {
if (mount.application) {
return mount.application.environment.project.organizationId;
}
if (mount.postgres) {
return mount.postgres.environment.project.organizationId;
if (mount.compose) {
return mount.compose.environment.project.organizationId;
}
if (mount.libsql) {
return mount.libsql.environment.project.organizationId;
}
if (mount.mariadb) {
return mount.mariadb.environment.project.organizationId;
@@ -198,13 +213,13 @@ export const findMountOrganizationId = async (mountId: string) => {
if (mount.mysql) {
return mount.mysql.environment.project.organizationId;
}
if (mount.postgres) {
return mount.postgres.environment.project.organizationId;
}
if (mount.redis) {
return mount.redis.environment.project.organizationId;
}
if (mount.compose) {
return mount.compose.environment.project.organizationId;
}
return null;
};
@@ -248,8 +263,8 @@ export const findMountsByApplicationId = async (
case "application":
sqlChunks.push(eq(mounts.applicationId, serviceId));
break;
case "postgres":
sqlChunks.push(eq(mounts.postgresId, serviceId));
case "libsql":
sqlChunks.push(eq(mounts.libsqlId, serviceId));
break;
case "mariadb":
sqlChunks.push(eq(mounts.mariadbId, serviceId));
@@ -260,6 +275,9 @@ export const findMountsByApplicationId = async (
case "mysql":
sqlChunks.push(eq(mounts.mysqlId, serviceId));
break;
case "postgres":
sqlChunks.push(eq(mounts.postgresId, serviceId));
break;
case "redis":
sqlChunks.push(eq(mounts.redisId, serviceId));
break;
@@ -362,6 +380,10 @@ export const getBaseFilesPath = async (mountId: string) => {
const { COMPOSE_PATH } = paths(!!mount.compose.serverId);
appName = mount.compose.appName;
absoluteBasePath = path.resolve(COMPOSE_PATH);
} else if (mount.serviceType === "libsql" && mount.libsql) {
const { APPLICATIONS_PATH } = paths(!!mount.libsql.serverId);
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
appName = mount.libsql.appName;
}
directoryPath = path.join(absoluteBasePath, appName, "files");
@@ -391,6 +413,9 @@ export const getServerId = async (mount: MountNested) => {
if (mount.serviceType === "compose" && mount?.compose?.serverId) {
return mount.compose.serverId;
}
if (mount.serviceType === "libsql" && mount?.libsql?.serverId) {
return mount.libsql.serverId;
}
return null;
};
+7 -1
View File
@@ -2,6 +2,7 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateProject,
applications,
libsql,
mariadb,
mongo,
mysql,
@@ -53,12 +54,13 @@ export const findProjectById = async (projectId: string) => {
environments: {
with: {
applications: true,
compose: true,
libsql: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: true,
},
},
projectTags: {
@@ -109,6 +111,9 @@ export const validUniqueServerAppName = async (appName: string) => {
applications: {
where: eq(applications.appName, appName),
},
libsql: {
where: eq(libsql.appName, appName),
},
mariadb: {
where: eq(mariadb.appName, appName),
},
@@ -131,6 +136,7 @@ export const validUniqueServerAppName = async (appName: string) => {
const nonEmptyProjects = query.filter(
(project) =>
project.applications.length > 0 ||
project.libsql.length > 0 ||
project.mariadb.length > 0 ||
project.mongo.length > 0 ||
project.mysql.length > 0 ||
+5 -3
View File
@@ -80,11 +80,12 @@ export const haveActiveServices = async (serverId: string) => {
with: {
applications: true,
compose: true,
redis: true,
libsql: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
},
});
@@ -95,11 +96,12 @@ export const haveActiveServices = async (serverId: string) => {
const total =
currentServer?.applications?.length +
currentServer?.compose?.length +
currentServer?.redis?.length +
currentServer?.libsql?.length +
currentServer?.mariadb?.length +
currentServer?.mongo?.length +
currentServer?.mysql?.length +
currentServer?.postgres?.length;
currentServer?.postgres?.length +
currentServer?.redis?.length;
if (total === 0) {
return false;
@@ -75,6 +75,15 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
},
},
},
libsql: {
with: {
environment: {
with: {
project: true,
},
},
},
},
destination: true,
},
});
+3 -1
View File
@@ -75,6 +75,7 @@ export const initCronJobs = async () => {
mariadb: true,
mysql: true,
mongo: true,
libsql: true,
user: true,
compose: true,
},
@@ -116,7 +117,8 @@ const getServiceAppName = (backup: BackupSchedule): string => {
backup.postgres?.appName ||
backup.mysql?.appName ||
backup.mariadb?.appName ||
backup.mongo?.appName;
backup.mongo?.appName ||
backup.libsql?.appName;
return serviceAppName || backup.appName;
};
@@ -0,0 +1,75 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Libsql } from "@dokploy/server/services/libsql";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runLibsqlBackup = async (
libsql: Libsql,
backup: BackupSchedule,
) => {
const { name, environmentId, appName } = libsql;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
title: "Initializing Backup",
description: "Initializing Backup",
});
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
const backupCommand = getBackupCommand(
backup,
rcloneCommand,
deployment.logPath,
);
if (libsql.serverId) {
await execAsyncRemote(libsql.serverId, backupCommand);
} else {
await execAsync(backupCommand, {
shell: "/bin/bash",
});
}
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "libsql",
type: "success",
organizationId: project.organizationId,
databaseName: backup.database,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
} catch (error) {
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "libsql",
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
organizationId: project.organizationId,
databaseName: backup.database,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
};
+30 -3
View File
@@ -4,6 +4,7 @@ import type { Destination } from "@dokploy/server/services/destination";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { keepLatestNBackups } from ".";
import { runComposeBackup } from "./compose";
import { runLibsqlBackup } from "./libsql";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
@@ -19,6 +20,7 @@ export const scheduleBackup = (backup: BackupSchedule) => {
mysql,
mongo,
mariadb,
libsql,
compose,
} = backup;
scheduleJob(backupId, schedule, async () => {
@@ -35,6 +37,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
} else if (databaseType === "mariadb" && mariadb) {
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb.serverId);
} else if (databaseType === "libsql" && libsql) {
await runLibsqlBackup(libsql, backup);
await keepLatestNBackups(backup, libsql.serverId);
} else if (databaseType === "web-server") {
await runWebServerBackup(backup);
await keepLatestNBackups(backup);
@@ -107,6 +112,10 @@ export const getMongoBackupCommand = (
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
};
export const getLibsqlBackupCommand = (database: string) => {
return `docker exec -i $CONTAINER_ID sh -c "tar cf - -C /var/lib/sqld ${database} | gzip"`;
};
export const getServiceContainerCommand = (appName: string) => {
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
};
@@ -123,12 +132,24 @@ export const getComposeContainerCommand = (
};
const getContainerSearchCommand = (backup: BackupSchedule) => {
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
backup;
const {
backupType,
postgres,
mysql,
mariadb,
mongo,
libsql,
compose,
serviceName,
} = backup;
if (backupType === "database") {
const appName =
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
postgres?.appName ||
mysql?.appName ||
mariadb?.appName ||
mongo?.appName ||
libsql?.appName;
return getServiceContainerCommand(appName || "");
}
if (backupType === "compose") {
@@ -209,6 +230,12 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
}
break;
}
case "libsql": {
if (backupType === "database") {
return getLibsqlBackupCommand(backup.database);
}
break;
}
default:
throw new Error(`Database type not supported: ${databaseType}`);
}
@@ -0,0 +1,155 @@
import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions, PortConfig } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
export type LibsqlNested = InferResultType<
"libsql",
{
mounts: true;
environment: { with: { project: true } };
}
>;
export const buildLibsql = async (libsql: LibsqlNested) => {
const {
appName,
env,
externalPort,
externalGRPCPort,
externalAdminPort,
memoryLimit,
memoryReservation,
databaseUser,
databasePassword,
sqldNode,
sqldPrimaryUrl,
cpuLimit,
cpuReservation,
command,
mounts,
enableNamespaces,
} = libsql;
const basicAuth = Buffer.from(
`${databaseUser}:${databasePassword}`,
"utf-8",
).toString("base64");
const defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
env ? `\n${env}` : ""
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(libsql);
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(
defaultLibsqlEnv,
libsql.environment.project.env,
libsql.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, libsql);
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: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(finalCommand
? {
Command: ["/bin/sh"],
Args: ["-c", finalCommand],
}
: {}),
Labels,
},
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: [
...(externalPort
? [
{
Protocol: "tcp",
TargetPort: 8080,
PublishedPort: externalPort,
PublishMode: "host",
} as PortConfig,
]
: []),
...(externalGRPCPort
? [
{
Protocol: "tcp",
TargetPort: 5001,
PublishedPort: externalGRPCPort,
PublishMode: "host",
} as PortConfig,
]
: []),
...(externalAdminPort
? [
{
Protocol: "tcp",
TargetPort: 5000,
PublishedPort: externalAdminPort,
PublishMode: "host",
} as PortConfig,
]
: []),
],
},
UpdateConfig,
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch {
await docker.createService(settings);
}
};
+34 -16
View File
@@ -1,11 +1,13 @@
import { db } from "@dokploy/server/db";
import {
libsql,
mariadb,
mongo,
mysql,
postgres,
redis,
} from "@dokploy/server/db/schema";
import { deployLibsql } from "@dokploy/server/services/libsql";
import { deployMariadb } from "@dokploy/server/services/mariadb";
import { deployMongo } from "@dokploy/server/services/mongo";
import { deployMySql } from "@dokploy/server/services/mysql";
@@ -15,7 +17,13 @@ import { eq } from "drizzle-orm";
import { removeService } from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
type DatabaseType =
| "libsql"
| "mariadb"
| "mongo"
| "mysql"
| "postgres"
| "redis";
export const rebuildDatabase = async (
databaseId: string,
@@ -41,31 +49,25 @@ export const rebuildDatabase = async (
}
}
if (type === "postgres") {
await deployPostgres(databaseId);
} else if (type === "mysql") {
await deployMySql(databaseId);
if (type === "libsql") {
await deployLibsql(databaseId);
} else if (type === "mariadb") {
await deployMariadb(databaseId);
} else if (type === "mongo") {
await deployMongo(databaseId);
} else if (type === "mysql") {
await deployMySql(databaseId);
} else if (type === "postgres") {
await deployPostgres(databaseId);
} else if (type === "redis") {
await deployRedis(databaseId);
}
};
const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
if (type === "postgres") {
return await db.query.postgres.findFirst({
where: eq(postgres.postgresId, databaseId),
with: {
mounts: true,
},
});
}
if (type === "mysql") {
return await db.query.mysql.findFirst({
where: eq(mysql.mysqlId, databaseId),
if (type === "libsql") {
return await db.query.libsql.findFirst({
where: eq(libsql.libsqlId, databaseId),
with: {
mounts: true,
},
@@ -87,6 +89,22 @@ const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
},
});
}
if (type === "mysql") {
return await db.query.mysql.findFirst({
where: eq(mysql.mysqlId, databaseId),
with: {
mounts: true,
},
});
}
if (type === "postgres") {
return await db.query.postgres.findFirst({
where: eq(postgres.postgresId, databaseId),
with: {
mounts: true,
},
});
}
if (type === "redis") {
return await db.query.redis.findFirst({
where: eq(redis.redisId, databaseId),
@@ -7,6 +7,7 @@ import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv";
import { quote } from "shell-quote";
import type { ApplicationNested } from "../builders";
import type { LibsqlNested } from "../databases/libsql";
import type { MariadbNested } from "../databases/mariadb";
import type { MongoNested } from "../databases/mongo";
import type { MysqlNested } from "../databases/mysql";
@@ -610,6 +611,7 @@ export const generateFileMounts = (
appName: string,
service:
| ApplicationNested
| LibsqlNested
| MongoNested
| MariadbNested
| MysqlNested
@@ -29,7 +29,7 @@ export const sendDatabaseBackupNotifications = async ({
}: {
projectName: string;
applicationName: string;
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
type: "error" | "success";
organizationId: string;
errorMessage?: string;
@@ -37,7 +37,8 @@ export const sendVolumeBackupNotifications = async ({
| "mongodb"
| "mariadb"
| "redis"
| "compose";
| "compose"
| "libsql";
type: "error" | "success";
organizationId: string;
errorMessage?: string;
+6 -2
View File
@@ -32,7 +32,7 @@ export const restoreComposeBackup = async (
rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
}
let credentials: DatabaseCredentials;
let credentials: DatabaseCredentials = {};
switch (backupInput.databaseType) {
case "postgres":
@@ -62,7 +62,11 @@ export const restoreComposeBackup = async (
const restoreCommand = getRestoreCommand({
appName: appName,
serviceName: backupInput.metadata?.serviceName,
type: backupInput.databaseType,
type: backupInput.databaseType as
| "postgres"
| "mariadb"
| "mysql"
| "mongo",
credentials: {
database: backupInput.databaseName,
...credentials,
@@ -1,4 +1,5 @@
export { restoreComposeBackup } from "./compose";
export { restoreLibsqlBackup } from "./libsql";
export { restoreMariadbBackup } from "./mariadb";
export { restoreMongoBackup } from "./mongo";
export { restoreMySqlBackup } from "./mysql";
@@ -0,0 +1,49 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination";
import type { Libsql } from "@dokploy/server/services/libsql";
import type { z } from "zod";
import { getS3Credentials, getServiceContainerCommand } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
export const restoreLibsqlBackup = async (
libsql: Libsql,
destination: Destination,
backupInput: z.infer<typeof apiRestoreBackup>,
emit: (log: string) => void,
) => {
try {
const { appName, serverId } = libsql;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`;
emit("Starting restore...");
emit(`Backup path: ${backupPath}`);
const containerSearch = getServiceContainerCommand(appName);
const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`;
const command = `CONTAINER_ID=$(${containerSearch}) && ${rcloneCommand} | ${restoreCommand}`;
emit(`Executing command: ${command}`);
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
emit("Restore completed successfully!");
} catch (error) {
emit(
`Error: ${
error instanceof Error ? error.message : "Error restoring libsql backup"
}`,
);
throw error;
}
};
@@ -18,7 +18,8 @@ export const getVolumeServiceAppName = (
volumeBackup.mysql?.appName ||
volumeBackup.mariadb?.appName ||
volumeBackup.mongo?.appName ||
volumeBackup.redis?.appName;
volumeBackup.redis?.appName ||
volumeBackup.libsql?.appName;
return serviceAppName || volumeBackup.appName;
};
@@ -26,6 +26,7 @@ const getProjectName = (
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {
@@ -48,6 +49,7 @@ const getOrganizationId = (
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {