Merge pull request #2303 from Dokploy/canary

🚀 Release v0.24.6
This commit is contained in:
Mauricio Siu
2025-08-03 02:42:22 -06:00
committed by GitHub
67 changed files with 7182 additions and 523 deletions

View File

@@ -58,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx
# Install Railpack
ARG RAILPACK_VERSION=0.0.64
ARG RAILPACK_VERSION=0.2.2
RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://dokploy.com">
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" />
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" />
</a>
</br>
</br>
@@ -13,7 +13,7 @@
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
## Features
Dokploy includes multiple features to make your life easier.
@@ -43,7 +43,7 @@ curl -sSL https://dokploy.com/install.sh | sh
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## Sponsors
## ♥️ Sponsors
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
@@ -95,7 +95,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Community Backers 🤝
#### Organizations:
[Sponsors on Open Collective](https://opencollective.com/dokploy)
@@ -107,15 +106,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Contributors 🤝
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" alt="Contributors" />
</a>
## Video Tutorial
## 📺 Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a>
## Contributing
## 🤝 Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToConfigsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllConfigs } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -108,4 +108,136 @@ describe("createDomainLabels", () => {
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
);
});
it("should add stripPath middleware when stripPath is enabled", async () => {
const stripPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
};
const labels = await createDomainLabels(appName, stripPathDomain, "web");
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1",
);
});
it("should add internalPath middleware when internalPath is set", async () => {
const internalPathDomain = {
...baseDomain,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(
appName,
internalPathDomain,
"web",
);
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Middleware definition should only appear in web entrypoint
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Both routers should reference the middleware
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1",
);
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
});
it("should combine HTTPS redirect with internalPath middleware in correct order", async () => {
const combinedDomain = {
...baseDomain,
https: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, combinedDomain, "web");
const websecureLabels = await createDomainLabels(
appName,
combinedDomain,
"websecure",
);
// Web entrypoint should have both middlewares with redirect first
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1",
);
// Websecure should only have the addprefix middleware
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
// Middleware definition should only appear once (in web)
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
});
it("should combine all middlewares in correct order", async () => {
const fullDomain = {
...baseDomain,
https: true,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, fullDomain, "web");
// Should have all middleware definitions (only in web)
expect(webLabels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Should have middlewares in correct order: redirect, stripprefix, addprefix
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should not add middleware definitions for websecure entrypoint", async () => {
const internalPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Should not contain any middleware definitions
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// But should reference the middlewares
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
});

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import { generateRandomHash } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllNetworks,
addSuffixToNetworksRoot,
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToSecretsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllServiceNames,
addSuffixToServiceNames,
} from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,9 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllVolumes,
addSuffixToVolumesRoot,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToVolumesInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllVolumes } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = {

View File

@@ -1,12 +1,12 @@
import fs from "node:fs/promises";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
@@ -25,6 +25,7 @@ if (typeof window === "undefined") {
}
const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
applicationId: "",
herokuVersion: "",
giteaBranch: "",
@@ -142,7 +143,7 @@ describe("unzipDrop using real zip files", () => {
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
console.log(`Output Path: ${outputPath}`);
const zipBuffer = zip.toBuffer();
const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });

View File

@@ -1,5 +1,6 @@
import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
describe("processLogs", () => {

View File

@@ -1,10 +1,9 @@
import type { Domain } from "@dokploy/server";
import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@dokploy/server";
import type { ApplicationNested, Domain, Redirect } from "@dokploy/server";
import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
rollbackActive: false,
applicationId: "",
herokuVersion: "",

View File

@@ -1,3 +1,5 @@
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
@@ -9,11 +11,10 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes";
import { UpdateVolume } from "./update-volume";
interface Props {
id: string;
type: ServiceType | "compose";
@@ -80,7 +81,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
<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-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
@@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span>
</div>
)}
{mount.type === "file" ? (
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div>
<div className="flex flex-row gap-1">
<UpdateVolume

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } 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 { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -15,12 +21,6 @@ import {
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
export enum BuildType {
dockerfile = "dockerfile",
@@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}),
z.object({
buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.2.2"),
}),
z.object({
buildType: z.literal(BuildType.static),
@@ -86,6 +87,7 @@ interface ApplicationData {
herokuVersion?: string | null;
publishDirectory?: string | null;
isStaticSpa?: boolean | null;
railpackVersion?: string | null | undefined;
}
function isValidBuildType(value: string): value is BuildType {
@@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.railpack:
return {
buildType: BuildType.railpack,
railpackVersion: data.railpackVersion || null,
};
default: {
const buildType = data.buildType as BuildType;
@@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
: null,
isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion:
data.buildType === BuildType.railpack
? data.railpackVersion || "0.2.2"
: null,
})
.then(async () => {
toast.success("Build type saved");
@@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)}
/>
)}
{buildType === BuildType.railpack && (
<FormField
control={form.control}
name="railpackVersion"
render={({ field }) => (
<FormItem>
<FormLabel>Railpack Version</FormLabel>
<FormControl>
<Input
placeholder="Railpack Version"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
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 { Button } from "@/components/ui/button";
import {
@@ -34,14 +41,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import z from "zod";
export type CacheType = "fetch" | "cache";
@@ -123,6 +122,7 @@ interface Props {
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const [isManualInput, setIsManualInput] = useState(false);
const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
@@ -325,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
{isManualInput ? (
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
<Input
placeholder="Enter service name manually"
{...field}
className="w-full"
/>
</FormControl>
) : (
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
</SelectContent>
</Select>
</SelectContent>
</Select>
)}
{!isManualInput && (
<>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and
load the services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services
from the last deployment/fetch from
the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
setIsManualInput(!isManualInput);
if (!isManualInput) {
field.onChange("");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
{isManualInput ? (
<RefreshCw className="size-4 text-muted-foreground" />
) : (
<span className="text-xs text-muted-foreground">
Manual
</span>
)}
</Button>
</TooltipTrigger>
<TooltipContent
@@ -373,40 +453,9 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and load
the services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services from
the last deployment/fetch from the
repository
{isManualInput
? "Switch to service selection"
: "Enter service name manually"}
</p>
</TooltipContent>
</Tooltip>

View File

@@ -24,12 +24,14 @@ export const AddGithubProvider = () => {
const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState("");
const randomString = () => Math.random().toString(36).slice(2, 8);
useEffect(() => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
url: origin,
hook_attributes: {
url: `${url}/api/deploy/github`,

View File

@@ -1,3 +1,4 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -19,7 +20,7 @@ import {
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash } from "@/lib/utils";
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, User } from "lucide-react";
@@ -257,6 +258,24 @@ export const ProfileForm = () => {
value={field.value}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
>
<FormItem key="no-avatar">
<FormLabel className="[&:has([data-state=checked])>.default-avatar]:border-primary [&:has([data-state=checked])>.default-avatar]:border-1 [&:has([data-state=checked])>.default-avatar]:p-px cursor-pointer">
<FormControl>
<RadioGroupItem
value=""
className="sr-only"
/>
</FormControl>
<Avatar className="default-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform">
<AvatarFallback className="rounded-lg">
{getFallbackAvatarInitials(
data?.user?.name,
)}
</AvatarFallback>
</Avatar>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => (
<FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -1,5 +1,6 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -10,8 +11,6 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
import { ShowModalLogs } from "../../web-server/show-modal-logs";

View File

@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import type React from "react";
import { useEffect, useState } from "react";
import { useFieldArray, 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 { Card, CardContent } from "@/components/ui/card";
@@ -19,15 +27,15 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import type React from "react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props {
children: React.ReactNode;
@@ -37,6 +45,7 @@ interface Props {
const PortSchema = z.object({
targetPort: z.number().min(1, "Target port is required"),
publishedPort: z.number().min(1, "Published port is required"),
protocol: z.enum(["tcp", "udp", "sctp"]),
});
const TraefikPortsSchema = z.object({
@@ -75,12 +84,17 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
useEffect(() => {
if (currentPorts) {
form.reset({ ports: currentPorts });
form.reset({
ports: currentPorts.map((port) => ({
...port,
protocol: port.protocol as "tcp" | "udp" | "sctp",
})),
});
}
}, [currentPorts, form]);
const handleAddPort = () => {
append({ targetPort: 0, publishedPort: 0 });
append({ targetPort: 0, publishedPort: 0, protocol: "tcp" });
};
const onSubmit = async (data: TraefikPortsForm) => {
@@ -96,7 +110,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
return (
<>
<div onClick={() => setOpen(true)}>{children}</div>
<button type="button" onClick={() => setOpen(true)}>
{children}
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
@@ -143,8 +159,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<ScrollArea className="h-[400px] pr-4">
<div className="grid gap-4">
{fields.map((field, index) => (
<Card key={field.id}>
<CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
<Card key={field.id} className="bg-transparent">
<CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent">
<FormField
control={form.control}
name={`ports.${index}.targetPort`}
@@ -168,7 +184,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
);
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
</FormControl>
@@ -200,7 +215,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
);
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>
</FormControl>
@@ -208,6 +222,42 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.protocol`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
Protocol
</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["tcp", "udp", "sctp"].map(
(protocol) => (
<SelectItem
key={protocol}
value={protocol}
>
{protocol}
</SelectItem>
),
)}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end">
<Button

View File

@@ -1,3 +1,4 @@
import { Layers, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -8,7 +9,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Layers, Loader2 } from "lucide-react";
import { type ApplicationList, columns } from "./columns";
import { DataTable } from "./data-table";
@@ -20,10 +20,10 @@ export const ShowNodeApplications = ({ serverId }: Props) => {
const { data: NodeApps, isLoading: NodeAppsLoading } =
api.swarm.getNodeApps.useQuery({ serverId });
let applicationList = "";
let applicationList: string[] = [];
if (NodeApps && NodeApps.length > 0) {
applicationList = NodeApps.map((app) => app.Name).join(" ");
applicationList = NodeApps.map((app) => app.Name);
}
const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } =

View File

@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
import type React from "react";
import { cn } from "@/lib/utils";
import { GithubIcon } from "../icons/data-tools-icons";
import { Logo } from "../shared/logo";
import { Button } from "../ui/button";

View File

@@ -1,4 +1,5 @@
"use client";
import type { inferRouterOutputs } from "@trpc/server";
import {
Activity,
BarChartHorizontalBigIcon,
@@ -29,10 +30,10 @@ import {
User,
Users,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import type * as React from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import {
Breadcrumb,
BreadcrumbItem,
@@ -77,10 +78,6 @@ import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import type { AppRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action";
import { Logo } from "../shared/logo";
@@ -770,9 +767,7 @@ export default function Page({ children }: Props) {
setIsLoaded(true);
}, []);
const router = useRouter();
const pathname = usePathname();
const _currentPath = router.pathname;
const { data: auth } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();

View File

@@ -1,8 +1,7 @@
import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index";
import { Download } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { api } from "@/utils/api";
import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button";
import {
@@ -11,6 +10,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UpdateServerButton = () => {
@@ -18,7 +18,6 @@ export const UpdateServerButton = () => {
latestVersion: null,
updateAvailable: false,
});
const _router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: getUpdateData } =
api.settings.getUpdateData.useMutation();
@@ -26,9 +25,6 @@ export const UpdateServerButton = () => {
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
if (isCloud) {
return null;
}
useEffect(() => {
// Handling of automatic check for server updates
if (isCloud) {
@@ -77,7 +73,7 @@ export const UpdateServerButton = () => {
};
}, []);
return updateData.updateAvailable ? (
return !isCloud && updateData.updateAvailable ? (
<div className="border-t pt-4">
<UpdateServer
updateData={updateData}

View File

@@ -1,3 +1,5 @@
import { ChevronsUpDown } from "lucide-react";
import { useRouter } from "next/router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
@@ -17,10 +19,9 @@ import {
} from "@/components/ui/select";
import { authClient } from "@/lib/auth-client";
import { Languages } from "@/lib/languages";
import { getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api";
import useLocale from "@/utils/hooks/use-locale";
import { ChevronsUpDown } from "lucide-react";
import { useRouter } from "next/router";
import { ModeToggle } from "../ui/modeToggle";
import { SidebarMenuButton } from "../ui/sidebar";
@@ -46,7 +47,9 @@ export const UserNav = () => {
src={data?.user?.image || ""}
alt={data?.user?.image || ""}
/>
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">
{getFallbackAvatarInitials(data?.user?.name)}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Account</span>
@@ -122,18 +125,16 @@ export const UserNav = () => {
)}
</>
) : (
<>
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings/servers");
}}
>
Servers
</DropdownMenuItem>
)}
</>
data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings/servers");
}}
>
Servers
</DropdownMenuItem>
)
)}
</DropdownMenuGroup>
{isCloud && data?.role === "owner" && (

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "railpackVersion" text DEFAULT '0.2.2';

File diff suppressed because it is too large Load Diff

View File

@@ -729,6 +729,13 @@
"when": 1752465764072,
"tag": "0103_cultured_pestilence",
"breakpoints": true
},
{
"idx": 104,
"version": "7",
"when": 1754207407121,
"tag": "0104_omniscient_randall",
"breakpoints": true
}
]
}

View File

@@ -27,3 +27,14 @@ export function formatTimestamp(timestamp: string | number) {
return "Fecha inválida";
}
}
export function getFallbackAvatarInitials(
fullName: string | undefined,
): string {
if (typeof fullName === "undefined" || fullName === "") return "CN";
const [name = "", surname = ""] = fullName.split(" ");
if (surname === "") {
return name.substring(0, 2).toUpperCase();
}
return (name.charAt(0) + surname.charAt(0)).toUpperCase();
}

View File

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

View File

@@ -1,31 +1,4 @@
import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGitProvider,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import {
IS_CLOUD,
addNewService,
checkServiceAccess,
createApplication,
@@ -34,6 +7,7 @@ import {
findGitProviderById,
findProjectById,
getApplicationStats,
IS_CLOUD,
mechanizeDockerContainer,
readConfig,
readRemoteConfig,
@@ -57,6 +31,32 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiSaveGitProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
export const applicationRouter = createTRPCRouter({
create: protectedProcedure
@@ -364,6 +364,7 @@ export const applicationRouter = createTRPCRouter({
dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa,
railpackVersion: input.railpackVersion,
});
return true;

View File

@@ -1,3 +1,54 @@
import {
canAccessToTraefikFiles,
checkGPUStatus,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
DEFAULT_UPDATE_DATA,
execAsync,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
IS_CLOUD,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readEnvironmentVariables,
readMainConfig,
readMonitoringConfig,
readPorts,
recreateDirectory,
reloadDockerResource,
sendDockerCleanupNotifications,
setupGPUSupport,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
writeTraefikSetup,
} from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { z } from "zod";
import { db } from "@/server/db";
import {
apiAssignDomain,
@@ -11,54 +62,6 @@ import {
apiUpdateDockerCleanup,
} from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup";
import {
DEFAULT_UPDATE_DATA,
IS_CLOUD,
canAccessToTraefikFiles,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
execAsync,
execAsyncRemote,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
initializeTraefik,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readMainConfig,
readMonitoringConfig,
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import packageInfo from "../../../package.json";
import { appRouter } from "../root";
import {
@@ -73,10 +76,7 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) {
return true;
}
const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
await reloadDockerResource("dokploy");
return true;
}),
cleanRedis: adminProcedure.mutation(async () => {
@@ -101,20 +101,15 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) {
return true;
}
await reloadDockerResource("dokploy-redis");
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true;
}),
reloadTraefik: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
try {
if (input?.serverId) {
await execAsync("docker restart dokploy-traefik");
} else if (!IS_CLOUD) {
await execAsync("docker restart dokploy-traefik");
}
await reloadDockerResource("dokploy-traefik", input?.serverId);
} catch (err) {
console.error(err);
}
@@ -124,17 +119,28 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
const ports = (await getTraefikPorts(input.serverId)).filter(
(port) =>
port.targetPort !== 80 &&
port.targetPort !== 443 &&
port.targetPort !== 8080,
const ports = await readPorts("dokploy-traefik", input.serverId);
const env = await readEnvironmentVariables(
"dokploy-traefik",
input.serverId,
);
await initializeTraefik({
additionalPorts: ports,
enableDashboard: input.enableDashboard,
const preparedEnv = prepareEnvironmentVariables(env);
let newPorts = ports;
// If receive true, add 8080 to ports
if (input.enableDashboard) {
newPorts.push({
targetPort: 8080,
publishedPort: 8080,
protocol: "tcp",
});
} else {
newPorts = ports.filter((port) => port.targetPort !== 8080);
}
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: newPorts,
serverId: input.serverId,
force: true,
});
return true;
}),
@@ -551,29 +557,23 @@ export const settingsRouter = createTRPCRouter({
readTraefikEnv: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command =
"docker container inspect dokploy-traefik --format '{{json .Config.Env}}'";
let result = "";
if (input?.serverId) {
const execResult = await execAsyncRemote(input.serverId, command);
result = execResult.stdout;
} else {
const execResult = await execAsync(command);
result = execResult.stdout;
}
const envVars = JSON.parse(result.trim());
return envVars.join("\n");
const envVars = await readEnvironmentVariables(
"dokploy-traefik",
input?.serverId,
);
return envVars;
}),
writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({
const ports = await readPorts("dokploy-traefik", input?.serverId);
await writeTraefikSetup({
env: envs,
additionalPorts: ports,
serverId: input.serverId,
force: true,
});
return true;
@@ -581,22 +581,8 @@ export const settingsRouter = createTRPCRouter({
haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const ports = JSON.parse(stdout.trim());
return Object.entries(ports).some(([containerPort, bindings]) => {
const [port] = containerPort.split("/");
return port === "8080" && bindings && (bindings as any[]).length > 0;
});
const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports.some((port) => port.targetPort === 8080);
}),
readStatsLogs: adminProcedure
@@ -793,6 +779,7 @@ export const settingsRouter = createTRPCRouter({
z.object({
targetPort: z.number(),
publishedPort: z.number(),
protocol: z.enum(["tcp", "udp", "sctp"]),
}),
),
}),
@@ -805,10 +792,16 @@ export const settingsRouter = createTRPCRouter({
message: "Please set a serverId to update Traefik ports",
});
}
await initializeTraefik({
serverId: input.serverId,
const env = await readEnvironmentVariables(
"dokploy-traefik",
input?.serverId,
);
const preparedEnv = prepareEnvironmentVariables(env);
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: input.additionalPorts,
force: true,
serverId: input.serverId,
});
return true;
} catch (error) {
@@ -825,7 +818,8 @@ export const settingsRouter = createTRPCRouter({
getTraefikPorts: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
return await getTraefikPorts(input?.serverId);
const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports;
}),
updateLogCleanup: adminProcedure
.input(
@@ -855,56 +849,3 @@ export const settingsRouter = createTRPCRouter({
return ips;
}),
});
export const getTraefikPorts = async (serverId?: string) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
try {
let stdout = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
}> = [];
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
});
}
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
};

View File

@@ -1,10 +1,10 @@
import {
findServerById,
getApplicationInfo,
getNodeApplications,
getNodeInfo,
getSwarmNodes,
} from "@dokploy/server";
import { findServerById } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -55,7 +55,12 @@ export const swarmRouter = createTRPCRouter({
getAppInfos: protectedProcedure
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
appName: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid app name.")
.array()
.min(1),
serverId: z.string().optional(),
}),
)

View File

@@ -55,7 +55,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
conn
.once("ready", () => {
conn.exec(
`docker exec -it ${containerId} ${activeWay}`,
`docker exec -it -w / ${containerId} ${activeWay}`,
{ pty: true },
(err, stream) => {
if (err) throw err;
@@ -107,7 +107,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
const shell = getShell();
const ptyProcess = spawn(
shell,
["-c", `docker exec -it ${containerId} ${activeWay}`],
["-c", `docker exec -it -w / ${containerId} ${activeWay}`],
{},
);

View File

@@ -1,10 +1,3 @@
import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/setup/traefik-setup";
import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
@@ -13,6 +6,13 @@ import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/setup/setup";
import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeStandaloneTraefik,
} from "@dokploy/server/setup/traefik-setup";
(async () => {
try {
setupDirectories();
@@ -22,7 +22,7 @@ import {
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.1.2");
await initializeTraefik();
await initializeStandaloneTraefik();
await initializeRedis();
await initializePostgres();
} catch (e) {

View File

@@ -27,7 +27,8 @@
"noUnsafeOptionalChaining": "off",
"noUnusedImports": "error",
"noUnusedFunctionParameters": "error",
"noUnusedVariables": "error"
"noUnusedVariables": "error",
"useHookAtTopLevel": "off"
},
"style": {
"noNonNullAssertion": "off",

View File

@@ -208,6 +208,7 @@ export const applications = pgTable("application", {
.notNull()
.default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"),
railpackVersion: text("railpackVersion").default("0.2.2"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
@@ -412,6 +413,7 @@ const createSchema = createInsertSchema(applications, {
"static",
"railpack",
]),
railpackVersion: z.string().optional(),
herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(),
@@ -466,6 +468,7 @@ export const apiSaveBuildType = createSchema
dockerContextPath: true,
dockerBuildStage: true,
herokuVersion: true,
railpackVersion: true,
})
.required()
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));

View File

@@ -1,8 +1,12 @@
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import {
type apiCreateCompose,
buildAppName,
cleanAppName,
compose,
} from "@dokploy/server/db/schema";
import {
buildCompose,
getBuildComposeCommand,
@@ -516,19 +520,20 @@ export const startCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {
const { COMPOSE_PATH } = paths(!!compose.serverId);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const path =
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
if (compose.composeType === "docker-compose") {
if (compose.serverId) {
await execAsyncRemote(
compose.serverId,
`cd ${join(
COMPOSE_PATH,
compose.appName,
"code",
)} && docker compose -p ${compose.appName} up -d`,
`cd ${projectPath} && ${baseCommand}`,
);
} else {
await execAsync(`docker compose -p ${compose.appName} up -d`, {
cwd: join(COMPOSE_PATH, compose.appName, "code"),
await execAsync(baseCommand, {
cwd: projectPath,
});
}
}

View File

@@ -441,13 +441,13 @@ export const getNodeApplications = async (serverId?: string) => {
};
export const getApplicationInfo = async (
appName: string,
appNames: string[],
serverId?: string,
) => {
try {
let stdout = "";
let stderr = "";
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`;
const command = `docker service ps ${appNames.join(" ")} --format '{{json .}}' --no-trunc`;
if (serverId) {
const result = await execAsyncRemote(serverId, command);

View File

@@ -2,18 +2,22 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type ServiceType,
type apiCreateMount,
mounts,
type ServiceType,
} from "@dokploy/server/db/schema";
import {
createFile,
encodeBase64,
getCreateFileCommand,
} from "@dokploy/server/utils/docker/utils";
import { removeFileOrDirectory } from "@dokploy/server/utils/filesystem/directory";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { type SQL, eq, sql } from "drizzle-orm";
import { eq, type SQL, sql } from "drizzle-orm";
export type Mount = typeof mounts.$inferSelect;
@@ -123,7 +127,7 @@ export const updateMount = async (
mountId: string,
mountData: Partial<Mount>,
) => {
return await db.transaction(async (tx) => {
const mount = await db.transaction(async (tx) => {
const mount = await tx
.update(mounts)
.set({
@@ -140,13 +144,13 @@ export const updateMount = async (
});
}
if (mount.type === "file") {
await deleteFileMount(mountId);
await createFileMount(mountId);
}
return await findMountById(mountId);
});
if (mount.type === "file") {
await updateFileMount(mountId);
}
return mount;
};
export const findMountsByApplicationId = async (
@@ -198,6 +202,26 @@ export const deleteMount = async (mountId: string) => {
return deletedMount[0];
};
export const updateFileMount = async (mountId: string) => {
const mount = await findMountById(mountId);
if (!mount || !mount.filePath) return;
const basePath = await getBaseFilesPath(mountId);
const fullPath = path.join(basePath, mount.filePath);
try {
const serverId = await getServerId(mount);
const encodedContent = encodeBase64(mount.content || "");
const command = `echo "${encodedContent}" | base64 -d > ${fullPath}`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} catch {
console.log("Error updating file mount");
}
};
export const deleteFileMount = async (mountId: string) => {
const mount = await findMountById(mountId);
if (!mount.filePath) return;

View File

@@ -5,6 +5,11 @@ import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import {
initializeStandaloneTraefik,
initializeTraefikService,
type TraefikOptions,
} from "../setup/traefik-setup";
export interface IUpdateData {
latestVersion: string | null;
@@ -243,3 +248,165 @@ export const cleanupFullDocker = async (serverId?: string | null) => {
console.log(error);
}
};
export const getDockerResourceType = async (
resourceName: string,
serverId?: string,
) => {
let result = "";
const command = `
RESOURCE_NAME="${resourceName}"
if docker service inspect "$RESOURCE_NAME" &>/dev/null; then
echo "service"
exit 0
fi
if docker inspect "$RESOURCE_NAME" &>/dev/null; then
echo "standalone"
exit 0
fi
echo "unknown"
exit 0
`;
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "service") {
return "service";
}
if (result === "standalone") {
return "standalone";
}
return "unknown";
};
export const reloadDockerResource = async (
resourceName: string,
serverId?: string,
) => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service update --force ${resourceName}`;
} else {
command = `docker restart ${resourceName}`;
}
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
};
export const readEnvironmentVariables = async (
resourceName: string,
serverId?: string,
) => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}'`;
} else {
command = `docker container inspect ${resourceName} --format '{{json .Config.Env}}'`;
}
let result = "";
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "null") {
return "";
}
return JSON.parse(result)?.join("\n");
};
export const readPorts = async (
resourceName: string,
serverId?: string,
): Promise<
{ targetPort: number; publishedPort: number; protocol?: string }[]
> => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
} else {
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
}
let result = "";
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "null") {
return [];
}
const parsedResult = JSON.parse(result);
if (resourceType === "service") {
return parsedResult
.map((port: any) => ({
targetPort: port.TargetPort,
publishedPort: port.PublishedPort,
protocol: port.Protocol,
}))
.filter((port: any) => port.targetPort !== 80 && port.targetPort !== 443);
}
const ports: {
targetPort: number;
publishedPort: number;
protocol?: string;
}[] = [];
for (const key in parsedResult) {
if (Object.hasOwn(parsedResult, key)) {
const containerPortMapppings = parsedResult[key];
const protocol = key.split("/")[1];
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
containerPortMapppings.forEach((mapping: any) => {
ports.push({
targetPort: targetPort,
publishedPort: Number.parseInt(mapping.HostPort, 10),
protocol: protocol,
});
});
}
}
return ports.filter(
(port: any) => port.targetPort !== 80 && port.targetPort !== 443,
);
};
export const writeTraefikSetup = async (
input: TraefikOptions,
serverId?: string,
) => {
const resourceType = await getDockerResourceType("dokploy-traefik", serverId);
if (resourceType === "service") {
await initializeTraefikService({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: serverId,
});
} else {
await initializeStandaloneTraefik({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: serverId,
});
}
};

View File

@@ -6,18 +6,17 @@ import {
} from "@dokploy/server/services/deployment";
import { findServerById } from "@dokploy/server/services/server";
import {
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
TRAEFIK_HTTP3_PORT,
TRAEFIK_PORT,
TRAEFIK_SSL_PORT,
TRAEFIK_VERSION,
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
} from "@dokploy/server/setup/traefik-setup";
import slug from "slugify";
import { Client } from "ssh2";
import { recreateDirectory } from "../utils/filesystem/directory";
import slug from "slugify";
export const slugify = (text: string | undefined) => {
if (!text) {
return "";
@@ -609,7 +608,7 @@ const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=0.0.64
export RAILPACK_VERSION=0.2.2
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi

View File

@@ -1,6 +1,6 @@
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { ContainerCreateOptions } from "dockerode";
import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode";
import { dump } from "js-yaml";
import { paths } from "../constants";
import { getRemoteDocker } from "../utils/servers/remote-docker";
@@ -15,23 +15,20 @@ export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
interface TraefikOptions {
enableDashboard?: boolean;
export interface TraefikOptions {
env?: string[];
serverId?: string;
additionalPorts?: {
targetPort: number;
publishedPort: number;
protocol?: string;
}[];
force?: boolean;
}
export const initializeTraefik = async ({
enableDashboard = false,
export const initializeStandaloneTraefik = async ({
env,
serverId,
additionalPorts = [],
force = false,
}: TraefikOptions = {}) => {
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const imageName = `traefik:v${TRAEFIK_VERSION}`;
@@ -51,13 +48,17 @@ export const initializeTraefik = async ({
],
};
const enableDashboard = additionalPorts.some(
(port) => port.targetPort === 8080,
);
if (enableDashboard) {
exposedPorts["8080/tcp"] = {};
portBindings["8080/tcp"] = [{ HostPort: "8080" }];
}
for (const port of additionalPorts) {
const portKey = `${port.targetPort}/tcp`;
const portKey = `${port.targetPort}/${port.protocol ?? "tcp"}`;
exposedPorts[portKey] = {};
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
}
@@ -87,68 +88,117 @@ export const initializeTraefik = async ({
const docker = await getRemoteDocker(serverId);
try {
try {
const service = docker.getService("dokploy-traefik");
await service?.remove({ force: true });
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
try {
await docker.listServices({
filters: { name: ["dokploy-traefik"] },
});
console.log("Waiting for service cleanup...");
await new Promise((resolve) => setTimeout(resolve, 5000));
attempts++;
} catch {
break;
}
}
} catch {
console.log("No existing service to remove");
}
// Then try to remove any existing container
const container = docker.getContainer(containerName);
try {
const inspect = await container.inspect();
if (inspect.State.Status === "running" && !force) {
console.log("Traefik already running");
return;
}
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch {
console.log("No existing container to remove");
}
// Create and start the new container
try {
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully");
} catch (error: any) {
if (error?.json?.message?.includes("port is already allocated")) {
console.log("Ports still in use, waiting longer for cleanup...");
await new Promise((resolve) => setTimeout(resolve, 10000));
// Try one more time
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully after retry");
} else {
throw error;
}
console.log("Traefik Started ✅");
} catch (error) {
console.error("Error in initializeStandaloneTraefik", error);
}
} catch (error) {
console.error("Failed to initialize Traefik:", error);
await docker.createContainer(settings);
console.error("Error in initializeStandaloneTraefik", error);
throw error;
}
};
export const initializeTraefikService = async ({
env,
additionalPorts = [],
serverId,
}: TraefikOptions) => {
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const imageName = `traefik:v${TRAEFIK_VERSION}`;
const appName = "dokploy-traefik";
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: imageName,
Env: env,
Mounts: [
{
Type: "bind",
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
Target: "/etc/traefik/traefik.yml",
},
{
Type: "bind",
Source: DYNAMIC_TRAEFIK_PATH,
Target: "/etc/dokploy/traefik/dynamic",
},
{
Type: "bind",
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
},
],
},
Networks: [{ Target: "dokploy-network" }],
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Ports: [
{
TargetPort: 443,
PublishedPort: TRAEFIK_SSL_PORT,
PublishMode: "host",
Protocol: "tcp",
},
{
TargetPort: 443,
PublishedPort: TRAEFIK_SSL_PORT,
PublishMode: "host",
Protocol: "udp",
},
{
TargetPort: 80,
PublishedPort: TRAEFIK_PORT,
PublishMode: "host",
Protocol: "tcp",
},
...additionalPorts.map((port) => ({
TargetPort: port.targetPort,
PublishedPort: port.publishedPort,
Protocol: port.protocol as "tcp" | "udp" | "sctp" | undefined,
PublishMode: "host" as const,
})),
],
},
};
const docker = await getRemoteDocker(serverId);
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
console.log("Traefik Updated ✅");
} catch {
await docker.createService(settings);
console.log("Traefik Started ✅");
}
};
export const createDefaultServerTraefikConfig = () => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");

View File

@@ -17,7 +17,7 @@ export const runComposeBackup = async (
const project = await findProjectById(projectId);
const { prefix, databaseType } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -1,7 +1,11 @@
import path from "node:path";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server";
import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
import { startLogCleanup } from "../access-log/handler";
import {
cleanUpDockerBuilder,
cleanUpSystemPrune,
@@ -11,11 +15,6 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");

View File

@@ -14,7 +14,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const project = await findProjectById(projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -1,7 +1,6 @@
import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
@@ -9,6 +8,7 @@ import {
import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256");
@@ -75,7 +75,7 @@ export const buildRailpack = async (
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
@@ -110,6 +110,8 @@ export const buildRailpack = async (
return true;
} catch (e) {
throw e;
} finally {
await execAsync("docker buildx rm builder-containerd");
}
};
@@ -155,7 +157,7 @@ export const getRailpackCommand = (
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
@@ -194,6 +196,7 @@ docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
exit 1;
}
echo "✅ Railpack build completed." >> ${logPath};
docker buildx rm builder-containerd
`;
return bashCommand;

View File

@@ -313,40 +313,46 @@ export const createDomainLabels = (
`traefik.http.routers.${routerName}.service=${routerName}`,
];
// Validate stripPath - it should only be used when path is defined and not "/"
if (stripPath) {
if (!path || path === "/") {
console.warn(
`stripPath is enabled but path is not defined or is "/" for domain ${host}`,
);
} else {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Collect middlewares for this router
const middlewares: string[] = [];
// Add HTTPS redirect for web entrypoint (must be first)
if (entrypoint === "web" && https) {
middlewares.push("redirect-to-https@file");
}
// Add stripPath middleware if needed
if (stripPath && path && path !== "/") {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Only define middleware once (on web entrypoint)
if (entrypoint === "web") {
labels.push(
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
);
}
middlewares.push(middlewareName);
}
// Validate internalPath - ensure it's a valid path format
if (internalPath && internalPath !== "/") {
if (!internalPath.startsWith("/")) {
console.warn(
`internalPath "${internalPath}" should start with "/" and not be empty for domain ${host}`,
);
} else {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
// Add internalPath middleware if needed
if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
// Only define middleware once (on web entrypoint)
if (entrypoint === "web") {
labels.push(
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
);
}
middlewares.push(middlewareName);
}
if (entrypoint === "web" && https) {
// Apply middlewares to router if any exist
if (middlewares.length > 0) {
labels.push(
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
`traefik.http.routers.${routerName}.middlewares=${middlewares.join(",")}`,
);
}
// Add TLS configuration for websecure
if (entrypoint === "websecure") {
if (certificateType === "letsencrypt") {
labels.push(

View File

@@ -3,8 +3,8 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import {
type Gitea,
findGiteaById,
type Gitea,
updateGitea,
} from "@dokploy/server/services/gitea";
import type { InferResultType } from "@dokploy/server/types/with";
@@ -118,7 +118,6 @@ export const getGiteaCloneCommand = async (
giteaOwner,
giteaRepository,
serverId,
gitea,
enableSubmodules,
} = entity;
@@ -145,6 +144,7 @@ export const getGiteaCloneCommand = async (
// Use paths(true) for remote operations
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGiteaToken(giteaId);
const gitea = await findGiteaById(giteaId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");

View File

@@ -4,8 +4,8 @@ import { paths } from "@dokploy/server/constants";
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import {
type Gitlab,
findGitlabById,
type Gitlab,
updateGitlab,
} from "@dokploy/server/services/gitlab";
import type { InferResultType } from "@dokploy/server/types/with";
@@ -112,7 +112,6 @@ export const cloneGitlabRepository = async (
appName,
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
@@ -125,6 +124,7 @@ export const cloneGitlabRepository = async (
}
await refreshGitlabToken(gitlabId);
const gitlab = await findGitlabById(gitlabId);
const requirements = getErrorCloneRequirements(entity);
@@ -187,7 +187,6 @@ export const getGitlabCloneCommand = async (
gitlabBranch,
gitlabId,
serverId,
gitlab,
enableSubmodules,
} = entity;
@@ -235,6 +234,7 @@ export const getGitlabCloneCommand = async (
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGitlabToken(gitlabId);
const gitlab = await findGitlabById(gitlabId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
@@ -310,22 +310,43 @@ export const getGitlabBranches = async (input: {
const gitlabProvider = await findGitlabById(input.gitlabId);
const branchesResponse = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
const allBranches = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
if (!branchesResponse.ok) {
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`);
while (true) {
const branchesResponse = await fetch(
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!branchesResponse.ok) {
throw new Error(
`Failed to fetch branches: ${branchesResponse.statusText}`,
);
}
const branches = await branchesResponse.json();
if (branches.length === 0) {
break;
}
allBranches.push(...branches);
page++;
// Check if we've reached the total using headers (optional optimization)
const total = branchesResponse.headers.get("x-total");
if (total && allBranches.length >= Number.parseInt(total)) {
break;
}
}
const branches = await branchesResponse.json();
return branches as {
return allBranches as {
id: string;
name: string;
commit: {
@@ -350,9 +371,9 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
});
}
const gitlabProvider = await findGitlabById(gitlabId);
const { COMPOSE_PATH } = paths();
await refreshGitlabToken(gitlabId);
const gitlabProvider = await findGitlabById(gitlabId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
@@ -398,9 +419,9 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
message: "Gitlab Provider not found",
});
}
const gitlabProvider = await findGitlabById(gitlabId);
const { COMPOSE_PATH } = paths(true);
await refreshGitlabToken(gitlabId);
const gitlabProvider = await findGitlabById(gitlabId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);

View File

@@ -81,7 +81,7 @@ const getMongoSpecificCommand = (
backupFile: string,
): string => {
const tempDir = "/tmp/dokploy-restore";
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
const fileName = backupFile.split("/").pop() || "backup.sql.gz";
const decompressedName = fileName.replace(".gz", "");
return `
rm -rf ${tempDir} && \

View File

@@ -2,8 +2,7 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
@@ -37,6 +36,9 @@ export const backupVolume = async (
echo "Starting upload to S3..."
${rcloneCommand}
echo "Upload to S3 done ✅"
echo "Cleaning up local backup file..."
rm "${volumeBackupPath}/${backupFileName}"
echo "Local backup file cleaned up ✅"
`;
if (!turnOff) {