Merge branch 'canary' into feat/internal-path-routing

This commit is contained in:
Jhonatan Caldeira
2025-07-05 15:50:04 -03:00
67 changed files with 347 additions and 181 deletions

View File

@@ -16,28 +16,29 @@ Here's how to install docker on different operating systems:
### Ubuntu
```bash
# Uninstall old versions
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Set up stable repository
# Add the repository to Apt sources
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
## Windows

View File

@@ -2,7 +2,7 @@
## Core License (Apache License 2.0)
Copyright 2024 Mauricio Siu.
Copyright 2025 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -62,46 +62,26 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Hero Sponsors 🎖
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
</a>
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
</a>
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
</a>
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
</a>
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/></a>
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;"><img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/></a>
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;"><img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/></a>
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;"><img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/></a>
</div>
### Premium Supporters 🥇
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
</a>
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
</a>
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/></a>
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/></a>
</div>
### Elite Contributors 🥈
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
</a>
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
<img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" height="80"/>
</a>
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/></a>
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;"><img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" height="80"/></a>
</div>
<!-- Elite Contributors 🥈 -->

View File

@@ -1,26 +0,0 @@
# License
## Core License (Apache License 2.0)
Copyright 2024 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
}
try {
return JSON.parse(str);
} catch (_e) {
} catch {
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
return z.NEVER;
}

View File

@@ -107,7 +107,7 @@ export const ShowImport = ({ composeId }: Props) => {
composeId,
});
setShowModal(false);
} catch (_error) {
} catch {
toast.error("Error importing template");
}
};
@@ -126,7 +126,7 @@ export const ShowImport = ({ composeId }: Props) => {
});
setTemplateInfo(result);
setShowModal(true);
} catch (_error) {
} catch {
toast.error("Error processing template");
}
};

View File

@@ -35,6 +35,9 @@ import { z } from "zod";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
publishMode: z.enum(["ingress", "host"], {
required_error: "Publish mode is required",
}),
targetPort: z.number().int().min(1).max(65535),
protocol: z.enum(["tcp", "udp"], {
required_error: "Protocol is required",
@@ -80,6 +83,7 @@ export const HandlePorts = ({
useEffect(() => {
form.reset({
publishedPort: data?.publishedPort ?? 0,
publishMode: data?.publishMode ?? "ingress",
targetPort: data?.targetPort ?? 0,
protocol: data?.protocol ?? "tcp",
});
@@ -165,6 +169,32 @@ export const HandlePorts = ({
</FormItem>
)}
/>
<FormField
control={form.control}
name="publishMode"
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>Published Port Mode</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a publish mode for the port" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"ingress"}>Ingress</SelectItem>
<SelectItem value={"host"}>Host</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="targetPort"

View File

@@ -60,13 +60,19 @@ export const ShowPorts = ({ applicationId }: Props) => {
{data?.ports.map((port) => (
<div key={port.portId}>
<div className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Published Port</span>
<span className="text-sm text-muted-foreground">
{port.publishedPort}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Published Port Mode</span>
<span className="text-sm text-muted-foreground">
{port?.publishMode?.toUpperCase()}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Target Port</span>
<span className="text-sm text-muted-foreground">

View File

@@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
composeId,
});
})
.catch((_e) => {
.catch(() => {
toast.error("Error updating the Compose config");
});
};

View File

@@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
.then(() => {
refetch();
})
.catch((_err) => {});
.catch(() => {});
}
}, [isOpen]);

View File

@@ -103,7 +103,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
projectId,
});
})
.catch((_e) => {
.catch(() => {
toast.error("Error creating the service");
});
};

View File

@@ -1063,7 +1063,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
});
}
toast.success("Connection Success");
} catch (_err) {
} catch {
toast.error("Error testing the provider");
}
}}

View File

@@ -63,7 +63,7 @@ export const Disable2FA = () => {
toast.success("2FA disabled successfully");
utils.user.get.invalidate();
setIsOpen(false);
} catch (_error) {
} catch {
form.setError("password", {
message: "Connection error. Please try again.",
});

View File

@@ -36,7 +36,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
await refetch();
}
toast.success("Docker Cleanup updated");
} catch (_error) {
} catch {
toast.error("Docker Cleanup Error");
}
};

View File

@@ -56,7 +56,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
try {
await utils.settings.checkGPUStatus.invalidate({ serverId });
await refetch();
} catch (_error) {
} catch {
toast.error("Failed to refresh GPU status");
} finally {
setIsRefreshing(false);
@@ -74,7 +74,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
try {
await setupGPU.mutateAsync({ serverId });
} catch (_error) {
} catch {
// Error handling is done in mutation's onError
}
};

View File

@@ -146,7 +146,7 @@ export const ShowInvitations = () => {
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(_e) => {
onSelect={() => {
copy(
`${origin}/invitation?token=${invitation.id}`,
);
@@ -162,7 +162,7 @@ export const ShowInvitations = () => {
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
onSelect={async () => {
const result =
await authClient.organization.cancelInvitation(
{
@@ -189,7 +189,7 @@ export const ShowInvitations = () => {
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
onSelect={async () => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {

View File

@@ -91,7 +91,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
});
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
setOpen(false);
} catch (_error) {}
} catch {}
};
return (

View File

@@ -17,7 +17,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 w-full rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}

View File

@@ -0,0 +1,2 @@
CREATE TYPE "public"."publishModeType" AS ENUM('ingress', 'host');--> statement-breakpoint
ALTER TABLE "port" ADD COLUMN "publishMode" "publishModeType" DEFAULT 'host' NOT NULL;

View File

@@ -1,5 +1,5 @@
{
"id": "4d928914-5268-4921-aa21-c1b087cc15cc",
"id": "71f68c87-ddb4-4e8c-b9fc-1db7fbcedf56",
"prevId": "edde8c54-b715-4db6-bc3a-85d435226083",
"version": "7",
"dialect": "postgresql",
@@ -1209,20 +1209,6 @@
"primaryKey": false,
"notNull": true,
"default": "'none'"
},
"internalPath": {
"name": "internalPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"stripPath": {
"name": "stripPath",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
@@ -2847,6 +2833,14 @@
"primaryKey": false,
"notNull": true
},
"publishMode": {
"name": "publishMode",
"type": "publishModeType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'host'"
},
"targetPort": {
"name": "targetPort",
"type": "integer",
@@ -5730,6 +5724,14 @@
"udp"
]
},
"public.publishModeType": {
"name": "publishModeType",
"schema": "public",
"values": [
"ingress",
"host"
]
},
"public.applicationStatus": {
"name": "applicationStatus",
"schema": "public",

View File

@@ -698,8 +698,8 @@
{
"idx": 99,
"version": "7",
"when": 1751293280505,
"tag": "0099_fast_the_order",
"when": 1751693569786,
"tag": "0099_wise_golden_guardian",
"breakpoints": true
}
]

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.23.6",
"version": "v0.23.7",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -390,7 +390,7 @@ const Project = (
break;
}
success++;
} catch (_error) {
} catch {
toast.error(`Error starting service ${serviceId}`);
}
}
@@ -437,7 +437,7 @@ const Project = (
break;
}
success++;
} catch (_error) {
} catch {
toast.error(`Error stopping service ${serviceId}`);
}
}
@@ -1107,7 +1107,7 @@ export async function getServerSideProps(
projectId: params?.projectId,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -413,7 +413,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -409,7 +409,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -338,7 +338,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -340,7 +340,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -324,7 +324,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -321,7 +321,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -328,7 +328,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -68,7 +68,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -69,7 +69,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -96,7 +96,7 @@ export default function Home({ IS_CLOUD }: Props) {
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while logging in");
} finally {
setIsLoginLoading(false);
@@ -124,7 +124,7 @@ export default function Home({ IS_CLOUD }: Props) {
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while verifying 2FA code");
} finally {
setIsTwoFactorLoading(false);
@@ -154,7 +154,7 @@ export default function Home({ IS_CLOUD }: Props) {
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while verifying backup code");
} finally {
setIsBackupCodeLoading(false);
@@ -478,7 +478,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
};
}
} catch (_error) {}
} catch {}
return {
props: {

View File

@@ -133,7 +133,7 @@ const Invitation = ({
toast.success("Account created successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while creating your account");
}
};

View File

@@ -1,5 +1,6 @@
import {
containerRestart,
findServerById,
getConfig,
getContainers,
getContainersByAppLabel,
@@ -9,6 +10,9 @@ import {
} from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure
@@ -17,14 +21,23 @@ export const dockerRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getContainers(input.serverId);
}),
restartContainer: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
}),
)
.mutation(async ({ input }) => {
@@ -34,11 +47,20 @@ export const dockerRouter = createTRPCRouter({
getConfig: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getConfig(input.containerId, input.serverId);
}),
@@ -48,11 +70,17 @@ export const dockerRouter = createTRPCRouter({
appType: z
.union([z.literal("stack"), z.literal("docker-compose")])
.optional(),
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getContainersByAppNameMatch(
input.appName,
input.appType,
@@ -63,12 +91,18 @@ export const dockerRouter = createTRPCRouter({
getContainersByAppLabel: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
type: z.enum(["standalone", "swarm"]),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getContainersByAppLabel(
input.appName,
input.type,
@@ -79,22 +113,34 @@ export const dockerRouter = createTRPCRouter({
getStackContainersByAppName: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getStackContainersByAppName(input.appName, input.serverId);
}),
getServiceContainersByAppName: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getServiceContainersByAppName(input.appName, input.serverId);
}),
});

View File

@@ -459,6 +459,15 @@ export const settingsRouter = createTRPCRouter({
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async ({ ctx }) => {

View File

@@ -6,6 +6,9 @@ import {
} from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { findServerById } from "@dokploy/server";
import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({
getNodes: protectedProcedure
@@ -14,12 +17,24 @@ export const swarmRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getSwarmNodes(input.serverId);
}),
getNodeInfo: protectedProcedure
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getNodeInfo(input.nodeId, input.serverId);
}),
getNodeApps: protectedProcedure
@@ -28,17 +43,29 @@ export const swarmRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return getNodeApplications(input.serverId);
}),
getAppInfos: protectedProcedure
.input(
z.object({
appName: z.string(),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getApplicationInfo(input.appName, input.serverId);
}),
});

View File

@@ -75,6 +75,24 @@ export const userRouter = createTRPCRouter({
},
});
// If user not found in the organization, deny access
if (!memberResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found in this organization",
});
}
// Allow access if:
// 1. User is requesting their own information
// 2. User has owner role (admin permissions) AND user is in the same organization
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this user",
});
}
return memberResult;
}),
get: protectedProcedure.query(async ({ ctx }) => {

View File

@@ -6,7 +6,7 @@ export const isWSL = async () => {
const { stdout } = await execAsync("uname -r");
const isWSL = stdout.includes("microsoft");
return isWSL;
} catch (_error) {
} catch {
return false;
}
};

View File

@@ -101,7 +101,7 @@ export const setupDeploymentLogsWebSocketServer = (
ws.close();
});
}
} catch (_error) {
} catch {
// @ts-ignore
// const errorMessage = error?.message as unknown as string;
// ws.send(errorMessage);

View File

@@ -12,6 +12,7 @@ import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/setup/setup";
import { execAsync } from "@dokploy/server";
(async () => {
try {
setupDirectories();
@@ -20,6 +21,7 @@ import {
await initializeNetwork();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.1.2");
await initializeTraefik();
await initializeRedis();
await initializePostgres();

View File

@@ -242,3 +242,8 @@
background-color: var(--terminal-paste) !important;
color: currentColor !important;
}
.cm-content,
.cm-lineWrapping {
@apply font-mono;
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright 2025 Mauricio Siu.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { applications } from "./application";
export const protocolType = pgEnum("protocolType", ["tcp", "udp"]);
export const publishModeType = pgEnum("publishModeType", ["ingress", "host"]);
export const ports = pgTable("port", {
portId: text("portId")
@@ -13,6 +14,7 @@ export const ports = pgTable("port", {
.primaryKey()
.$defaultFn(() => nanoid()),
publishedPort: integer("publishedPort").notNull(),
publishMode: publishModeType("publishMode").notNull().default("host"),
targetPort: integer("targetPort").notNull(),
protocol: protocolType("protocol").notNull(),
@@ -32,6 +34,7 @@ const createSchema = createInsertSchema(ports, {
portId: z.string().min(1),
applicationId: z.string().min(1),
publishedPort: z.number(),
publishMode: z.enum(["ingress", "host"]).default("ingress"),
targetPort: z.number(),
protocol: z.enum(["tcp", "udp"]).default("tcp"),
});
@@ -39,6 +42,7 @@ const createSchema = createInsertSchema(ports, {
export const apiCreatePort = createSchema
.pick({
publishedPort: true,
publishMode: true,
targetPort: true,
protocol: true,
applicationId: true,
@@ -55,6 +59,7 @@ export const apiUpdatePort = createSchema
.pick({
portId: true,
publishedPort: true,
publishMode: true,
targetPort: true,
protocol: true,
})

View File

@@ -15,6 +15,7 @@ import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
import { paths } from "@dokploy/server/constants";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -236,7 +237,31 @@ export const apiModifyTraefikConfig = z.object({
serverId: z.string().optional(),
});
export const apiReadTraefikConfig = z.object({
path: z.string().min(1),
path: z
.string()
.min(1)
.refine(
(path) => {
// Prevent directory traversal attacks
if (path.includes("../") || path.includes("..\\")) {
return false;
}
const { MAIN_TRAEFIK_PATH } = paths();
if (path.startsWith("/") && !path.startsWith(MAIN_TRAEFIK_PATH)) {
return false;
}
// Prevent null bytes and other dangerous characters
if (path.includes("\0") || path.includes("\x00")) {
return false;
}
return true;
},
{
message:
"Invalid path: path traversal or unauthorized directory access detected",
},
),
serverId: z.string().optional(),
});

View File

@@ -73,7 +73,7 @@ export const readStatsFile = async (
const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
const data = await promises.readFile(filePath, "utf-8");
return JSON.parse(data);
} catch (_error) {
} catch {
return [];
}
};
@@ -108,7 +108,7 @@ export const readLastValueStatsFile = async (
const data = await promises.readFile(filePath, "utf-8");
const stats = JSON.parse(data);
return stats[stats.length - 1] || null;
} catch (_error) {
} catch {
return null;
}
};

View File

@@ -98,7 +98,7 @@ export const getConfig = async (
const config = JSON.parse(stdout);
return config;
} catch (_error) {}
} catch {}
};
export const getContainersByAppNameMatch = async (
@@ -156,7 +156,7 @@ export const getContainersByAppNameMatch = async (
});
return containers || [];
} catch (_error) {}
} catch {}
return [];
};
@@ -214,7 +214,7 @@ export const getStackContainersByAppName = async (
});
return containers || [];
} catch (_error) {}
} catch {}
return [];
};
@@ -274,7 +274,7 @@ export const getServiceContainersByAppName = async (
});
return containers || [];
} catch (_error) {}
} catch {}
return [];
};
@@ -331,7 +331,7 @@ export const getContainersByAppLabel = async (
});
return containers || [];
} catch (_error) {}
} catch {}
return [];
};
@@ -350,7 +350,7 @@ export const containerRestart = async (containerId: string) => {
const config = JSON.parse(stdout);
return config;
} catch (_error) {}
} catch {}
};
export const getSwarmNodes = async (serverId?: string) => {
@@ -379,7 +379,7 @@ export const getSwarmNodes = async (serverId?: string) => {
.split("\n")
.map((line) => JSON.parse(line));
return nodesArray;
} catch (_error) {}
} catch {}
};
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
@@ -405,7 +405,7 @@ export const getNodeInfo = async (nodeId: string, serverId?: string) => {
const nodeInfo = JSON.parse(stdout);
return nodeInfo;
} catch (_error) {}
} catch {}
};
export const getNodeApplications = async (serverId?: string) => {
@@ -437,7 +437,7 @@ export const getNodeApplications = async (serverId?: string) => {
.filter((service) => !service.Name.startsWith("dokploy-"));
return appArray;
} catch (_error) {}
} catch {}
};
export const getApplicationInfo = async (
@@ -470,5 +470,5 @@ export const getApplicationInfo = async (
.map((line) => JSON.parse(line));
return appArray;
} catch (_error) {}
} catch {}
};

View File

@@ -121,7 +121,7 @@ export const issueCommentExists = async ({
comment_id: comment_id,
});
return true;
} catch (_error) {
} catch {
return false;
}
};

View File

@@ -212,7 +212,7 @@ export const deleteFileMount = async (mountId: string) => {
} else {
await removeFileOrDirectory(fullPath);
}
} catch (_error) {}
} catch {}
};
export const getBaseFilesPath = async (mountId: string) => {

View File

@@ -104,7 +104,7 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => {
for (const operation of cleanupOperations) {
try {
await operation();
} catch (_error) {}
} catch {}
}
return deployment[0];
} catch (error) {

View File

@@ -195,7 +195,7 @@ const rollbackApplication = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (_error: unknown) {
} catch {
await docker.createService(settings);
}
};

View File

@@ -66,7 +66,7 @@ export const setupMonitoring = async (serverId: string) => {
await container.inspect();
await container.remove({ force: true });
console.log("Removed existing container");
} catch (_error) {
} catch {
// Container doesn't exist, continue
}
@@ -135,7 +135,7 @@ export const setupWebMonitoring = async (userId: string) => {
await container.inspect();
await container.remove({ force: true });
console.log("Removed existing container");
} catch (_error) {}
} catch {}
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);

View File

@@ -419,14 +419,26 @@ if ! [ -x "$(command -v docker)" ]; then
systemctl enable docker >/dev/null 2>&1
;;
"opencloudos")
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
# Special handling for OpenCloud OS
echo " - Installing Docker for OpenCloud OS..."
dnf install -y docker >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
systemctl start docker >/dev/null 2>&1
# Remove --live-restore parameter from Docker configuration if it exists
if [ -f "/etc/sysconfig/docker" ]; then
echo " - Removing --live-restore parameter from Docker configuration..."
sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
# Clean up any double spaces that might be left
sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
fi
systemctl enable docker >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
echo " - Docker configured for OpenCloud OS"
;;
"alpine")
apk add docker docker-cli-compose >/dev/null 2>&1

View File

@@ -18,7 +18,7 @@ export const dockerSwarmInitialized = async () => {
await docker.swarmInspect();
return true;
} catch (_e) {
} catch {
return false;
}
};
@@ -41,7 +41,7 @@ export const dockerNetworkInitialized = async () => {
try {
await docker.getNetwork("dokploy-network").inspect();
return true;
} catch (_e) {
} catch {
return false;
}
};

View File

@@ -101,11 +101,11 @@ export const initializeTraefik = async ({
console.log("Waiting for service cleanup...");
await new Promise((resolve) => setTimeout(resolve, 5000));
attempts++;
} catch (_e) {
} catch {
break;
}
}
} catch (_err) {
} catch {
console.log("No existing service to remove");
}
@@ -120,7 +120,7 @@ export const initializeTraefik = async ({
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (_err) {
} catch {
console.log("No existing container to remove");
}

View File

@@ -183,6 +183,7 @@ export const mechanizeDockerContainer = async (
RollbackConfig,
EndpointSpec: {
Ports: ports.map((port) => ({
PublishMode: port.publishMode,
Protocol: port.protocol,
TargetPort: port.targetPort,
PublishedPort: port.publishedPort,
@@ -203,7 +204,7 @@ export const mechanizeDockerContainer = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (_error: unknown) {
} catch {
await docker.createService(settings);
}
};

View File

@@ -98,7 +98,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (_error) {
} catch {
await docker.createService(settings);
}
};

View File

@@ -152,7 +152,7 @@ ${command ?? "wait $MONGOD_PID"}`;
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (_error) {
} catch {
await docker.createService(settings);
}
};

View File

@@ -104,7 +104,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (_error) {
} catch {
await docker.createService(settings);
}
};

View File

@@ -95,7 +95,7 @@ export const buildRedis = async (redis: RedisNested) => {
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (_error) {
} catch {
await docker.createService(settings);
}
};

View File

@@ -117,7 +117,7 @@ export const loadDockerComposeRemote = async (
if (!stdout) return null;
const parsedConfig = load(stdout) as ComposeSpecification;
return parsedConfig;
} catch (_err) {
} catch {
return null;
}
};

View File

@@ -101,7 +101,7 @@ export const containerExists = async (containerName: string) => {
try {
await container.inspect();
return true;
} catch (_error) {
} catch {
return false;
}
};

View File

@@ -34,7 +34,7 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
...gpuInfo,
...cudaInfo,
};
} catch (_error) {
} catch {
return {
driverInstalled: false,
driverVersion: undefined,
@@ -315,7 +315,7 @@ const setupLocalServer = async (daemonConfig: any) => {
try {
await execAsync(setupCommands);
} catch (_error) {
} catch {
throw new Error(
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
);

View File

@@ -67,7 +67,7 @@ export const removeTraefikConfig = async (
if (fs.existsSync(configPath)) {
await fs.promises.unlink(configPath);
}
} catch (_error) {}
} catch {}
};
export const removeTraefikConfigRemote = async (
@@ -78,7 +78,7 @@ export const removeTraefikConfigRemote = async (
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
await execAsyncRemote(serverId, `rm ${configPath}`);
} catch (_error) {}
} catch {}
};
export const loadOrCreateConfig = (appName: string): FileConfig => {
@@ -110,7 +110,7 @@ export const loadOrCreateConfigRemote = async (
http: { routers: {}, services: {} },
};
return parsedConfig;
} catch (_err) {
} catch {
return fileConfig;
}
};
@@ -132,7 +132,7 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
if (!stdout) return null;
return stdout;
} catch (_err) {
} catch {
return null;
}
};