feat: implement Docker network management functionality

- Added components for handling and displaying Docker networks, including creation, editing, and listing of networks.
- Introduced a new API router for network operations, integrating with the database schema for network management.
- Updated the sidebar layout to include a link to the networks dashboard, ensuring user access to network features.
- Created necessary database migrations for the network table and its associated types.
- Enhanced the dashboard layout to support the new network management interface.
This commit is contained in:
Mauricio Siu
2026-02-21 14:53:27 -06:00
parent 3e461f642e
commit 69598821ed
15 changed files with 8742 additions and 0 deletions
+2
View File
@@ -7,6 +7,7 @@ import {
timestamp,
} from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { network } from "./network";
import { projects } from "./project";
import { server } from "./server";
import { ssoProvider } from "./sso";
@@ -77,6 +78,7 @@ export const organizationRelations = relations(
references: [user.id],
}),
servers: many(server),
networks: many(network),
projects: many(projects),
members: many(member),
ssoProviders: many(ssoProvider),
+1
View File
@@ -16,6 +16,7 @@ export * from "./gitlab";
export * from "./mariadb";
export * from "./mongo";
export * from "./mount";
export * from "./network";
export * from "./mysql";
export * from "./notification";
export * from "./patch";
+137
View File
@@ -0,0 +1,137 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { server } from "./server";
/** Docker network driver types */
export const networkDriver = pgEnum("networkDriver", [
"bridge",
"host",
"overlay",
"macvlan",
"none",
"ipvlan",
]);
export const network = pgTable("network", {
networkId: text("networkId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
driver: networkDriver("driver").notNull().default("bridge"),
scope: text("scope"), // e.g. "local", "swarm"
internal: boolean("internal").notNull().default(false),
attachable: boolean("attachable").notNull().default(false),
ingress: boolean("ingress").notNull().default(false),
configOnly: boolean("configOnly").notNull().default(false),
enableIPv4: boolean("enableIPv4").notNull().default(true),
enableIPv6: boolean("enableIPv6").notNull().default(false),
ipam: jsonb("ipam")
.$type<{
driver?: string;
config?: Array<{ subnet?: string; gateway?: string; ipRange?: string }>;
}>()
.default({}),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const networkRelations = relations(network, ({ one }) => ({
organization: one(organization, {
fields: [network.organizationId],
references: [organization.id],
}),
server: one(server, {
fields: [network.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(network, {
networkId: z.string().min(1),
name: z.string().min(1),
driver: z
.enum(["bridge", "host", "overlay", "macvlan", "none", "ipvlan"])
.optional(),
scope: z.string().optional(),
internal: z.boolean().optional(),
attachable: z.boolean().optional(),
ingress: z.boolean().optional(),
configOnly: z.boolean().optional(),
enableIPv4: z.boolean().optional(),
enableIPv6: z.boolean().optional(),
ipam: z
.object({
driver: z.string().optional(),
config: z
.array(
z.object({
subnet: z.string().optional(),
gateway: z.string().optional(),
ipRange: z.string().optional(),
}),
)
.optional(),
})
.optional(),
organizationId: z.string().min(1),
serverId: z.string().optional().nullable(),
});
export const apiCreateNetwork = createSchema
.pick({
name: true,
driver: true,
scope: true,
internal: true,
attachable: true,
ingress: true,
configOnly: true,
enableIPv4: true,
enableIPv6: true,
ipam: true,
serverId: true,
})
.partial()
.required({ name: true });
export const apiFindOneNetwork = createSchema
.pick({
networkId: true,
})
.required();
export const apiRemoveNetwork = createSchema
.pick({
networkId: true,
})
.required();
export const apiUpdateNetwork = createSchema
.pick({
networkId: true,
name: true,
driver: true,
scope: true,
internal: true,
attachable: true,
ingress: true,
configOnly: true,
enableIPv4: true,
enableIPv6: true,
ipam: true,
serverId: true,
})
.partial()
.required({ networkId: true });
+2
View File
@@ -18,6 +18,7 @@ import { deployments } from "./deployment";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { network } from "./network";
import { postgres } from "./postgres";
import { redis } from "./redis";
import { schedules } from "./schedule";
@@ -122,6 +123,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
mysql: many(mysql),
postgres: many(postgres),
certificates: many(certificates),
networks: many(network),
organization: one(organization, {
fields: [server.organizationId],
references: [organization.id],
+1
View File
@@ -26,6 +26,7 @@ export * from "./services/mariadb";
export * from "./services/mongo";
export * from "./services/mount";
export * from "./services/mysql";
export * from "./services/network";
export * from "./services/notification";
export * from "./services/patch";
export * from "./services/patch-repo";
+114
View File
@@ -0,0 +1,114 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateNetwork,
type apiUpdateNetwork,
network,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { getRemoteDocker } from "../utils/servers/remote-docker";
export const findNetworkById = async (networkId: string) => {
const [row] = await db
.select()
.from(network)
.where(eq(network.networkId, networkId))
.limit(1);
if (!row) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Network not found",
});
}
return row;
};
export const createNetwork = async (
input: typeof apiCreateNetwork._type,
organizationId: string,
) => {
const created = await db.transaction(async (tx) => {
const [row] = await tx
.insert(network)
.values({
...input,
organizationId,
})
.returning();
if (!row) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to create network",
});
}
const ipam = row.ipam ?? {};
const ipamConfig = (ipam.config ?? [])
.map((c) => {
const entry: Record<string, string> = {};
if (c.subnet) entry.Subnet = c.subnet;
if (c.gateway) entry.Gateway = c.gateway;
if (c.ipRange) entry.IPRange = c.ipRange;
return entry;
})
.filter((e) => Object.keys(e).length > 0);
const docker = await getRemoteDocker(input.serverId ?? null);
await docker.createNetwork({
Name: row.name,
Driver: row.driver,
Internal: row.internal,
Attachable: row.attachable,
Ingress: row.ingress,
EnableIPv6: row.enableIPv6,
IPAM:
ipamConfig.length > 0 || ipam.driver
? {
Driver: ipam.driver ?? "default",
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
}
: undefined,
});
return row;
});
return created;
};
export const updateNetwork = async (input: typeof apiUpdateNetwork._type) => {
const { networkId, ...rest } = input;
const [updated] = await db
.update(network)
.set(rest)
.where(eq(network.networkId, networkId))
.returning();
if (!updated) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Network not found",
});
}
return updated;
};
export const removeNetwork = async (networkId: string) => {
const [deleted] = await db
.delete(network)
.where(eq(network.networkId, networkId))
.returning();
if (!deleted) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Network not found",
});
}
return deleted;
};