diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..4ffcfaed7
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,21 @@
+# Dockerfile for DevContainer
+FROM node:24.4.0-bullseye-slim
+
+# Install essential packages
+RUN apt-get update && apt-get install -y \
+ curl \
+ bash \
+ git \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Set up PNPM
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable && corepack prepare pnpm@10.22.0 --activate
+
+# Create workspace directory
+WORKDIR /workspaces/dokploy
+
+# Set up user permissions
+USER node
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..eafddd06d
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,53 @@
+{
+ "name": "Dokploy development container",
+ "build": {
+ "dockerfile": "Dockerfile",
+ "context": ".."
+ },
+ "features": {
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "moby": true,
+ "version": "latest"
+ },
+ "ghcr.io/devcontainers/features/git:1": {
+ "ppa": true,
+ "version": "latest"
+ },
+ "ghcr.io/devcontainers/features/go:1": {
+ "version": "1.20"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-vscode.vscode-typescript-next",
+ "bradlc.vscode-tailwindcss",
+ "ms-vscode.vscode-json",
+ "biomejs.biome",
+ "golang.go",
+ "redhat.vscode-xml",
+ "github.vscode-github-actions",
+ "github.copilot",
+ "github.copilot-chat"
+ ]
+ }
+ },
+ "forwardPorts": [3000, 5432, 6379],
+ "portsAttributes": {
+ "3000": {
+ "label": "Dokploy App",
+ "onAutoForward": "notify"
+ },
+ "5432": {
+ "label": "PostgreSQL",
+ "onAutoForward": "silent"
+ },
+ "6379": {
+ "label": "Redis",
+ "onAutoForward": "silent"
+ }
+ },
+ "remoteUser": "node",
+ "workspaceFolder": "/workspaces/dokploy",
+ "runArgs": ["--name", "dokploy-devcontainer"]
+}
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 3ed957b72..321fb2029 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -13,6 +13,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
+ - name: Set tag and version
+ id: meta-cloud
+ run: |
+ VERSION=$(jq -r .version apps/dokploy/package.json)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "tags=siumauricio/cloud:latest,siumauricio/cloud:${VERSION}" >> $GITHUB_OUTPUT
+ else
+ echo "tags=siumauricio/cloud:canary" >> $GITHUB_OUTPUT
+ fi
+
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -25,8 +36,7 @@ jobs:
context: .
file: ./Dockerfile.cloud
push: true
- tags: |
- siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
+ tags: ${{ steps.meta-cloud.outputs.tags }}
platforms: linux/amd64
build-args: |
NEXT_PUBLIC_UMAMI_HOST=${{ secrets.NEXT_PUBLIC_UMAMI_HOST }}
@@ -40,6 +50,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
+ - name: Set tag and version
+ id: meta-schedule
+ run: |
+ VERSION=$(jq -r .version apps/dokploy/package.json)
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "tags=siumauricio/schedule:latest,siumauricio/schedule:${VERSION}" >> $GITHUB_OUTPUT
+ else
+ echo "tags=siumauricio/schedule:canary" >> $GITHUB_OUTPUT
+ fi
+
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -52,8 +72,7 @@ jobs:
context: .
file: ./Dockerfile.schedule
push: true
- tags: |
- siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
+ tags: ${{ steps.meta-schedule.outputs.tags }}
platforms: linux/amd64
build-and-push-server-image:
@@ -63,6 +82,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
+ - name: Set tag and version
+ id: meta-server
+ run: |
+ VERSION=$(jq -r .version apps/dokploy/package.json)
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "tags=siumauricio/server:latest,siumauricio/server:${VERSION}" >> $GITHUB_OUTPUT
+ else
+ echo "tags=siumauricio/server:canary" >> $GITHUB_OUTPUT
+ fi
+
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -75,6 +104,5 @@ jobs:
context: .
file: ./Dockerfile.server
push: true
- tags: |
- siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
+ tags: ${{ steps.meta-server.outputs.tags }}
platforms: linux/amd64
diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml
new file mode 100644
index 000000000..3554babb2
--- /dev/null
+++ b/.github/workflows/pr-quality.yml
@@ -0,0 +1,22 @@
+
+name: PR Quality
+
+permissions:
+ contents: read
+ issues: read
+ pull-requests: write
+
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ anti-slop:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: peakoss/anti-slop@v0
+ with:
+ max-failures: 4
+ blocked-commit-authors: "claude,copilot"
+ require-description: true
+ min-account-age: 5
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index bfdc8c48b..2ad24fc0c 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.16.0
+ node-version: 24.4.0
cache: "pnpm"
- name: Install Nixpacks
diff --git a/.github/workflows/sync-openapi-docs.yml b/.github/workflows/sync-openapi-docs.yml
index ddc51355a..549af945b 100644
--- a/.github/workflows/sync-openapi-docs.yml
+++ b/.github/workflows/sync-openapi-docs.yml
@@ -24,7 +24,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.16.0
+ node-version: 24.4.0
cache: "pnpm"
- name: Install dependencies
diff --git a/.gitignore b/.gitignore
index ab2fe76c6..d531bab01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,7 +43,4 @@ yarn-error.log*
*.pem
-.db
-
-# Development environment
-.devcontainer
\ No newline at end of file
+.db
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
index 593cb75bc..84e5de6ef 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.16.0
\ No newline at end of file
+24.4.0
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6ac16b14e..ad37899e6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -53,7 +53,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
-We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
+We use Node v24.4.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 24.4.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git
@@ -165,10 +165,11 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
### Important Considerations for Pull Requests
-- **Testing is Mandatory:** All Pull Requests **must be tested** before submission. You must verify that your changes work as expected in a local development environment (see [Setup](#setup)). **Pull Requests that have not been tested will be closed.** This policy ensures clean contributions and reduces the time maintainers spend reviewing untested or broken code.
+- **Testing is Mandatory:** All Pull Requests **must be tested** by the PR author before submission. You must verify that your changes work as expected in a local development environment (see [Setup](#setup)). **Pull Requests that have not been tested by their creator will be rejected.** This policy keeps the PR history clean and values contributors who submit verified, working code. Untested PRs are often recognizable by disproportionately large or scattered changes for simple tasks—please test first.
- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects.
- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
+- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
Thank you for your contribution!
diff --git a/Dockerfile b/Dockerfile
index 262862ca6..ed936508f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
-RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
+RUN pnpm --filter=./apps/dokploy --prod deploy --legacy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
diff --git a/Dockerfile.cloud b/Dockerfile.cloud
index a0de32021..05e7cde49 100644
--- a/Dockerfile.cloud
+++ b/Dockerfile.cloud
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -29,7 +29,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
-RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
+RUN pnpm --filter=./apps/dokploy --prod deploy --legacy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
diff --git a/Dockerfile.schedule b/Dockerfile.schedule
index ce1f96edf..81b13fd64 100644
--- a/Dockerfile.schedule
+++ b/Dockerfile.schedule
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/schedules run build
-RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
+RUN pnpm --filter=./apps/schedules --prod deploy --legacy /prod/schedules
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
diff --git a/Dockerfile.server b/Dockerfile.server
index f5aa25c1e..8990ece4d 100644
--- a/Dockerfile.server
+++ b/Dockerfile.server
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
-FROM node:20.16.0-slim AS base
+FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
-RUN corepack prepare pnpm@9.12.0 --activate
+RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/api run build
-RUN pnpm --filter=./apps/api --prod deploy /prod/api
+RUN pnpm --filter=./apps/api --prod deploy --legacy /prod/api
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
diff --git a/apps/api/package.json b/apps/api/package.json
index 70c8aaac8..c7e76afc7 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "PORT=4000 tsx watch src/index.ts",
- "build": "tsc --project tsconfig.json",
+ "build": "rimraf dist && tsc --project tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
@@ -12,7 +12,7 @@
"inngest": "3.40.1",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.14.3",
- "@hono/zod-validator": "0.3.0",
+ "@hono/zod-validator": "0.7.6",
"dotenv": "^16.4.5",
"hono": "^4.11.7",
"pino": "9.4.0",
@@ -20,18 +20,19 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "4.7.0",
- "zod": "^3.25.32"
+ "zod": "^4.3.6"
},
"devDependencies": {
- "@types/node": "^20.16.0",
+ "@types/node": "^24.4.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
+ "rimraf": "6.1.3",
"tsx": "^4.16.2",
"typescript": "^5.8.3"
},
- "packageManager": "pnpm@9.12.0",
+ "packageManager": "pnpm@10.22.0",
"engines": {
- "node": "^20.16.0",
- "pnpm": ">=9.12.0"
+ "node": "^24.4.0",
+ "pnpm": ">=10.22.0"
}
}
diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc
deleted file mode 100644
index 593cb75bc..000000000
--- a/apps/dokploy/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-20.16.0
\ No newline at end of file
diff --git a/apps/dokploy/__test__/deploy/application.command.test.ts b/apps/dokploy/__test__/deploy/application.command.test.ts
index be29748eb..c81fab44c 100644
--- a/apps/dokploy/__test__/deploy/application.command.test.ts
+++ b/apps/dokploy/__test__/deploy/application.command.test.ts
@@ -28,6 +28,9 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
+ patch: {
+ findMany: vi.fn().mockResolvedValue([]),
+ },
},
},
};
diff --git a/apps/dokploy/__test__/deploy/application.real.test.ts b/apps/dokploy/__test__/deploy/application.real.test.ts
index 43ff07836..498281776 100644
--- a/apps/dokploy/__test__/deploy/application.real.test.ts
+++ b/apps/dokploy/__test__/deploy/application.real.test.ts
@@ -29,6 +29,9 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
+ patch: {
+ findMany: vi.fn().mockResolvedValue([]),
+ },
},
},
};
diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts
index 46be44883..d2e773dfc 100644
--- a/apps/dokploy/__test__/deploy/github.test.ts
+++ b/apps/dokploy/__test__/deploy/github.test.ts
@@ -83,6 +83,14 @@ describe("GitHub Webhook Skip CI", () => {
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
+
+ // Soft Serve
+ expect(
+ extractCommitMessage(
+ { "x-softserve-event": "push" },
+ { commits: [{ message: "[skip ci] test" }] },
+ ),
+ ).toBe("[skip ci] test");
});
it("should handle missing commit message", () => {
@@ -99,6 +107,9 @@ describe("GitHub Webhook Skip CI", () => {
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
"NEW COMMIT",
);
+ expect(extractCommitMessage({ "x-softserve-event": "push" }, {})).toBe(
+ "NEW COMMIT",
+ );
});
});
diff --git a/apps/dokploy/__test__/deploy/soft-serve.test.ts b/apps/dokploy/__test__/deploy/soft-serve.test.ts
new file mode 100644
index 000000000..609f15dee
--- /dev/null
+++ b/apps/dokploy/__test__/deploy/soft-serve.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, it } from "vitest";
+import {
+ extractBranchName,
+ extractCommitMessage,
+ extractHash,
+ getProviderByHeader,
+} from "@/pages/api/deploy/[refreshToken]";
+
+describe("Soft Serve Webhook", () => {
+ const mockSoftServeHeaders = {
+ "x-softserve-event": "push",
+ };
+
+ const createMockBody = (message: string, hash: string, branch: string) => ({
+ event: "push",
+ ref: `refs/heads/${branch}`,
+ after: hash,
+ commits: [{ message: message }],
+ });
+ const message: string = "feat: add new feature";
+ const hash: string = "3c91c24ef9560bddc695bce138bf8a7094ec3df5";
+ const branch: string = "feat/add-new";
+ const goodWebhook = createMockBody(message, hash, branch);
+
+ it("should properly extract the provider name", () => {
+ expect(getProviderByHeader(mockSoftServeHeaders)).toBe("soft-serve");
+ });
+
+ it("should properly extract the commit message", () => {
+ expect(extractCommitMessage(mockSoftServeHeaders, goodWebhook)).toBe(
+ message,
+ );
+ });
+
+ it("should properly extract hash", () => {
+ expect(extractHash(mockSoftServeHeaders, goodWebhook)).toBe(hash);
+ });
+
+ it("should properly extract branch name", () => {
+ expect(extractBranchName(mockSoftServeHeaders, goodWebhook)).toBe(branch);
+ });
+
+ it("should gracefully handle invalid webhook", () => {
+ expect(getProviderByHeader({})).toBeNull();
+ expect(extractCommitMessage(mockSoftServeHeaders, {})).toBe("NEW COMMIT");
+ expect(extractHash(mockSoftServeHeaders, {})).toBe("NEW COMMIT");
+ expect(extractBranchName(mockSoftServeHeaders, {})).toBeNull();
+ });
+});
diff --git a/apps/dokploy/__test__/drop/drop.test.ts b/apps/dokploy/__test__/drop/drop.test.ts
index dc795fd35..6e9940d6d 100644
--- a/apps/dokploy/__test__/drop/drop.test.ts
+++ b/apps/dokploy/__test__/drop/drop.test.ts
@@ -6,6 +6,7 @@ import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
+const OUTPUT_BASE = "./__test__/drop/zips/output";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
@@ -13,7 +14,10 @@ vi.mock("@dokploy/server/constants", async (importOriginal) => {
// @ts-ignore
...actual,
paths: () => ({
- APPLICATIONS_PATH: "./__test__/drop/zips/output",
+ // @ts-ignore
+ ...actual.paths(),
+ BASE_PATH: OUTPUT_BASE,
+ APPLICATIONS_PATH: OUTPUT_BASE,
}),
};
});
@@ -150,6 +154,176 @@ const baseApp: ApplicationNested = {
ulimitsSwarm: null,
};
+/**
+ * GHSA-66v7-g3fh-47h3: Remote Code Execution through Path Traversal.
+ * Validates the exact PoC: ZIP with path traversal entry ../../../../../etc/cron.d/malicious-cron
+ * plus cover files (package.json, index.js). unzipDrop must reject and never write outside output.
+ */
+describe("GHSA-66v7-g3fh-47h3 path traversal RCE", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("rejects PoC ZIP: traversal ../../../../../etc/cron.d/malicious-cron + package.json + index.js", async () => {
+ baseApp.appName = "ghsa-rce";
+ // PoC payload: same entry name as advisory (Python zipfile keeps it; AdmZip normalizes on add → use placeholder + replace)
+ const traversalEntry = "../../../../../etc/cron.d/malicious-cron";
+ const cronPayload = "* * * * * root id\n";
+ const placeholder = "x".repeat(traversalEntry.length);
+ const zip = new AdmZip();
+ zip.addFile(
+ "package.json",
+ Buffer.from('{"name": "app", "version": "1.0.0"}'),
+ );
+ zip.addFile("index.js", Buffer.from('console.log("Application");'));
+ zip.addFile(placeholder, Buffer.from(cronPayload));
+ let buf = Buffer.from(zip.toBuffer());
+ buf = Buffer.from(
+ buf.toString("binary").split(placeholder).join(traversalEntry),
+ "binary",
+ );
+ const file = new File([buf as unknown as ArrayBuffer], "exploit.zip");
+ await expect(unzipDrop(file, baseApp)).rejects.toThrow(
+ /Path traversal detected.*resolved path escapes output directory/,
+ );
+ });
+});
+
+describe("security: existing symlink escape", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("should NOT write outside base when directory is a symlink", async () => {
+ const appName = "symlink-existing";
+ const output = path.join(APPLICATIONS_PATH, appName, "code");
+ await fs.mkdir(output, { recursive: true });
+
+ // outside target (attacker wants to write here)
+ const outside = path.join(APPLICATIONS_PATH, "..", "outside");
+ await fs.mkdir(outside, { recursive: true });
+
+ // attacker-controlled symlink inside project
+ await fs.symlink(outside, path.join(output, "logs"));
+
+ // zip looks totally harmless
+ const zip = new AdmZip();
+ zip.addFile("logs/pwned.txt", Buffer.from("owned"));
+
+ const file = new File([zip.toBuffer() as any], "exploit.zip");
+
+ await unzipDrop(file, { ...baseApp, appName });
+
+ // if vulnerable -> file exists outside sandbox
+ const escaped = await fs
+ .readFile(path.join(outside, "pwned.txt"), "utf8")
+ .then(() => true)
+ .catch(() => false);
+
+ expect(escaped).toBe(false);
+ });
+});
+
+describe("security: zip symlink entry blocked", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("rejects zip containing real symlink entry", async () => {
+ const appName = "zip-symlink";
+
+ const zipBuffer = await fs.readFile(
+ path.join(__dirname, "./zips/payload/symlink-entry.zip"),
+ );
+
+ const file = new File([zipBuffer as any], "exploit.zip");
+
+ await expect(unzipDrop(file, { ...baseApp, appName })).rejects.toThrow(
+ /Dangerous node entries are not allowed/,
+ );
+ });
+});
+
+describe("unzipDrop path under output (no traversal)", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("allows entry etc/cron.d/malicious-cron when under output (no path traversal)", async () => {
+ baseApp.appName = "cron-under-output";
+ const zip = new AdmZip();
+ zip.addFile(
+ "etc/cron.d/malicious-cron",
+ Buffer.from("* * * * * root id\n"),
+ );
+ zip.addFile("package.json", Buffer.from('{"name":"app"}'));
+ const file = new File(
+ [zip.toBuffer() as unknown as ArrayBuffer],
+ "app.zip",
+ );
+ const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
+ await unzipDrop(file, baseApp);
+ const content = await fs.readFile(
+ path.join(outputPath, "etc/cron.d/malicious-cron"),
+ "utf8",
+ );
+ expect(content).toBe("* * * * * root id\n");
+ });
+});
+
+describe("security: traversal inside BASE_PATH (sandbox escape)", () => {
+ beforeAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ afterAll(async () => {
+ await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
+ });
+
+ it("should NOT allow writing outside application directory but inside BASE_PATH", async () => {
+ const appName = "sandbox-escape";
+
+ const base = APPLICATIONS_PATH.replace("/applications", "");
+ const output = path.join(APPLICATIONS_PATH, appName, "code");
+
+ await fs.mkdir(output, { recursive: true });
+
+ // attacker writes into traefik config inside base
+ const zip = new AdmZip();
+ zip.addFile(
+ "../../../traefik/dynamic/evil.yml",
+ Buffer.from("pwned: true"),
+ );
+
+ const file = new File([zip.toBuffer() as any], "exploit.zip");
+
+ await unzipDrop(file, { ...baseApp, appName });
+
+ const escapedPath = path.join(base, "traefik/dynamic/evil.yml");
+
+ const exists = await fs
+ .readFile(escapedPath)
+ .then(() => true)
+ .catch(() => false);
+
+ expect(exists).toBe(false);
+ });
+});
+
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {
@@ -166,14 +340,12 @@ describe("unzipDrop using real zip files", () => {
try {
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() as Buffer;
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true);
} catch (err) {
- console.log(err);
} finally {
}
});
diff --git a/apps/dokploy/__test__/drop/zips/payload/link b/apps/dokploy/__test__/drop/zips/payload/link
new file mode 120000
index 000000000..3594e94c0
--- /dev/null
+++ b/apps/dokploy/__test__/drop/zips/payload/link
@@ -0,0 +1 @@
+/etc/passwd
\ No newline at end of file
diff --git a/apps/dokploy/__test__/drop/zips/payload/symlink-entry.zip b/apps/dokploy/__test__/drop/zips/payload/symlink-entry.zip
new file mode 100644
index 000000000..b30279c6b
Binary files /dev/null and b/apps/dokploy/__test__/drop/zips/payload/symlink-entry.zip differ
diff --git a/apps/dokploy/__test__/wss/readValidDirectory.test.ts b/apps/dokploy/__test__/wss/readValidDirectory.test.ts
new file mode 100644
index 000000000..8107bb591
--- /dev/null
+++ b/apps/dokploy/__test__/wss/readValidDirectory.test.ts
@@ -0,0 +1,81 @@
+import path from "node:path";
+import { describe, expect, it, vi } from "vitest";
+
+const BASE = "/base";
+
+vi.mock("@dokploy/server/constants", async (importOriginal) => {
+ const actual =
+ await importOriginal();
+ return {
+ ...actual,
+ paths: () => ({
+ ...actual.paths(),
+ BASE_PATH: BASE,
+ LOGS_PATH: `${BASE}/logs`,
+ APPLICATIONS_PATH: `${BASE}/applications`,
+ }),
+ };
+});
+
+// Import after mock so paths() uses our BASE
+const { readValidDirectory } = await import("@dokploy/server");
+
+describe("readValidDirectory (path traversal)", () => {
+ it("returns true when directory is exactly BASE_PATH", () => {
+ expect(readValidDirectory(BASE)).toBe(true);
+ expect(readValidDirectory(path.resolve(BASE))).toBe(true);
+ });
+
+ it("returns true when directory is under BASE_PATH", () => {
+ expect(readValidDirectory(`${BASE}/logs`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/logs/app/foo.log`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/applications/myapp/code`)).toBe(true);
+ });
+
+ it("returns false for path traversal escaping base (absolute)", () => {
+ expect(readValidDirectory("/etc/passwd")).toBe(false);
+ expect(readValidDirectory("/etc/cron.d/malicious")).toBe(false);
+ expect(readValidDirectory("/tmp/outside")).toBe(false);
+ });
+
+ it("returns false when resolved path escapes base via ..", () => {
+ // Resolved: /etc/passwd (outside /base)
+ expect(readValidDirectory(`${BASE}/../etc/passwd`)).toBe(false);
+ expect(readValidDirectory(`${BASE}/logs/../../etc/passwd`)).toBe(false);
+ expect(readValidDirectory(`${BASE}/..`)).toBe(false);
+ });
+
+ it("returns true when .. stays within base", () => {
+ // e.g. /base/logs/../applications -> /base/applications (still under /base)
+ expect(readValidDirectory(`${BASE}/logs/../applications`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/foo/../bar`)).toBe(true);
+ });
+
+ it("accepts serverId for remote base path", () => {
+ // With our mock, serverId doesn't change BASE_PATH; just ensure it doesn't throw
+ expect(readValidDirectory(BASE, "server-1")).toBe(true);
+ expect(readValidDirectory("/etc/passwd", "server-1")).toBe(false);
+ });
+
+ it("returns false for null/undefined-like paths that resolve outside", () => {
+ // Paths that might resolve to cwd or root
+ expect(readValidDirectory(".")).toBe(false);
+ expect(readValidDirectory("..")).toBe(false);
+ });
+
+ it("returns true for BASE_PATH with trailing slash or double slashes under base", () => {
+ expect(readValidDirectory(`${BASE}/`)).toBe(true);
+ expect(readValidDirectory(`${BASE}//logs`)).toBe(true);
+ expect(readValidDirectory(`${BASE}/applications///myapp/code`)).toBe(true);
+ });
+
+ it("returns false when path looks like base but is a sibling or prefix", () => {
+ expect(readValidDirectory("/base-evil")).toBe(false);
+ expect(readValidDirectory("/bas")).toBe(false);
+ expect(readValidDirectory(`${BASE}/../base-evil`)).toBe(false);
+ });
+
+ it("returns false for empty string (resolves to cwd)", () => {
+ expect(readValidDirectory("")).toBe(false);
+ });
+});
diff --git a/apps/dokploy/__test__/wss/utils.test.ts b/apps/dokploy/__test__/wss/utils.test.ts
new file mode 100644
index 000000000..209bd5f86
--- /dev/null
+++ b/apps/dokploy/__test__/wss/utils.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect, it } from "vitest";
+import {
+ isValidContainerId,
+ isValidSearch,
+ isValidSince,
+ isValidTail,
+} from "../../server/wss/utils";
+
+describe("isValidTail (docker-container-logs)", () => {
+ it("accepts valid numeric tail values", () => {
+ expect(isValidTail("0")).toBe(true);
+ expect(isValidTail("1")).toBe(true);
+ expect(isValidTail("100")).toBe(true);
+ expect(isValidTail("10000")).toBe(true);
+ });
+
+ it("rejects tail above 10000", () => {
+ expect(isValidTail("10001")).toBe(false);
+ expect(isValidTail("99999")).toBe(false);
+ });
+
+ it("rejects non-numeric tail", () => {
+ expect(isValidTail("")).toBe(false);
+ expect(isValidTail("abc")).toBe(false);
+ expect(isValidTail("10a")).toBe(false);
+ expect(isValidTail("-1")).toBe(false);
+ });
+
+ it("rejects command injection payloads in tail", () => {
+ expect(isValidTail("10; whoami; #")).toBe(false);
+ expect(isValidTail("100 | cat /etc/passwd")).toBe(false);
+ expect(isValidTail("$(id)")).toBe(false);
+ expect(isValidTail("`id`")).toBe(false);
+ expect(isValidTail("100\nid")).toBe(false);
+ expect(isValidTail("100 && id")).toBe(false);
+ expect(isValidTail("100; env | grep DATABASE")).toBe(false);
+ });
+});
+
+describe("isValidSince (docker-container-logs)", () => {
+ it("accepts 'all'", () => {
+ expect(isValidSince("all")).toBe(true);
+ });
+
+ it("accepts valid duration format (number + s|m|h|d)", () => {
+ expect(isValidSince("5s")).toBe(true);
+ expect(isValidSince("10m")).toBe(true);
+ expect(isValidSince("1h")).toBe(true);
+ expect(isValidSince("2d")).toBe(true);
+ expect(isValidSince("0s")).toBe(true);
+ expect(isValidSince("999d")).toBe(true);
+ });
+
+ it("rejects invalid duration format", () => {
+ expect(isValidSince("")).toBe(false);
+ expect(isValidSince("5")).toBe(false);
+ expect(isValidSince("s")).toBe(false);
+ expect(isValidSince("5x")).toBe(false);
+ expect(isValidSince("5sec")).toBe(false);
+ expect(isValidSince("5 m")).toBe(false);
+ });
+
+ it("rejects command injection payloads in since", () => {
+ expect(isValidSince("5s; whoami")).toBe(false);
+ expect(isValidSince("all; id")).toBe(false);
+ expect(isValidSince("1m$(id)")).toBe(false);
+ expect(isValidSince("1m | cat /etc/passwd")).toBe(false);
+ });
+});
+
+describe("isValidSearch (docker-container-logs)", () => {
+ it("accepts empty string", () => {
+ expect(isValidSearch("")).toBe(true);
+ });
+
+ it("accepts only alphanumeric, space, dot, underscore, hyphen", () => {
+ expect(isValidSearch("error")).toBe(true);
+ expect(isValidSearch("foo bar")).toBe(true);
+ expect(isValidSearch("a-zA-Z0-9_.-")).toBe(true);
+ expect(isValidSearch("")).toBe(true);
+ });
+
+ it("rejects strings longer than 500 chars", () => {
+ expect(isValidSearch("a".repeat(501))).toBe(false);
+ expect(isValidSearch("a".repeat(500))).toBe(true);
+ });
+
+ it("rejects control characters and non-printable", () => {
+ expect(isValidSearch("foo\nbar")).toBe(false);
+ expect(isValidSearch("foo\rbar")).toBe(false);
+ expect(isValidSearch("\x00")).toBe(false);
+ expect(isValidSearch("a\x19b")).toBe(false);
+ });
+
+ it("rejects command injection vectors in search (search is concatenated into shell)", () => {
+ // Double-quoted context (SSH line 99): $ and ` execute
+ expect(isValidSearch("$(whoami)")).toBe(false);
+ expect(isValidSearch("`id`")).toBe(false);
+ expect(isValidSearch("$(id)")).toBe(false);
+ // Single-quoted context (local line 153): ' breaks out
+ expect(isValidSearch("'$(whoami)'")).toBe(false);
+ expect(isValidSearch("error'")).toBe(false);
+ expect(isValidSearch("'; whoami; #")).toBe(false);
+ // Other shell-metacharacters
+ expect(isValidSearch("error; id")).toBe(false);
+ expect(isValidSearch("a|b")).toBe(false);
+ expect(isValidSearch('error"')).toBe(false);
+ expect(isValidSearch("a&b")).toBe(false);
+ });
+});
+
+describe("isValidContainerId (docker-container-logs)", () => {
+ it("accepts valid hex container IDs", () => {
+ expect(isValidContainerId("a".repeat(12))).toBe(true);
+ expect(isValidContainerId("abc123def456")).toBe(true);
+ expect(isValidContainerId("a".repeat(64))).toBe(true);
+ });
+
+ it("accepts valid container names", () => {
+ expect(isValidContainerId("my-container")).toBe(true);
+ expect(isValidContainerId("app_1")).toBe(true);
+ expect(isValidContainerId("service.name")).toBe(true);
+ });
+
+ it("rejects command injection in container ID", () => {
+ expect(isValidContainerId("dummy; whoami")).toBe(false);
+ expect(isValidContainerId("$(id)")).toBe(false);
+ expect(isValidContainerId("`id`")).toBe(false);
+ expect(isValidContainerId("container|cat /etc/passwd")).toBe(false);
+ expect(isValidContainerId("x; env | grep DATABASE")).toBe(false);
+ });
+});
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx
index a3bc8079a..8de863957 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -73,7 +73,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -236,7 +236,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
)}
-
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx
index 7ee31e5b6..6d95634be 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/endpoint-spec-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx
index 1e0d032f0..f62037fca 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/health-check-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx
index d1681dcd0..41ce741ae 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/labels-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx
index 839f5d519..a6885a7e4 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/mode-form.tsx
@@ -105,7 +105,14 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
const modeData =
formData.type === "Replicated"
- ? { Replicated: { Replicas: formData.Replicas } }
+ ? {
+ Replicated: {
+ Replicas:
+ formData.Replicas !== undefined && formData.Replicas !== ""
+ ? Number(formData.Replicas)
+ : undefined,
+ },
+ }
: { Global: {} };
await mutateAsync({
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx
index f2c640cfe..7d6ebbaf3 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx
index 25a72b3c9..b4091aac0 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/placement-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx
index b7fb649be..db7be5629 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/restart-policy-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx
index d53215348..528b9d1cc 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/rollback-config-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx
index 4119c41f8..af2d826db 100644
--- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/update-config-form.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx
index a7c5f7288..602e6877d 100644
--- a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
@@ -50,7 +50,7 @@ export const AddCommand = ({ applicationId }: Props) => {
const utils = api.useUtils();
- const { mutateAsync, isLoading } = api.application.update.useMutation();
+ const { mutateAsync, isPending } = api.application.update.useMutation();
const form = useForm({
defaultValues: {
@@ -177,7 +177,7 @@ export const AddCommand = ({ applicationId }: Props) => {
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
index 17d033cf2..7b1614fda 100644
--- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Code2, Globe2, HardDrive } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -69,11 +69,11 @@ export const ShowImport = ({ composeId }: Props) => {
} | null>(null);
const utils = api.useUtils();
- const { mutateAsync: processTemplate, isLoading: isLoadingTemplate } =
+ const { mutateAsync: processTemplate, isPending: isLoadingTemplate } =
api.compose.processTemplate.useMutation();
const {
mutateAsync: importTemplate,
- isLoading: isImporting,
+ isPending: isImporting,
isSuccess: isImportSuccess,
} = api.compose.import.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx
index 568792461..91570d2db 100644
--- a/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
@@ -35,13 +35,9 @@ import { api } from "@/utils/api";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
- publishMode: z.enum(["ingress", "host"], {
- required_error: "Publish mode is required",
- }),
+ publishMode: z.enum(["ingress", "host"]),
targetPort: z.number().int().min(1).max(65535),
- protocol: z.enum(["tcp", "udp"], {
- required_error: "Protocol is required",
- }),
+ protocol: z.enum(["tcp", "udp"]),
});
type AddPort = z.infer;
@@ -68,7 +64,7 @@ export const HandlePorts = ({
enabled: !!portId,
},
);
- const { mutateAsync, isLoading, error, isError } = portId
+ const { mutateAsync, isPending, error, isError } = portId
? api.port.update.useMutation()
: api.port.create.useMutation();
@@ -270,7 +266,7 @@ export const HandlePorts = ({
diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx
index 816949f2b..4816d224d 100644
--- a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx
@@ -25,7 +25,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync: deletePort, isLoading: isRemoving } =
+ const { mutateAsync: deletePort, isPending: isRemoving } =
api.port.delete.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx
index c4d38ef18..172c042f1 100644
--- a/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -100,11 +100,11 @@ export const HandleRedirect = ({
const utils = api.useUtils();
- const { mutateAsync, isLoading, error, isError } = redirectId
+ const { mutateAsync, isPending, error, isError } = redirectId
? api.redirects.update.useMutation()
: api.redirects.create.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
permanent: false,
regex: "",
@@ -268,7 +268,7 @@ export const HandleRedirect = ({
diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx
index f1b14bfc0..a14074ec5 100644
--- a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx
@@ -24,7 +24,7 @@ export const ShowRedirects = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
+ const { mutateAsync: deleteRedirect, isPending: isRemoving } =
api.redirects.delete.useMutation();
const utils = api.useUtils();
diff --git a/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx
index c52976eb1..49a126881 100644
--- a/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -46,7 +46,7 @@ export const HandleSecurity = ({
}: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
- const { data } = api.security.one.useQuery(
+ const { data, refetch } = api.security.one.useQuery(
{
securityId: securityId ?? "",
},
@@ -55,7 +55,7 @@ export const HandleSecurity = ({
},
);
- const { mutateAsync, isLoading, error, isError } = securityId
+ const { mutateAsync, isPending, error, isError } = securityId
? api.security.update.useMutation()
: api.security.create.useMutation();
@@ -88,6 +88,7 @@ export const HandleSecurity = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
+ await refetch();
setIsOpen(false);
})
.catch(() => {
@@ -163,7 +164,7 @@ export const HandleSecurity = ({
diff --git a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx
index 5676e6f00..724953afe 100644
--- a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx
@@ -27,7 +27,7 @@ export const ShowSecurity = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
+ const { mutateAsync: deleteSecurity, isPending: isRemoving } =
api.security.delete.useMutation();
const utils = api.useUtils();
diff --git a/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx b/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx
index 545a5f705..eaeafde1a 100644
--- a/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/show-build-server.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -74,7 +74,7 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
const { data: buildServers } = api.server.buildServers.useQuery();
const { data: registries } = api.registry.all.useQuery();
- const { mutateAsync, isLoading } = api.application.update.useMutation();
+ const { mutateAsync, isPending } = api.application.update.useMutation();
const form = useForm({
defaultValues: {
@@ -274,7 +274,7 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
/>
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx
index 8978d346a..3b30155bf 100644
--- a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { InfoIcon, Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
@@ -128,11 +128,11 @@ export const ShowResources = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
cpuLimit: "",
cpuReservation: "",
@@ -452,6 +452,11 @@ export const ShowResources = ({ id, type }: Props) => {
min={-1}
placeholder="65535"
{...field}
+ value={
+ typeof field.value === "number"
+ ? field.value
+ : ""
+ }
onChange={(e) =>
field.onChange(Number(e.target.value))
}
@@ -475,6 +480,11 @@ export const ShowResources = ({ id, type }: Props) => {
min={-1}
placeholder="65535"
{...field}
+ value={
+ typeof field.value === "number"
+ ? field.value
+ : ""
+ }
onChange={(e) =>
field.onChange(Number(e.target.value))
}
@@ -507,7 +517,7 @@ export const ShowResources = ({ id, type }: Props) => {
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx
index ae23f1866..5d8943197 100644
--- a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx
@@ -15,7 +15,7 @@ interface Props {
}
export const ShowTraefikConfig = ({ applicationId }: Props) => {
- const { data, isLoading } = api.application.readTraefikConfig.useQuery(
+ const { data, isPending } = api.application.readTraefikConfig.useQuery(
{
applicationId,
},
@@ -35,7 +35,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx
index 21893802b..a8ec9053f 100644
--- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -69,7 +69,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.application.updateTraefikConfig.useMutation();
const form = useForm({
@@ -126,7 +126,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
}}
>
- Modify
+ Modify
@@ -198,7 +198,7 @@ routers:
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx
index 2bfd6bbc0..7c8dff068 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PlusIcon } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
index d3803c42a..92b259140 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx
@@ -37,7 +37,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
- const { mutateAsync: deleteVolume, isLoading: isRemoving } =
+ const { mutateAsync: deleteVolume, isPending: isRemoving } =
api.mounts.remove.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx
index 44fb050bc..9f31cc694 100644
--- a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -93,7 +93,7 @@ export const UpdateVolume = ({
},
);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.mounts.update.useMutation();
const form = useForm({
@@ -187,7 +187,7 @@ export const UpdateVolume = ({
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
- isLoading={isLoading}
+ isLoading={isPending}
>
@@ -310,7 +310,7 @@ PORT=3000
diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx
index 7f92157f2..32aee23d3 100644
--- a/apps/dokploy/components/dashboard/application/build/show.tsx
+++ b/apps/dokploy/components/dashboard/application/build/show.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Cog } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -74,12 +74,7 @@ const buildTypeDisplayMap: Record = {
const mySchema = z.discriminatedUnion("buildType", [
z.object({
buildType: z.literal(BuildType.dockerfile),
- dockerfile: z
- .string({
- required_error: "Dockerfile path is required",
- invalid_type_error: "Dockerfile path is required",
- })
- .min(1, "Dockerfile required"),
+ dockerfile: z.string().nullable().default(""),
dockerContextPath: z.string().nullable().default(""),
dockerBuildStage: z.string().nullable().default(""),
}),
@@ -168,14 +163,14 @@ const resetData = (data: ApplicationData): AddTemplate => {
};
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveBuildType.useMutation();
const { data, refetch } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildType: BuildType.nixpacks,
},
@@ -347,7 +342,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
Docker File
@@ -533,7 +528,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
>
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx
index e957a496c..ed1373aa0 100644
--- a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx
@@ -1,4 +1,4 @@
-import { Paintbrush } from "lucide-react";
+import { Ban } from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
@@ -20,7 +20,7 @@ interface Props {
}
export const CancelQueues = ({ id, type }: Props) => {
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
type === "application"
? api.application.cleanQueues.useMutation()
: api.compose.cleanQueues.useMutation();
@@ -33,9 +33,9 @@ export const CancelQueues = ({ id, type }: Props) => {
return (
-
+
Cancel Queues
-
+
diff --git a/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx
new file mode 100644
index 000000000..81f998a9d
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx
@@ -0,0 +1,73 @@
+import { Paintbrush } from "lucide-react";
+import { toast } from "sonner";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { Button } from "@/components/ui/button";
+import { api } from "@/utils/api";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+}
+
+export const ClearDeployments = ({ id, type }: Props) => {
+ const utils = api.useUtils();
+ const { mutateAsync, isPending } =
+ type === "application"
+ ? api.application.clearDeployments.useMutation()
+ : api.compose.clearDeployments.useMutation();
+
+ return (
+
+
+
+ Clear deployments
+
+
+
+
+
+
+ Are you sure you want to clear old deployments?
+
+
+ This will delete all old deployment records and logs, keeping only
+ the active deployment (the most recent successful one).
+
+
+
+ Cancel
+ {
+ await mutateAsync({
+ applicationId: id || "",
+ composeId: id || "",
+ })
+ .then(async () => {
+ toast.success("Old deployments cleared successfully");
+ await utils.deployment.allByType.invalidate({
+ id,
+ type: type as "application" | "compose",
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ }}
+ >
+ Confirm
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
index 784534dd6..ad5e9b058 100644
--- a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
@@ -20,7 +20,7 @@ interface Props {
}
export const KillBuild = ({ id, type }: Props) => {
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
type === "application"
? api.application.killBuild.useMutation()
: api.compose.killBuild.useMutation();
@@ -28,7 +28,7 @@ export const KillBuild = ({ id, type }: Props) => {
return (
-
+
Kill Build
diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
index cfe747d27..61841e294 100644
--- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
+++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx
@@ -6,6 +6,7 @@ import {
RefreshCcw,
RocketIcon,
Settings,
+ Trash2,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
@@ -25,6 +26,7 @@ import {
import { api, type RouterOutputs } from "@/utils/api";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues";
+import { ClearDeployments } from "./clear-deployments";
import { KillBuild } from "./kill-build";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
@@ -59,7 +61,7 @@ export const ShowDeployments = ({
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
- const { data: deployments, isLoading: isLoadingDeployments } =
+ const { data: deployments, isPending: isLoadingDeployments } =
api.deployment.allByType.useQuery(
{
id,
@@ -73,19 +75,21 @@ export const ShowDeployments = ({
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { mutateAsync: rollback, isLoading: isRollingBack } =
+ const { mutateAsync: rollback, isPending: isRollingBack } =
api.rollback.rollback.useMutation();
- const { mutateAsync: killProcess, isLoading: isKillingProcess } =
+ const { mutateAsync: killProcess, isPending: isKillingProcess } =
api.deployment.killProcess.useMutation();
+ const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
+ api.deployment.removeDeployment.useMutation();
// Cancel deployment mutations
const {
mutateAsync: cancelApplicationDeployment,
- isLoading: isCancellingApp,
+ isPending: isCancellingApp,
} = api.application.cancelDeployment.useMutation();
const {
mutateAsync: cancelComposeDeployment,
- isLoading: isCancellingCompose,
+ isPending: isCancellingCompose,
} = api.compose.cancelDeployment.useMutation();
const [url, setUrl] = React.useState("");
@@ -144,6 +148,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && (
+
+ )}
{(type === "application" || type === "compose") && (
)}
@@ -252,6 +259,8 @@ export const ShowDeployments = ({
const isExpanded = expandedDescriptions.has(
deployment.deploymentId,
);
+ const canDelete =
+ deployment.status === "done" || deployment.status === "error";
return (
+ {canDelete && (
+
{
+ try {
+ await removeDeployment({
+ deploymentId: deployment.deploymentId,
+ });
+ toast.success("Deployment deleted successfully");
+ } catch (error) {
+ toast.error("Error deleting deployment");
+ }
+ }}
+ >
+
+ Delete
+
+
+
+ )}
+
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
index 6af0e1e8c..00eb62272 100644
--- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -159,11 +159,11 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
},
);
- const { mutateAsync, isError, error, isLoading } = domainId
+ const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
- const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
+ const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { data: canGenerateTraefikMeDomains } =
@@ -240,7 +240,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
domainType: type,
});
}
- }, [form, data, isLoading, domainId]);
+ }, [form, data, isPending, domainId]);
// Separate effect for handling custom cert resolver validation
useEffect(() => {
@@ -730,7 +730,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
-
+
{dictionary.submit}
diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
index 1fd3d82e9..c207ba59c 100644
--- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
@@ -97,7 +97,7 @@ export const ShowDomains = ({ id, type }: Props) => {
const { mutateAsync: validateDomain } =
api.domain.validateDomain.useMutation();
- const { mutateAsync: deleteDomain, isLoading: isRemoving } =
+ const { mutateAsync: deleteDomain, isPending: isRemoving } =
api.domain.delete.useMutation();
const handleValidateDomain = async (host: string) => {
diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
index 797a317a8..8ff0f6a63 100644
--- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
+++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -60,7 +60,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
compose: () => api.compose.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -111,7 +111,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -121,7 +121,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -196,7 +196,7 @@ PORT=3000
)}
{
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveEnvironment.useMutation();
const { data, refetch } = api.application.one.useQuery(
@@ -104,7 +104,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -114,7 +114,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -214,7 +214,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
)}
{
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingBitbucketProvider } =
+ const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.application.saveBitbucketProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -333,7 +333,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -350,7 +350,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx
index fcdcf0a93..078271bca 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx
index 00e18c2ab..583b865c5 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { TrashIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -24,10 +24,10 @@ interface Props {
export const SaveDragNDrop = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.dropDeployment.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(uploadFileSchema),
});
@@ -129,8 +129,8 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
Deploy{" "}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx
index e9be3a2f5..624adeb55 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,10 +58,10 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveGitProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
branch: "",
buildPath: "/",
@@ -317,7 +317,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx
index 3f7943252..02cae2c4a 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -88,10 +88,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGiteaProvider } =
+ const { mutateAsync, isPending: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
- const form = useForm
({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -353,7 +353,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -371,7 +371,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -463,7 +463,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
{
- const newPaths = [...field.value];
+ const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
@@ -481,7 +481,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
- field.onChange([...field.value, path]);
+ field.onChange([...(field.value || []), path]);
input.value = "";
}
}
@@ -498,7 +498,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
- field.onChange([...field.value, path]);
+ field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
index 1fa42b9c0..69a25b940 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGithubProvider } =
+ const { mutateAsync, isPending: isSavingGithubProvider } =
api.application.saveGithubProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
- const { data: repositories, isLoading: isLoadingRepositories } =
+ const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -320,7 +320,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -337,7 +337,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx
index f5ba24e4c..e829a56ff 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGitlabProvider } =
+ const { mutateAsync, isPending: isSavingGitlabProvider } =
api.application.saveGitlabProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -351,7 +351,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -368,7 +368,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx
index a60db800c..9a49b204e 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx
@@ -36,13 +36,13 @@ interface Props {
}
export const ShowProviderForm = ({ applicationId }: Props) => {
- const { data: githubProviders, isLoading: isLoadingGithub } =
+ const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
- const { data: gitlabProviders, isLoading: isLoadingGitlab } =
+ const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
- const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
+ const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
- const { data: giteaProviders, isLoading: isLoadingGitea } =
+ const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { data: application, refetch } = api.application.one.useQuery({
diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx
index 5387659ad..ee42caa5e 100644
--- a/apps/dokploy/components/dashboard/application/general/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/show.tsx
@@ -37,14 +37,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: update } = api.application.update.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.application.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.application.stop.useMutation();
const { mutateAsync: deploy } = api.application.deploy.useMutation();
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.application.reload.useMutation();
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx
index 941ddef50..cbb6bce09 100644
--- a/apps/dokploy/components/dashboard/application/logs/show.tsx
+++ b/apps/dokploy/components/dashboard/application/logs/show.tsx
@@ -56,7 +56,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const [containerId, setContainerId] = useState();
const [option, setOption] = useState<"swarm" | "native">("native");
- const { data: services, isLoading: servicesLoading } =
+ const { data: services, isPending: servicesLoading } =
api.docker.getServiceContainersByAppName.useQuery(
{
appName,
@@ -67,7 +67,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
},
);
- const { data: containers, isLoading: containersLoading } =
+ const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
diff --git a/apps/dokploy/components/dashboard/application/patches/create-file-dialog.tsx b/apps/dokploy/components/dashboard/application/patches/create-file-dialog.tsx
new file mode 100644
index 000000000..5f6f88e36
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/create-file-dialog.tsx
@@ -0,0 +1,107 @@
+import { FilePlus } from "lucide-react";
+import { useState } from "react";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+interface Props {
+ folderPath: string;
+ onCreate: (filename: string, content: string) => void;
+ onOpenChange: (open: boolean) => void;
+ alwaysVisible?: boolean;
+}
+
+export const CreateFileDialog = ({
+ folderPath,
+ onCreate,
+ onOpenChange,
+ alwaysVisible = false,
+}: Props) => {
+ const [filename, setFilename] = useState("");
+ const [content, setContent] = useState("");
+
+ const handleCreate = () => {
+ if (!filename.trim()) return;
+ onCreate(filename.trim(), content);
+ setFilename("");
+ setContent("");
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx b/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx
new file mode 100644
index 000000000..8c5a42836
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx
@@ -0,0 +1,102 @@
+import { Loader2, Pencil } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { api } from "@/utils/api";
+
+interface Props {
+ patchId: string;
+ entityId: string;
+ type: "application" | "compose";
+ onSuccess?: () => void;
+}
+
+export const EditPatchDialog = ({
+ patchId,
+ entityId,
+ type,
+ onSuccess,
+}: Props) => {
+ const { data: patch, isPending: isPatchLoading } = api.patch.one.useQuery(
+ { patchId },
+ { enabled: !!patchId },
+ );
+ const [content, setContent] = useState("");
+
+ useEffect(() => {
+ if (patch) {
+ setContent(patch.content);
+ }
+ }, [patch]);
+
+ const utils = api.useUtils();
+ const updatePatch = api.patch.update.useMutation();
+
+ const handleSave = () => {
+ updatePatch
+ .mutateAsync({ patchId, content })
+ .then(() => {
+ toast.success("Patch saved");
+ utils.patch.byEntityId.invalidate({ id: entityId, type });
+ onSuccess?.();
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/index.ts b/apps/dokploy/components/dashboard/application/patches/index.ts
new file mode 100644
index 000000000..1854bd3e5
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/index.ts
@@ -0,0 +1,2 @@
+export * from "./show-patches";
+export * from "./patch-editor";
diff --git a/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx b/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx
new file mode 100644
index 000000000..4b212b004
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx
@@ -0,0 +1,368 @@
+import {
+ ArrowLeft,
+ ChevronRight,
+ File,
+ Folder,
+ Loader2,
+ Save,
+ Trash2,
+} from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
+import { toast } from "sonner";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { api } from "@/utils/api";
+import { CreateFileDialog } from "./create-file-dialog";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+ repoPath: string;
+ onClose: () => void;
+}
+
+type DirectoryEntry = {
+ name: string;
+ path: string;
+ type: "file" | "directory";
+ children?: DirectoryEntry[];
+};
+
+export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [fileContent, setFileContent] = useState("");
+ const [createFolderPath, setCreateFolderPath] = useState(null);
+ const [expandedFolders, setExpandedFolders] = useState>(
+ new Set(),
+ );
+
+ const utils = api.useUtils();
+ const { data: directories, isPending: isDirLoading } =
+ api.patch.readRepoDirectories.useQuery(
+ { id: id, type, repoPath },
+ { enabled: !!repoPath },
+ );
+
+ const { data: patches } = api.patch.byEntityId.useQuery(
+ { id, type },
+ { enabled: !!id },
+ );
+
+ const { mutateAsync: saveAsPatch, isPending: isSavingPatch } =
+ api.patch.saveFileAsPatch.useMutation();
+
+ const { mutateAsync: markForDeletion, isPending: isMarkingDeletion } =
+ api.patch.markFileForDeletion.useMutation();
+
+ const updatePatch = api.patch.update.useMutation();
+
+ const { data: fileData, isFetching: isFileLoading } =
+ api.patch.readRepoFile.useQuery(
+ {
+ id,
+ type,
+ filePath: selectedFile || "",
+ },
+ {
+ enabled: !!selectedFile,
+ },
+ );
+
+ useEffect(() => {
+ if (fileData !== undefined) {
+ setFileContent(fileData);
+ }
+ }, [fileData]);
+
+ const handleFileSelect = (filePath: string) => {
+ setSelectedFile(filePath);
+ };
+
+ const toggleFolder = (path: string) => {
+ setExpandedFolders((prev) => {
+ const next = new Set(prev);
+ if (next.has(path)) {
+ next.delete(path);
+ } else {
+ next.add(path);
+ }
+ return next;
+ });
+ };
+
+ const handleSave = () => {
+ if (!selectedFile) return;
+ saveAsPatch({
+ id,
+ type,
+ filePath: selectedFile,
+ content: fileContent,
+ patchType: "update",
+ })
+ .then(() => {
+ toast.success("Patch saved");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to save patch");
+ });
+ };
+
+ const handleMarkForDeletion = () => {
+ if (!selectedFile) return;
+ markForDeletion({ id, type, filePath: selectedFile })
+ .then(() => {
+ toast.success("File marked for deletion");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to mark file for deletion");
+ });
+ };
+
+ const handleCreateFile = useCallback(
+ (folderPath: string, filename: string, content: string) => {
+ const filePath = folderPath ? `${folderPath}/${filename}` : filename;
+ saveAsPatch({
+ id,
+ type,
+ filePath,
+ content,
+ patchType: "create",
+ })
+ .then(() => {
+ toast.success("File created");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to create file");
+ });
+ },
+ [id, type, saveAsPatch, utils],
+ );
+
+ const selectedFilePatch = patches?.find(
+ (p) => p.filePath === selectedFile && p.type === "delete",
+ );
+
+ const handleUnmarkDeletion = () => {
+ if (!selectedFilePatch) return;
+ updatePatch
+ .mutateAsync({
+ patchId: selectedFilePatch.patchId,
+ type: "update",
+ content: fileData || "",
+ })
+ .then(() => {
+ toast.success("Deletion unmarked");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to unmark deletion");
+ });
+ };
+
+ const hasChanges = fileData !== undefined && fileContent !== fileData;
+
+ const renderTree = useCallback(
+ (entries: DirectoryEntry[], depth = 0) => {
+ return entries
+ .sort((a, b) => {
+ // Directories first, then alphabetically
+ if (a.type !== b.type) {
+ return a.type === "directory" ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ })
+ .map((entry) => {
+ const isExpanded = expandedFolders.has(entry.path);
+ const isSelected = selectedFile === entry.path;
+
+ if (entry.type === "directory") {
+ return (
+
+
+ toggleFolder(entry.path)}
+ className={
+ "flex-1 flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors text-left min-w-0"
+ }
+ style={{ paddingLeft: `${depth * 12 + 8}px` }}
+ >
+
+
+ {entry.name}
+
+
+ handleCreateFile(entry.path, filename, content)
+ }
+ onOpenChange={(open) =>
+ setCreateFolderPath(open ? entry.path : null)
+ }
+ />
+
+ {isExpanded && entry.children && (
+
{renderTree(entry.children, depth + 1)}
+ )}
+
+ );
+ }
+
+ const isMarkedForDeletion = patches?.some(
+ (p) => p.filePath === entry.path && p.type === "delete",
+ );
+
+ return (
+ handleFileSelect(entry.path)}
+ className={`w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors ${
+ isSelected ? "bg-muted" : ""
+ } ${isMarkedForDeletion ? "text-destructive" : ""}`}
+ style={{ paddingLeft: `${depth * 12 + 28}px` }}
+ >
+
+ {entry.name}
+ {isMarkedForDeletion && (
+
+ )}
+
+ );
+ });
+ },
+ [expandedFolders, selectedFile, patches, handleCreateFile],
+ );
+
+ return (
+
+
+
+
+
+
+
+ Edit File
+
+ {selectedFile
+ ? `Editing: ${selectedFile}`
+ : "Select a file from the tree to edit"}
+
+
+
+ {selectedFile && (
+
+ {selectedFilePatch ? (
+
+ {updatePatch.isPending && (
+
+ )}
+ Unmark deletion
+
+ ) : (
+ <>
+
+ {isMarkingDeletion && (
+
+ )}
+
+ Mark for deletion
+
+
+ {isSavingPatch && (
+
+ )}
+
+ Save Patch
+
+ >
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ handleCreateFile("", filename, content)
+ }
+ onOpenChange={(open) =>
+ setCreateFolderPath(open ? "" : null)
+ }
+ />
+
+ New file in root
+
+
+ {isDirLoading ? (
+
+
+
+ ) : directories ? (
+ renderTree(directories)
+ ) : (
+
+ No files found
+
+ )}
+
+
+
+
+ {isFileLoading ? (
+
+
+
+ ) : selectedFile ? (
+
setFileContent(value || "")}
+ className="h-full w-full"
+ wrapperClassName="h-full"
+ lineWrapping
+ />
+ ) : (
+
+ Select a file to edit
+
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/show-patches.tsx b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx
new file mode 100644
index 000000000..e471b3fc1
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx
@@ -0,0 +1,225 @@
+import { File, FilePlus2, Loader2, Trash2 } from "lucide-react";
+import { useState } from "react";
+import { toast } from "sonner";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Switch } from "@/components/ui/switch";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { api } from "@/utils/api";
+import { EditPatchDialog } from "./edit-patch-dialog";
+import { PatchEditor } from "./patch-editor";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+}
+
+export const ShowPatches = ({ id, type }: Props) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [repoPath, setRepoPath] = useState(null);
+ const [isLoadingRepo, setIsLoadingRepo] = useState(false);
+
+ const utils = api.useUtils();
+
+ const { data: patches, isPending: isPatchesLoading } =
+ api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id });
+
+ const mutationMap = {
+ application: () => api.patch.delete.useMutation(),
+ compose: () => api.patch.delete.useMutation(),
+ };
+
+ const ensureRepo = api.patch.ensureRepo.useMutation();
+
+ const togglePatch = api.patch.toggleEnabled.useMutation();
+
+ const { mutateAsync } = mutationMap[type]
+ ? mutationMap[type]()
+ : api.patch.delete.useMutation();
+
+ const handleCloseEditor = () => {
+ setSelectedFile(null);
+ setRepoPath(null);
+ };
+
+ if (repoPath) {
+ return (
+
+ );
+ }
+
+ const handleOpenEditor = async () => {
+ setIsLoadingRepo(true);
+ await ensureRepo
+ .mutateAsync({ id, type })
+ .then((result) => {
+ setRepoPath(result);
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ })
+ .finally(() => {
+ setIsLoadingRepo(false);
+ });
+ };
+
+ return (
+
+
+
+ Patches
+
+ Apply code patches to your repository during build. Patches are
+ applied after cloning the repository and before building.
+
+
+ {patches && patches?.length > 0 && (
+
+ {isLoadingRepo && }
+
+ Create Patch
+
+ )}
+
+
+ {isPatchesLoading ? (
+
+
+
+ ) : patches?.length === 0 ? (
+
+
+
+
+
+
No patches yet
+
+ Add file patches to modify your repo before each build—configs,
+ env, or code. Create your first patch to get started.
+
+
+
+ {isLoadingRepo && (
+
+ )}
+
+ Create Patch
+
+
+ ) : (
+
+
+
+ File Path
+ Type
+ Enabled
+ Actions
+
+
+
+ {patches?.map((patch) => (
+
+
+
+
+ {patch.filePath}
+
+
+
+
+ {patch.type}
+
+
+
+ {
+ togglePatch
+ .mutateAsync({
+ patchId: patch.patchId,
+ enabled: checked,
+ })
+ .then(() => {
+ toast.success("Patch updated");
+ utils.patch.byEntityId.invalidate({
+ id,
+ type,
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ })
+ .finally(() => {
+ setIsLoadingRepo(false);
+ });
+ }}
+ />
+
+
+
+ {(patch.type === "update" || patch.type === "create") && (
+
+ )}
+ {
+ mutateAsync({ patchId: patch.patchId })
+ .then(() => {
+ toast.success("Patch deleted");
+ utils.patch.byEntityId.invalidate({
+ id,
+ type,
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ }}
+ title="Delete patch"
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx
index bb9321a51..72815fd8f 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Dices } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -75,11 +75,11 @@ export const AddPreviewDomain = ({
},
);
- const { mutateAsync, isError, error, isLoading } = domainId
+ const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
- const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
+ const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm({
@@ -103,7 +103,7 @@ export const AddPreviewDomain = ({
if (!domainId) {
form.reset({});
}
- }, [form, form.reset, data, isLoading]);
+ }, [form, form.reset, data, isPending]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
@@ -301,7 +301,7 @@ export const AddPreviewDomain = ({
-
+
{dictionary.submit}
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
index 6cf8d8830..e12400a7c 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
@@ -43,7 +43,7 @@ interface Props {
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery({ applicationId });
- const { mutateAsync: deletePreviewDeployment, isLoading } =
+ const { mutateAsync: deletePreviewDeployment, isPending } =
api.previewDeployment.delete.useMutation();
const { mutateAsync: redeployPreviewDeployment } =
@@ -57,8 +57,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
- refetchInterval: (data) =>
- data?.some((d) => d.previewStatus === "running") ? 2000 : false,
+ refetchInterval: 2000,
},
);
@@ -282,7 +281,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx
index f8e6fab68..d2840cd67 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -80,7 +80,7 @@ interface Props {
export const ShowPreviewSettings = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
- const { mutateAsync: updateApplication, isLoading } =
+ const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data, refetch } = api.application.one.useQuery({ applicationId });
@@ -535,7 +535,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
Cancel
diff --git a/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx b/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
index a06cf5697..b119aa778 100644
--- a/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
+++ b/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -71,7 +71,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
},
);
- const { mutateAsync: updateApplication, isLoading } =
+ const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data: registries } = api.registry.all.useQuery();
@@ -212,7 +212,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
/>
)}
-
+
Save Settings
diff --git a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
index e85b1b004..36ddb53f1 100644
--- a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
CheckIcon,
ChevronsUpDown,
@@ -220,8 +220,8 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState("cache");
const utils = api.useUtils();
- const form = useForm>({
- resolver: zodResolver(formSchema),
+ const form = useForm({
+ resolver: standardSchemaResolver(formSchema),
defaultValues: {
name: "",
cronExpression: "",
@@ -275,11 +275,11 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
}
}, [form, schedule, scheduleId]);
- const { mutateAsync, isLoading } = scheduleId
+ const { mutateAsync, isPending } = scheduleId
? api.schedule.update.useMutation()
: api.schedule.create.useMutation();
- const onSubmit = async (values: z.infer) => {
+ const onSubmit = async (values: z.output) => {
if (!id && !scheduleId) return;
await mutateAsync({
@@ -662,7 +662,7 @@ echo "Hello, world!"
)}
/>
-
+
{scheduleId ? "Update" : "Create"} Schedule
diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
index 26bfa9421..a9550fda2 100644
--- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
@@ -51,7 +51,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
},
);
const utils = api.useUtils();
- const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
+ const { mutateAsync: deleteSchedule, isPending: isDeleting } =
api.schedule.delete.useMutation();
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/update-application.tsx b/apps/dokploy/components/dashboard/application/update-application.tsx
index 754074d75..98c49a999 100644
--- a/apps/dokploy/components/dashboard/application/update-application.tsx
+++ b/apps/dokploy/components/dashboard/application/update-application.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.application.update.useMutation();
const { data } = api.application.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
/>
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx
index e179713de..d0df60098 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -116,7 +116,7 @@ export const HandleVolumeBackups = ({
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
const utils = api.useUtils();
- const form = useForm>({
+ const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
@@ -195,7 +195,7 @@ export const HandleVolumeBackups = ({
}
}, [form, volumeBackup, volumeBackupId]);
- const { mutateAsync, isLoading } = volumeBackupId
+ const { mutateAsync, isPending } = volumeBackupId
? api.volumeBackups.update.useMutation()
: api.volumeBackups.create.useMutation();
@@ -207,7 +207,7 @@ export const HandleVolumeBackups = ({
await mutateAsync({
...values,
- keepLatestCount: preparedKeepLatestCount,
+ keepLatestCount: preparedKeepLatestCount ?? undefined,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
@@ -630,7 +630,7 @@ export const HandleVolumeBackups = ({
)}
/>
-
+
{volumeBackupId ? "Update" : "Create"} Volume Backup
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
index 6eda33648..684620947 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
-import { debounce } from "lodash";
+import debounce from "lodash/debounce";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -53,27 +53,15 @@ interface Props {
}
const RestoreBackupSchema = z.object({
- destinationId: z
- .string({
- required_error: "Please select a destination",
- })
- .min(1, {
- message: "Destination is required",
- }),
- backupFile: z
- .string({
- required_error: "Please select a backup file",
- })
- .min(1, {
- message: "Backup file is required",
- }),
- volumeName: z
- .string({
- required_error: "Please enter a volume name",
- })
- .min(1, {
- message: "Volume name is required",
- }),
+ destinationId: z.string().min(1, {
+ message: "Destination is required",
+ }),
+ backupFile: z.string().min(1, {
+ message: "Backup file is required",
+ }),
+ volumeName: z.string().min(1, {
+ message: "Volume name is required",
+ }),
});
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
@@ -83,7 +71,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const { data: destinations = [] } = api.destination.all.useQuery();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
destinationId: "",
backupFile: "",
@@ -105,7 +93,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
debouncedSetSearch(value);
};
- const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
+ const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destinationId,
search: debouncedSearchTerm,
@@ -294,7 +282,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
onValueChange={handleSearchChange}
className="h-9"
/>
- {isLoading ? (
+ {isPending ? (
Loading backup files...
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
index 2e4dac472..526bcfa77 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
@@ -54,7 +54,7 @@ export const ShowVolumeBackups = ({
},
);
const utils = api.useUtils();
- const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
+ const { mutateAsync: deleteVolumeBackup, isPending: isDeleting } =
api.volumeBackups.delete.useMutation();
const { mutateAsync: runManually } =
api.volumeBackups.runManually.useMutation();
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
index 52eb18907..c5f9334ec 100644
--- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
+++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -52,7 +52,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
const utils = api.useUtils();
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
const form = useForm({
defaultValues: {
@@ -128,7 +128,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
/>
-
+
Save
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
index 5b6e04154..0fad7d20e 100644
--- a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
+++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx
index 5c8577dff..9d417ee91 100644
--- a/apps/dokploy/components/dashboard/compose/delete-service.tsx
+++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx
@@ -1,5 +1,5 @@
import type { ServiceType } from "@dokploy/server/db/schema";
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
@@ -74,7 +74,7 @@ export const DeleteService = ({ id, type }: Props) => {
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
@@ -130,7 +130,7 @@ export const DeleteService = ({ id, type }: Props) => {
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
- isLoading={isLoading}
+ isLoading={isPending}
>
@@ -228,7 +228,7 @@ export const DeleteService = ({ id, type }: Props) => {
{
const { mutateAsync: update } = api.compose.update.useMutation();
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.compose.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.compose.stop.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
index cb727e2a9..8193ec8b6 100644
--- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -34,7 +34,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
{ enabled: !!composeId },
);
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm
({
@@ -93,7 +93,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -103,7 +103,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
<>
@@ -167,7 +167,7 @@ services:
Save
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
index 7622906df..3e099251e 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -74,10 +74,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingBitbucketProvider } =
+ const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -335,7 +335,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -352,7 +352,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
index d8c9d4d8f..4ad4f741c 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,9 +58,9 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
branch: "",
repositoryURL: "",
@@ -318,7 +318,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
-
+
Save{" "}
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
index 5e546d050..39f025438 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ interface Props {
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGiteaProvider } =
+ const { mutateAsync, isPending: isSavingGiteaProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -331,7 +331,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
index b52fa2097..827ce1a8a 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGithubProvider } =
+ const { mutateAsync, isPending: isSavingGithubProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository");
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
- const { data: repositories, isLoading: isLoadingRepositories } =
+ const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -321,7 +321,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -338,7 +338,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
index 9f9babb3e..63de87d8f 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGitlabProvider } =
+ const { mutateAsync, isPending: isSavingGitlabProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -353,7 +353,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -370,7 +370,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
index 798f72249..759fe728c 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
@@ -27,13 +27,13 @@ interface Props {
}
export const ShowProviderFormCompose = ({ composeId }: Props) => {
- const { data: githubProviders, isLoading: isLoadingGithub } =
+ const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
- const { data: gitlabProviders, isLoading: isLoadingGitlab } =
+ const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
- const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
+ const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
- const { data: giteaProviders, isLoading: isLoadingGitea } =
+ const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { mutateAsync: disconnectGitProvider } =
diff --git a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
index 2c488aefe..99c749c26 100644
--- a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
index fac6c2a34..211f5f5c7 100644
--- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
@@ -32,7 +32,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
},
);
- const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
+ const { mutateAsync, isPending } = api.compose.fetchSourceType.useMutation();
useEffect(() => {
if (isOpen) {
@@ -66,7 +66,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
- {isLoading ? (
+ {isPending ? (
@@ -82,7 +82,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
{
mutateAsync({ composeId })
.then(() => {
diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
index 16dd1f246..159ab3485 100644
--- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
+++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
@@ -41,7 +41,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState();
- const { data: services, isLoading: servicesLoading } =
+ const { data: services, isPending: servicesLoading } =
api.docker.getStackContainersByAppName.useQuery(
{
appName,
@@ -52,7 +52,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
},
);
- const { data: containers, isLoading: containersLoading } =
+ const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx
index 20c70e739..bc47f1b6e 100644
--- a/apps/dokploy/components/dashboard/compose/logs/show.tsx
+++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx
@@ -42,7 +42,7 @@ export const ShowDockerLogsCompose = ({
appType,
serverId,
}: Props) => {
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
@@ -73,7 +73,7 @@ export const ShowDockerLogsCompose = ({