-
-
## Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc
index 43bff1f8..593cb75b 100644
--- a/apps/dokploy/.nvmrc
+++ b/apps/dokploy/.nvmrc
@@ -1 +1 @@
-20.9.0
\ No newline at end of file
+20.16.0
\ No newline at end of file
diff --git a/apps/dokploy/Dockerfile b/apps/dokploy/Dockerfile
deleted file mode 100644
index 0537b03e..00000000
--- a/apps/dokploy/Dockerfile
+++ /dev/null
@@ -1,26 +0,0 @@
-FROM node:18-slim AS base
-ENV PNPM_HOME="/pnpm"
-ENV PATH="$PNPM_HOME:$PATH"
-RUN corepack enable
-
-FROM base AS build
-COPY . /usr/src/app
-WORKDIR /usr/src/app
-
-
-RUN apt-get update && apt-get install -y python3 make g++ git git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
-
-# Install dependencies
-RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
-
-# Build only the dokploy app
-RUN pnpm run dokploy:build
-
-# Deploy only the dokploy app
-RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
-
-FROM base AS dokploy
-COPY --from=build /prod/dokploy /prod/dokploy
-WORKDIR /prod/dokploy
-EXPOSE 3000
-CMD [ "pnpm", "start" ]
\ No newline at end of file
diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts
index 74a4eb66..b18d7b4b 100644
--- a/apps/dokploy/__test__/drop/drop.test.test.ts
+++ b/apps/dokploy/__test__/drop/drop.test.test.ts
@@ -105,6 +105,7 @@ const baseApp: ApplicationNested = {
ports: [],
projectId: "",
publishDirectory: null,
+ isStaticSpa: null,
redirects: [],
refreshToken: "",
registry: null,
@@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => {
} finally {
}
});
-
- it("should correctly extract a zip with a single root folder and a subfolder", async () => {
- baseApp.appName = "folderwithfile";
- // const appName = "folderwithfile";
- const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
- const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
-
- const zipBuffer = zip.toBuffer();
- 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 === "folder1.txt")).toBe(true);
- });
-
- it("should correctly extract a zip with multiple root folders", async () => {
- baseApp.appName = "two-folders";
- // const appName = "two-folders";
- const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
- const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
-
- const zipBuffer = zip.toBuffer();
- 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 === "folder1")).toBe(true);
- expect(files.some((f) => f.name === "folder2")).toBe(true);
- });
-
- it("should correctly extract a zip with a single root with a file", async () => {
- baseApp.appName = "nested";
- // const appName = "nested";
- const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
- const zip = new AdmZip("./__test__/drop/zips/nested.zip");
-
- const zipBuffer = zip.toBuffer();
- 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 === "folder1")).toBe(true);
- expect(files.some((f) => f.name === "folder2")).toBe(true);
- expect(files.some((f) => f.name === "folder3")).toBe(true);
- });
-
- it("should correctly extract a zip with a single root with a folder", async () => {
- baseApp.appName = "folder-with-sibling-file";
- // const appName = "folder-with-sibling-file";
- const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
- const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
-
- const zipBuffer = zip.toBuffer();
- 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 === "folder1")).toBe(true);
- expect(files.some((f) => f.name === "test.txt")).toBe(true);
- });
});
+
+// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
+// baseApp.appName = "folderwithfile";
+// // const appName = "folderwithfile";
+// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
+// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
+
+// const zipBuffer = zip.toBuffer();
+// 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 === "folder1.txt")).toBe(true);
+// });
+
+// it("should correctly extract a zip with multiple root folders", async () => {
+// baseApp.appName = "two-folders";
+// // const appName = "two-folders";
+// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
+// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
+
+// const zipBuffer = zip.toBuffer();
+// 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 === "folder1")).toBe(true);
+// expect(files.some((f) => f.name === "folder2")).toBe(true);
+// });
+
+// it("should correctly extract a zip with a single root with a file", async () => {
+// baseApp.appName = "nested";
+// // const appName = "nested";
+// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
+// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
+
+// const zipBuffer = zip.toBuffer();
+// 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 === "folder1")).toBe(true);
+// expect(files.some((f) => f.name === "folder2")).toBe(true);
+// expect(files.some((f) => f.name === "folder3")).toBe(true);
+// });
+
+// it("should correctly extract a zip with a single root with a folder", async () => {
+// baseApp.appName = "folder-with-sibling-file";
+// // const appName = "folder-with-sibling-file";
+// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
+// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
+
+// const zipBuffer = zip.toBuffer();
+// 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 === "folder1")).toBe(true);
+// expect(files.some((f) => f.name === "test.txt")).toBe(true);
+// });
+// });
diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts
index 5437e64d..6c136b25 100644
--- a/apps/dokploy/__test__/traefik/traefik.test.ts
+++ b/apps/dokploy/__test__/traefik/traefik.test.ts
@@ -85,6 +85,7 @@ const baseApp: ApplicationNested = {
ports: [],
projectId: "",
publishDirectory: null,
+ isStaticSpa: null,
redirects: [],
refreshToken: "",
registry: null,
diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts
index c7bc310c..2c1e5dec 100644
--- a/apps/dokploy/__test__/utils/backups.test.ts
+++ b/apps/dokploy/__test__/utils/backups.test.ts
@@ -1,5 +1,5 @@
-import { describe, expect, test } from "vitest";
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
+import { describe, expect, test } from "vitest";
describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => {
diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx
index 9535a318..291026d4 100644
--- a/apps/dokploy/components/dashboard/application/build/show.tsx
+++ b/apps/dokploy/components/dashboard/application/build/show.tsx
@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
@@ -63,10 +64,11 @@ const mySchema = z.discriminatedUnion("buildType", [
publishDirectory: z.string().optional(),
}),
z.object({
- buildType: z.literal(BuildType.static),
+ buildType: z.literal(BuildType.railpack),
}),
z.object({
- buildType: z.literal(BuildType.railpack),
+ buildType: z.literal(BuildType.static),
+ isStaticSpa: z.boolean().default(false),
}),
]);
@@ -83,6 +85,7 @@ interface ApplicationData {
dockerBuildStage?: string | null;
herokuVersion?: string | null;
publishDirectory?: string | null;
+ isStaticSpa?: boolean | null;
}
function isValidBuildType(value: string): value is BuildType {
@@ -115,16 +118,18 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.static:
return {
buildType: BuildType.static,
+ isStaticSpa: data.isStaticSpa ?? false,
};
case BuildType.railpack:
return {
buildType: BuildType.railpack,
};
- default:
+ default: {
const buildType = data.buildType as BuildType;
return {
buildType,
} as AddTemplate;
+ }
}
};
@@ -174,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion
: null,
+ isStaticSpa:
+ data.buildType === BuildType.static ? data.isStaticSpa : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -364,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)}
/>
)}
+ {buildType === BuildType.static && (
+