diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 2ac54229..e9591f3c 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -12,7 +12,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.9.0
+ node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.9.0
+ node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
- node-version: 20.9.0
+ node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
diff --git a/.nvmrc b/.nvmrc
index 43bff1f8..593cb75b 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.9.0
\ No newline at end of file
+20.16.0
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a69fa686..0ac5a358 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -52,7 +52,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.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
+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.
```bash
git clone https://github.com/dokploy/dokploy.git
diff --git a/Dockerfile b/Dockerfile
index 98ed9851..00043b0c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,7 +29,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
-RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
-ARG NIXPACKS_VERSION=1.35.0
+ARG NIXPACKS_VERSION=1.39.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh \
@@ -63,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
-CMD [ "pnpm", "start" ]
\ No newline at end of file
+CMD [ "pnpm", "start" ]
diff --git a/README.md b/README.md
index 90c651b0..d192d6f7 100644
--- a/README.md
+++ b/README.md
@@ -148,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
-
-
## 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 f4188c54..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 && 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/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
index 0e848fec..aa359d67 100644
--- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
+++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx
@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => (
{env}
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
Mount File Content
-
+
{
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 && (
+ (
+
+
+
+
+
+ Single Page Application (SPA)
+
+
+
+
+
+ )}
+ />
+ )}