Compare commits

..

16 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
4c96d8559a Fix zombie processes from Docker cleanup by adding wait command
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-22 16:54:09 +00:00
copilot-swe-agent[bot]
39cf706053 Initial plan 2025-12-22 16:48:27 +00:00
Mauricio Siu
e77f276785 refactor(issue-template): remove unnecessary dropdowns for git providers in bug report 2025-12-21 01:06:12 -06:00
Mauricio Siu
78c9a047b0 feat(issue-template): add dropdowns for affected areas and git providers in bug report 2025-12-21 01:05:33 -06:00
Mauricio Siu
84e0f5856b Merge pull request #3164 from difagume/fix/log-warning-detection
fix(docker-logs): fix warning symbol detection
2025-12-20 23:02:00 -06:00
Mauricio Siu
2bfa4643fc Merge pull request #3186 from divaltor/slider-resources
feat(resources): Add number component to have better UX control over Docker resources
2025-12-20 22:56:54 -06:00
Mauricio Siu
8c7bc82712 Merge pull request #3323 from Dokploy/copilot/fix-shell-command-issue
fix: quote registry username in docker login to prevent shell variable expansion
2025-12-20 21:24:57 -06:00
copilot-swe-agent[bot]
44645a6fbe fix: properly quote registry username in docker login to handle special characters like $
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 19:41:56 +00:00
copilot-swe-agent[bot]
771d0dd8ab Initial plan 2025-12-20 19:35:55 +00:00
Mauricio Siu
67725759e6 Merge pull request #3318 from Dokploy/copilot/fix-perplexity-ai-models-endpoint
Fix Perplexity AI provider models endpoint by returning hardcoded model list
2025-12-20 13:34:55 -06:00
Mauricio Siu
2065372d4f fix: update test command in package.json to remove specific test target 2025-12-20 13:34:32 -06:00
copilot-swe-agent[bot]
69d5c6f0cb Fix Perplexity AI provider by adding hardcoded model list
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:43:11 +00:00
copilot-swe-agent[bot]
53f67c6eb2 Initial plan 2025-12-20 06:38:09 +00:00
Mauricio Siu
7c53a3ef75 Merge pull request #3316 from Dokploy/feat/add-admin-creation-projects
fix: update project handling permissions to include admin role
2025-12-19 23:26:44 -06:00
Vlad Vladov
d465fb4da1 feat(resources): Add number component to have better UX control over Docker resources 2025-12-07 13:45:14 +02:00
diego fabricio
698104e7b7 fix(docker-logs): fix warning symbol detection
- Added support for detecting warning symbols (⚠, ⚠️) in log messages
2025-12-04 11:33:29 -05:00
14 changed files with 246 additions and 37 deletions

View File

@@ -206,4 +206,38 @@ describe("getRegistryTag", () => {
expect(result).toBe("docker.io/myuser/repo");
});
});
describe("special characters in username", () => {
it("should handle Harbor robot account username with $ (e.g. robot$library+dokploy)", () => {
const registry = createMockRegistry({
username: "robot$library+dokploy",
});
const result = getRegistryTag(registry, "nginx");
expect(result).toBe("docker.io/robot$library+dokploy/nginx");
});
it("should handle username with $ and other special characters", () => {
const registry = createMockRegistry({
username: "robot$test+app",
});
const result = getRegistryTag(registry, "myapp:latest");
expect(result).toBe("docker.io/robot$test+app/myapp:latest");
});
it("should handle username with multiple $ symbols", () => {
const registry = createMockRegistry({
username: "user$name$test",
});
const result = getRegistryTag(registry, "app");
expect(result).toBe("docker.io/user$name$test/app");
});
it("should handle username with + and - symbols", () => {
const registry = createMockRegistry({
username: "robot+test-user",
});
const result = getRegistryTag(registry, "nginx:latest");
expect(result).toBe("docker.io/robot+test-user/nginx:latest");
});
});
});

View File

@@ -1,7 +1,7 @@
import type { Domain } from "@dokploy/server";
import { createDomainLabels } from "@dokploy/server";
import { parse, stringify } from "yaml";
import { describe, expect, it } from "vitest";
import { parse, stringify } from "yaml";
/**
* Regression tests for Traefik Host rule label format.

View File

@@ -21,7 +21,10 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
createConverter,
NumberInputWithSteps,
} from "@/components/ui/number-input";
import {
Tooltip,
TooltipContent,
@@ -30,6 +33,23 @@ import {
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
const CPU_STEP = 0.25;
const MEMORY_STEP_MB = 256;
const formatNumber = (value: number, decimals = 2): string =>
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
);
const memoryConverter = createConverter(1024 * 1024, (mb) => {
if (mb <= 0) return "";
return mb >= 1024
? `${formatNumber(mb / 1024)} GB`
: `${formatNumber(mb)} MB`;
});
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
@@ -51,6 +71,7 @@ interface Props {
}
type AddResources = z.infer<typeof addResourcesSchema>;
export const ShowResources = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
@@ -163,16 +184,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
1073741824 bytes. Use +/- buttons to adjust by
256 MB.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="1073741824 (1GB in bytes)"
{...field}
step={MEMORY_STEP_MB}
converter={memoryConverter}
/>
</FormControl>
<FormMessage />
@@ -198,16 +223,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
268435456 bytes. Use +/- buttons to adjust by 256
MB.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="268435456 (256MB in bytes)"
{...field}
step={MEMORY_STEP_MB}
converter={memoryConverter}
/>
</FormControl>
<FormMessage />
@@ -234,17 +263,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
CPUs = 2000000000. Use +/- buttons to adjust by
0.25 CPU.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
step={CPU_STEP}
converter={cpuConverter}
/>
</FormControl>
<FormMessage />
@@ -271,14 +303,21 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
1000000000. Use +/- buttons to adjust by 0.25
CPU.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input placeholder="1000000000 (1 CPU)" {...field} />
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="1000000000 (1 CPU)"
step={CPU_STEP}
converter={cpuConverter}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -108,7 +108,8 @@ export const getLogType = (message: string): LogStyle => {
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
/\b(?:unstable|experimental)\b/i.test(lowerMessage) ||
/⚠|⚠️/i.test(lowerMessage)
) {
return LOG_STYLES.warning;
}

View File

@@ -1,5 +1,6 @@
import { Activity } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -7,7 +8,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowStorageActions } from "./show-storage-actions";
import { ShowTraefikActions } from "./show-traefik-actions";

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Pencil } from "lucide-react";
import { Pencil, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";

View File

@@ -1,17 +1,17 @@
import { format } from "date-fns";
import {
Clock,
Key,
KeyIcon,
Loader2,
MoreHorizontal,
ServerIcon,
Clock,
User,
Key,
Network,
Terminal,
Settings,
Pencil,
ServerIcon,
Settings,
Terminal,
Trash2,
User,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";

View File

@@ -1,13 +1,13 @@
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import { Fragment } from "react";
import { ChevronDown } from "lucide-react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
DropdownMenu,

View File

@@ -0,0 +1,84 @@
import { MinusIcon, PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export interface UnitConverter {
toValue: (raw: string | undefined) => number;
fromValue: (value: number) => string;
formatDisplay: (value: number) => string;
}
export const createConverter = (
multiplier: number,
formatDisplay: (value: number) => string,
): UnitConverter => ({
toValue: (raw) => {
if (!raw) return 0;
const value = Number.parseInt(raw, 10);
return Number.isNaN(value) ? 0 : value / multiplier;
},
fromValue: (value) =>
value <= 0 ? "" : String(Math.round(value * multiplier)),
formatDisplay,
});
interface NumberInputWithStepsProps {
value: string | undefined;
onChange: (value: string) => void;
placeholder: string;
step: number;
converter: UnitConverter;
}
export const NumberInputWithSteps = ({
value,
onChange,
placeholder,
step,
converter,
}: NumberInputWithStepsProps) => {
const numericValue = converter.toValue(value);
const displayValue = converter.formatDisplay(numericValue);
const handleIncrement = () =>
onChange(converter.fromValue(numericValue + step));
const handleDecrement = () =>
onChange(converter.fromValue(Math.max(0, numericValue - step)));
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleDecrement}
disabled={numericValue <= 0}
>
<MinusIcon className="h-4 w-4" />
</Button>
<Input
placeholder={placeholder}
value={value || ""}
onChange={(e) => onChange(e.target.value)}
className="text-center"
/>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleIncrement}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
{displayValue && (
<span className="text-xs text-muted-foreground text-center">
{displayValue}
</span>
)}
</div>
);
};

View File

@@ -33,7 +33,7 @@
"docker:build:canary": "./docker/build.sh canary",
"docker:push:canary": "./docker/push.sh canary",
"version": "echo $(node -p \"require('./package.json').version\")",
"test": "vitest --config __test__/vitest.config.ts volume-backups",
"test": "vitest --config __test__/vitest.config.ts",
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
},
"dependencies": {

View File

@@ -68,6 +68,40 @@ export const aiRouter = createTRPCRouter({
{ headers: {} },
);
break;
case "perplexity":
// Perplexity doesn't have a /models endpoint, return hardcoded list
return [
{
id: "sonar-deep-research",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-reasoning-pro",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-reasoning",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-pro",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
] as Model[];
default:
if (!input.apiKey)
throw new TRPCError({

View File

@@ -1,21 +1,32 @@
{
"name": "@dokploy/server",
"version": "1.0.0",
"main": "./src/index.ts",
"main": "./dist/index.js",
"type": "module",
"exports": {
".": "./src/index.ts",
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./db": {
"import": "./src/db/index.ts",
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./setup/*": {
"import": "./src/setup/*.ts",
"require": "./dist/setup/index.cjs.js"
"./*": {
"import": "./dist/*",
"require": "./dist/*.cjs"
},
"./constants": {
"import": "./src/constants/index.ts",
"require": "./dist/constants.cjs.js"
"./dist": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./dist/db": {
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./dist/db/schema": {
"import": "./dist/db/schema/index.js",
"require": "./dist/db/schema/index.cjs.js"
}
},
"scripts": {

View File

@@ -117,7 +117,7 @@ const getRegistryCommands = (
): string => {
return `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
echo "❌ DockerHub Failed" ;
exit 1;
}

View File

@@ -167,9 +167,15 @@ while true; do
fi
done
# Execute command and capture exit code
${exec}
EXIT_CODE=$?
echo "Execution completed."
# Wait for all background processes to complete to prevent zombie processes
wait
echo "Execution completed with exit code: $EXIT_CODE"
exit $EXIT_CODE
`;
const cleanupCommands = {