diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index dd91309d3..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,119 +0,0 @@ -version: 2.1 - -jobs: - build-amd64: - machine: - image: ubuntu-2004:current - steps: - - checkout - - run: - name: Prepare .env file - command: | - cp apps/dokploy/.env.production.example .env.production - cp apps/dokploy/.env.production.example apps/dokploy/.env.production - - - run: - name: Build and push AMD64 image - command: | - docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN - if [ "${CIRCLE_BRANCH}" == "main" ]; then - TAG="latest" - elif [ "${CIRCLE_BRANCH}" == "canary" ]; then - TAG="canary" - else - TAG="feature" - fi - docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 . - docker push dokploy/dokploy:${TAG}-amd64 - - build-arm64: - machine: - image: ubuntu-2004:current - resource_class: arm.large - steps: - - checkout - - run: - name: Prepare .env file - command: | - cp apps/dokploy/.env.production.example .env.production - cp apps/dokploy/.env.production.example apps/dokploy/.env.production - - run: - name: Build and push ARM64 image - command: | - docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN - if [ "${CIRCLE_BRANCH}" == "main" ]; then - TAG="latest" - elif [ "${CIRCLE_BRANCH}" == "canary" ]; then - TAG="canary" - else - TAG="feature" - fi - docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 . - docker push dokploy/dokploy:${TAG}-arm64 - - combine-manifests: - docker: - - image: cimg/node:18.18.0 - steps: - - checkout - - setup_remote_docker - - run: - name: Create and push multi-arch manifest - command: | - docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN - - if [ "${CIRCLE_BRANCH}" == "main" ]; then - VERSION=$(node -p "require('./apps/dokploy/package.json').version") - echo $VERSION - TAG="latest" - - docker manifest create dokploy/dokploy:${TAG} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${TAG} - - docker manifest create dokploy/dokploy:${VERSION} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${VERSION} - elif [ "${CIRCLE_BRANCH}" == "canary" ]; then - TAG="canary" - docker manifest create dokploy/dokploy:${TAG} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${TAG} - else - TAG="feature" - docker manifest create dokploy/dokploy:${TAG} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${TAG} - fi - -workflows: - build-all: - jobs: - - build-amd64: - filters: - branches: - only: - - main - - canary - - 379-preview-deployment - - build-arm64: - filters: - branches: - only: - - main - - canary - - 379-preview-deployment - - combine-manifests: - requires: - - build-amd64 - - build-arm64 - filters: - branches: - only: - - main - - canary - - 379-preview-deployment diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7746a40c2..23e8debbd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Create a bug report -labels: ["bug"] +labels: ["needs-triage🔍"] body: - type: markdown attributes: @@ -62,6 +62,7 @@ body: - "Docker" - "Remote server" - "Local Development" + - "Cloud Version" validations: required: true - type: dropdown diff --git a/.github/sponsors/agentdock.png b/.github/sponsors/agentdock.png new file mode 100644 index 000000000..bd76dc171 Binary files /dev/null and b/.github/sponsors/agentdock.png differ diff --git a/.github/sponsors/american-cloud.png b/.github/sponsors/american-cloud.png new file mode 100644 index 000000000..daa902078 Binary files /dev/null and b/.github/sponsors/american-cloud.png differ diff --git a/.github/sponsors/its.png b/.github/sponsors/its.png new file mode 100644 index 000000000..85e7ad71a Binary files /dev/null and b/.github/sponsors/its.png differ diff --git a/.github/sponsors/light-node.webp b/.github/sponsors/light-node.webp new file mode 100644 index 000000000..56729452c Binary files /dev/null and b/.github/sponsors/light-node.webp differ diff --git a/.github/sponsors/openalternative.png b/.github/sponsors/openalternative.png new file mode 100644 index 000000000..e7b899ec3 Binary files /dev/null and b/.github/sponsors/openalternative.png differ diff --git a/.github/sponsors/synexa.png b/.github/sponsors/synexa.png new file mode 100644 index 000000000..737ccd576 Binary files /dev/null and b/.github/sponsors/synexa.png differ diff --git a/.github/workflows/create-pr.yml b/.github/workflows/create-pr.yml new file mode 100644 index 000000000..e3f6aa234 --- /dev/null +++ b/.github/workflows/create-pr.yml @@ -0,0 +1,83 @@ +name: Auto PR to main when version changes + +on: + push: + branches: + - canary + +permissions: + contents: write + pull-requests: write + +jobs: + create-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from package.json + id: package_version + run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV + + - name: Get latest GitHub tag + id: latest_tag + run: | + LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + echo $LATEST_TAG + - name: Compare versions + id: compare_versions + run: | + if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then + VERSION_CHANGED="true" + else + VERSION_CHANGED="false" + fi + echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV + echo "Comparing versions:" + echo "Current version: ${{ env.VERSION }}" + echo "Latest tag: ${{ env.LATEST_TAG }}" + echo "Version changed: $VERSION_CHANGED" + - name: Check if a PR already exists + id: check_pr + run: | + PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') + echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + + - name: Create Pull Request + if: env.VERSION_CHANGED == 'true' && env.PR_EXISTS == '0' + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git fetch origin main + git checkout canary + git push origin canary + + gh pr create \ + --title "🚀 Release ${{ env.VERSION }}" \ + --body ' + This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}. + + ### 🔍 Changes Include: + - Version bump to ${{ env.VERSION }} + - All changes from canary branch + + ### ✅ Pre-merge Checklist: + - [ ] All tests passing + - [ ] Documentation updated + - [ ] Docker images built and tested + + > 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \ + --base main \ + --head canary \ + --label "release" --label "automated pr" || true \ + --reviewer siumauricio \ + --assignee siumauricio + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 892e8ea14..3ed957b72 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,8 @@ name: Build Docker images on: push: - branches: ["canary", "main"] + branches: [main, canary] + workflow_dispatch: jobs: build-and-push-cloud-image: @@ -17,7 +18,7 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + - name: Build and push Docker image uses: docker/build-push-action@v4 with: @@ -53,8 +54,7 @@ jobs: push: true tags: | siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }} - platforms: linux/amd64 - + platforms: linux/amd64 build-and-push-server-image: runs-on: ubuntu-latest @@ -77,4 +77,4 @@ jobs: push: true tags: | siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }} - platforms: linux/amd64 \ No newline at end of file + platforms: linux/amd64 diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml new file mode 100644 index 000000000..afb4ba4d2 --- /dev/null +++ b/.github/workflows/dokploy.yml @@ -0,0 +1,162 @@ +name: Dokploy Docker Build + +on: + push: + branches: [main, canary] + workflow_dispatch: + +env: + IMAGE_NAME: dokploy/dokploy + +jobs: + docker-amd: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set tag and version + id: meta + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT + + - name: Prepare env file + run: | + cp apps/dokploy/.env.production.example .env.production + cp apps/dokploy/.env.production.example apps/dokploy/.env.production + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + docker-arm: + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set tag and version + id: meta + run: | + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT + + - name: Prepare env file + run: | + cp apps/dokploy/.env.production.example .env.production + cp apps/dokploy/.env.production.example apps/dokploy/.env.production + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + + combine-manifests: + needs: [docker-amd, docker-arm] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create and push manifests + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + TAG="latest" + + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + else + TAG="feature" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + fi + + generate-release: + needs: [combine-manifests] + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version + id: get_version + run: | + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.version }} + name: ${{ steps.get_version.outputs.version }} + generate_release_notes: true + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 000000000..cfddad7b2 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +name: autofix.ci + +on: + push: + branches: [canary] + pull_request: + branches: [canary] + +jobs: + format: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup biomeJs + uses: biomejs/setup-biome@v2 + + - name: Run Biome formatter + run: biome format --write + + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 diff --git a/.github/workflows/monitoring.yml b/.github/workflows/monitoring.yml new file mode 100644 index 000000000..378b019d4 --- /dev/null +++ b/.github/workflows/monitoring.yml @@ -0,0 +1,118 @@ +name: Dokploy Monitoring Build + +on: + push: + branches: [main, canary] + +env: + IMAGE_NAME: dokploy/monitoring + +jobs: + docker-amd: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set tag + id: meta + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.monitoring + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + docker-arm: + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set + id: meta + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.monitoring + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + + combine-manifests: + needs: [docker-amd, docker-arm] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create and push manifests + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + else + TAG="feature" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + fi diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ad409e69f..e9591f3cc 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: 18.18.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: 18.18.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: 18.18.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build diff --git a/.gitignore b/.gitignore index 368c5ed01..5e6e4eb3c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,9 +34,11 @@ yarn-debug.log* yarn-error.log* # Editor -.vscode .idea # Misc .DS_Store *.pem + + +.db \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 67a228a44..593cb75bc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.18.0 \ No newline at end of file +20.16.0 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..16e8e6664 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..99357f236 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91bb70494..0ac5a3581 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,12 +14,10 @@ We have a few guidelines to follow when contributing to this project: ## Commit Convention - Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. ### Commit Message Format - ``` [optional scope]: @@ -54,6 +52,8 @@ 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. + ```bash git clone https://github.com/dokploy/dokploy.git cd dokploy @@ -61,9 +61,9 @@ pnpm install cp apps/dokploy/.env.example apps/dokploy/.env ``` -## Development +## Requirements -Is required to have **Docker** installed on your machine. +- [Docker](/GUIDES.md#docker) ### Setup @@ -73,9 +73,10 @@ Run the command that will spin up all the required services and files. pnpm run dokploy:setup ``` -Run this script +Run this script + ```bash -pnpm run server:script +pnpm run server:script ``` Now run the development server. @@ -86,6 +87,8 @@ pnpm run dokploy:dev Go to http://localhost:3000 to see the development server +Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. + ## Build ```bash @@ -137,9 +140,14 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \ && ./install.sh ``` +```bash +# Install Railpack +curl -sSL https://railpack.com/install.sh | sh +``` + ```bash # Install Buildpacks -curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack +curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack ``` ## Pull Request @@ -157,85 +165,7 @@ Thank you for your contribution! ## Templates -To add a new template, go to `templates` folder and create a new folder with the name of the template. - -Let's take the example of `plausible` template. - -1. create a folder in `templates/plausible` -2. create a `docker-compose.yml` file inside the folder with the content of compose. -3. create a `index.ts` file inside the folder with the following code as base: -4. When creating a pull request, please provide a video of the template working in action. - -```typescript -// EXAMPLE -import { - generateHash, - generateRandomDomain, - type Template, - type Schema, - type DomainSchema, -} from "../utils"; - -export function generate(schema: Schema): Template { - // do your stuff here, like create a new domain, generate random passwords, mounts. - const mainServiceHash = generateHash(schema.projectName); - const mainDomain = generateRandomDomain(schema); - const secretBase = generateBase64(64); - const toptKeyBase = generateBase64(32); - - const domains: DomainSchema[] = [ - { - host: mainDomain, - port: 8000, - serviceName: "plausible", - }, - ]; - - const envs = [ - `BASE_URL=http://${mainDomain}`, - `SECRET_KEY_BASE=${secretBase}`, - `TOTP_VAULT_KEY=${toptKeyBase}`, - `HASH=${mainServiceHash}`, - ]; - - const mounts: Template["mounts"] = [ - { - mountPath: "./clickhouse/clickhouse-config.xml", - content: `some content......`, - }, - ]; - - return { - envs, - mounts, - domains, - }; -} -``` - -4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties: - -**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.** - -```typescript -{ - id: "plausible", - name: "Plausible", - version: "v2.1.0", - description: - "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", - logo: "plausible.svg", // we defined the name and the extension of the logo - links: { - github: "https://github.com/plausible/plausible", - website: "https://plausible.io/", - docs: "https://plausible.io/docs", - }, - tags: ["analytics"], - load: () => import("./plausible/index").then((m) => m.generate), -}, -``` - -5. Add the logo or image of the template to `public/templates/plausible.svg` +To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. ### Recommendations @@ -247,4 +177,3 @@ export function generate(schema: Schema): Template { ## Docs & Website To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website). - diff --git a/Dockerfile b/Dockerfile index 74b70db0e..4d18a99ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,15 @@ -FROM node:18-slim AS base +# syntax=docker/dockerfile:1 +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -29,7 +31,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl unzip apache2-utils && 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 @@ -48,13 +50,19 @@ 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.39.0 RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && chmod +x install.sh \ && ./install.sh \ && pnpm install -g tsx +# Install Railpack +ARG RAILPACK_VERSION=0.0.64 +RUN curl -sSL https://railpack.com/install.sh | bash + # Install buildpacks 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/Dockerfile.cloud b/Dockerfile.cloud index 020ea3d69..8e4bac215 100644 --- a/Dockerfile.cloud +++ b/Dockerfile.cloud @@ -1,13 +1,15 @@ -FROM node:18-slim AS base +# syntax=docker/dockerfile:1 +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile diff --git a/Dockerfile.monitoring b/Dockerfile.monitoring new file mode 100644 index 000000000..c54580ee1 --- /dev/null +++ b/Dockerfile.monitoring @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1 +# Build stage +FROM golang:1.21-alpine3.19 AS builder + +# Instalar dependencias necesarias +RUN apk add --no-cache gcc musl-dev sqlite-dev + +# Establecer el directorio de trabajo +WORKDIR /app + +# Copiar todo el código fuente primero +COPY . . + +# Movernos al directorio de la aplicación golang +WORKDIR /app/apps/monitoring + +# Descargar dependencias +RUN go mod download + +# Compilar la aplicación +RUN CGO_ENABLED=1 GOOS=linux go build -o main main.go + +# Etapa final +FROM alpine:3.19 + +# Instalar SQLite y otras dependencias necesarias +RUN apk add --no-cache sqlite-libs docker-cli + +WORKDIR /app + +# Copiar el binario compilado y el archivo monitor.go +COPY --from=builder /app/apps/monitoring/main ./main +COPY --from=builder /app/apps/monitoring/main.go ./monitor.go + +# COPY --from=builder /app/apps/golang/.env ./.env + +# Exponer el puerto +ENV PORT=3001 +EXPOSE 3001 + +# Ejecutar la aplicación +CMD ["./main"] \ No newline at end of file diff --git a/Dockerfile.schedule b/Dockerfile.schedule index 5eca3420e..ecb125e09 100644 --- a/Dockerfile.schedule +++ b/Dockerfile.schedule @@ -1,13 +1,15 @@ -FROM node:18-slim AS base +# syntax=docker/dockerfile:1 +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile diff --git a/Dockerfile.server b/Dockerfile.server index a25b22e52..ea6b372e8 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -1,13 +1,15 @@ -FROM node:18-slim AS base +# syntax=docker/dockerfile:1 +FROM node:20.16.0-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN corepack prepare pnpm@9.12.0 --activate 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile diff --git a/GUIDES.md b/GUIDES.md new file mode 100644 index 000000000..90fba522d --- /dev/null +++ b/GUIDES.md @@ -0,0 +1,50 @@ +# Docker + +Here's how to install docker on different operating systems: + +## macOS + +1. Visit [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop) +2. Download the Docker Desktop installer +3. Double-click the downloaded `.dmg` file +4. Drag Docker to your Applications folder +5. Open Docker Desktop from Applications +6. Follow the onboarding tutorial if desired + +## Linux + +### Ubuntu + +```bash +# Uninstall old versions +for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done + +# Update package index +sudo apt-get update + +# Install prerequisites +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings + +# Add Docker's official GPG key +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Add the repository to Apt sources +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# Install Docker Engine +sudo apt-get update +sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +``` + +## Windows + +1. Enable WSL2 if not already enabled +2. Visit [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop) +3. Download the installer +4. Run the installer and follow the prompts +5. Start Docker Desktop from the Start menu \ No newline at end of file diff --git a/LICENSE.MD b/LICENSE.MD index 8a508efb4..6cbef2c6d 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -2,7 +2,7 @@ ## Core License (Apache License 2.0) -Copyright 2024 Mauricio Siu. +Copyright 2025 Mauricio Siu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ See the License for the specific language governing permissions and limitations The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. For further inquiries or permissions, please contact us directly. diff --git a/README.md b/README.md index ff25f7d0d..bd27474e0 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,16 @@
-
- - Dokploy - Open Source Alternative to Vercel, Heroku and Netlify. - -
- -
-
-
Join us on Discord for help, feedback, and discussions!
+ + Dokploy - Open Source Alternative to Vercel, Heroku and Netlify. +
+
+

Join us on Discord for help, feedback, and discussions!

Discord Shield
-

+ Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. ### Features @@ -61,48 +57,48 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). ### Hero Sponsors 🎖 -
- - Hostinger - - - LX Aer - - - Mandarin - +
+ Hostinger + LX Aer
+ + + + ### Premium Supporters 🥇 -
-Supafort.com +
+ Supafort.com + agentdock.ai
+### Elite Contributors 🥈 + +
+ AmericanCloud + Tolgee +
+ ### Supporting Members 🥉 -
-Lightspeed.run -Cloudblast.io -Startupfame +
+ + Cloudblast.io + + Synexa
### Community Backers 🤝 -
-Steamsets.com -Rivo.gg -Rivo.gg - -
#### Organizations: -[![Sponsors on Open Collective](https://opencollective.com/dokploy/organizations.svg?width=890)](https://opencollective.com/dokploy) +[Sponsors on Open Collective](https://opencollective.com/dokploy) #### Individuals: @@ -112,27 +108,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). - + ## Video Tutorial - Watch the video + Watch the video - - ## Contributing Check out the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..47633ab95 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# Dokploy Security Policy + +At Dokploy, security is a top priority. We appreciate the help of security researchers and the community in identifying and reporting vulnerabilities. + +## How to Report a Vulnerability + +If you have discovered a security vulnerability in Dokploy, we ask that you report it responsibly by following these guidelines: + +1. **Contact us:** Send an email to [contact@dokploy.com](mailto:contact@dokploy.com). +2. **Provide clear details:** Include as much information as possible to help us understand and reproduce the vulnerability. This should include: + * A clear description of the vulnerability. + * Steps to reproduce the vulnerability. + * Any sample code, screenshots, or videos that might be helpful. + * The potential impact of the vulnerability. +3. **Do not make the vulnerability public:** Please refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address it. This is crucial for protecting our users. +4. **Allow us time:** We will endeavor to acknowledge receipt of your report as soon as possible and keep you informed of our progress. The time to resolve the vulnerability may vary depending on its complexity and severity. + +## What We Expect From You + +* Do not access user data or systems beyond what is necessary to demonstrate the vulnerability. +* Do not perform denial-of-service (DoS) attacks, spamming, or social engineering. +* Do not modify or destroy data that does not belong to you. + +## Our Commitment + +We are committed to working with you quickly and responsibly to address any legitimate security vulnerability. + +Thank you for helping us keep Dokploy secure for everyone. diff --git a/apps/api/package.json b/apps/api/package.json index 0da226afa..f0d22fedf 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,29 +5,33 @@ "scripts": { "dev": "PORT=4000 tsx watch src/index.ts", "build": "tsc --project tsconfig.json", - "start": "node --experimental-specifier-resolution=node dist/index.js", + "start": "node dist/index.js", "typecheck": "tsc --noEmit" }, "dependencies": { + "@dokploy/server": "workspace:*", + "@hono/node-server": "^1.14.3", + "@hono/zod-validator": "0.3.0", + "@nerimity/mimiqueue": "1.2.3", + "dotenv": "^16.4.5", + "hono": "^4.7.10", "pino": "9.4.0", "pino-pretty": "11.2.2", - "@hono/zod-validator": "0.3.0", - "zod": "^3.23.4", "react": "18.2.0", "react-dom": "18.2.0", - "@dokploy/server": "workspace:*", - "@hono/node-server": "^1.12.1", - "hono": "^4.5.8", - "dotenv": "^16.3.1", "redis": "4.7.0", - "@nerimity/mimiqueue": "1.2.3" + "zod": "^3.25.32" }, "devDependencies": { - "typescript": "^5.4.2", + "@types/node": "^20.17.51", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "@types/node": "^20.11.17", - "tsx": "^4.7.1" + "tsx": "^4.16.2", + "typescript": "^5.8.3" }, - "packageManager": "pnpm@9.5.0" + "packageManager": "pnpm@9.12.0", + "engines": { + "node": "^20.16.0", + "pnpm": ">=9.12.0" + } } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index bf91b0400..0db565995 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -4,9 +4,9 @@ import "dotenv/config"; import { zValidator } from "@hono/zod-validator"; import { Queue } from "@nerimity/mimiqueue"; import { createClient } from "redis"; -import { logger } from "./logger"; -import { type DeployJob, deployJobSchema } from "./schema"; -import { deploy } from "./utils"; +import { logger } from "./logger.js"; +import { type DeployJob, deployJobSchema } from "./schema.js"; +import { deploy } from "./utils.js"; const app = new Hono(); const redisClient = createClient({ @@ -28,7 +28,7 @@ app.use(async (c, next) => { app.post("/deploy", zValidator("json", deployJobSchema), (c) => { const data = c.req.valid("json"); - const res = queue.add(data, { groupName: data.serverId }); + queue.add(data, { groupName: data.serverId }); return c.json( { message: "Deployment Added", diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index d919f29e9..3f3c9698b 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => { } } } - } catch (error) { + } catch (_) { if (job.applicationType === "application") { await updateApplicationStatus(job.applicationId, "error"); } else if (job.applicationType === "compose") { diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc index 67a228a44..593cb75bc 100644 --- a/apps/dokploy/.nvmrc +++ b/apps/dokploy/.nvmrc @@ -1 +1 @@ -18.18.0 \ No newline at end of file +20.16.0 \ No newline at end of file diff --git a/apps/dokploy/CONTRIBUTING.md b/apps/dokploy/CONTRIBUTING.md deleted file mode 100644 index 8686b98a8..000000000 --- a/apps/dokploy/CONTRIBUTING.md +++ /dev/null @@ -1,242 +0,0 @@ - - -# Contributing - -Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute. - - -Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues. - -We have a few guidelines to follow when contributing to this project: - -- [Commit Convention](#commit-convention) -- [Setup](#setup) -- [Development](#development) -- [Build](#build) -- [Pull Request](#pull-request) - -## Commit Convention - -Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. - -### Commit Message Format -``` -[optional scope]: - -[optional body] - -[optional footer(s)] -``` - -#### Type -Must be one of the following: - -* **feat**: A new feature -* **fix**: A bug fix -* **docs**: Documentation only changes -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -* **refactor**: A code change that neither fixes a bug nor adds a feature -* **perf**: A code change that improves performance -* **test**: Adding missing tests or correcting existing tests -* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) -* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) -* **chore**: Other changes that don't modify `src` or `test` files -* **revert**: Reverts a previous commit - -Example: -``` -feat: add new feature -``` - - - - -## Setup - -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. - -```bash -git clone https://github.com/dokploy/dokploy.git -cd dokploy -pnpm install -cp .env.example .env -``` - -## Development - -Is required to have **Docker** installed on your machine. - - -### Setup - -Run the command that will spin up all the required services and files. - -```bash -pnpm run setup -``` - -Now run the development server. - -```bash -pnpm run dev -``` - - -Go to http://localhost:3000 to see the development server - -## Build - -```bash -pnpm run build -``` - -## Docker - -To build the docker image -```bash -pnpm run docker:build -``` - -To push the docker image -```bash -pnpm run docker:push -``` - -## Password Reset - -In the case you lost your password, you can reset it using the following command - -```bash -pnpm run reset-password -``` - -If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` - -```bash -bunx lt --port 3000 -``` - -If you run into permission issues of docker run the following command - -```bash -sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker -``` - -## Application deploy - -In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first. - -```bash -# Install Nixpacks -curl -sSL https://nixpacks.com/install.sh -o install.sh \ - && chmod +x install.sh \ - && ./install.sh -``` - -```bash -# Install Buildpacks -curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack -``` - - -## Pull Request - -- The `main` branch is the source of truth and should always reflect the latest stable release. -- Create a new branch for each feature or bug fix. -- Make sure to add tests for your changes. -- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. -- When creating a pull request, please provide a clear and concise description of the changes made. -- If you include a video or screenshot, would be awesome so we can see the changes in action. -- If your pull request fixes an open issue, please reference the issue in the pull request description. -- Once your pull request is merged, you will be automatically added as a contributor to the project. - -Thank you for your contribution! - - - - - -## Templates - -To add a new template, go to `templates` folder and create a new folder with the name of the template. - -Let's take the example of `plausible` template. - -1. create a folder in `templates/plausible` -2. create a `docker-compose.yml` file inside the folder with the content of compose. -3. create a `index.ts` file inside the folder with the following code as base: -4. When creating a pull request, please provide a video of the template working in action. - -```typescript -// EXAMPLE -import { - generateHash, - generateRandomDomain, - type Template, - type Schema, -} from "../utils"; - - -export function generate(schema: Schema): Template { - - // do your stuff here, like create a new domain, generate random passwords, mounts. - const mainServiceHash = generateHash(schema.projectName); - const randomDomain = generateRandomDomain(schema); - const secretBase = generateBase64(64); - const toptKeyBase = generateBase64(32); - - const envs = [ -// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. - `PLAUSIBLE_HOST=${randomDomain}`, - "PLAUSIBLE_PORT=8000", - `BASE_URL=http://${randomDomain}`, - `SECRET_KEY_BASE=${secretBase}`, - `TOTP_VAULT_KEY=${toptKeyBase}`, - `HASH=${mainServiceHash}`, - ]; - - const mounts: Template["mounts"] = [ - { - mountPath: "./clickhouse/clickhouse-config.xml", - content: `some content......`, - }, - ]; - - return { - envs, - mounts, - }; -} -``` - -4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties: - -**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.** - -```typescript -{ - id: "plausible", - name: "Plausible", - version: "v2.1.0", - description: - "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", - logo: "plausible.svg", // we defined the name and the extension of the logo - links: { - github: "https://github.com/plausible/plausible", - website: "https://plausible.io/", - docs: "https://plausible.io/docs", - }, - tags: ["analytics"], - load: () => import("./plausible/index").then((m) => m.generate), -}, -``` - -5. Add the logo or image of the template to `public/templates/plausible.svg` - - -### Recomendations -- Use the same name of the folder as the id of the template. -- The logo should be in the public folder. -- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. -- Test first on a vps or a server to make sure the template works. - diff --git a/apps/dokploy/Dockerfile b/apps/dokploy/Dockerfile deleted file mode 100644 index f4188c54e..000000000 --- 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/LICENSE.MD b/apps/dokploy/LICENSE.MD deleted file mode 100644 index 8a508efb4..000000000 --- a/apps/dokploy/LICENSE.MD +++ /dev/null @@ -1,26 +0,0 @@ -# License - -## Core License (Apache License 2.0) - -Copyright 2024 Mauricio Siu. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and limitations under the License. - -## Additional Terms for Specific Features - -The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: - -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service. - -For further inquiries or permissions, please contact us directly. diff --git a/apps/dokploy/__test__/compose/compose.test.ts b/apps/dokploy/__test__/compose/compose.test.ts index 9d4ba20f5..69d3a5212 100644 --- a/apps/dokploy/__test__/compose/compose.test.ts +++ b/apps/dokploy/__test__/compose/compose.test.ts @@ -1,5 +1,5 @@ -import { addSuffixToAllProperties } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllProperties } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/config/config-root.test.ts b/apps/dokploy/__test__/compose/config/config-root.test.ts index 4b40c073e..668e17902 100644 --- a/apps/dokploy/__test__/compose/config/config-root.test.ts +++ b/apps/dokploy/__test__/compose/config/config-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToConfigsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/config/config-service.test.ts b/apps/dokploy/__test__/compose/config/config-service.test.ts index de014eb5e..246872f09 100644 --- a/apps/dokploy/__test__/compose/config/config-service.test.ts +++ b/apps/dokploy/__test__/compose/config/config-service.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToConfigsInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToConfigsInServices, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/config/config.test.ts b/apps/dokploy/__test__/compose/config/config.test.ts index 3f98525a2..2d5feeb9a 100644 --- a/apps/dokploy/__test__/compose/config/config.test.ts +++ b/apps/dokploy/__test__/compose/config/config.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index 8bc9fbccb..172bff2af 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -9,6 +9,7 @@ describe("createDomainLabels", () => { port: 8080, https: false, uniqueConfigKey: 1, + customCertResolver: null, certificateType: "none", applicationId: "", composeId: "", @@ -18,6 +19,8 @@ describe("createDomainLabels", () => { path: "/", createdAt: "", previewDeploymentId: "", + internalPath: "/", + stripPath: false, }; it("should create basic labels for web entrypoint", async () => { diff --git a/apps/dokploy/__test__/compose/network/network-root.test.ts b/apps/dokploy/__test__/compose/network/network-root.test.ts index 7e06a9f0b..c55f6fa86 100644 --- a/apps/dokploy/__test__/compose/network/network-root.test.ts +++ b/apps/dokploy/__test__/compose/network/network-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToNetworksRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; @@ -293,29 +292,6 @@ networks: dokploy-network: `; -const expectedComposeFile7 = ` -version: "3.8" - -services: - web: - image: nginx:latest - networks: - - dokploy-network - -networks: - dokploy-network: - driver: bridge - driver_opts: - com.docker.network.driver.mtu: 1200 - - backend: - driver: bridge - attachable: true - - external_network: - external: true - name: dokploy-network -`; test("It shoudn't add suffix to dokploy-network", () => { const composeData = load(composeFile7) as ComposeSpecification; diff --git a/apps/dokploy/__test__/compose/network/network-service.test.ts b/apps/dokploy/__test__/compose/network/network-service.test.ts index ee07d9de9..3cf46d4ab 100644 --- a/apps/dokploy/__test__/compose/network/network-service.test.ts +++ b/apps/dokploy/__test__/compose/network/network-service.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNetworks } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToServiceNetworks, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/network/network.test.ts b/apps/dokploy/__test__/compose/network/network.test.ts index 39cf03958..7ba1c6a83 100644 --- a/apps/dokploy/__test__/compose/network/network.test.ts +++ b/apps/dokploy/__test__/compose/network/network.test.ts @@ -1,10 +1,10 @@ -import { generateRandomHash } from "@dokploy/server"; +import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllNetworks, + addSuffixToNetworksRoot, addSuffixToServiceNetworks, + generateRandomHash, } from "@dokploy/server"; -import { addSuffixToNetworksRoot } from "@dokploy/server"; -import type { ComposeSpecification } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts index 2bd91b58a..b8cef56e4 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts @@ -1,7 +1,6 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToSecretsRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; -import { dump, load } from "js-yaml"; +import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server"; +import { load } from "js-yaml"; import { expect, test } from "vitest"; test("Generate random hash with 8 characters", () => { diff --git a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts index 5206bbbaf..e12f611d0 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToSecretsInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToSecretsInServices, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/secrets/secret.test.ts b/apps/dokploy/__test__/compose/secrets/secret.test.ts index d874dc5e7..3ff524ad7 100644 --- a/apps/dokploy/__test__/compose/secrets/secret.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret.test.ts @@ -1,5 +1,5 @@ -import { addSuffixToAllSecrets } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllSecrets } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-container-name.test.ts b/apps/dokploy/__test__/compose/service/service-container-name.test.ts index bcb51fd04..6ad45c588 100644 --- a/apps/dokploy/__test__/compose/service/service-container-name.test.ts +++ b/apps/dokploy/__test__/compose/service/service-container-name.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts index b27414be5..14a5789c4 100644 --- a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts +++ b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-extends.test.ts b/apps/dokploy/__test__/compose/service/service-extends.test.ts index 8309a32fd..0b7e92c53 100644 --- a/apps/dokploy/__test__/compose/service/service-extends.test.ts +++ b/apps/dokploy/__test__/compose/service/service-extends.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-links.test.ts b/apps/dokploy/__test__/compose/service/service-links.test.ts index 5f9b01ab2..6c8cde39e 100644 --- a/apps/dokploy/__test__/compose/service/service-links.test.ts +++ b/apps/dokploy/__test__/compose/service/service-links.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service-names.test.ts b/apps/dokploy/__test__/compose/service/service-names.test.ts index 936a32ecc..c65299b03 100644 --- a/apps/dokploy/__test__/compose/service/service-names.test.ts +++ b/apps/dokploy/__test__/compose/service/service-names.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/service.test.ts b/apps/dokploy/__test__/compose/service/service.test.ts index c6050f75a..38895e073 100644 --- a/apps/dokploy/__test__/compose/service/service.test.ts +++ b/apps/dokploy/__test__/compose/service/service.test.ts @@ -1,8 +1,8 @@ +import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllServiceNames, addSuffixToServiceNames, } from "@dokploy/server"; -import type { ComposeSpecification } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts index 8066a6dd7..8aa8296e8 100644 --- a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts +++ b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToServiceNames } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume-2.test.ts b/apps/dokploy/__test__/compose/volume/volume-2.test.ts index bf34ed494..6aa9d01d3 100644 --- a/apps/dokploy/__test__/compose/volume/volume-2.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-2.test.ts @@ -1,6 +1,9 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToAllVolumes, + addSuffixToVolumesRoot, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; @@ -1006,7 +1009,7 @@ services: volumes: db-config-testhash: -`) as ComposeSpecification; +`); test("Expect to change the suffix in all the possible places (4 Try)", () => { const composeData = load(composeFileComplex) as ComposeSpecification; @@ -1115,3 +1118,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => { expect(updatedComposeData).toEqual(expectedDockerComposeExample1); }); + +const composeFileBackrest = ` +services: + backrest: + image: garethgeorge/backrest:v1.7.3 + restart: unless-stopped + ports: + - 9898 + environment: + - BACKREST_PORT=9898 + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TZ=\${TZ} + volumes: + - backrest/data:/data + - backrest/config:/config + - backrest/cache:/cache + - /:/userdata:ro + +volumes: + backrest: + backrest-cache: +`; + +const expectedDockerComposeBackrest = load(` +services: + backrest: + image: garethgeorge/backrest:v1.7.3 + restart: unless-stopped + ports: + - 9898 + environment: + - BACKREST_PORT=9898 + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TZ=\${TZ} + volumes: + - backrest-testhash/data:/data + - backrest-testhash/config:/config + - backrest-testhash/cache:/cache + - /:/userdata:ro + +volumes: + backrest-testhash: + backrest-cache-testhash: +`) as ComposeSpecification; + +test("Should handle volume paths with subdirectories correctly", () => { + const composeData = load(composeFileBackrest) as ComposeSpecification; + const suffix = "testhash"; + + const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); + + expect(updatedComposeData).toEqual(expectedDockerComposeBackrest); +}); diff --git a/apps/dokploy/__test__/compose/volume/volume-root.test.ts b/apps/dokploy/__test__/compose/volume/volume-root.test.ts index d91cb64d3..80db1f0cc 100644 --- a/apps/dokploy/__test__/compose/volume/volume-root.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-root.test.ts @@ -1,6 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToVolumesRoot } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume-services.test.ts b/apps/dokploy/__test__/compose/volume/volume-services.test.ts index 04a1a45ae..0e9cb018f 100644 --- a/apps/dokploy/__test__/compose/volume/volume-services.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-services.test.ts @@ -1,6 +1,8 @@ -import { generateRandomHash } from "@dokploy/server"; -import { addSuffixToVolumesInServices } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { + addSuffixToVolumesInServices, + generateRandomHash, +} from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/compose/volume/volume.test.ts b/apps/dokploy/__test__/compose/volume/volume.test.ts index d4623aeb1..6f8e76708 100644 --- a/apps/dokploy/__test__/compose/volume/volume.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume.test.ts @@ -1,9 +1,5 @@ -import { generateRandomHash } from "@dokploy/server"; -import { - addSuffixToAllVolumes, - addSuffixToVolumesInServices, -} from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server"; +import { addSuffixToAllVolumes } from "@dokploy/server"; import { load } from "js-yaml"; import { expect, test } from "vitest"; diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts new file mode 100644 index 000000000..03805b08d --- /dev/null +++ b/apps/dokploy/__test__/deploy/github.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from "vitest"; +import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; + +describe("GitHub Webhook Skip CI", () => { + const mockGithubHeaders = { + "x-github-event": "push", + }; + + const createMockBody = (message: string) => ({ + head_commit: { + message, + }, + }); + + const skipKeywords = [ + "[skip ci]", + "[ci skip]", + "[no ci]", + "[skip actions]", + "[actions skip]", + ]; + + it("should detect skip keywords in commit message", () => { + for (const keyword of skipKeywords) { + const message = `feat: add new feature ${keyword}`; + const commitMessage = extractCommitMessage( + mockGithubHeaders, + createMockBody(message), + ); + expect(commitMessage.includes(keyword)).toBe(true); + } + }); + + it("should not detect skip keywords in normal commit message", () => { + const message = "feat: add new feature"; + const commitMessage = extractCommitMessage( + mockGithubHeaders, + createMockBody(message), + ); + for (const keyword of skipKeywords) { + expect(commitMessage.includes(keyword)).toBe(false); + } + }); + + it("should handle different webhook sources", () => { + // GitHub + expect( + extractCommitMessage( + { "x-github-event": "push" }, + { head_commit: { message: "[skip ci] test" } }, + ), + ).toBe("[skip ci] test"); + + // GitLab + expect( + extractCommitMessage( + { "x-gitlab-event": "push" }, + { commits: [{ message: "[skip ci] test" }] }, + ), + ).toBe("[skip ci] test"); + + // Bitbucket + expect( + extractCommitMessage( + { "x-event-key": "repo:push" }, + { + push: { + changes: [{ new: { target: { message: "[skip ci] test" } } }], + }, + }, + ), + ).toBe("[skip ci] test"); + + // Gitea + expect( + extractCommitMessage( + { "x-gitea-event": "push" }, + { commits: [{ message: "[skip ci] test" }] }, + ), + ).toBe("[skip ci] test"); + }); + + it("should handle missing commit message", () => { + expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT"); + expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe( + "NEW COMMIT", + ); + expect( + extractCommitMessage( + { "x-event-key": "repo:push" }, + { push: { changes: [] } }, + ), + ).toBe("NEW COMMIT"); + expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe( + "NEW COMMIT", + ); + }); +}); diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.ts similarity index 52% rename from apps/dokploy/__test__/drop/drop.test.test.ts rename to apps/dokploy/__test__/drop/drop.test.ts index c4b2ba8d8..3945e6a0c 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.ts @@ -1,12 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { paths } from "@dokploy/server/constants"; -const { APPLICATIONS_PATH } = paths(); import type { ApplicationNested } from "@dokploy/server"; import { unzipDrop } from "@dokploy/server"; +import { paths } from "@dokploy/server/constants"; import AdmZip from "adm-zip"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +const { APPLICATIONS_PATH } = paths(); vi.mock("@dokploy/server/constants", async (importOriginal) => { const actual = await importOriginal(); return { @@ -27,7 +27,17 @@ if (typeof window === "undefined") { const baseApp: ApplicationNested = { applicationId: "", herokuVersion: "", + giteaBranch: "", + giteaBuildPath: "", + previewRequireCollaboratorPermissions: false, + giteaId: "", + giteaOwner: "", + giteaRepository: "", + cleanCache: false, + watchPaths: [], + enableSubmodules: false, applicationStatus: "done", + triggerType: "push", appName: "", autoDeploy: true, serverId: "", @@ -37,6 +47,7 @@ const baseApp: ApplicationNested = { isPreviewDeploymentsActive: false, previewBuildArgs: null, previewCertificateType: "none", + previewCustomCertResolver: null, previewEnv: null, previewHttps: false, previewPath: "/", @@ -45,7 +56,7 @@ const baseApp: ApplicationNested = { previewWildcard: "", project: { env: "", - adminId: "", + organizationId: "", name: "", description: "", createdAt: "", @@ -95,6 +106,7 @@ const baseApp: ApplicationNested = { ports: [], projectId: "", publishDirectory: null, + isStaticSpa: null, redirects: [], refreshToken: "", registry: null, @@ -110,6 +122,7 @@ const baseApp: ApplicationNested = { updateConfigSwarm: null, username: null, dockerContextPath: null, + rollbackActive: false, }; describe("unzipDrop using real zip files", () => { @@ -129,7 +142,7 @@ describe("unzipDrop using real zip files", () => { const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); console.log(`Output Path: ${outputPath}`); - const zipBuffer = zip.toBuffer(); + const zipBuffer = zip.toBuffer() as Buffer; const file = new File([zipBuffer], "single.zip"); await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); @@ -139,67 +152,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__/requests/request.test.ts b/apps/dokploy/__test__/requests/request.test.ts index 997bd9ec5..53ca8d777 100644 --- a/apps/dokploy/__test__/requests/request.test.ts +++ b/apps/dokploy/__test__/requests/request.test.ts @@ -1,5 +1,6 @@ import { parseRawConfig, processLogs } from "@dokploy/server"; import { describe, expect, it } from "vitest"; + const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; describe("processLogs", () => { diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts new file mode 100644 index 000000000..202abdf2d --- /dev/null +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -0,0 +1,497 @@ +import type { Schema } from "@dokploy/server/templates"; +import type { CompleteTemplate } from "@dokploy/server/templates/processors"; +import { processTemplate } from "@dokploy/server/templates/processors"; +import { describe, expect, it } from "vitest"; + +describe("processTemplate", () => { + // Mock schema for testing + const mockSchema: Schema = { + projectName: "test", + serverIp: "127.0.0.1", + }; + + describe("variables processing", () => { + it("should process basic variables with utility functions", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + secret_base: "${base64:64}", + totp_key: "${base64:32}", + password: "${password:32}", + hash: "${hash:16}", + }, + config: { + domains: [], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(0); + expect(result.domains).toHaveLength(0); + expect(result.mounts).toHaveLength(0); + }); + + it("should allow referencing variables in other variables", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + api_domain: "api.${main_domain}", + }, + config: { + domains: [], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(0); + expect(result.domains).toHaveLength(0); + expect(result.mounts).toHaveLength(0); + }); + + it("should allow creation of real jwt secret", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ", + anon_payload: JSON.stringify({ + role: "tester", + iss: "dockploy", + iat: "${timestamps:2025-01-01T00:00:00Z}", + exp: "${timestamps:2030-01-01T00:00:00Z}", + }), + anon_key: "${jwt:jwt_secret:anon_payload}", + }, + config: { + domains: [], + env: { + ANON_KEY: "${anon_key}", + }, + }, + }; + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(1); + expect(result.envs).toContain( + "ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY", + ); + expect(result.mounts).toHaveLength(0); + expect(result.domains).toHaveLength(0); + }); + }); + + describe("domains processing", () => { + it("should process domains with explicit host", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + }, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${main_domain}", + }, + ], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.domains).toHaveLength(1); + const domain = result.domains[0]; + expect(domain).toBeDefined(); + if (!domain) return; + expect(domain).toMatchObject({ + serviceName: "plausible", + port: 8000, + }); + expect(domain.host).toBeDefined(); + expect(domain.host).toContain(mockSchema.projectName); + }); + + it("should generate random domain if host is not specified", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + }, + ], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.domains).toHaveLength(1); + const domain = result.domains[0]; + expect(domain).toBeDefined(); + if (!domain || !domain.host) return; + expect(domain.host).toBeDefined(); + expect(domain.host).toContain(mockSchema.projectName); + }); + + it("should allow using ${domain} directly in host", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${domain}", + }, + ], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.domains).toHaveLength(1); + const domain = result.domains[0]; + expect(domain).toBeDefined(); + if (!domain || !domain.host) return; + expect(domain.host).toBeDefined(); + expect(domain.host).toContain(mockSchema.projectName); + }); + }); + + describe("environment variables processing", () => { + it("should process env vars with variable references", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + secret_base: "${base64:64}", + }, + config: { + domains: [], + env: { + BASE_URL: "http://${main_domain}", + SECRET_KEY_BASE: "${secret_base}", + }, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(2); + const baseUrl = result.envs.find((env: string) => + env.startsWith("BASE_URL="), + ); + const secretKey = result.envs.find((env: string) => + env.startsWith("SECRET_KEY_BASE="), + ); + + expect(baseUrl).toBeDefined(); + expect(secretKey).toBeDefined(); + if (!baseUrl || !secretKey) return; + + expect(baseUrl).toContain(mockSchema.projectName); + const base64Value = secretKey.split("=")[1]; + expect(base64Value).toBeDefined(); + if (!base64Value) return; + expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(base64Value.length).toBeGreaterThanOrEqual(86); + expect(base64Value.length).toBeLessThanOrEqual(88); + }); + + it("should process env vars when provided as an array", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: [ + 'CLOUDFLARE_TUNNEL_TOKEN=""', + 'ANOTHER_VAR="some value"', + "DOMAIN=${domain}", + ], + mounts: [], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + + // Should preserve exact format for static values + expect(result.envs[0]).toBe('CLOUDFLARE_TUNNEL_TOKEN=""'); + expect(result.envs[1]).toBe('ANOTHER_VAR="some value"'); + + // Should process variables in array items + expect(result.envs[2]).toContain(mockSchema.projectName); + }); + + it("should allow using utility functions directly in env vars", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: { + RANDOM_DOMAIN: "${domain}", + SECRET_KEY: "${base64:32}", + }, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(2); + const randomDomainEnv = result.envs.find((env: string) => + env.startsWith("RANDOM_DOMAIN="), + ); + const secretKeyEnv = result.envs.find((env: string) => + env.startsWith("SECRET_KEY="), + ); + expect(randomDomainEnv).toBeDefined(); + expect(secretKeyEnv).toBeDefined(); + if (!randomDomainEnv || !secretKeyEnv) return; + + expect(randomDomainEnv).toContain(mockSchema.projectName); + const base64Value = secretKeyEnv.split("=")[1]; + expect(base64Value).toBeDefined(); + if (!base64Value) return; + expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(base64Value.length).toBeGreaterThanOrEqual(42); + expect(base64Value.length).toBeLessThanOrEqual(44); + }); + + it("should handle boolean values in env vars when provided as an array", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: [ + "ENABLE_USER_SIGN_UP=false", + "DEBUG_MODE=true", + "SOME_NUMBER=42", + ], + mounts: [], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false"); + expect(result.envs).toContain("DEBUG_MODE=true"); + expect(result.envs).toContain("SOME_NUMBER=42"); + }); + + it("should handle boolean values in env vars when provided as an object", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: { + ENABLE_USER_SIGN_UP: false, + DEBUG_MODE: true, + SOME_NUMBER: 42, + }, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false"); + expect(result.envs).toContain("DEBUG_MODE=true"); + expect(result.envs).toContain("SOME_NUMBER=42"); + }); + }); + + describe("mounts processing", () => { + it("should process mounts with variable references", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + config_path: "/etc/config", + secret_key: "${base64:32}", + }, + config: { + domains: [], + env: {}, + mounts: [ + { + filePath: "${config_path}/config.xml", + content: "secret_key=${secret_key}", + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.mounts).toHaveLength(1); + const mount = result.mounts[0]; + expect(mount).toBeDefined(); + if (!mount) return; + expect(mount.filePath).toContain("/etc/config"); + expect(mount.content).toMatch(/secret_key=[A-Za-z0-9+/]{32}/); + }); + + it("should allow using utility functions directly in mount content", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: {}, + mounts: [ + { + filePath: "/config/secrets.txt", + content: "random_domain=${domain}\nsecret=${base64:32}", + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.mounts).toHaveLength(1); + const mount = result.mounts[0]; + expect(mount).toBeDefined(); + if (!mount) return; + expect(mount.content).toContain(mockSchema.projectName); + expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{32}/); + }); + }); + + describe("complex template processing", () => { + it("should process a complete template with all features", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + secret_base: "${base64:64}", + totp_key: "${base64:32}", + }, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${main_domain}", + }, + { + serviceName: "api", + port: 3000, + host: "api.${main_domain}", + }, + ], + env: { + BASE_URL: "http://${main_domain}", + SECRET_KEY_BASE: "${secret_base}", + TOTP_VAULT_KEY: "${totp_key}", + }, + mounts: [ + { + filePath: "/config/app.conf", + content: ` + domain=\${main_domain} + secret=\${secret_base} + totp=\${totp_key} + `, + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + + // Check domains + expect(result.domains).toHaveLength(2); + const [domain1, domain2] = result.domains; + expect(domain1).toBeDefined(); + expect(domain2).toBeDefined(); + if (!domain1 || !domain2) return; + expect(domain1.host).toBeDefined(); + expect(domain1.host).toContain(mockSchema.projectName); + expect(domain2.host).toContain("api."); + expect(domain2.host).toContain(mockSchema.projectName); + + // Check env vars + expect(result.envs).toHaveLength(3); + const baseUrl = result.envs.find((env: string) => + env.startsWith("BASE_URL="), + ); + const secretKey = result.envs.find((env: string) => + env.startsWith("SECRET_KEY_BASE="), + ); + const totpKey = result.envs.find((env: string) => + env.startsWith("TOTP_VAULT_KEY="), + ); + + expect(baseUrl).toBeDefined(); + expect(secretKey).toBeDefined(); + expect(totpKey).toBeDefined(); + if (!baseUrl || !secretKey || !totpKey) return; + + expect(baseUrl).toContain(mockSchema.projectName); + + // Check base64 lengths and format + const secretKeyValue = secretKey.split("=")[1]; + const totpKeyValue = totpKey.split("=")[1]; + + expect(secretKeyValue).toBeDefined(); + expect(totpKeyValue).toBeDefined(); + if (!secretKeyValue || !totpKeyValue) return; + + expect(secretKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(secretKeyValue.length).toBeGreaterThanOrEqual(86); + expect(secretKeyValue.length).toBeLessThanOrEqual(88); + + expect(totpKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(totpKeyValue.length).toBeGreaterThanOrEqual(42); + expect(totpKeyValue.length).toBeLessThanOrEqual(44); + + // Check mounts + expect(result.mounts).toHaveLength(1); + const mount = result.mounts[0]; + expect(mount).toBeDefined(); + if (!mount) return; + expect(mount.content).toContain(mockSchema.projectName); + expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{86,88}/); + expect(mount.content).toMatch(/totp=[A-Za-z0-9+/]{42,44}/); + }); + }); + + describe("Should populate envs, domains and mounts in the case we didn't used any variable", () => { + it("should populate envs, domains and mounts in the case we didn't used any variable", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${hash}", + }, + ], + env: { + BASE_URL: "http://${domain}", + SECRET_KEY_BASE: "${password:32}", + TOTP_VAULT_KEY: "${base64:128}", + }, + mounts: [ + { + filePath: "/config/secrets.txt", + content: "random_domain=${domain}\nsecret=${password:32}", + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + expect(result.domains).toHaveLength(1); + expect(result.mounts).toHaveLength(1); + }); + }); +}); diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts new file mode 100644 index 000000000..1144b65fe --- /dev/null +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -0,0 +1,232 @@ +import type { Schema } from "@dokploy/server/templates"; +import { processValue } from "@dokploy/server/templates/processors"; +import { describe, expect, it } from "vitest"; + +describe("helpers functions", () => { + // Mock schema for testing + const mockSchema: Schema = { + projectName: "test", + serverIp: "127.0.0.1", + }; + // some helpers to test jwt + type JWTParts = [string, string, string]; + const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; + const jwtBase64Decode = (str: string) => { + const base64 = str.replace(/-/g, "+").replace(/_/g, "/"); + const padding = "=".repeat((4 - (base64.length % 4)) % 4); + const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8"); + return JSON.parse(decoded); + }; + const jwtCheckHeader = (jwtHeader: string) => { + const decodedHeader = jwtBase64Decode(jwtHeader); + expect(decodedHeader).toHaveProperty("alg"); + expect(decodedHeader).toHaveProperty("typ"); + expect(decodedHeader.alg).toEqual("HS256"); + expect(decodedHeader.typ).toEqual("JWT"); + }; + + describe("${domain}", () => { + it("should generate a random domain", () => { + const domain = processValue("${domain}", {}, mockSchema); + expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); + expect( + domain.endsWith( + `${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`, + ), + ).toBeTruthy(); + }); + }); + + describe("${base64}", () => { + it("should generate a base64 string", () => { + const base64 = processValue("${base64}", {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + }); + it.each([ + [4, 8], + [8, 12], + [16, 24], + [32, 44], + [64, 88], + [128, 172], + ])( + "should generate a base64 string from parameter %d bytes length", + (length, finalLength) => { + const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + expect(base64.length).toBe(finalLength); + }, + ); + }); + + describe("${password}", () => { + it("should generate a password string", () => { + const password = processValue("${password}", {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + }); + it.each([6, 8, 12, 16, 32])( + "should generate a password string respecting parameter %d length", + (length) => { + const password = processValue(`\${password:${length}}`, {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + expect(password.length).toBe(length); + }, + ); + }); + + describe("${hash}", () => { + it("should generate a hash string", () => { + const hash = processValue("${hash}", {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + }); + it.each([6, 8, 12, 16, 32])( + "should generate a hash string respecting parameter %d length", + (length) => { + const hash = processValue(`\${hash:${length}}`, {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + expect(hash.length).toBe(length); + }, + ); + }); + + describe("${uuid}", () => { + it("should generate a UUID string", () => { + const uuid = processValue("${uuid}", {}, mockSchema); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + }); + }); + + describe("${timestamp}", () => { + it("should generate a timestamp string in milliseconds", () => { + const timestamp = processValue("${timestamp}", {}, mockSchema); + const nowLength = Math.floor(Date.now()).toString().length; + expect(timestamp).toMatch(/^\d+$/); + expect(timestamp.length).toBe(nowLength); + }); + }); + describe("${timestampms}", () => { + it("should generate a timestamp string in milliseconds", () => { + const timestamp = processValue("${timestampms}", {}, mockSchema); + const nowLength = Date.now().toString().length; + expect(timestamp).toMatch(/^\d+$/); + expect(timestamp.length).toBe(nowLength); + }); + it("should generate a timestamp string in milliseconds from parameter", () => { + const timestamp = processValue( + "${timestampms:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamp).toEqual("1735689600000"); + }); + }); + describe("${timestamps}", () => { + it("should generate a timestamp string in seconds", () => { + const timestamps = processValue("${timestamps}", {}, mockSchema); + const nowLength = Math.floor(Date.now() / 1000).toString().length; + expect(timestamps).toMatch(/^\d+$/); + expect(timestamps.length).toBe(nowLength); + }); + it("should generate a timestamp string in seconds from parameter", () => { + const timestamps = processValue( + "${timestamps:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamps).toEqual("1735689600"); + }); + }); + + describe("${randomPort}", () => { + it("should generate a random port string", () => { + const randomPort = processValue("${randomPort}", {}, mockSchema); + expect(randomPort).toMatch(/^\d+$/); + expect(Number(randomPort)).toBeLessThan(65536); + }); + }); + + describe("${username}", () => { + it("should generate a username string", () => { + const username = processValue("${username}", {}, mockSchema); + expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/); + }); + }); + + describe("${email}", () => { + it("should generate an email string", () => { + const email = processValue("${email}", {}, mockSchema); + expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/); + }); + }); + + describe("${jwt}", () => { + it("should generate a JWT string", () => { + const jwt = processValue("${jwt}", {}, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + const decodedPayload = jwtBase64Decode(parts[1]); + jwtCheckHeader(parts[0]); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); + }); + it.each([6, 8, 12, 16, 32])( + "should generate a random hex string from parameter %d byte length", + (length) => { + const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); + expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); + expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length + expect(jwt.length).toBeLessThanOrEqual(length * 2); + }, + ); + }); + describe("${jwt:secret}", () => { + it("should generate a JWT string respecting parameter secret from variable", () => { + const jwt = processValue( + "${jwt:secret}", + { secret: "mysecret" }, + mockSchema, + ); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + const decodedPayload = jwtBase64Decode(parts[1]); + jwtCheckHeader(parts[0]); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); + }); + }); + describe("${jwt:secret:payload}", () => { + it("should generate a JWT string respecting parameters secret and payload from variables", () => { + const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000); + const expiry = iat + 3600; + const jwt = processValue( + "${jwt:secret:payload}", + { + secret: "mysecret", + payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, + }, + mockSchema, + ); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + jwtCheckHeader(parts[0]); + const decodedPayload = jwtBase64Decode(parts[1]); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload.iat).toEqual(iat); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload.iss).toEqual("test-issuer"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.exp).toEqual(expiry); + expect(decodedPayload).toHaveProperty("customprop"); + expect(decodedPayload.customprop).toEqual("customvalue"); + expect(jwt).toEqual( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI", + ); + }); + }); +}); diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index c966748a7..6858f0f00 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({ default: fs, })); -import type { Admin, FileConfig } from "@dokploy/server"; +import type { FileConfig, User } from "@dokploy/server"; import { createDefaultServerTraefikConfig, loadOrCreateConfig, @@ -13,20 +13,60 @@ import { } from "@dokploy/server"; import { beforeEach, expect, test, vi } from "vitest"; -const baseAdmin: Admin = { - createdAt: "", - authId: "", - adminId: "string", +const baseAdmin: User = { + https: false, + enablePaidFeatures: false, + allowImpersonation: false, + role: "user", + metricsConfig: { + containers: { + refreshRate: 20, + services: { + include: [], + exclude: [], + }, + }, + server: { + type: "Dokploy", + cronJob: "", + port: 4500, + refreshRate: 20, + retentionDays: 2, + token: "", + thresholds: { + cpu: 0, + memory: 0, + }, + urlCallback: "", + }, + }, + cleanupCacheApplications: false, + cleanupCacheOnCompose: false, + cleanupCacheOnPreviews: false, + createdAt: new Date(), serverIp: null, certificateType: "none", host: null, letsEncryptEmail: null, sshPrivateKey: null, enableDockerCleanup: false, - enableLogRotation: false, + logCleanupCron: null, serversQuantity: 0, stripeCustomerId: "", stripeSubscriptionId: "", + banExpires: new Date(), + banned: true, + banReason: "", + email: "", + expirationDate: "", + id: "", + isRegistered: false, + name: "", + createdAt2: new Date().toISOString(), + emailVerified: false, + image: "", + updatedAt: new Date(), + twoFactorEnabled: false, }; beforeEach(() => { @@ -36,7 +76,6 @@ beforeEach(() => { test("Should read the configuration file", () => { const config: FileConfig = loadOrCreateConfig("dokploy"); - expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe( "dokploy-service-app", ); @@ -46,6 +85,7 @@ test("Should apply redirect-to-https", () => { updateServerTraefik( { ...baseAdmin, + https: true, certificateType: "letsencrypt", }, "example.com", @@ -77,8 +117,6 @@ test("Should not touch config without host", () => { }); test("Should remove websecure if https rollback to http", () => { - const originalConfig: FileConfig = loadOrCreateConfig("dokploy"); - updateServerTraefik( { ...baseAdmin, certificateType: "letsencrypt" }, "example.com", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index d05dda81d..2bea75fb7 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -1,32 +1,42 @@ -import type { Domain } from "@dokploy/server"; -import type { Redirect } from "@dokploy/server"; -import type { ApplicationNested } from "@dokploy/server"; +import type { ApplicationNested, Domain, Redirect } from "@dokploy/server"; import { createRouterConfig } from "@dokploy/server"; import { expect, test } from "vitest"; const baseApp: ApplicationNested = { + rollbackActive: false, applicationId: "", herokuVersion: "", + giteaRepository: "", + giteaOwner: "", + giteaBranch: "", + giteaBuildPath: "", + giteaId: "", + cleanCache: false, applicationStatus: "done", appName: "", autoDeploy: true, + enableSubmodules: false, + previewRequireCollaboratorPermissions: false, serverId: "", branch: null, dockerBuildStage: "", registryUrl: "", + watchPaths: [], buildArgs: null, isPreviewDeploymentsActive: false, previewBuildArgs: null, + triggerType: "push", previewCertificateType: "none", previewEnv: null, previewHttps: false, previewPath: "/", previewPort: 3000, previewLimit: 0, + previewCustomCertResolver: null, previewWildcard: "", project: { env: "", - adminId: "", + organizationId: "", name: "", description: "", createdAt: "", @@ -75,6 +85,7 @@ const baseApp: ApplicationNested = { ports: [], projectId: "", publishDirectory: null, + isStaticSpa: null, redirects: [], refreshToken: "", registry: null, @@ -103,9 +114,12 @@ const baseDomain: Domain = { port: null, serviceName: "", composeId: "", + customCertResolver: null, domainType: "application", uniqueConfigKey: 1, previewDeploymentId: "", + internalPath: "/", + stripPath: false, }; const baseRedirect: Redirect = { diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts new file mode 100644 index 000000000..2c1e5decc --- /dev/null +++ b/apps/dokploy/__test__/utils/backups.test.ts @@ -0,0 +1,61 @@ +import { normalizeS3Path } from "@dokploy/server/utils/backups/utils"; +import { describe, expect, test } from "vitest"; + +describe("normalizeS3Path", () => { + test("should handle empty and whitespace-only prefix", () => { + expect(normalizeS3Path("")).toBe(""); + expect(normalizeS3Path("/")).toBe(""); + expect(normalizeS3Path(" ")).toBe(""); + expect(normalizeS3Path("\t")).toBe(""); + expect(normalizeS3Path("\n")).toBe(""); + expect(normalizeS3Path(" \n \t ")).toBe(""); + }); + + test("should trim whitespace from prefix", () => { + expect(normalizeS3Path(" prefix")).toBe("prefix/"); + expect(normalizeS3Path("prefix ")).toBe("prefix/"); + expect(normalizeS3Path(" prefix ")).toBe("prefix/"); + expect(normalizeS3Path("\tprefix\t")).toBe("prefix/"); + expect(normalizeS3Path(" prefix/nested ")).toBe("prefix/nested/"); + }); + + test("should remove leading slashes", () => { + expect(normalizeS3Path("/prefix")).toBe("prefix/"); + expect(normalizeS3Path("///prefix")).toBe("prefix/"); + }); + + test("should remove trailing slashes", () => { + expect(normalizeS3Path("prefix/")).toBe("prefix/"); + expect(normalizeS3Path("prefix///")).toBe("prefix/"); + }); + + test("should remove both leading and trailing slashes", () => { + expect(normalizeS3Path("/prefix/")).toBe("prefix/"); + expect(normalizeS3Path("///prefix///")).toBe("prefix/"); + }); + + test("should handle nested paths", () => { + expect(normalizeS3Path("prefix/nested")).toBe("prefix/nested/"); + expect(normalizeS3Path("/prefix/nested/")).toBe("prefix/nested/"); + expect(normalizeS3Path("///prefix/nested///")).toBe("prefix/nested/"); + }); + + test("should preserve middle slashes", () => { + expect(normalizeS3Path("prefix/nested/deep")).toBe("prefix/nested/deep/"); + expect(normalizeS3Path("/prefix/nested/deep/")).toBe("prefix/nested/deep/"); + }); + + test("should handle special characters", () => { + expect(normalizeS3Path("prefix-with-dashes")).toBe("prefix-with-dashes/"); + expect(normalizeS3Path("prefix_with_underscores")).toBe( + "prefix_with_underscores/", + ); + expect(normalizeS3Path("prefix.with.dots")).toBe("prefix.with.dots/"); + }); + + test("should handle the cases from the bug report", () => { + expect(normalizeS3Path("instance-backups/")).toBe("instance-backups/"); + expect(normalizeS3Path("/instance-backups/")).toBe("instance-backups/"); + expect(normalizeS3Path("instance-backups")).toBe("instance-backups/"); + }); +}); diff --git a/apps/dokploy/__test__/vitest.config.ts b/apps/dokploy/__test__/vitest.config.ts index 14eabf695..ddc84d6ac 100644 --- a/apps/dokploy/__test__/vitest.config.ts +++ b/apps/dokploy/__test__/vitest.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ NODE: "test", }, }, + plugins: [tsconfigPaths()], resolve: { alias: { "@dokploy/server": path.resolve( diff --git a/apps/dokploy/components/auth/login-2fa.tsx b/apps/dokploy/components/auth/login-2fa.tsx deleted file mode 100644 index 7c4915fa6..000000000 --- a/apps/dokploy/components/auth/login-2fa.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; - -import { CardTitle } from "@/components/ui/card"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from "@/components/ui/input-otp"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; -import { useRouter } from "next/router"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const Login2FASchema = z.object({ - pin: z.string().min(6, { - message: "Pin is required", - }), -}); - -type Login2FA = z.infer; - -interface Props { - authId: string; -} - -export const Login2FA = ({ authId }: Props) => { - const { push } = useRouter(); - - const { mutateAsync, isLoading, isError, error } = - api.auth.verifyLogin2FA.useMutation(); - - const form = useForm({ - defaultValues: { - pin: "", - }, - resolver: zodResolver(Login2FASchema), - }); - - useEffect(() => { - form.reset({ - pin: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - - const onSubmit = async (data: Login2FA) => { - await mutateAsync({ - pin: data.pin, - id: authId, - }) - .then(() => { - toast.success("Signin successfully", { - duration: 2000, - }); - - push("/dashboard/projects"); - }) - .catch(() => { - toast.error("Signin failed", { - duration: 2000, - }); - }); - }; - return ( -
- - {isError && ( -
- - - {error?.message} - -
- )} - 2FA Setup - - ( - - Pin - - - - - - - - - - - - - - Please enter the 6 digits code provided by your authenticator - app. - - - - )} - /> - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 6750527d2..b8a272e15 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => { } try { return JSON.parse(str); - } catch (e) { + } catch { ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); return z.NEVER; } @@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { refetch(); }) .catch(() => { - toast.error("Error to update the swarm settings"); + toast.error("Error updating the swarm settings"); }); }; return ( @@ -270,8 +270,8 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { Swarm Settings - - + + Swarm Settings Update certain settings using a json object. @@ -753,7 +753,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { )} /> - +