diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index ba405c22..00000000 --- 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:20.9.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 - - feat/add-sidebar - - build-arm64: - filters: - branches: - only: - - main - - canary - - feat/add-sidebar - - combine-manifests: - requires: - - build-amd64 - - build-arm64 - filters: - branches: - only: - - main - - canary - - feat/add-sidebar diff --git a/.github/workflows/create-pr.yml b/.github/workflows/create-pr.yml new file mode 100644 index 00000000..e3f6aa23 --- /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/dokploy.yml b/.github/workflows/dokploy.yml new file mode 100644 index 00000000..2ba2e84c --- /dev/null +++ b/.github/workflows/dokploy.yml @@ -0,0 +1,161 @@ +name: Dokploy Docker Build + +on: + push: + branches: [main, canary, feat/github-runners] + +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/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts new file mode 100644 index 00000000..18d7619a --- /dev/null +++ b/apps/dokploy/__test__/deploy/github.test.ts @@ -0,0 +1,98 @@ +import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; +import { describe, expect, it } from "vitest"; + +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__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index c966748a..fac90cc7 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -14,6 +14,9 @@ import { import { beforeEach, expect, test, vi } from "vitest"; const baseAdmin: Admin = { + cleanupCacheApplications: false, + cleanupCacheOnCompose: false, + cleanupCacheOnPreviews: false, createdAt: "", authId: "", adminId: "string", diff --git a/apps/dokploy/__test__/vitest.config.ts b/apps/dokploy/__test__/vitest.config.ts index 14eabf69..ddc84d6a 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/dashboard/application/advanced/show-resources.tsx b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx index bcf0ccbd..227bca55 100644 --- a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx @@ -144,38 +144,6 @@ export const ShowResources = ({ id, type }: Props) => { className="grid w-full gap-8 " >
- ( - -
- Memory Reservation - - - - - - -

- Memory soft limit in bytes. Example: 256MB = - 268435456 bytes -

-
-
-
-
- - - - -
- )} - /> - { ); }} /> + ( + +
+ Memory Reservation + + + + + + +

+ Memory soft limit in bytes. Example: 256MB = + 268435456 bytes +

+
+
+
+
+ + + + +
+ )} + /> {
Volumes - If you want to persist data in this postgres database use the - following config to setup the volumes + If you want to persist data in this service use the following config + to setup the volumes
@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => { {mount.type === "file" && (
Content - + {mount.content}
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} -
- Mount Path - - {mount.mountPath} - -
+ {mount.type === "file" ? ( +
+ File Path + + {mount.filePath} + +
+ ) : ( +
+ Mount Path + + {mount.mountPath} + +
+ )}
void; serverId?: string; + errorMessage?: string; } -export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { +export const ShowDeployment = ({ + logPath, + open, + onClose, + serverId, + errorMessage, +}: Props) => { const [data, setData] = useState(""); const [showExtraLogs, setShowExtraLogs] = useState(false); const [filteredLogs, setFilteredLogs] = useState([]); @@ -99,6 +106,8 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { } }, [filteredLogs, autoScroll]); + const optionalErrors = parseLogs(errorMessage || ""); + return ( { )) ) : ( -
- -
+ <> + {optionalErrors.length > 0 ? ( + optionalErrors.map((log: LogLine, index: number) => ( + + )) + ) : ( +
+ +
+ )} + )}
diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index a767350f..d33936f5 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -8,7 +8,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/utils/api"; +import { type RouterOutputs, api } from "@/utils/api"; import { RocketIcon } from "lucide-react"; import React, { useEffect, useState } from "react"; import { CancelQueues } from "./cancel-queues"; @@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment"; interface Props { applicationId: string; } + export const ShowDeployments = ({ applicationId }: Props) => { - const [activeLog, setActiveLog] = useState(null); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); const { data } = api.application.one.useQuery({ applicationId }); const { data: deployments } = api.deployment.all.useQuery( { applicationId }, @@ -100,7 +103,7 @@ export const ShowDeployments = ({ applicationId }: Props) => { { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -245,7 +245,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -236,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.login} + + { {repositories?.map((repo) => { return ( { form.setValue("repository", { @@ -260,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { - const [activeLog, setActiveLog] = useState(null); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); const [isOpen, setIsOpen] = useState(false); return ( @@ -77,7 +79,7 @@ export const ShowPreviewBuilds = ({ ); diff --git a/apps/dokploy/components/dashboard/compose/delete-compose.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx similarity index 61% rename from apps/dokploy/components/dashboard/compose/delete-compose.tsx rename to apps/dokploy/components/dashboard/compose/delete-service.tsx index 764de95e..00656f6c 100644 --- a/apps/dokploy/components/dashboard/compose/delete-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx @@ -20,9 +20,10 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; +import type { ServiceType } from "@dokploy/server/db/schema"; import { zodResolver } from "@hookform/resolvers/zod"; +import copy from "copy-to-clipboard"; import { Copy, Trash2 } from "lucide-react"; -import { TrashIcon } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -39,16 +40,42 @@ const deleteComposeSchema = z.object({ type DeleteCompose = z.infer; interface Props { - composeId: string; + id: string; + type: ServiceType | "application"; } -export const DeleteCompose = ({ composeId }: Props) => { +export const DeleteService = ({ id, type }: Props) => { const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.compose.delete.useMutation(); - const { data } = api.compose.one.useQuery( - { composeId }, - { enabled: !!composeId }, - ); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + compose: () => + api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.remove.useMutation(), + redis: () => api.redis.remove.useMutation(), + mysql: () => api.mysql.remove.useMutation(), + mariadb: () => api.mariadb.remove.useMutation(), + application: () => api.application.delete.useMutation(), + mongo: () => api.mongo.remove.useMutation(), + compose: () => api.compose.delete.useMutation(), + }; + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.remove.useMutation(); const { push } = useRouter(); const form = useForm({ defaultValues: { @@ -62,14 +89,23 @@ export const DeleteCompose = ({ composeId }: Props) => { const expectedName = `${data?.name}/${data?.appName}`; if (formData.projectName === expectedName) { const { deleteVolumes } = formData; - await mutateAsync({ composeId, deleteVolumes }) + await mutateAsync({ + mongoId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + applicationId: id || "", + composeId: id || "", + deleteVolumes, + }) .then((result) => { push(`/dashboard/project/${result?.projectId}`); - toast.success("Compose deleted successfully"); + toast.success("deleted successfully"); setIsOpen(false); }) .catch(() => { - toast.error("Error deleting the compose"); + toast.error("Error deleting the service"); }); } else { form.setError("projectName", { @@ -95,8 +131,8 @@ export const DeleteCompose = ({ composeId }: Props) => { Are you absolutely sure? This action cannot be undone. This will permanently delete the - compose. If you are sure please enter the compose name to delete - this compose. + service. If you are sure please enter the service name to delete + this service.
@@ -119,9 +155,7 @@ export const DeleteCompose = ({ composeId }: Props) => { variant="outline" onClick={() => { if (data?.name && data?.appName) { - navigator.clipboard.writeText( - `${data.name}/${data.appName}`, - ); + copy(`${data.name}/${data.appName}`); toast.success("Copied to clipboard. Be careful!"); } }} @@ -142,27 +176,29 @@ export const DeleteCompose = ({ composeId }: Props) => { )} /> - ( - -
- - - + {type === "compose" && ( + ( + +
+ + + - - Delete volumes associated with this compose - -
- -
- )} - /> + + Delete volumes associated with this compose + +
+ +
+ )} + /> + )}
diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index 45869ed2..7c191a14 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -17,12 +17,14 @@ interface Props { serverId?: string; open: boolean; onClose: () => void; + errorMessage?: string; } export const ShowDeploymentCompose = ({ logPath, open, onClose, serverId, + errorMessage, }: Props) => { const [data, setData] = useState(""); const [filteredLogs, setFilteredLogs] = useState([]); @@ -105,6 +107,8 @@ export const ShowDeploymentCompose = ({ } }, [filteredLogs, autoScroll]); + const optionalErrors = parseLogs(errorMessage || ""); + return ( )) ) : ( -
- -
+ <> + {optionalErrors.length > 0 ? ( + optionalErrors.map((log: LogLine, index: number) => ( + + )) + ) : ( +
+ +
+ )} + )} diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx index 54c0ad2e..fce4f33f 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx @@ -8,7 +8,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/utils/api"; +import { type RouterOutputs, api } from "@/utils/api"; import { RocketIcon } from "lucide-react"; import React, { useEffect, useState } from "react"; import { CancelQueuesCompose } from "./cancel-queues-compose"; @@ -19,7 +19,9 @@ interface Props { composeId: string; } export const ShowDeploymentsCompose = ({ composeId }: Props) => { - const [activeLog, setActiveLog] = useState(null); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); const { data } = api.compose.one.useQuery({ composeId }); const { data: deployments } = api.deployment.allByCompose.useQuery( { composeId }, @@ -100,7 +102,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => { { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -247,7 +247,12 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -238,7 +238,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.login} + + { {repositories?.map((repo) => { return ( { form.setValue("repository", { @@ -262,7 +262,12 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { {data?.length === 0 ? (
- + To create a backup it is required to set at least 1 provider. Please, go to{" "} - Settings + S3 Destinations {" "} to do so. diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index cf0b30bb..698311a7 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -43,7 +43,7 @@ const LOG_STYLES: Record = { export function parseLogs(logString: string): LogLine[] { // Regex to match the log line format - // Exemple of return : + // Example of return : // 1 2024-12-10T10:00:00.000Z The server is running on port 8080 // Should return : // { timestamp: new Date("2024-12-10T10:00:00.000Z"), diff --git a/apps/dokploy/components/dashboard/monitoring/docker/show.tsx b/apps/dokploy/components/dashboard/monitoring/docker/show.tsx index 2e5eecef..a457f35e 100644 --- a/apps/dokploy/components/dashboard/monitoring/docker/show.tsx +++ b/apps/dokploy/components/dashboard/monitoring/docker/show.tsx @@ -200,7 +200,7 @@ export const DockerMonitoring = ({
-
+
CPU Usage diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 2c30df8d..da30cfee 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -70,7 +70,7 @@ interface Props { export const AddApplication = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); - + const { data: isCloud } = api.settings.isCloud.useQuery(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); const { data: servers } = api.server.withSSHKey.useQuery(); @@ -166,7 +166,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { - Select a Server (Optional) + Select a Server {!isCloud ? "(Optional)" : ""} @@ -197,7 +197,12 @@ export const AddApplication = ({ projectId, projectName }: Props) => { key={server.serverId} value={server.serverId} > - {server.name} + + {server.name} + + {server.ipAddress} + + ))} Servers ({servers?.length}) diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index dc753e94..ea8690a8 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -73,6 +73,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery(); const { mutateAsync, isLoading, error, isError } = api.compose.create.useMutation(); @@ -173,7 +174,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { - Select a Server (Optional) + Select a Server {!isCloud ? "(Optional)" : ""} @@ -204,7 +205,12 @@ export const AddCompose = ({ projectId, projectName }: Props) => { key={server.serverId} value={server.serverId} > - {server.name} + + {server.name} + + {server.ipAddress} + + ))} Servers ({servers?.length}) diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index fc86d253..1ca0d6a5 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -89,7 +89,7 @@ const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("postgres"), - databaseName: z.string().min(1, "Database name required"), + databaseName: z.string().default("postgres"), databaseUser: z.string().default("postgres"), }) .merge(baseDatabaseSchema), @@ -110,7 +110,7 @@ const mySchema = z.discriminatedUnion("type", [ type: z.literal("mysql"), databaseRootPassword: z.string().default(""), databaseUser: z.string().default("mysql"), - databaseName: z.string().min(1, "Database name required"), + databaseName: z.string().default("mysql"), }) .merge(baseDatabaseSchema), z @@ -119,7 +119,7 @@ const mySchema = z.discriminatedUnion("type", [ dockerImage: z.string().default("mariadb:4"), databaseRootPassword: z.string().default(""), databaseUser: z.string().default("mariadb"), - databaseName: z.string().min(1, "Database name required"), + databaseName: z.string().default("mariadb"), }) .merge(baseDatabaseSchema), ]); @@ -206,7 +206,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { promise = postgresMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, - databaseName: data.databaseName, + databaseName: data.databaseName || "postgres", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], @@ -233,7 +233,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { ...commonParams, databasePassword: data.databasePassword, databaseRootPassword: data.databaseRootPassword, - databaseName: data.databaseName, + databaseName: data.databaseName || "mariadb", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId, @@ -242,7 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { promise = mysqlMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, - databaseName: data.databaseName, + databaseName: data.databaseName || "mysql", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], databaseRootPassword: data.databaseRootPassword, diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 068fc984..cc6962aa 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -80,6 +80,7 @@ export const AddTemplate = ({ projectId }: Props) => { const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed"); const [selectedTags, setSelectedTags] = useState([]); const { data } = api.compose.templates.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery(); const { data: tags, isLoading: isLoadingTags } = api.compose.getTags.useQuery(); @@ -114,26 +115,28 @@ export const AddTemplate = ({ projectId }: Props) => { -
-
+
+
Create from Template Create an open source application from a template
-
+
setQuery(e.target.value)} - className="w-[200px]" + className="w-full sm:w-[200px]" value={query} /> - + {project.applications.length > 0 || + project.compose.length > 0 ? ( + + + + + e.stopPropagation()} + > + {project.applications.length > 0 && ( + + + Applications + + {project.applications.map((app) => ( +
+ + + + {app.name} + + + {app.domains.map((domain) => ( + + + {domain.host} + + + + ))} + +
+ ))} +
+ )} + {project.compose.length > 0 && ( + + + Compose + + {project.compose.map((comp) => ( +
+ + + + {comp.name} + + + {comp.domains.map((domain) => ( + + + {domain.host} + + + + ))} + +
+ ))} +
+ )} +
+
+ ) : null} @@ -179,7 +261,10 @@ export const ShowProjects = () => { - + e.stopPropagation()} + > Actions diff --git a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx index 3d03d7c6..c6546f2d 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx @@ -61,6 +61,7 @@ export const AddCertificate = () => { const [open, setOpen] = useState(false); const utils = api.useUtils(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { mutateAsync, isError, error, isLoading } = api.certificates.create.useMutation(); const { data: servers } = api.server.withSSHKey.useQuery(); @@ -181,7 +182,7 @@ export const AddCertificate = () => { - Select a Server (Optional) + Select a Server {!isCloud && "(Optional)"} @@ -202,7 +203,12 @@ export const AddCertificate = () => { key={server.serverId} value={server.serverId} > - {server.name} + + {server.name} + + {server.ipAddress} + + ))} Servers ({servers?.length}) diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index f28a814d..6aaa2563 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -50,7 +50,7 @@ export const ShowCertificates = () => { {data?.length === 0 ? (
- + You don't have any certificates created diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 08961f2f..f4c016a0 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -28,7 +28,13 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react"; +import { + AlertTriangle, + Mail, + MessageCircleMore, + PenBoxIcon, + PlusIcon, +} from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -84,6 +90,15 @@ export const notificationSchema = z.discriminatedUnion("type", [ .min(1, { message: "At least one email is required" }), }) .merge(notificationBaseSchema), + z + .object({ + type: z.literal("gotify"), + serverUrl: z.string().min(1, { message: "Server URL is required" }), + appToken: z.string().min(1, { message: "App Token is required" }), + priority: z.number().min(1).max(10).default(5), + decoration: z.boolean().default(true), + }) + .merge(notificationBaseSchema), ]); export const notificationsMap = { @@ -103,6 +118,10 @@ export const notificationsMap = { icon: , label: "Email", }, + gotify: { + icon: , + label: "Gotify", + }, }; export type NotificationSchema = z.infer; @@ -126,13 +145,14 @@ export const HandleNotifications = ({ notificationId }: Props) => { ); const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = api.notification.testSlackConnection.useMutation(); - const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = api.notification.testTelegramConnection.useMutation(); const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = api.notification.testDiscordConnection.useMutation(); const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = api.notification.testEmailConnection.useMutation(); + const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } = + api.notification.testGotifyConnection.useMutation(); const slackMutation = notificationId ? api.notification.updateSlack.useMutation() : api.notification.createSlack.useMutation(); @@ -145,6 +165,9 @@ export const HandleNotifications = ({ notificationId }: Props) => { const emailMutation = notificationId ? api.notification.updateEmail.useMutation() : api.notification.createEmail.useMutation(); + const gotifyMutation = notificationId + ? api.notification.updateGotify.useMutation() + : api.notification.createGotify.useMutation(); const form = useForm({ defaultValues: { @@ -222,6 +245,20 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, }); + } else if (notification.notificationType === "gotify") { + form.reset({ + appBuildError: notification.appBuildError, + appDeploy: notification.appDeploy, + dokployRestart: notification.dokployRestart, + databaseBackup: notification.databaseBackup, + type: notification.notificationType, + appToken: notification.gotify?.appToken, + decoration: notification.gotify?.decoration || undefined, + priority: notification.gotify?.priority, + serverUrl: notification.gotify?.serverUrl, + name: notification.name, + dockerCleanup: notification.dockerCleanup, + }); } } else { form.reset(); @@ -233,6 +270,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { telegram: telegramMutation, discord: discordMutation, email: emailMutation, + gotify: gotifyMutation, }; const onSubmit = async (data: NotificationSchema) => { @@ -300,6 +338,21 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", emailId: notification?.emailId || "", }); + } else if (data.type === "gotify") { + promise = gotifyMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + serverUrl: data.serverUrl, + appToken: data.appToken, + priority: data.priority, + name: data.name, + dockerCleanup: dockerCleanup, + decoration: data.decoration, + notificationId: notificationId || "", + gotifyId: notification?.gotifyId || "", + }); } if (promise) { @@ -700,6 +753,94 @@ export const HandleNotifications = ({ notificationId }: Props) => { )} + + {type === "gotify" && ( + <> + ( + + Server URL + + + + + + )} + /> + ( + + App Token + + + + + + )} + /> + ( + + Priority + + { + const value = e.target.value; + if (value) { + const port = Number.parseInt(value); + if (port > 0 && port < 10) { + field.onChange(port); + } + } + }} + type="number" + /> + + + Message priority (1-10, default: 5) + + + + )} + /> + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )}
@@ -824,7 +965,8 @@ export const HandleNotifications = ({ notificationId }: Props) => { isLoadingSlack || isLoadingTelegram || isLoadingDiscord || - isLoadingEmail + isLoadingEmail || + isLoadingGotify } variant="secondary" onClick={async () => { @@ -853,6 +995,13 @@ export const HandleNotifications = ({ notificationId }: Props) => { toAddresses: form.getValues("toAddresses"), fromAddress: form.getValues("fromAddress"), }); + } else if (type === "gotify") { + await testGotifyConnection({ + serverUrl: form.getValues("serverUrl"), + appToken: form.getValues("appToken"), + priority: form.getValues("priority"), + decoration: form.getValues("decoration"), + }); } toast.success("Connection Success"); } catch (err) { diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 75b66622..d65069d4 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -13,7 +13,7 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Bell, Loader2, Mail, Trash2 } from "lucide-react"; +import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { HandleNotifications } from "./handle-notifications"; @@ -47,7 +47,7 @@ export const ShowNotifications = () => { {data?.length === 0 ? (
- + To send notifications it is required to set at least 1 provider. @@ -83,6 +83,11 @@ export const ShowNotifications = () => {
)} + {notification.notificationType === "gotify" && ( +
+ +
+ )} {notification.name} diff --git a/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx b/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx index f100979e..fc48134a 100644 --- a/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/ssh-keys/handle-ssh-keys.tsx @@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea"; import { sshKeyCreate, type sshKeyType } from "@/server/db/validations"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, PlusIcon } from "lucide-react"; +import { DownloadIcon, PenBoxIcon, PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -111,6 +111,26 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => { toast.error("Error generating the SSH Key"); }); + const downloadKey = ( + content: string, + defaultFilename: string, + keyType: "private" | "public", + ) => { + const keyName = form.watch("name"); + const filename = keyName + ? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}` + : `${keyType}_${defaultFilename}`; + const blob = new Blob([content], { type: "text/plain" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + return ( @@ -245,7 +265,41 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => { )} /> - + +
+ {form.watch("privateKey") && ( + + )} + {form.watch("publicKey") && ( + + )} +
diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index bb82c982..7c1f5037 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -30,8 +30,8 @@ import { toast } from "sonner"; import { z } from "zod"; const addPermissions = z.object({ - accesedProjects: z.array(z.string()).optional(), - accesedServices: z.array(z.string()).optional(), + accessedProjects: z.array(z.string()).optional(), + accessedServices: z.array(z.string()).optional(), canCreateProjects: z.boolean().optional().default(false), canCreateServices: z.boolean().optional().default(false), canDeleteProjects: z.boolean().optional().default(false), @@ -66,8 +66,8 @@ export const AddUserPermissions = ({ userId }: Props) => { const form = useForm({ defaultValues: { - accesedProjects: [], - accesedServices: [], + accessedProjects: [], + accessedServices: [], }, resolver: zodResolver(addPermissions), }); @@ -75,8 +75,8 @@ export const AddUserPermissions = ({ userId }: Props) => { useEffect(() => { if (data) { form.reset({ - accesedProjects: data.accesedProjects || [], - accesedServices: data.accesedServices || [], + accessedProjects: data.accessedProjects || [], + accessedServices: data.accessedServices || [], canCreateProjects: data.canCreateProjects, canCreateServices: data.canCreateServices, canDeleteProjects: data.canDeleteProjects, @@ -98,8 +98,8 @@ export const AddUserPermissions = ({ userId }: Props) => { canDeleteServices: data.canDeleteServices, canDeleteProjects: data.canDeleteProjects, canAccessToTraefikFiles: data.canAccessToTraefikFiles, - accesedProjects: data.accesedProjects || [], - accesedServices: data.accesedServices || [], + accessedProjects: data.accessedProjects || [], + accessedServices: data.accessedServices || [], canAccessToDocker: data.canAccessToDocker, canAccessToAPI: data.canAccessToAPI, canAccessToSSHKeys: data.canAccessToSSHKeys, @@ -318,7 +318,7 @@ export const AddUserPermissions = ({ userId }: Props) => { /> (
@@ -339,7 +339,7 @@ export const AddUserPermissions = ({ userId }: Props) => { { return ( { { return ( = ({ id, serverId }) => { foreground: "currentColor", }, }); + const addonFit = new FitAddon(); const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; @@ -54,6 +56,8 @@ export const Terminal: React.FC = ({ id, serverId }) => { const ws = new WebSocket(wsUrl); const addonAttach = new AttachAddon(ws); + const clipboardAddon = new ClipboardAddon(); + term.loadAddon(clipboardAddon); // @ts-ignore term.open(termRef.current); @@ -68,7 +72,7 @@ export const Terminal: React.FC = ({ id, serverId }) => { return (
-
+
diff --git a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx index 8901ba58..52c90c0f 100644 --- a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx @@ -60,7 +60,7 @@ export function NodeCard({ node, serverId }: Props) {
{node.Hostname}
{node.ManagerStatus || "Worker"}
-
+
TLS Status: {node.TLSStatus} Availability: {node.Availability}
diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 98dc0d96..0c38b509 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -72,7 +72,7 @@ export default function SwarmMonitorCard({ serverId }: Props) { return (
-
+
diff --git a/apps/dokploy/components/layouts/dashboard-layout.tsx b/apps/dokploy/components/layouts/dashboard-layout.tsx index 13e9061d..00697e7c 100644 --- a/apps/dokploy/components/layouts/dashboard-layout.tsx +++ b/apps/dokploy/components/layouts/dashboard-layout.tsx @@ -5,9 +5,5 @@ interface Props { } export const DashboardLayout = ({ children }: Props) => { - return ( - -
{children}
-
- ); + return {children}; }; diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx index 093c1492..9d4068cf 100644 --- a/apps/dokploy/components/layouts/onboarding-layout.tsx +++ b/apps/dokploy/components/layouts/onboarding-layout.tsx @@ -11,7 +11,7 @@ interface Props { export const OnboardingLayout = ({ children }: Props) => { return (
-
+
{
-

+

“The Open Source alternative to Netlify, Vercel, Heroku.”

- {/*
Sofia Davis
*/}
{children} - - {/*

- By clicking continue, you agree to our{" "} - - Terms of Service - {" "} - and{" "} - - Privacy Policy - - . -

*/}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index bdde2af5..3ef433ed 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -13,6 +13,7 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments"; import { UpdateApplication } from "@/components/dashboard/application/update-application"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { ProjectLayout } from "@/components/layouts/project-layout"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; @@ -82,8 +83,6 @@ const Service = ( }, ); - const { mutateAsync, isLoading: isRemoving } = - api.application.delete.useMutation(); const { data: auth } = api.auth.get.useQuery(); const { data: user } = api.user.byAuthId.useQuery( { @@ -177,34 +176,7 @@ const Service = (
{(auth?.rol === "admin" || user?.canDeleteServices) && ( - { - await mutateAsync({ - applicationId: applicationId, - }) - .then(() => { - router.push( - `/dashboard/project/${data?.projectId}`, - ); - toast.success("Application deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting application"); - }); - }} - > - - + )}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index dba3aea8..66c3ef53 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -1,7 +1,7 @@ import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment"; import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command"; -import { DeleteCompose } from "@/components/dashboard/compose/delete-compose"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose"; import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; @@ -168,7 +168,7 @@ const Service = ( {(auth?.rol === "admin" || user?.canDeleteServices) && ( - + )}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx index fed8faa3..4edf9f14 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx @@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show- import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { ShowExternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-external-mariadb-credentials"; import { ShowGeneralMariadb } from "@/components/dashboard/mariadb/general/show-general-mariadb"; @@ -67,8 +68,7 @@ const Mariadb = ( enabled: !!auth?.id && auth?.rol === "user", }, ); - const { mutateAsync: remove, isLoading: isRemoving } = - api.mariadb.remove.useMutation(); + return (
)}
-
+
{(auth?.rol === "admin" || user?.canDeleteServices) && ( - { - await remove({ mariadbId }) - .then(() => { - router.push( - `/dashboard/project/${data?.projectId}`, - ); - toast.success("Mariadb deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting the mariadb"); - }); - }} - > - - + )}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx index 49dcfa65..ecd42841 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx @@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show- import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { ShowExternalMongoCredentials } from "@/components/dashboard/mongo/general/show-external-mongo-credentials"; import { ShowGeneralMongo } from "@/components/dashboard/mongo/general/show-general-mongo"; @@ -69,8 +70,6 @@ const Mongo = ( enabled: !!auth?.id && auth?.rol === "user", }, ); - const { mutateAsync: remove, isLoading: isRemoving } = - api.mongo.remove.useMutation(); return (
@@ -155,32 +154,7 @@ const Mongo = (
{(auth?.rol === "admin" || user?.canDeleteServices) && ( - { - await remove({ mongoId }) - .then(() => { - router.push( - `/dashboard/project/${data?.projectId}`, - ); - toast.success("Mongo deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting the mongo"); - }); - }} - > - - + )}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx index a6117a3a..5b851015 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx @@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show- import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { ShowExternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-external-mysql-credentials"; @@ -68,8 +69,6 @@ const MySql = ( }, ); - const { mutateAsync: remove, isLoading: isRemoving } = - api.mysql.remove.useMutation(); return (
{(auth?.rol === "admin" || user?.canDeleteServices) && ( - { - await remove({ mysqlId }) - .then(() => { - router.push( - `/dashboard/project/${data?.projectId}`, - ); - toast.success("Mysql deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting the mysql"); - }); - }} - > - - + )}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx index d7b4b8d3..78637254 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx @@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show- import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command"; @@ -70,9 +71,6 @@ const Postgresql = ( }, ); - const { mutateAsync: remove, isLoading: isRemoving } = - api.postgres.remove.useMutation(); - return (
{(auth?.rol === "admin" || user?.canDeleteServices) && ( - { - await remove({ postgresId }) - .then(() => { - router.push( - `/dashboard/project/${data?.projectId}`, - ); - toast.success("Postgres deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting the postgres"); - }); - }} - > - - + )}
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx index 4a85eac0..1eef98e4 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx @@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show- import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; +import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command"; import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials"; @@ -68,8 +69,6 @@ const Redis = ( }, ); - const { mutateAsync: remove, isLoading: isRemoving } = - api.redis.remove.useMutation(); return (
{(auth?.rol === "admin" || user?.canDeleteServices) && ( - { - await remove({ redisId }) - .then(() => { - router.push( - `/dashboard/project/${data?.projectId}`, - ); - toast.success("Redis deleted successfully"); - }) - .catch(() => { - toast.error("Error deleting the redis"); - }); - }} - > - - + )}
diff --git a/apps/dokploy/pages/dashboard/settings/index.tsx b/apps/dokploy/pages/dashboard/settings/index.tsx new file mode 100644 index 00000000..bf76607b --- /dev/null +++ b/apps/dokploy/pages/dashboard/settings/index.tsx @@ -0,0 +1,220 @@ +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; + +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { DialogFooter } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; +import { appRouter } from "@/server/api/root"; +import { api } from "@/utils/api"; +import { validateRequest } from "@dokploy/server"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import { Settings } from "lucide-react"; +import type { GetServerSidePropsContext } from "next"; +import React, { useEffect, type ReactElement } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import superjson from "superjson"; +import { z } from "zod"; + +const settings = z.object({ + cleanCacheOnApplications: z.boolean(), + cleanCacheOnCompose: z.boolean(), + cleanCacheOnPreviews: z.boolean(), +}); + +type SettingsType = z.infer; + +const Page = () => { + const { data, refetch } = api.admin.one.useQuery(); + const { mutateAsync, isLoading, isError, error } = + api.admin.update.useMutation(); + const form = useForm({ + defaultValues: { + cleanCacheOnApplications: false, + cleanCacheOnCompose: false, + cleanCacheOnPreviews: false, + }, + resolver: zodResolver(settings), + }); + useEffect(() => { + form.reset({ + cleanCacheOnApplications: data?.cleanupCacheApplications || false, + cleanCacheOnCompose: data?.cleanupCacheOnCompose || false, + cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false, + }); + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); + + const onSubmit = async (values: SettingsType) => { + await mutateAsync({ + cleanupCacheApplications: values.cleanCacheOnApplications, + cleanupCacheOnCompose: values.cleanCacheOnCompose, + cleanupCacheOnPreviews: values.cleanCacheOnPreviews, + }) + .then(() => { + toast.success("Settings updated"); + refetch(); + }) + .catch(() => { + toast.error("Something went wrong"); + }); + }; + return ( +
+ +
+ + + + Settings + + Manage your Dokploy settings + {isError && {error?.message}} + + +
+ + ( + +
+ Clean Cache on Applications + + Clean the cache after every application deployment + +
+ + + +
+ )} + /> + ( + +
+ Clean Cache on Previews + + Clean the cache after every preview deployment + +
+ + + +
+ )} + /> + ( + +
+ Clean Cache on Compose + + Clean the cache after every compose deployment + +
+ + + +
+ )} + /> + + + + + +
+
+
+
+ ); +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return {page}; +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }>, +) { + const { req, res } = ctx; + const { user, session } = await validateRequest(ctx.req, ctx.res); + if (!user) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + if (user.rol === "user") { + return { + redirect: { + permanent: true, + destination: "/dashboard/settings/profile", + }, + }; + } + + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session, + user: user, + }, + transformer: superjson, + }); + await helpers.auth.get.prefetch(); + + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; +} diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index f40a0a83..3a8a60b2 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -8,11 +8,7 @@ import type { ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { - return ( - <> - - - ); + return ; }; export default Dashboard; diff --git a/apps/dokploy/public/locales/ml/common.json b/apps/dokploy/public/locales/ml/common.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/apps/dokploy/public/locales/ml/common.json @@ -0,0 +1 @@ +{} diff --git a/apps/dokploy/public/locales/ml/settings.json b/apps/dokploy/public/locales/ml/settings.json new file mode 100644 index 00000000..cb62b6ec --- /dev/null +++ b/apps/dokploy/public/locales/ml/settings.json @@ -0,0 +1,58 @@ +{ + "settings.common.save": "സേവ് ചെയ്യുക", + "settings.common.enterTerminal": "ടർമിനലിൽ പ്രവേശിക്കുക", + "settings.server.domain.title": "സർവർ ഡോമെയ്ൻ", + "settings.server.domain.description": "നിങ്ങളുടെ സർവർ അപ്ലിക്കേഷനിൽ ഒരു ഡോമെയ്ൻ ചേർക്കുക.", + "settings.server.domain.form.domain": "ഡോമെയ്ൻ", + "settings.server.domain.form.letsEncryptEmail": "ലെറ്റ്സ് എൻക്രിപ്റ്റ് ഇമെയിൽ", + "settings.server.domain.form.certificate.label": "സർട്ടിഫിക്കറ്റ് പ്രൊവൈഡർ", + "settings.server.domain.form.certificate.placeholder": "ഒരു സർട്ടിഫിക്കറ്റ് തിരഞ്ഞെടുക്കുക", + "settings.server.domain.form.certificateOptions.none": "ഒന്നുമില്ല", + "settings.server.domain.form.certificateOptions.letsencrypt": "ലെറ്റ്സ് എൻക്രിപ്റ്റ്", + + "settings.server.webServer.title": "വെബ് സർവർ", + "settings.server.webServer.description": "വെബ് സർവർ റീലോഡ് ചെയ്യുക അല്ലെങ്കിൽ ശുചീകരിക്കുക.", + "settings.server.webServer.actions": "നടപടികൾ", + "settings.server.webServer.reload": "റീലോഡ് ചെയ്യുക", + "settings.server.webServer.watchLogs": "ലോഗുകൾ കാണുക", + "settings.server.webServer.updateServerIp": "സർവർ IP അപ്ഡേറ്റ് ചെയ്യുക", + "settings.server.webServer.server.label": "സർവർ", + "settings.server.webServer.traefik.label": "ട്രാഫിക്", + "settings.server.webServer.traefik.modifyEnv": "ചുറ്റുപാടുകൾ മാറ്റുക", + "settings.server.webServer.traefik.managePorts": "അധിക പോർട്ട് മാപ്പിംഗ്", + "settings.server.webServer.traefik.managePortsDescription": "ട്രാഫിക്കിനായി അധിക പോർട്ടുകൾ ചേർക്കുക അല്ലെങ്കിൽ നീക്കം ചെയ്യുക", + "settings.server.webServer.traefik.targetPort": "ടാർഗറ്റ് പോർട്ട്", + "settings.server.webServer.traefik.publishedPort": "പ്രസിദ്ധീകരിച്ച പോർട്ട്", + "settings.server.webServer.traefik.addPort": "പോർട്ട് ചേർക്കുക", + "settings.server.webServer.traefik.portsUpdated": "പോർട്ടുകൾ വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു", + "settings.server.webServer.traefik.portsUpdateError": "പോർട്ടുകൾ അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു", + "settings.server.webServer.traefik.publishMode": "പ്രസിദ്ധീകരണ മോഡ്", + "settings.server.webServer.storage.label": "ഇടം", + "settings.server.webServer.storage.cleanUnusedImages": "ഉപയോഗിക്കാത്ത ഇമേജുകൾ ശുചീകരിക്കുക", + "settings.server.webServer.storage.cleanUnusedVolumes": "ഉപയോഗിക്കാത്ത വോള്യങ്ങൾ ശുചീകരിക്കുക", + "settings.server.webServer.storage.cleanStoppedContainers": "നിർത്തിയ കണ്ടെയ്‌നറുകൾ ശുചീകരിക്കുക", + "settings.server.webServer.storage.cleanDockerBuilder": "ഡോക്കർ ബിൽഡറും സിസ്റ്റവും ശുചീകരിക്കുക", + "settings.server.webServer.storage.cleanMonitoring": "മോണിറ്ററിംഗ് ശുചീകരിക്കുക", + "settings.server.webServer.storage.cleanAll": "എല്ലാം ശുചീകരിക്കുക", + + "settings.profile.title": "അക്കൗണ്ട്", + "settings.profile.description": "നിങ്ങളുടെ പ്രൊഫൈൽ വിശദാംശങ്ങൾ ഇവിടെ മാറ്റുക.", + "settings.profile.email": "ഇമെയിൽ", + "settings.profile.password": "പാസ്വേഡ്", + "settings.profile.avatar": "അവതാർ", + + "settings.appearance.title": "ദൃശ്യമാനം", + "settings.appearance.description": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന്റെ തീം ഇഷ്ടാനുസൃതമാക്കുക.", + "settings.appearance.theme": "തീം", + "settings.appearance.themeDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു തീം തിരഞ്ഞെടുക്കുക", + "settings.appearance.themes.light": "ലൈറ്റ്", + "settings.appearance.themes.dark": "ഡാർക്ക്", + "settings.appearance.themes.system": "സിസ്റ്റം", + "settings.appearance.language": "ഭാഷ", + "settings.appearance.languageDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക", + + "settings.terminal.connectionSettings": "കണക്ഷൻ ക്രമീകരണങ്ങൾ", + "settings.terminal.ipAddress": "IP വിലാസം", + "settings.terminal.port": "പോർട്ട്", + "settings.terminal.username": "ഉപയോക്തൃനാമം" +} diff --git a/apps/dokploy/public/locales/ru/settings.json b/apps/dokploy/public/locales/ru/settings.json index 1e71d710..0d87ed15 100644 --- a/apps/dokploy/public/locales/ru/settings.json +++ b/apps/dokploy/public/locales/ru/settings.json @@ -1,5 +1,6 @@ { "settings.common.save": "Сохранить", + "settings.common.enterTerminal": "Открыть терминал", "settings.server.domain.title": "Домен сервера", "settings.server.domain.description": "Установите домен для вашего серверного приложения Dokploy.", "settings.server.domain.form.domain": "Домен", @@ -7,18 +8,26 @@ "settings.server.domain.form.certificate.label": "Сертификат", "settings.server.domain.form.certificate.placeholder": "Выберите сертификат", "settings.server.domain.form.certificateOptions.none": "Нет", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (По умолчанию)", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", "settings.server.webServer.title": "Веб-сервер", "settings.server.webServer.description": "Перезагрузка или очистка веб-сервера.", - "settings.server.webServer.server.label": "Сервер", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.storage.label": "Дисковое пространство", "settings.server.webServer.actions": "Действия", "settings.server.webServer.reload": "Перезагрузить", "settings.server.webServer.watchLogs": "Просмотр логов", "settings.server.webServer.updateServerIp": "Изменить IP адрес", + "settings.server.webServer.server.label": "Сервер", + "settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.modifyEnv": "Изменить переменные окружения", + "settings.server.webServer.traefik.managePorts": "Назначение портов", + "settings.server.webServer.traefik.managePortsDescription": "Добавить или удалить дополнительные порты для Traefik", + "settings.server.webServer.traefik.targetPort": "Внутренний порт", + "settings.server.webServer.traefik.publishedPort": "Внешний порт", + "settings.server.webServer.traefik.addPort": "Добавить порт", + "settings.server.webServer.traefik.portsUpdated": "Порты успешно обновлены", + "settings.server.webServer.traefik.portsUpdateError": "Не удалось обновить порты", + "settings.server.webServer.traefik.publishMode": "Режим сопоставления", + "settings.server.webServer.storage.label": "Дисковое пространство", "settings.server.webServer.storage.cleanUnusedImages": "Очистить неиспользуемые образы", "settings.server.webServer.storage.cleanUnusedVolumes": "Очистить неиспользуемые тома", "settings.server.webServer.storage.cleanStoppedContainers": "Очистить остановленные контейнеры", @@ -40,5 +49,10 @@ "settings.appearance.themes.dark": "Темная", "settings.appearance.themes.system": "Системная", "settings.appearance.language": "Язык", - "settings.appearance.languageDescription": "Select a language for your dashboard" + "settings.appearance.languageDescription": "Выберите язык для панели управления", + + "settings.terminal.connectionSettings": "Настройки подключения", + "settings.terminal.ipAddress": "IP адрес", + "settings.terminal.port": "Порт", + "settings.terminal.username": "Имя пользователя" } diff --git a/apps/dokploy/public/locales/uk/common.json b/apps/dokploy/public/locales/uk/common.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/apps/dokploy/public/locales/uk/common.json @@ -0,0 +1 @@ +{} diff --git a/apps/dokploy/public/locales/uk/settings.json b/apps/dokploy/public/locales/uk/settings.json new file mode 100644 index 00000000..766a1bff --- /dev/null +++ b/apps/dokploy/public/locales/uk/settings.json @@ -0,0 +1,58 @@ +{ + "settings.common.save": "Зберегти", + "settings.common.enterTerminal": "Увійти в термінал", + "settings.server.domain.title": "Домен сервера", + "settings.server.domain.description": "Додайте домен до вашого серверного застосунку.", + "settings.server.domain.form.domain": "Домен", + "settings.server.domain.form.letsEncryptEmail": "Електронна пошта для Let's Encrypt", + "settings.server.domain.form.certificate.label": "Постачальник сертифікатів", + "settings.server.domain.form.certificate.placeholder": "Оберіть сертифікат", + "settings.server.domain.form.certificateOptions.none": "Відсутній", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", + + "settings.server.webServer.title": "Веб-сервер", + "settings.server.webServer.description": "Перезавантажте або очистьте веб-сервер.", + "settings.server.webServer.actions": "Дії", + "settings.server.webServer.reload": "Перезавантажити", + "settings.server.webServer.watchLogs": "Перегляд логів", + "settings.server.webServer.updateServerIp": "Оновити IP-адресу сервера", + "settings.server.webServer.server.label": "Сервер", + "settings.server.webServer.traefik.label": "Traefik", + "settings.server.webServer.traefik.modifyEnv": "Змінити середовище", + "settings.server.webServer.traefik.managePorts": "Додаткові порти", + "settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik", + "settings.server.webServer.traefik.targetPort": "Цільовий порт", + "settings.server.webServer.traefik.publishedPort": "Опублікований порт", + "settings.server.webServer.traefik.addPort": "Додати порт", + "settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено", + "settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти", + "settings.server.webServer.traefik.publishMode": "Режим публікації", + "settings.server.webServer.storage.label": "Дисковий простір", + "settings.server.webServer.storage.cleanUnusedImages": "Очистити невикористані образи", + "settings.server.webServer.storage.cleanUnusedVolumes": "Очистити невикористані томи", + "settings.server.webServer.storage.cleanStoppedContainers": "Очистити зупинені контейнери", + "settings.server.webServer.storage.cleanDockerBuilder": "Очистити Docker Builder і систему", + "settings.server.webServer.storage.cleanMonitoring": "Очистити моніторинг", + "settings.server.webServer.storage.cleanAll": "Очистити все", + + "settings.profile.title": "Обліковий запис", + "settings.profile.description": "Змініть дані вашого профілю.", + "settings.profile.email": "Електронна пошта", + "settings.profile.password": "Пароль", + "settings.profile.avatar": "Аватар", + + "settings.appearance.title": "Зовнішній вигляд", + "settings.appearance.description": "Налаштуйте тему вашої панелі керування.", + "settings.appearance.theme": "Тема", + "settings.appearance.themeDescription": "Оберіть тему для вашої панелі керування", + "settings.appearance.themes.light": "Світла", + "settings.appearance.themes.dark": "Темна", + "settings.appearance.themes.system": "Системна", + "settings.appearance.language": "Мова", + "settings.appearance.languageDescription": "Оберіть мову для вашої панелі керування", + + "settings.terminal.connectionSettings": "Налаштування з'єднання", + "settings.terminal.ipAddress": "IP-адреса", + "settings.terminal.port": "Порт", + "settings.terminal.username": "Ім'я користувача" +} diff --git a/apps/dokploy/public/templates/alist.svg b/apps/dokploy/public/templates/alist.svg new file mode 100644 index 00000000..37d5fdcd --- /dev/null +++ b/apps/dokploy/public/templates/alist.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/apps/dokploy/public/templates/answer.png b/apps/dokploy/public/templates/answer.png new file mode 100644 index 00000000..3fca604d Binary files /dev/null and b/apps/dokploy/public/templates/answer.png differ diff --git a/apps/dokploy/public/templates/couchdb.png b/apps/dokploy/public/templates/couchdb.png new file mode 100644 index 00000000..7dc4cc4a Binary files /dev/null and b/apps/dokploy/public/templates/couchdb.png differ diff --git a/apps/dokploy/public/templates/erpnext.svg b/apps/dokploy/public/templates/erpnext.svg new file mode 100644 index 00000000..d699ea2a --- /dev/null +++ b/apps/dokploy/public/templates/erpnext.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/dokploy/public/templates/glance.png b/apps/dokploy/public/templates/glance.png new file mode 100644 index 00000000..54fc4131 Binary files /dev/null and b/apps/dokploy/public/templates/glance.png differ diff --git a/apps/dokploy/public/templates/homarr.png b/apps/dokploy/public/templates/homarr.png new file mode 100644 index 00000000..25581ea5 Binary files /dev/null and b/apps/dokploy/public/templates/homarr.png differ diff --git a/apps/dokploy/public/templates/it-tools.svg b/apps/dokploy/public/templates/it-tools.svg new file mode 100644 index 00000000..1e3b614d --- /dev/null +++ b/apps/dokploy/public/templates/it-tools.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/dokploy/public/templates/maybe.svg b/apps/dokploy/public/templates/maybe.svg new file mode 100644 index 00000000..a4a87736 --- /dev/null +++ b/apps/dokploy/public/templates/maybe.svg @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/apps/dokploy/public/templates/spacedrive.png b/apps/dokploy/public/templates/spacedrive.png new file mode 100644 index 00000000..a10fffd5 Binary files /dev/null and b/apps/dokploy/public/templates/spacedrive.png differ diff --git a/apps/dokploy/public/templates/superset.svg b/apps/dokploy/public/templates/superset.svg new file mode 100644 index 00000000..522c3b28 --- /dev/null +++ b/apps/dokploy/public/templates/superset.svg @@ -0,0 +1,9 @@ + + + Superset + + + + + + diff --git a/apps/dokploy/public/templates/twenty.svg b/apps/dokploy/public/templates/twenty.svg index cf5223b9..bad18fab 100644 --- a/apps/dokploy/public/templates/twenty.svg +++ b/apps/dokploy/public/templates/twenty.svg @@ -1,6 +1,12 @@ - - - - - + + + + + + + + + + + diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index 09f4d675..6e85d274 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -9,6 +9,7 @@ import { apiSaveExternalPortMariaDB, apiUpdateMariaDB, } from "@/server/db/schema"; +import { cancelJobs } from "@/server/utils/backup"; import { IS_CLOUD, addNewService, @@ -16,6 +17,7 @@ import { createMariadb, createMount, deployMariadb, + findBackupsByDbId, findMariadbById, findProjectById, findServerById, @@ -211,8 +213,10 @@ export const mariadbRouter = createTRPCRouter({ }); } + const backups = await findBackupsByDbId(input.mariadbId, "mariadb"); const cleanupOperations = [ async () => await removeService(mongo?.appName, mongo.serverId), + async () => await cancelJobs(backups), async () => await removeMariadbById(input.mariadbId), ]; diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index b114b8d8..2bca3ec5 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -9,6 +9,7 @@ import { apiSaveExternalPortMongo, apiUpdateMongo, } from "@/server/db/schema"; +import { cancelJobs } from "@/server/utils/backup"; import { IS_CLOUD, addNewService, @@ -16,6 +17,7 @@ import { createMongo, createMount, deployMongo, + findBackupsByDbId, findMongoById, findProjectById, removeMongoById, @@ -252,9 +254,11 @@ export const mongoRouter = createTRPCRouter({ message: "You are not authorized to delete this mongo", }); } + const backups = await findBackupsByDbId(input.mongoId, "mongo"); const cleanupOperations = [ async () => await removeService(mongo?.appName, mongo.serverId), + async () => await cancelJobs(backups), async () => await removeMongoById(input.mongoId), ]; diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index 44d2537a..7ebf4623 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -12,6 +12,7 @@ import { import { TRPCError } from "@trpc/server"; +import { cancelJobs } from "@/server/utils/backup"; import { IS_CLOUD, addNewService, @@ -19,6 +20,7 @@ import { createMount, createMysql, deployMySql, + findBackupsByDbId, findMySqlById, findProjectById, removeMySqlById, @@ -249,8 +251,10 @@ export const mysqlRouter = createTRPCRouter({ }); } + const backups = await findBackupsByDbId(input.mysqlId, "mysql"); const cleanupOperations = [ async () => await removeService(mongo?.appName, mongo.serverId), + async () => await cancelJobs(backups), async () => await removeMySqlById(input.mysqlId), ]; diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index f8869503..2eafc66d 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -2,20 +2,24 @@ import { adminProcedure, createTRPCRouter, protectedProcedure, + publicProcedure, } from "@/server/api/trpc"; import { db } from "@/server/db"; import { apiCreateDiscord, apiCreateEmail, + apiCreateGotify, apiCreateSlack, apiCreateTelegram, apiFindOneNotification, apiTestDiscordConnection, apiTestEmailConnection, + apiTestGotifyConnection, apiTestSlackConnection, apiTestTelegramConnection, apiUpdateDiscord, apiUpdateEmail, + apiUpdateGotify, apiUpdateSlack, apiUpdateTelegram, notifications, @@ -24,16 +28,19 @@ import { IS_CLOUD, createDiscordNotification, createEmailNotification, + createGotifyNotification, createSlackNotification, createTelegramNotification, findNotificationById, removeNotificationById, sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, updateDiscordNotification, updateEmailNotification, + updateGotifyNotification, updateSlackNotification, updateTelegramNotification, } from "@dokploy/server"; @@ -300,10 +307,61 @@ export const notificationRouter = createTRPCRouter({ telegram: true, discord: true, email: true, + gotify: true, }, orderBy: desc(notifications.createdAt), ...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }), // TODO: Remove this line when the cloud version is ready }); }), + createGotify: adminProcedure + .input(apiCreateGotify) + .mutation(async ({ input, ctx }) => { + try { + return await createGotifyNotification(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateGotify: adminProcedure + .input(apiUpdateGotify) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateGotifyNotification({ + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw error; + } + }), + testGotifyConnection: adminProcedure + .input(apiTestGotifyConnection) + .mutation(async ({ input }) => { + try { + await sendGotifyNotification( + input, + "Test Notification", + "Hi, From Dokploy 👋", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), }); diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 7d178943..92603a61 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -14,6 +14,7 @@ import { apiSaveExternalPortPostgres, apiUpdatePostgres, } from "@/server/db/schema"; +import { cancelJobs } from "@/server/utils/backup"; import { IS_CLOUD, addNewService, @@ -21,6 +22,7 @@ import { createMount, createPostgres, deployPostgres, + findBackupsByDbId, findPostgresById, findProjectById, removePostgresById, @@ -231,8 +233,11 @@ export const postgresRouter = createTRPCRouter({ }); } + const backups = await findBackupsByDbId(input.postgresId, "postgres"); + const cleanupOperations = [ removeService(postgres.appName, postgres.serverId), + cancelJobs(backups), removePostgresById(input.postgresId), ]; diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 967b39e3..9c2608cc 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -68,7 +68,7 @@ export const projectRouter = createTRPCRouter({ .input(apiFindOneProject) .query(async ({ input, ctx }) => { if (ctx.user.rol === "user") { - const { accesedServices } = await findUserByAuthId(ctx.user.authId); + const { accessedServices } = await findUserByAuthId(ctx.user.authId); await checkProjectAccess(ctx.user.authId, "access", input.projectId); @@ -79,28 +79,28 @@ export const projectRouter = createTRPCRouter({ ), with: { compose: { - where: buildServiceFilter(compose.composeId, accesedServices), + where: buildServiceFilter(compose.composeId, accessedServices), }, applications: { where: buildServiceFilter( applications.applicationId, - accesedServices, + accessedServices, ), }, mariadb: { - where: buildServiceFilter(mariadb.mariadbId, accesedServices), + where: buildServiceFilter(mariadb.mariadbId, accessedServices), }, mongo: { - where: buildServiceFilter(mongo.mongoId, accesedServices), + where: buildServiceFilter(mongo.mongoId, accessedServices), }, mysql: { - where: buildServiceFilter(mysql.mysqlId, accesedServices), + where: buildServiceFilter(mysql.mysqlId, accessedServices), }, postgres: { - where: buildServiceFilter(postgres.postgresId, accesedServices), + where: buildServiceFilter(postgres.postgresId, accessedServices), }, redis: { - where: buildServiceFilter(redis.redisId, accesedServices), + where: buildServiceFilter(redis.redisId, accessedServices), }, }, }); @@ -125,18 +125,18 @@ export const projectRouter = createTRPCRouter({ }), all: protectedProcedure.query(async ({ ctx }) => { if (ctx.user.rol === "user") { - const { accesedProjects, accesedServices } = await findUserByAuthId( + const { accessedProjects, accessedServices } = await findUserByAuthId( ctx.user.authId, ); - if (accesedProjects.length === 0) { + if (accessedProjects.length === 0) { return []; } const query = await db.query.projects.findMany({ where: and( sql`${projects.projectId} IN (${sql.join( - accesedProjects.map((projectId) => sql`${projectId}`), + accessedProjects.map((projectId) => sql`${projectId}`), sql`, `, )})`, eq(projects.adminId, ctx.user.adminId), @@ -145,27 +145,27 @@ export const projectRouter = createTRPCRouter({ applications: { where: buildServiceFilter( applications.applicationId, - accesedServices, + accessedServices, ), with: { domains: true }, }, mariadb: { - where: buildServiceFilter(mariadb.mariadbId, accesedServices), + where: buildServiceFilter(mariadb.mariadbId, accessedServices), }, mongo: { - where: buildServiceFilter(mongo.mongoId, accesedServices), + where: buildServiceFilter(mongo.mongoId, accessedServices), }, mysql: { - where: buildServiceFilter(mysql.mysqlId, accesedServices), + where: buildServiceFilter(mysql.mysqlId, accessedServices), }, postgres: { - where: buildServiceFilter(postgres.postgresId, accesedServices), + where: buildServiceFilter(postgres.postgresId, accessedServices), }, redis: { - where: buildServiceFilter(redis.redisId, accesedServices), + where: buildServiceFilter(redis.redisId, accessedServices), }, compose: { - where: buildServiceFilter(compose.composeId, accesedServices), + where: buildServiceFilter(compose.composeId, accessedServices), with: { domains: true }, }, }, @@ -239,10 +239,13 @@ export const projectRouter = createTRPCRouter({ } }), }); -function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) { - return accesedServices.length > 0 +function buildServiceFilter( + fieldName: AnyPgColumn, + accessedServices: string[], +) { + return accessedServices.length > 0 ? sql`${fieldName} IN (${sql.join( - accesedServices.map((serviceId) => sql`${serviceId}`), + accessedServices.map((serviceId) => sql`${serviceId}`), sql`, `, )})` : sql`1 = 0`; diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 5883e50b..967fb51a 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -244,7 +244,6 @@ export const redisRouter = createTRPCRouter({ message: "You are not authorized to delete this Redis", }); } - const cleanupOperations = [ async () => await removeService(redis?.appName, redis.serverId), async () => await removeRedisById(input.redisId), diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 449a2233..cb0e32d9 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -345,7 +345,7 @@ export const settingsRouter = createTRPCRouter({ writeConfig("middlewares", input.traefikConfig); return true; }), - getUpdateData: adminProcedure.mutation(async () => { + getUpdateData: protectedProcedure.mutation(async () => { if (IS_CLOUD) { return DEFAULT_UPDATE_DATA; } @@ -373,10 +373,10 @@ export const settingsRouter = createTRPCRouter({ return true; }), - getDokployVersion: adminProcedure.query(() => { + getDokployVersion: protectedProcedure.query(() => { return packageInfo.version; }), - getReleaseTag: adminProcedure.query(() => { + getReleaseTag: protectedProcedure.query(() => { return getDokployImageTag(); }), readDirectories: protectedProcedure diff --git a/apps/dokploy/server/utils/backup.ts b/apps/dokploy/server/utils/backup.ts index a178063f..2f141971 100644 --- a/apps/dokploy/server/utils/backup.ts +++ b/apps/dokploy/server/utils/backup.ts @@ -1,3 +1,10 @@ +import { + type BackupScheduleList, + IS_CLOUD, + removeScheduleBackup, + scheduleBackup, +} from "@dokploy/server/index"; + type QueueJob = | { type: "backup"; @@ -59,3 +66,19 @@ export const updateJob = async (job: QueueJob) => { throw error; } }; + +export const cancelJobs = async (backups: BackupScheduleList) => { + for (const backup of backups) { + if (backup.enabled) { + if (IS_CLOUD) { + await removeJob({ + cronSchedule: backup.schedule, + backupId: backup.backupId, + type: "backup", + }); + } else { + removeScheduleBackup(backup.backupId); + } + } + } +}; diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css index 4cf9bef4..74a1d276 100644 --- a/apps/dokploy/styles/globals.css +++ b/apps/dokploy/styles/globals.css @@ -4,6 +4,7 @@ @layer base { :root { + --terminal-paste: rgba(0, 0, 0, 0.2); --background: 0 0% 100%; --foreground: 240 10% 3.9%; @@ -51,6 +52,7 @@ } .dark { + --terminal-paste: rgba(255, 255, 255, 0.2); --background: 0 0% 0%; --foreground: 0 0% 98%; @@ -101,9 +103,29 @@ * { @apply border-border; } + body { @apply bg-background text-foreground; } + + /* Custom scrollbar styling */ + ::-webkit-scrollbar { + width: 0.3125rem; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 0.3125rem; + } + + * { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border)) transparent; + } } .xterm-viewport { @@ -215,3 +237,8 @@ background-color: hsl(var(--muted-foreground) / 0.5); } } + +.xterm-bg-257.xterm-fg-257 { + background-color: var(--terminal-paste) !important; + color: currentColor !important; +} diff --git a/apps/dokploy/templates/alist/docker-compose.yml b/apps/dokploy/templates/alist/docker-compose.yml new file mode 100644 index 00000000..9ff67c94 --- /dev/null +++ b/apps/dokploy/templates/alist/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.3' +services: + alist: + image: xhofe/alist:v3.41.0 + volumes: + - alist-data:/opt/alist/data + environment: + - PUID=0 + - PGID=0 + - UMASK=022 + restart: unless-stopped + +volumes: + alist-data: \ No newline at end of file diff --git a/apps/dokploy/templates/alist/index.ts b/apps/dokploy/templates/alist/index.ts new file mode 100644 index 00000000..2a27f570 --- /dev/null +++ b/apps/dokploy/templates/alist/index.ts @@ -0,0 +1,22 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 5244, + serviceName: "alist", + }, + ]; + + return { + domains, + }; +} diff --git a/apps/dokploy/templates/answer/docker-compose.yml b/apps/dokploy/templates/answer/docker-compose.yml new file mode 100644 index 00000000..e17a6d1e --- /dev/null +++ b/apps/dokploy/templates/answer/docker-compose.yml @@ -0,0 +1,31 @@ +services: + answer: + image: apache/answer:1.4.1 + ports: + - '80' + restart: on-failure + volumes: + - answer-data:/data + depends_on: + db: + condition: service_healthy + db: + image: postgres:16 + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - dokploy-network + volumes: + - db-data:/var/lib/postgresql/data + environment: + POSTGRES_DB: answer + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + +volumes: + answer-data: + db-data: diff --git a/apps/dokploy/templates/answer/index.ts b/apps/dokploy/templates/answer/index.ts new file mode 100644 index 00000000..36d48cb3 --- /dev/null +++ b/apps/dokploy/templates/answer/index.ts @@ -0,0 +1,33 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateHash, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainServiceHash = generateHash(schema.projectName); + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 9080, + serviceName: "answer", + }, + ]; + + const envs = [ + `ANSWER_HOST=http://${mainDomain}`, + `SERVICE_HASH=${mainServiceHash}`, + ]; + + const mounts: Template["mounts"] = []; + + return { + envs, + mounts, + domains, + }; +} diff --git a/apps/dokploy/templates/couchdb/docker-compose.yml b/apps/dokploy/templates/couchdb/docker-compose.yml new file mode 100644 index 00000000..cb00bf69 --- /dev/null +++ b/apps/dokploy/templates/couchdb/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + couchdb: + image: couchdb:latest + ports: + - '5984' + volumes: + - couchdb-data:/opt/couchdb/data + environment: + - COUCHDB_USER=${COUCHDB_USER} + - COUCHDB_PASSWORD=${COUCHDB_PASSWORD} + restart: unless-stopped + +volumes: + couchdb-data: + driver: local diff --git a/apps/dokploy/templates/couchdb/index.ts b/apps/dokploy/templates/couchdb/index.ts new file mode 100644 index 00000000..70d71669 --- /dev/null +++ b/apps/dokploy/templates/couchdb/index.ts @@ -0,0 +1,28 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const username = generatePassword(16); + const password = generatePassword(32); + + const domains: DomainSchema[] = [ + { + serviceName: "couchdb", + host: mainDomain, + port: 5984, + }, + ]; + + const envs = [`COUCHDB_USER=${username}`, `COUCHDB_PASSWORD=${password}`]; + + return { + envs, + domains, + }; +} diff --git a/apps/dokploy/templates/erpnext/docker-compose.yml b/apps/dokploy/templates/erpnext/docker-compose.yml new file mode 100644 index 00000000..def916be --- /dev/null +++ b/apps/dokploy/templates/erpnext/docker-compose.yml @@ -0,0 +1,346 @@ +x-custom-image: &custom_image + image: ${IMAGE_NAME:-docker.io/frappe/erpnext}:${VERSION:-version-15} + pull_policy: ${PULL_POLICY:-always} + deploy: + restart_policy: + condition: always + +services: + backend: + <<: *custom_image + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - '0.0.0.0:8000' + interval: 2s + timeout: 10s + retries: 30 + + frontend: + <<: *custom_image + command: + - nginx-entrypoint.sh + depends_on: + backend: + condition: service_started + required: true + websocket: + condition: service_started + required: true + environment: + BACKEND: backend:8000 + FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host} + SOCKETIO: websocket:9000 + UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 + UPSTREAM_REAL_IP_HEADER: X-Forwarded-For + UPSTREAM_REAL_IP_RECURSIVE: "off" + volumes: + - sites:/home/frappe/frappe-bench/sites + + networks: + - bench-network + + healthcheck: + test: + - CMD + - wait-for-it + - '0.0.0.0:8080' + interval: 2s + timeout: 30s + retries: 30 + + queue-default: + <<: *custom_image + command: + - bench + - worker + - --queue + - default + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + required: true + + queue-long: + <<: *custom_image + command: + - bench + - worker + - --queue + - long + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + required: true + + queue-short: + <<: *custom_image + command: + - bench + - worker + - --queue + - short + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + required: true + + scheduler: + <<: *custom_image + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + command: + - bench + - schedule + depends_on: + configurator: + condition: service_completed_successfully + required: true + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + websocket: + <<: *custom_image + healthcheck: + test: + - CMD + - wait-for-it + - '0.0.0.0:9000' + interval: 2s + timeout: 10s + retries: 30 + command: + - node + - /home/frappe/frappe-bench/apps/frappe/socketio.js + depends_on: + configurator: + condition: service_completed_successfully + required: true + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + configurator: + <<: *custom_image + deploy: + mode: replicated + replicas: ${CONFIGURE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + [[ $${REGENERATE_APPS_TXT} == "1" ]] && ls -1 apps > sites/apps.txt; + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && exit 0; + bench set-config -g db_host $$DB_HOST; + bench set-config -gp db_port $$DB_PORT; + bench set-config -g redis_cache "redis://$$REDIS_CACHE"; + bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; + bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; + bench set-config -gp socketio_port $$SOCKETIO_PORT; + environment: + DB_HOST: db + DB_PORT: "3306" + REDIS_CACHE: redis-cache:6379 + REDIS_QUEUE: redis-queue:6379 + SOCKETIO_PORT: "9000" + REGENERATE_APPS_TXT: "${REGENERATE_APPS_TXT:-0}" + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + create-site: + <<: *custom_image + deploy: + mode: replicated + replicas: ${CREATE_SITE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + wait-for-it -t 120 db:3306; + wait-for-it -t 120 redis-cache:6379; + wait-for-it -t 120 redis-queue:6379; + export start=`date +%s`; + until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; + do + echo "Waiting for sites/common_site_config.json to be created"; + sleep 5; + if (( `date +%s`-start > 120 )); then + echo "could not find sites/common_site_config.json with required keys"; + exit 1 + fi + done; + echo "sites/common_site_config.json found"; + [[ -d "sites/${SITE_NAME}" ]] && echo "${SITE_NAME} already exists" && exit 0; + bench new-site --mariadb-user-host-login-scope='%' --admin-password=$${ADMIN_PASSWORD} --db-root-username=root --db-root-password=$${DB_ROOT_PASSWORD} $${INSTALL_APP_ARGS} $${SITE_NAME}; + volumes: + - sites:/home/frappe/frappe-bench/sites + environment: + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + INSTALL_APP_ARGS: ${INSTALL_APP_ARGS} + SITE_NAME: ${SITE_NAME} + networks: + - bench-network + + migration: + <<: *custom_image + deploy: + mode: replicated + replicas: ${MIGRATE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + curl -f http://${SITE_NAME}:8080/api/method/ping || echo "Site busy" && exit 0; + bench --site all set-config -p maintenance_mode 1; + bench --site all set-config -p pause_scheduler 1; + bench --site all migrate; + bench --site all set-config -p maintenance_mode 0; + bench --site all set-config -p pause_scheduler 0; + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + db: + image: mariadb:10.6 + deploy: + restart_policy: + condition: always + healthcheck: + test: mysqladmin ping -h localhost --password=${DB_ROOT_PASSWORD} + interval: 1s + retries: 20 + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --skip-character-set-client-handshake + - --skip-innodb-read-only-compressed + environment: + - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + volumes: + - db-data:/var/lib/mysql + networks: + - bench-network + + redis-cache: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-cache-data:/data + networks: + - bench-network + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 3 + + redis-queue: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-queue-data:/data + networks: + - bench-network + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 3 + + redis-socketio: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-socketio-data:/data + networks: + - bench-network + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 3 + +volumes: + db-data: + redis-cache-data: + redis-queue-data: + redis-socketio-data: + sites: + +networks: + bench-network: \ No newline at end of file diff --git a/apps/dokploy/templates/erpnext/index.ts b/apps/dokploy/templates/erpnext/index.ts new file mode 100644 index 00000000..0be46c44 --- /dev/null +++ b/apps/dokploy/templates/erpnext/index.ts @@ -0,0 +1,37 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const dbRootPassword = generatePassword(32); + const adminPassword = generatePassword(32); + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "frontend", + }, + ]; + + const envs = [ + `SITE_NAME=${mainDomain}`, + `ADMIN_PASSWORD=${adminPassword}`, + `DB_ROOT_PASSWORD=${dbRootPassword}`, + "MIGRATE=1", + "CREATE_SITE=1", + "CONFIGURE=1", + "REGENERATE_APPS_TXT=1", + "INSTALL_APP_ARGS=--install-app erpnext", + "IMAGE_NAME=docker.io/frappe/erpnext", + "VERSION=version-15", + "FRAPPE_SITE_NAME_HEADER=", + ]; + + return { envs, domains }; +} diff --git a/apps/dokploy/templates/glance/docker-compose.yml b/apps/dokploy/templates/glance/docker-compose.yml new file mode 100644 index 00000000..e931d6e4 --- /dev/null +++ b/apps/dokploy/templates/glance/docker-compose.yml @@ -0,0 +1,8 @@ +services: + glance: + image: glanceapp/glance + volumes: + - ../files/app/glance.yml:/app/glance.yml + ports: + - 8080 + restart: unless-stopped diff --git a/apps/dokploy/templates/glance/index.ts b/apps/dokploy/templates/glance/index.ts new file mode 100644 index 00000000..4b229786 --- /dev/null +++ b/apps/dokploy/templates/glance/index.ts @@ -0,0 +1,108 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "glance", + }, + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "/app/glance.yml", + content: ` +branding: + hide-footer: true + logo-text: P + +pages: + - name: Home + columns: + - size: small + widgets: + - type: calendar + + - type: releases + show-source-icon: true + repositories: + - Dokploy/dokploy + - n8n-io/n8n + - Budibase/budibase + - home-assistant/core + - tidbyt/pixlet + + - type: twitch-channels + channels: + - nmplol + - extraemily + - qtcinderella + - ludwig + - timthetatman + - mizkif + + - size: full + widgets: + - type: hacker-news + + - type: videos + style: grid-cards + channels: + - UC3GzdWYwUYI1ACxuP9Nm-eg + - UCGbg3DjQdcqWwqOLHpYHXIg + - UC24RSoLcjiNZbQcT54j5l7Q + limit: 3 + + - type: rss + limit: 10 + collapse-after: 3 + cache: 3h + feeds: + - url: https://daringfireball.net/feeds/main + title: Daring Fireball + + - size: small + widgets: + - type: weather + location: Gansevoort, New York, United States + show-area-name: false + units: imperial + hour-format: 12h + + - type: markets + markets: + - symbol: SPY + name: S&P 500 + - symbol: VOO + name: Vanguard + - symbol: BTC-USD + name: Bitcoin + - symbol: ETH-USD + name: Etherium + - symbol: NVDA + name: NVIDIA + - symbol: AAPL + name: Apple + - symbol: MSFT + name: Microsoft + - symbol: GOOGL + name: Google + - symbol: AMD + name: AMD + - symbol: TSLA + name: Tesla`, + }, + ]; + + return { + domains, + mounts, + }; +} diff --git a/apps/dokploy/templates/homarr/docker-compose.yml b/apps/dokploy/templates/homarr/docker-compose.yml new file mode 100644 index 00000000..876ea3f6 --- /dev/null +++ b/apps/dokploy/templates/homarr/docker-compose.yml @@ -0,0 +1,11 @@ +services: + homarr: + image: ghcr.io/homarr-labs/homarr:latest + restart: unless-stopped + volumes: + # - /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration + - ../homarr/appdata:/appdata + environment: + - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} + ports: + - 7575 diff --git a/apps/dokploy/templates/homarr/index.ts b/apps/dokploy/templates/homarr/index.ts new file mode 100644 index 00000000..eb5a9f82 --- /dev/null +++ b/apps/dokploy/templates/homarr/index.ts @@ -0,0 +1,27 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretKey = generatePassword(64); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 7575, + serviceName: "homarr", + }, + ]; + + const envs = [`SECRET_ENCRYPTION_KEY=${secretKey}`]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/it-tools/docker-compose.yml b/apps/dokploy/templates/it-tools/docker-compose.yml new file mode 100644 index 00000000..b26665f8 --- /dev/null +++ b/apps/dokploy/templates/it-tools/docker-compose.yml @@ -0,0 +1,8 @@ +services: + it-tools: + image: corentinth/it-tools:latest + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:80"] + interval: 30s + timeout: 10s + retries: 3 diff --git a/apps/dokploy/templates/it-tools/index.ts b/apps/dokploy/templates/it-tools/index.ts new file mode 100644 index 00000000..9912c4ba --- /dev/null +++ b/apps/dokploy/templates/it-tools/index.ts @@ -0,0 +1,20 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 80, + serviceName: "it-tools", + }, + ]; + + return { + domains, + }; +} diff --git a/apps/dokploy/templates/maybe/docker-compose.yml b/apps/dokploy/templates/maybe/docker-compose.yml new file mode 100644 index 00000000..017ca29b --- /dev/null +++ b/apps/dokploy/templates/maybe/docker-compose.yml @@ -0,0 +1,37 @@ +services: + app: + image: ghcr.io/maybe-finance/maybe:sha-68c570eed8810fd59b5b33cca51bbad5eabb4cb4 + restart: unless-stopped + volumes: + - ../files/uploads:/app/uploads + environment: + DATABASE_URL: postgresql://maybe:maybe@db:5432/maybe + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + SELF_HOSTED: true + SYNTH_API_KEY: ${SYNTH_API_KEY} + RAILS_FORCE_SSL: "false" + RAILS_ASSUME_SSL: "false" + GOOD_JOB_EXECUTION_MODE: async + depends_on: + db: + condition: service_healthy + + db: + image: postgres:16 + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - dokploy-network + volumes: + - db-data:/var/lib/postgresql/data + environment: + POSTGRES_DB: maybe + POSTGRES_USER: maybe + POSTGRES_PASSWORD: maybe + +volumes: + db-data: diff --git a/apps/dokploy/templates/maybe/index.ts b/apps/dokploy/templates/maybe/index.ts new file mode 100644 index 00000000..5eaf7a81 --- /dev/null +++ b/apps/dokploy/templates/maybe/index.ts @@ -0,0 +1,43 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretKeyBase = generateBase64(64); + const synthApiKey = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "app", + }, + ]; + + const envs = [ + `SECRET_KEY_BASE=${secretKeyBase}`, + "SELF_HOSTED=true", + `SYNTH_API_KEY=${synthApiKey}`, + "RAILS_FORCE_SSL=false", + "RAILS_ASSUME_SSL=false", + "GOOD_JOB_EXECUTION_MODE=async", + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "./uploads", + content: "This is where user uploads will be stored", + }, + ]; + + return { + envs, + mounts, + domains, + }; +} diff --git a/apps/dokploy/templates/spacedrive/docker-compose.yml b/apps/dokploy/templates/spacedrive/docker-compose.yml new file mode 100644 index 00000000..b98d55ab --- /dev/null +++ b/apps/dokploy/templates/spacedrive/docker-compose.yml @@ -0,0 +1,9 @@ +services: + server: + image: ghcr.io/spacedriveapp/spacedrive/server:latest + ports: + - 8080 + environment: + - SD_AUTH=${SD_USERNAME}:${SD_PASSWORD} + volumes: + - /var/spacedrive:/var/spacedrive diff --git a/apps/dokploy/templates/spacedrive/index.ts b/apps/dokploy/templates/spacedrive/index.ts new file mode 100644 index 00000000..15db4b19 --- /dev/null +++ b/apps/dokploy/templates/spacedrive/index.ts @@ -0,0 +1,28 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + const secretKey = generatePassword(); + const randomUsername = "admin"; // Default username + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 8080, + serviceName: "server", + }, + ]; + + const envs = [`SD_USERNAME=${randomUsername}`, `SD_PASSWORD=${secretKey}`]; + + return { + envs, + domains, + }; +} diff --git a/apps/dokploy/templates/superset/docker-compose.yml b/apps/dokploy/templates/superset/docker-compose.yml new file mode 100644 index 00000000..e3fb3454 --- /dev/null +++ b/apps/dokploy/templates/superset/docker-compose.yml @@ -0,0 +1,65 @@ +# Note: this is an UNOFFICIAL production docker image build for Superset: +# - https://github.com/amancevice/docker-superset +# +# After deploying this image, you will need to run one of the two +# commands below in a terminal within the superset container: +# $ superset-demo # Initialise database + load demo charts/datasets +# $ superset-init # Initialise database only +# +# You will be prompted to enter the credentials for the admin user. + +services: + superset: + image: amancevice/superset + restart: always + depends_on: + - superset_postgres + - superset_redis + volumes: + # This superset_config.py can be edited in Dokploy's UI Advanced -> Volume Mount + - ../files/superset/superset_config.py:/etc/superset/superset_config.py + environment: + SECRET_KEY: ${SECRET_KEY} + MAPBOX_API_KEY: ${MAPBOX_API_KEY} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + REDIS_PASSWORD: ${REDIS_PASSWORD} + # Ensure the hosts matches your service names below. + POSTGRES_HOST: superset_postgres + REDIS_HOST: superset_redis + + superset_postgres: + image: postgres + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - superset_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - dokploy-network + + superset_redis: + image: redis + restart: always + volumes: + - superset_redis_data:/data + command: redis-server --requirepass ${REDIS_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - dokploy-network + +volumes: + superset_postgres_data: + superset_redis_data: diff --git a/apps/dokploy/templates/superset/index.ts b/apps/dokploy/templates/superset/index.ts new file mode 100644 index 00000000..4994b639 --- /dev/null +++ b/apps/dokploy/templates/superset/index.ts @@ -0,0 +1,67 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mapboxApiKey = ""; + const secretKey = generatePassword(30); + const postgresDb = "superset"; + const postgresUser = "superset"; + const postgresPassword = generatePassword(30); + const redisPassword = generatePassword(30); + + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 8088, + serviceName: "superset", + }, + ]; + + const envs = [ + `SECRET_KEY=${secretKey}`, + `MAPBOX_API_KEY=${mapboxApiKey}`, + `POSTGRES_DB=${postgresDb}`, + `POSTGRES_USER=${postgresUser}`, + `POSTGRES_PASSWORD=${postgresPassword}`, + `REDIS_PASSWORD=${redisPassword}`, + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "./superset/superset_config.py", + content: ` +import os + +SECRET_KEY = os.getenv("SECRET_KEY") +MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "") + +CACHE_CONFIG = { + "CACHE_TYPE": "RedisCache", + "CACHE_DEFAULT_TIMEOUT": 300, + "CACHE_KEY_PREFIX": "superset_", + "CACHE_REDIS_HOST": "redis", + "CACHE_REDIS_PORT": 6379, + "CACHE_REDIS_DB": 1, + "CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@{os.getenv('REDIS_HOST')}:6379/1", +} + +FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"} +EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"} + +SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@{os.getenv('POSTGRES_HOST')}:5432/{os.getenv('POSTGRES_DB')}" +SQLALCHEMY_TRACK_MODIFICATIONS = True + `.trim(), + }, + ]; + + return { + envs, + domains, + mounts, + }; +} diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 45d5c2ac..238154a6 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -538,7 +538,7 @@ export const templates: TemplateData[] = [ website: "https://filebrowser.org/", docs: "https://filebrowser.org/", }, - tags: ["file", "manager"], + tags: ["file-manager", "storage"], load: () => import("./filebrowser/index").then((m) => m.generate), }, { @@ -834,7 +834,7 @@ export const templates: TemplateData[] = [ website: "https://nextcloud.com/", docs: "https://docs.nextcloud.com/", }, - tags: ["file", "sync"], + tags: ["file-manager", "sync"], load: () => import("./nextcloud-aio/index").then((m) => m.generate), }, { @@ -1074,7 +1074,7 @@ export const templates: TemplateData[] = [ website: "https://penpot.app/", docs: "https://docs.penpot.app/", }, - tags: ["desing", "collaboration"], + tags: ["design", "collaboration"], load: () => import("./penpot/index").then((m) => m.generate), }, { @@ -1097,7 +1097,7 @@ export const templates: TemplateData[] = [ name: "Unsend", version: "v1.2.4", description: "Open source alternative to Resend,Sendgrid, Postmark etc. ", - logo: "unsend.png", // we defined the name and the extension of the logo + logo: "unsend.png", links: { github: "https://github.com/unsend-dev/unsend", website: "https://unsend.dev/", @@ -1285,4 +1285,157 @@ export const templates: TemplateData[] = [ tags: ["cloud", "networking", "security", "tunnel"], load: () => import("./cloudflared/index").then((m) => m.generate), }, + { + id: "couchdb", + name: "CouchDB", + version: "latest", + description: + "CouchDB is a document-oriented NoSQL database that excels at replication and horizontal scaling.", + logo: "couchdb.png", + links: { + github: "https://github.com/apache/couchdb", + website: "https://couchdb.apache.org/", + docs: "https://docs.couchdb.org/en/stable/", + }, + tags: ["database", "storage"], + load: () => import("./couchdb/index").then((m) => m.generate), + }, + { + id: "it-tools", + name: "IT Tools", + version: "latest", + description: "A collection of handy online it-tools for developers.", + logo: "it-tools.svg", + links: { + github: "https://github.com/CorentinTh/it-tools", + website: "https://it-tools.tech", + }, + tags: ["developer", "tools"], + load: () => import("./it-tools/index").then((m) => m.generate), + }, + { + id: "superset", + name: "Superset (Unofficial)", + version: "latest", + description: "Data visualization and data exploration platform.", + logo: "superset.svg", + links: { + github: "https://github.com/amancevice/docker-superset", + website: "https://superset.apache.org", + docs: "https://superset.apache.org/docs/intro", + }, + tags: ["analytics", "bi", "dashboard", "database", "sql"], + load: () => import("./superset/index").then((m) => m.generate), + }, + { + id: "glance", + name: "Glance", + version: "latest", + description: + "A self-hosted dashboard that puts all your feeds in one place. Features RSS feeds, weather, bookmarks, site monitoring, and more in a minimal, fast interface.", + logo: "glance.png", + links: { + github: "https://github.com/glanceapp/glance", + docs: "https://github.com/glanceapp/glance/blob/main/docs/configuration.md", + }, + tags: ["dashboard", "monitoring", "widgets", "rss"], + load: () => import("./glance/index").then((m) => m.generate), + }, + { + id: "homarr", + name: "Homarr", + version: "latest", + description: + "A sleek, modern dashboard that puts all your apps and services in one place with Docker integration.", + logo: "homarr.png", + links: { + github: "https://github.com/homarr-labs/homarr", + docs: "https://homarr.dev/docs/getting-started/installation/docker", + website: "https://homarr.dev/", + }, + tags: ["dashboard", "monitoring"], + load: () => import("./homarr/index").then((m) => m.generate), + }, + { + id: "erpnext", + name: "ERPNext", + version: "version-15", + description: "100% Open Source and highly customizable ERP software.", + logo: "erpnext.svg", + links: { + github: "https://github.com/frappe/erpnext", + docs: "https://docs.frappe.io/erpnext", + website: "https://erpnext.com", + }, + tags: [ + "erp", + "accounts", + "manufacturing", + "retail", + "sales", + "pos", + "hrms", + ], + load: () => import("./erpnext/index").then((m) => m.generate), + }, + { + id: "maybe", + name: "Maybe", + version: "latest", + description: + "Maybe is a self-hosted finance tracking application designed to simplify budgeting and expenses.", + logo: "maybe.svg", + links: { + github: "https://github.com/maybe-finance/maybe", + website: "https://maybe.finance/", + docs: "https://docs.maybe.finance/", + }, + tags: ["finance", "self-hosted"], + load: () => import("./maybe/index").then((m) => m.generate), + }, + { + id: "spacedrive", + name: "Spacedrive", + version: "latest", + description: + "Spacedrive is a cross-platform file manager. It connects your devices together to help you organize files from anywhere. powered by a virtual distributed filesystem (VDFS) written in Rust. Organize files across many devices in one place.", + links: { + github: "https://github.com/spacedriveapp/spacedrive", + website: "https://spacedrive.com/", + docs: "https://www.spacedrive.com/docs/product/getting-started/introduction", + }, + logo: "spacedrive.png", + tags: ["file-manager", "vdfs", "storage"], + load: () => import("./spacedrive/index").then((m) => m.generate), + }, + { + id: "alist", + name: "AList", + version: "v3.41.0", + description: + "🗂️A file list/WebDAV program that supports multiple storages, powered by Gin and Solidjs.", + logo: "alist.svg", + links: { + github: "https://github.com/AlistGo/alist", + website: "https://alist.nn.ci", + docs: "https://alist.nn.ci/guide/install/docker.html", + }, + tags: ["file", "webdav", "storage"], + load: () => import("./alist/index").then((m) => m.generate), + }, + { + id: "answer", + name: "Answer", + version: "v1.4.1", + description: + "Answer is an open-source Q&A platform for building a self-hosted question-and-answer service.", + logo: "answer.png", + links: { + github: "https://github.com/apache/answer", + website: "https://answer.apache.org/", + docs: "https://answer.apache.org/docs", + }, + tags: ["q&a", "self-hosted"], + load: () => import("./answer/index").then((m) => m.generate), + }, ]; diff --git a/packages/server/src/db/schema/admin.ts b/packages/server/src/db/schema/admin.ts index 222fb16c..1bbb6b40 100644 --- a/packages/server/src/db/schema/admin.ts +++ b/packages/server/src/db/schema/admin.ts @@ -31,6 +31,15 @@ export const admins = pgTable("admin", { stripeCustomerId: text("stripeCustomerId"), stripeSubscriptionId: text("stripeSubscriptionId"), serversQuantity: integer("serversQuantity").notNull().default(0), + cleanupCacheApplications: boolean("cleanupCacheApplications") + .notNull() + .default(false), + cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews") + .notNull() + .default(false), + cleanupCacheOnCompose: boolean("cleanupCacheOnCompose") + .notNull() + .default(false), }); export const adminsRelations = relations(admins, ({ one, many }) => ({ diff --git a/packages/server/src/db/schema/deployment.ts b/packages/server/src/db/schema/deployment.ts index ccaf6466..1be5db5e 100644 --- a/packages/server/src/db/schema/deployment.ts +++ b/packages/server/src/db/schema/deployment.ts @@ -47,6 +47,7 @@ export const deployments = pgTable("deployment", { createdAt: text("createdAt") .notNull() .$defaultFn(() => new Date().toISOString()), + errorMessage: text("errorMessage"), }); export const deploymentsRelations = relations(deployments, ({ one }) => ({ diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 5501621d..12c7698e 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [ "telegram", "discord", "email", + "gotify", ]); export const notifications = pgTable("notification", { @@ -39,6 +40,9 @@ export const notifications = pgTable("notification", { emailId: text("emailId").references(() => email.emailId, { onDelete: "cascade", }), + gotifyId: text("gotifyId").references(() => gotify.gotifyId, { + onDelete: "cascade", + }), adminId: text("adminId").references(() => admins.adminId, { onDelete: "cascade", }), @@ -84,6 +88,17 @@ export const email = pgTable("email", { toAddresses: text("toAddress").array().notNull(), }); +export const gotify = pgTable("gotify", { + gotifyId: text("gotifyId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + serverUrl: text("serverUrl").notNull(), + appToken: text("appToken").notNull(), + priority: integer("priority").notNull().default(5), + decoration: boolean("decoration"), +}); + export const notificationsRelations = relations(notifications, ({ one }) => ({ slack: one(slack, { fields: [notifications.slackId], @@ -101,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({ fields: [notifications.emailId], references: [email.emailId], }), + gotify: one(gotify, { + fields: [notifications.gotifyId], + references: [gotify.gotifyId], + }), admin: one(admins, { fields: [notifications.adminId], references: [admins.adminId], @@ -224,6 +243,39 @@ export const apiTestEmailConnection = apiCreateEmail.pick({ fromAddress: true, }); +export const apiCreateGotify = notificationsSchema + .pick({ + appBuildError: true, + databaseBackup: true, + dokployRestart: true, + name: true, + appDeploy: true, + dockerCleanup: true, + }) + .extend({ + serverUrl: z.string().min(1), + appToken: z.string().min(1), + priority: z.number().min(1), + decoration: z.boolean(), + }) + .required(); + +export const apiUpdateGotify = apiCreateGotify.partial().extend({ + notificationId: z.string().min(1), + gotifyId: z.string().min(1), + adminId: z.string().optional(), +}); + +export const apiTestGotifyConnection = apiCreateGotify + .pick({ + serverUrl: true, + appToken: true, + priority: true, + }) + .extend({ + decoration: z.boolean().optional(), + }); + export const apiFindOneNotification = notificationsSchema .pick({ notificationId: true, @@ -242,5 +294,8 @@ export const apiSendTest = notificationsSchema username: z.string(), password: z.string(), toAddresses: z.array(z.string()), + serverUrl: z.string(), + appToken: z.string(), + priority: z.number(), }) .partial(); diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index fec3d127..735898f9 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -40,11 +40,11 @@ export const users = pgTable("user", { canAccessToTraefikFiles: boolean("canAccessToTraefikFiles") .notNull() .default(false), - accesedProjects: text("accesedProjects") + accessedProjects: text("accesedProjects") .array() .notNull() .default(sql`ARRAY[]::text[]`), - accesedServices: text("accesedServices") + accessedServices: text("accesedServices") .array() .notNull() .default(sql`ARRAY[]::text[]`), @@ -73,8 +73,8 @@ const createSchema = createInsertSchema(users, { token: z.string().min(1), isRegistered: z.boolean().optional(), adminId: z.string(), - accesedProjects: z.array(z.string()).optional(), - accesedServices: z.array(z.string()).optional(), + accessedProjects: z.array(z.string()).optional(), + accessedServices: z.array(z.string()).optional(), canCreateProjects: z.boolean().optional(), canCreateServices: z.boolean().optional(), canDeleteProjects: z.boolean().optional(), @@ -106,8 +106,8 @@ export const apiAssignPermissions = createSchema canCreateServices: true, canDeleteProjects: true, canDeleteServices: true, - accesedProjects: true, - accesedServices: true, + accessedProjects: true, + accessedServices: true, canAccessToTraefikFiles: true, canAccessToDocker: true, canAccessToAPI: true, diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index e2ed407f..da1b50af 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -40,7 +40,7 @@ import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; -import { getDokployUrl } from "./admin"; +import { findAdminById, getDokployUrl } from "./admin"; import { createDeployment, createDeploymentPreview, @@ -58,6 +58,7 @@ import { updatePreviewDeployment, } from "./preview-deployment"; import { validUniqueServerAppName } from "./project"; +import { cleanupFullDocker } from "./settings"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( @@ -175,6 +176,7 @@ export const deployApplication = async ({ descriptionLog: string; }) => { const application = await findApplicationById(applicationId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`; const deployment = await createDeployment({ applicationId: applicationId, @@ -183,6 +185,12 @@ export const deployApplication = async ({ }); try { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } + if (application.sourceType === "github") { await cloneGithubRepository({ ...application, @@ -213,6 +221,7 @@ export const deployApplication = async ({ applicationType: "application", buildLink, adminId: application.project.adminId, + domains: application.domains, }); } catch (error) { await updateDeploymentStatus(deployment.deploymentId, "error"); @@ -243,6 +252,7 @@ export const rebuildApplication = async ({ descriptionLog: string; }) => { const application = await findApplicationById(applicationId); + const deployment = await createDeployment({ applicationId: applicationId, title: titleLog, @@ -250,6 +260,11 @@ export const rebuildApplication = async ({ }); try { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } if (application.sourceType === "github") { await buildApplication(application, deployment.logPath); } else if (application.sourceType === "gitlab") { @@ -284,6 +299,7 @@ export const deployRemoteApplication = async ({ descriptionLog: string; }) => { const application = await findApplicationById(applicationId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`; const deployment = await createDeployment({ applicationId: applicationId, @@ -293,6 +309,11 @@ export const deployRemoteApplication = async ({ try { if (application.serverId) { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } let command = "set -e;"; if (application.sourceType === "github") { command += await getGithubCloneCommand({ @@ -332,6 +353,7 @@ export const deployRemoteApplication = async ({ applicationType: "application", buildLink, adminId: application.project.adminId, + domains: application.domains, }); } catch (error) { // @ts-ignore @@ -357,14 +379,6 @@ export const deployRemoteApplication = async ({ adminId: application.project.adminId, }); - console.log( - "Error on ", - application.buildType, - "/", - application.sourceType, - error, - ); - throw error; } @@ -383,6 +397,7 @@ export const deployPreviewApplication = async ({ previewDeploymentId: string; }) => { const application = await findApplicationById(applicationId); + const deployment = await createDeploymentPreview({ title: titleLog, description: descriptionLog, @@ -436,9 +451,15 @@ export const deployPreviewApplication = async ({ body: `### Dokploy Preview Deployment\n\n${buildingComment}`, }); application.appName = previewDeployment.appName; - application.env = application.previewEnv; + application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`; application.buildArgs = application.previewBuildArgs; + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheOnPreviews) { + await cleanupFullDocker(application?.serverId); + } + if (application.sourceType === "github") { await cloneGithubRepository({ ...application, @@ -448,7 +469,6 @@ export const deployPreviewApplication = async ({ }); await buildApplication(application, deployment.logPath); } - // 4eef09efc46009187d668cf1c25f768d0bde4f91 const successComment = getIssueComment( application.name, "success", @@ -490,6 +510,7 @@ export const deployRemotePreviewApplication = async ({ previewDeploymentId: string; }) => { const application = await findApplicationById(applicationId); + const deployment = await createDeploymentPreview({ title: titleLog, description: descriptionLog, @@ -543,14 +564,21 @@ export const deployRemotePreviewApplication = async ({ body: `### Dokploy Preview Deployment\n\n${buildingComment}`, }); application.appName = previewDeployment.appName; - application.env = application.previewEnv; + application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`; application.buildArgs = application.previewBuildArgs; if (application.serverId) { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheOnPreviews) { + await cleanupFullDocker(application?.serverId); + } let command = "set -e;"; if (application.sourceType === "github") { command += await getGithubCloneCommand({ ...application, + appName: previewDeployment.appName, + branch: previewDeployment.branch, serverId: application.serverId, logPath: deployment.logPath, }); @@ -600,6 +628,7 @@ export const rebuildRemoteApplication = async ({ descriptionLog: string; }) => { const application = await findApplicationById(applicationId); + const deployment = await createDeployment({ applicationId: applicationId, title: titleLog, @@ -608,6 +637,11 @@ export const rebuildRemoteApplication = async ({ try { if (application.serverId) { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } if (application.sourceType !== "docker") { let command = "set -e;"; command += getBuildCommand(application, deployment.logPath); diff --git a/packages/server/src/services/backup.ts b/packages/server/src/services/backup.ts index 70e37af4..ef3d0446 100644 --- a/packages/server/src/services/backup.ts +++ b/packages/server/src/services/backup.ts @@ -2,11 +2,13 @@ import { db } from "@dokploy/server/db"; import { type apiCreateBackup, backups } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { IS_CLOUD } from "../constants"; +import { removeScheduleBackup, scheduleBackup } from "../utils/backups/utils"; export type Backup = typeof backups.$inferSelect; export type BackupSchedule = Awaited>; - +export type BackupScheduleList = Awaited>; export const createBackup = async (input: typeof apiCreateBackup._type) => { const newBackup = await db .insert(backups) @@ -69,3 +71,20 @@ export const removeBackupById = async (backupId: string) => { return result[0]; }; + +export const findBackupsByDbId = async ( + id: string, + type: "postgres" | "mysql" | "mariadb" | "mongo", +) => { + const result = await db.query.backups.findMany({ + where: eq(backups[`${type}Id`], id), + with: { + postgres: true, + mysql: true, + mariadb: true, + mongo: true, + destination: true, + }, + }); + return result || []; +}; diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 50459c45..a0492314 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -3,7 +3,6 @@ import { paths } from "@dokploy/server/constants"; import { db } from "@dokploy/server/db"; import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; -import { generatePassword } from "@dokploy/server/templates/utils"; import { buildCompose, getBuildComposeCommand, @@ -45,9 +44,10 @@ import { import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; -import { getDokployUrl } from "./admin"; +import { findAdminById, getDokployUrl } from "./admin"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { validUniqueServerAppName } from "./project"; +import { cleanupFullDocker } from "./settings"; export type Compose = typeof compose.$inferSelect; @@ -206,6 +206,7 @@ export const deployCompose = async ({ descriptionLog: string; }) => { const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${ compose.projectId }/services/compose/${compose.composeId}?tab=deployments`; @@ -216,6 +217,10 @@ export const deployCompose = async ({ }); try { + const admin = await findAdminById(compose.project.adminId); + if (admin.cleanupCacheOnCompose) { + await cleanupFullDocker(compose?.serverId); + } if (compose.sourceType === "github") { await cloneGithubRepository({ ...compose, @@ -243,6 +248,7 @@ export const deployCompose = async ({ applicationType: "compose", buildLink, adminId: compose.project.adminId, + domains: compose.domains, }); } catch (error) { await updateDeploymentStatus(deployment.deploymentId, "error"); @@ -272,6 +278,7 @@ export const rebuildCompose = async ({ descriptionLog: string; }) => { const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ composeId: composeId, title: titleLog, @@ -279,6 +286,10 @@ export const rebuildCompose = async ({ }); try { + const admin = await findAdminById(compose.project.adminId); + if (admin.cleanupCacheOnCompose) { + await cleanupFullDocker(compose?.serverId); + } if (compose.serverId) { await getBuildComposeCommand(compose, deployment.logPath); } else { @@ -310,6 +321,7 @@ export const deployRemoteCompose = async ({ descriptionLog: string; }) => { const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${ compose.projectId }/services/compose/${compose.composeId}?tab=deployments`; @@ -320,6 +332,10 @@ export const deployRemoteCompose = async ({ }); try { if (compose.serverId) { + const admin = await findAdminById(compose.project.adminId); + if (admin.cleanupCacheOnCompose) { + await cleanupFullDocker(compose?.serverId); + } let command = "set -e;"; if (compose.sourceType === "github") { @@ -366,6 +382,7 @@ export const deployRemoteCompose = async ({ applicationType: "compose", buildLink, adminId: compose.project.adminId, + domains: compose.domains, }); } catch (error) { // @ts-ignore @@ -405,6 +422,7 @@ export const rebuildRemoteCompose = async ({ descriptionLog: string; }) => { const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ composeId: composeId, title: titleLog, @@ -412,6 +430,10 @@ export const rebuildRemoteCompose = async ({ }); try { + const admin = await findAdminById(compose.project.adminId); + if (admin.cleanupCacheOnCompose) { + await cleanupFullDocker(compose?.serverId); + } if (compose.serverId) { await getBuildComposeCommand(compose, deployment.logPath); } diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 0e55ea32..096bdf19 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -98,6 +98,17 @@ export const createDeployment = async ( } return deploymentCreate[0]; } catch (error) { + await db + .insert(deployments) + .values({ + applicationId: deployment.applicationId, + title: deployment.title || "Deployment", + status: "error", + logPath: "", + description: deployment.description || "", + errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`, + }) + .returning(); await updateApplicationStatus(application.applicationId, "error"); console.log(error); throw new TRPCError({ @@ -164,6 +175,17 @@ export const createDeploymentPreview = async ( } return deploymentCreate[0]; } catch (error) { + await db + .insert(deployments) + .values({ + previewDeploymentId: deployment.previewDeploymentId, + title: deployment.title || "Deployment", + status: "error", + logPath: "", + description: deployment.description || "", + errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`, + }) + .returning(); await updatePreviewDeployment(deployment.previewDeploymentId, { previewStatus: "error", }); @@ -226,6 +248,17 @@ echo "Initializing deployment" >> ${logFilePath}; } return deploymentCreate[0]; } catch (error) { + await db + .insert(deployments) + .values({ + composeId: deployment.composeId, + title: deployment.title || "Deployment", + status: "error", + logPath: "", + description: deployment.description || "", + errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`, + }) + .returning(); await updateCompose(compose.composeId, { composeStatus: "error", }); diff --git a/packages/server/src/services/mount.ts b/packages/server/src/services/mount.ts index dd7bd3e9..38e82d1a 100644 --- a/packages/server/src/services/mount.ts +++ b/packages/server/src/services/mount.ts @@ -64,7 +64,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => { console.log(error); throw new TRPCError({ code: "BAD_REQUEST", - message: "Error creating the mount", + message: `Error ${error instanceof Error ? error.message : error}`, cause: error, }); } @@ -91,7 +91,7 @@ export const createFileMount = async (mountId: string) => { console.log(`Error creating the file mount: ${error}`); throw new TRPCError({ code: "BAD_REQUEST", - message: "Error creating the mount", + message: `Error creating the mount ${error instanceof Error ? error.message : error}`, cause: error, }); } diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index e75154df..2b62b457 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db"; import { type apiCreateDiscord, type apiCreateEmail, + type apiCreateGotify, type apiCreateSlack, type apiCreateTelegram, type apiUpdateDiscord, type apiUpdateEmail, + type apiUpdateGotify, type apiUpdateSlack, type apiUpdateTelegram, discord, email, + gotify, notifications, slack, telegram, @@ -379,6 +382,96 @@ export const updateEmailNotification = async ( }); }; +export const createGotifyNotification = async ( + input: typeof apiCreateGotify._type, + adminId: string, +) => { + await db.transaction(async (tx) => { + const newGotify = await tx + .insert(gotify) + .values({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .returning() + .then((value) => value[0]); + + if (!newGotify) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting gotify", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + gotifyId: newGotify.gotifyId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "gotify", + adminId: adminId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateGotifyNotification = async ( + input: typeof apiUpdateGotify._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + adminId: input.adminId, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(gotify) + .set({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .where(eq(gotify.gotifyId, input.gotifyId)); + + return newDestination; + }); +}; + export const findNotificationById = async (notificationId: string) => { const notification = await db.query.notifications.findFirst({ where: eq(notifications.notificationId, notificationId), @@ -387,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => { telegram: true, discord: true, email: true, + gotify: true, }, }); if (!notification) { diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 37f7b2ee..d37793ea 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -5,6 +5,7 @@ import { execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; +import { findAdminById } from "./admin"; // import packageInfo from "../../../package.json"; export interface IUpdateData { @@ -213,3 +214,35 @@ echo "$json_output" } return result; }; + +export const cleanupFullDocker = async (serverId?: string | null) => { + const cleanupImages = "docker image prune --force"; + const cleanupVolumes = "docker volume prune --force"; + const cleanupContainers = "docker container prune --force"; + const cleanupSystem = "docker system prune --all --force --volumes"; + const cleanupBuilder = "docker builder prune --all --force"; + + try { + if (serverId) { + await execAsyncRemote( + serverId, + ` + ${cleanupImages} + ${cleanupVolumes} + ${cleanupContainers} + ${cleanupSystem} + ${cleanupBuilder} + `, + ); + } + await execAsync(` + ${cleanupImages} + ${cleanupVolumes} + ${cleanupContainers} + ${cleanupSystem} + ${cleanupBuilder} + `); + } catch (error) { + console.log(error); + } +}; diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index 1cfe1260..d8d9862c 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -54,7 +54,7 @@ export const addNewProject = async (authId: string, projectId: string) => { await db .update(users) .set({ - accesedProjects: [...user.accesedProjects, projectId], + accessedProjects: [...user.accessedProjects, projectId], }) .where(eq(users.authId, authId)); }; @@ -64,7 +64,7 @@ export const addNewService = async (authId: string, serviceId: string) => { await db .update(users) .set({ - accesedServices: [...user.accesedServices, serviceId], + accessedServices: [...user.accessedServices, serviceId], }) .where(eq(users.authId, authId)); }; @@ -73,8 +73,9 @@ export const canPerformCreationService = async ( userId: string, projectId: string, ) => { - const { accesedProjects, canCreateServices } = await findUserByAuthId(userId); - const haveAccessToProject = accesedProjects.includes(projectId); + const { accessedProjects, canCreateServices } = + await findUserByAuthId(userId); + const haveAccessToProject = accessedProjects.includes(projectId); if (canCreateServices && haveAccessToProject) { return true; @@ -87,8 +88,8 @@ export const canPerformAccessService = async ( userId: string, serviceId: string, ) => { - const { accesedServices } = await findUserByAuthId(userId); - const haveAccessToService = accesedServices.includes(serviceId); + const { accessedServices } = await findUserByAuthId(userId); + const haveAccessToService = accessedServices.includes(serviceId); if (haveAccessToService) { return true; @@ -101,8 +102,9 @@ export const canPeformDeleteService = async ( authId: string, serviceId: string, ) => { - const { accesedServices, canDeleteServices } = await findUserByAuthId(authId); - const haveAccessToService = accesedServices.includes(serviceId); + const { accessedServices, canDeleteServices } = + await findUserByAuthId(authId); + const haveAccessToService = accessedServices.includes(serviceId); if (canDeleteServices && haveAccessToService) { return true; @@ -135,9 +137,9 @@ export const canPerformAccessProject = async ( authId: string, projectId: string, ) => { - const { accesedProjects } = await findUserByAuthId(authId); + const { accessedProjects } = await findUserByAuthId(authId); - const haveAccessToProject = accesedProjects.includes(projectId); + const haveAccessToProject = accessedProjects.includes(projectId); if (haveAccessToProject) { return true; diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index 270a4447..6a2a0133 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -189,10 +189,12 @@ export const getDefaultTraefikConfig = () => { : { swarm: { exposedByDefault: false, - watch: false, + watch: true, }, docker: { exposedByDefault: false, + watch: true, + network: "dokploy-network", }, }), file: { @@ -243,10 +245,12 @@ export const getDefaultServerTraefikConfig = () => { providers: { swarm: { exposedByDefault: false, - watch: false, + watch: true, }, docker: { exposedByDefault: false, + watch: true, + network: "dokploy-network", }, file: { directory: "/etc/dokploy/traefik/dynamic", diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 654d8330..64e98306 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -144,10 +144,11 @@ export const getContainerByName = (name: string): Promise => { }; export const cleanUpUnusedImages = async (serverId?: string) => { try { + const command = "docker image prune --force"; if (serverId) { - await execAsyncRemote(serverId, "docker image prune --all --force"); + await execAsyncRemote(serverId, command); } else { - await execAsync("docker image prune --all --force"); + await execAsync(command); } } catch (error) { console.error(error); @@ -157,10 +158,11 @@ export const cleanUpUnusedImages = async (serverId?: string) => { export const cleanStoppedContainers = async (serverId?: string) => { try { + const command = "docker container prune --force"; if (serverId) { - await execAsyncRemote(serverId, "docker container prune --force"); + await execAsyncRemote(serverId, command); } else { - await execAsync("docker container prune --force"); + await execAsync(command); } } catch (error) { console.error(error); @@ -170,10 +172,11 @@ export const cleanStoppedContainers = async (serverId?: string) => { export const cleanUpUnusedVolumes = async (serverId?: string) => { try { + const command = "docker volume prune --force"; if (serverId) { - await execAsyncRemote(serverId, "docker volume prune --all --force"); + await execAsyncRemote(serverId, command); } else { - await execAsync("docker volume prune --all --force"); + await execAsync(command); } } catch (error) { console.error(error); @@ -199,21 +202,20 @@ export const cleanUpInactiveContainers = async () => { }; export const cleanUpDockerBuilder = async (serverId?: string) => { + const command = "docker builder prune --all --force"; if (serverId) { - await execAsyncRemote(serverId, "docker builder prune --all --force"); + await execAsyncRemote(serverId, command); } else { - await execAsync("docker builder prune --all --force"); + await execAsync(command); } }; export const cleanUpSystemPrune = async (serverId?: string) => { + const command = "docker system prune --all --force --volumes"; if (serverId) { - await execAsyncRemote( - serverId, - "docker system prune --all --force --volumes", - ); + await execAsyncRemote(serverId, command); } else { - await execAsync("docker system prune --all --force --volumes"); + await execAsync(command); } }; diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 695b3786..95936652 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed"; import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; @@ -39,11 +41,12 @@ export const sendBuildErrorNotifications = async ({ discord: true, telegram: true, slack: true, + gotify: true, }, }); for (const notification of notificationList) { - const { email, discord, telegram, slack } = notification; + const { email, discord, telegram, slack, gotify } = notification; if (email) { const template = await renderAsync( BuildFailedEmail({ @@ -112,22 +115,35 @@ export const sendBuildErrorNotifications = async ({ }); } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("⚠️", "Build Failed"), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("❔", `Type: ${applicationType}`)}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${decorate("⚠️", `Error:\n${errorMessage}`)}` + + `${decorate("🔗", `Build details:\n${buildLink}`)}`, + ); + } + if (telegram) { + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ]; + await sendTelegramNotification( telegram, - ` - ⚠️ Build Failed - - Project: ${projectName} - Application: ${applicationName} - Type: ${applicationType} - Time: ${date.toLocaleString()} - - Error: -
${errorMessage}
- - Build Details: ${buildLink} - `, + `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, + inlineButton, ); } diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 16aa4a58..960f7a6a 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -1,11 +1,14 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success"; +import type { Domain } from "@dokploy/server/services/domain"; import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; @@ -16,6 +19,7 @@ interface Props { applicationType: string; buildLink: string; adminId: string; + domains: Domain[]; } export const sendBuildSuccessNotifications = async ({ @@ -24,6 +28,7 @@ export const sendBuildSuccessNotifications = async ({ applicationType, buildLink, adminId, + domains, }: Props) => { const date = new Date(); const unixDate = ~~(Number(date) / 1000); @@ -37,11 +42,12 @@ export const sendBuildSuccessNotifications = async ({ discord: true, telegram: true, slack: true, + gotify: true, }, }); for (const notification of notificationList) { - const { email, discord, telegram, slack } = notification; + const { email, discord, telegram, slack, gotify } = notification; if (email) { const template = await renderAsync( @@ -106,19 +112,45 @@ export const sendBuildSuccessNotifications = async ({ }); } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("✅", "Build Success"), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("❔", `Type: ${applicationType}`)}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${decorate("🔗", `Build details:\n${buildLink}`)}`, + ); + } + if (telegram) { + const chunkArray = (array: T[], chunkSize: number): T[][] => + Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => + array.slice(i * chunkSize, i * chunkSize + chunkSize), + ); + + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ...chunkArray(domains, 2).map((chunk) => + chunk.map((data) => ({ + text: data.host, + url: `${data.https ? "https" : "http"}://${data.host}`, + })), + ), + ]; + await sendTelegramNotification( telegram, - ` - ✅ Build Success - - Project: ${projectName} - Application: ${applicationName} - Type: ${applicationType} - Time: ${date.toLocaleString()} - - Build Details: ${buildLink} - `, + `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + inlineButton, ); } diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 3aec6f3d..0b1d61f7 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -1,11 +1,14 @@ +import { error } from "node:console"; import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; @@ -37,11 +40,12 @@ export const sendDatabaseBackupNotifications = async ({ discord: true, telegram: true, slack: true, + gotify: true, }, }); for (const notification of notificationList) { - const { email, discord, telegram, slack } = notification; + const { email, discord, telegram, slack, gotify } = notification; if (email) { const template = await renderAsync( @@ -120,19 +124,35 @@ export const sendDatabaseBackupNotifications = async ({ }); } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + + await sendGotifyNotification( + gotify, + decorate( + type === "success" ? "✅" : "❌", + `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + ), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("❔", `Type: ${databaseType}`)}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`, + ); + } + if (telegram) { + const isError = type === "error" && errorMessage; + const statusEmoji = type === "success" ? "✅" : "❌"; - const messageText = ` - ${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"} + const typeStatus = type === "success" ? "Successful" : "Failed"; + const errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; - Project: ${projectName} - Application: ${applicationName} - Type: ${databaseType} - Time: ${date.toLocaleString()} + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - Status: ${type === "success" ? "Successful" : "Failed"} - ${type === "error" && errorMessage ? `Error: ${errorMessage}` : ""} - `; await sendTelegramNotification(telegram, messageText); } diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index c95c7906..b60e3b0a 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup"; import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; @@ -26,11 +28,12 @@ export const sendDockerCleanupNotifications = async ( discord: true, telegram: true, slack: true, + gotify: true, }, }); for (const notification of notificationList) { - const { email, discord, telegram, slack } = notification; + const { email, discord, telegram, slack, gotify } = notification; if (email) { const template = await renderAsync( @@ -79,14 +82,21 @@ export const sendDockerCleanupNotifications = async ( }); } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("✅", "Docker Cleanup"), + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${decorate("📜", `Message:\n${message}`)}`, + ); + } + if (telegram) { await sendTelegramNotification( telegram, - ` - ✅ Docker Cleanup - Message: ${message} - Time: ${date.toLocaleString()} - `, + `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); } diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index 16170349..5a156aff 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart"; import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, } from "./utils"; @@ -20,11 +22,12 @@ export const sendDokployRestartNotifications = async () => { discord: true, telegram: true, slack: true, + gotify: true, }, }); for (const notification of notificationList) { - const { email, discord, telegram, slack } = notification; + const { email, discord, telegram, slack, gotify } = notification; if (email) { const template = await renderAsync( @@ -64,13 +67,20 @@ export const sendDokployRestartNotifications = async () => { }); } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("✅", "Dokploy Server Restarted"), + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}`, + ); + } + if (telegram) { await sendTelegramNotification( telegram, - ` - ✅ Dokploy Serverd Restarted - Time: ${date.toLocaleString()} - `, + `✅ Dokploy Server Restarted\n\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); } diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index 2f8324bb..4f8bb1a5 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -1,6 +1,7 @@ import type { discord, email, + gotify, slack, telegram, } from "@dokploy/server/db/schema"; @@ -55,6 +56,10 @@ export const sendDiscordNotification = async ( export const sendTelegramNotification = async ( connection: typeof telegram.$inferInsert, messageText: string, + inlineButton?: { + text: string; + url: string; + }[][], ) => { try { const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; @@ -66,6 +71,9 @@ export const sendTelegramNotification = async ( text: messageText, parse_mode: "HTML", disable_web_page_preview: true, + reply_markup: { + inline_keyboard: inlineButton, + }, }), }); } catch (err) { @@ -87,3 +95,33 @@ export const sendSlackNotification = async ( console.log(err); } }; + +export const sendGotifyNotification = async ( + connection: typeof gotify.$inferInsert, + title: string, + message: string, +) => { + const response = await fetch(`${connection.serverUrl}/message`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Gotify-Key": connection.appToken, + }, + body: JSON.stringify({ + title: title, + message: message, + priority: connection.priority, + extras: { + "client::display": { + contentType: "text/plain", + }, + }, + }), + }); + + if (!response.ok) { + throw new Error( + `Failed to send Gotify notification: ${response.statusText}`, + ); + } +}; diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 6377b557..8f8a3830 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -69,6 +69,7 @@ export const cloneGitRepository = async ( }); } + const { port } = sanitizeRepoPathSSH(customGitUrl); await spawnAsync( "git", [ @@ -91,7 +92,7 @@ export const cloneGitRepository = async ( env: { ...process.env, ...(customGitSSHKeyId && { - GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`, + GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, }), }, }, @@ -168,7 +169,8 @@ export const getCustomGitCloneCommand = async ( ); if (customGitSSHKeyId) { const sshKey = await findSSHKeyById(customGitSSHKeyId); - const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`; + const { port } = sanitizeRepoPathSSH(customGitUrl); + const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; command.push( ` echo "${sshKey.privateKey}" > /tmp/id_rsa @@ -304,6 +306,7 @@ export const cloneGitRawRepository = async (entity: { }); } + const { port } = sanitizeRepoPathSSH(customGitUrl); await spawnAsync( "git", [ @@ -322,7 +325,7 @@ export const cloneGitRawRepository = async (entity: { env: { ...process.env, ...(customGitSSHKeyId && { - GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`, + GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, }), }, }, @@ -381,7 +384,8 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => { command.push(`mkdir -p ${outputPath};`); if (customGitSSHKeyId) { const sshKey = await findSSHKeyById(customGitSSHKeyId); - const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`; + const { port } = sanitizeRepoPathSSH(customGitUrl); + const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; command.push( ` echo "${sshKey.privateKey}" > /tmp/id_rsa diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6341be34..6f2cd6bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,12 @@ importers: '@hookform/resolvers': specifier: ^3.3.4 version: 3.9.0(react-hook-form@7.52.1(react@18.2.0)) + '@lucia-auth/adapter-drizzle': + specifier: 1.0.7 + version: 1.0.7(lucia@3.2.0) + '@octokit/auth-app': + specifier: ^6.0.4 + version: 6.1.1 '@octokit/webhooks': specifier: ^13.2.7 version: 13.3.0 @@ -184,6 +190,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@react-email/components': + specifier: ^0.0.21 + version: 0.0.21(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@stepperize/react': specifier: 4.0.1 version: 4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -217,6 +226,9 @@ importers: '@xterm/addon-attach': specifier: 0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/addon-clipboard': + specifier: 0.1.0 + version: 0.1.0(@xterm/xterm@5.5.0) '@xterm/xterm': specifier: ^5.4.0 version: 5.5.0 @@ -226,6 +238,12 @@ importers: bcrypt: specifier: 5.1.1 version: 5.1.1 + bl: + specifier: 6.0.11 + version: 6.0.11 + boxen: + specifier: ^7.1.1 + version: 7.1.1 bullmq: specifier: 5.4.2 version: 5.4.2 @@ -247,6 +265,9 @@ importers: date-fns: specifier: 3.6.0 version: 3.6.0 + dockerode: + specifier: 4.0.2 + version: 4.0.2 dotenv: specifier: 16.4.5 version: 16.4.5 @@ -259,6 +280,9 @@ importers: fancy-ansi: specifier: ^0.1.3 version: 0.1.3 + hi-base32: + specifier: ^0.5.1 + version: 0.5.1 i18next: specifier: ^23.16.4 version: 23.16.5 @@ -292,21 +316,33 @@ importers: next-themes: specifier: ^0.2.1 version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + node-os-utils: + specifier: 1.3.7 + version: 1.3.7 node-pty: specifier: 1.0.0 version: 1.0.0 node-schedule: specifier: 2.1.1 version: 2.1.1 + nodemailer: + specifier: 6.9.14 + version: 6.9.14 octokit: specifier: 3.1.2 version: 3.1.2 + otpauth: + specifier: ^9.2.3 + version: 9.3.4 postgres: specifier: 3.4.4 version: 3.4.4 public-ip: specifier: 6.0.2 version: 6.0.2 + qrcode: + specifier: ^1.5.3 + version: 1.5.4 react: specifier: 18.2.0 version: 18.2.0 @@ -325,6 +361,9 @@ importers: recharts: specifier: ^2.12.7 version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rotating-file-stream: + specifier: 3.2.3 + version: 3.2.3 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -386,9 +425,18 @@ importers: '@types/node': specifier: ^18.17.0 version: 18.19.42 + '@types/node-os-utils': + specifier: 1.3.4 + version: 1.3.4 '@types/node-schedule': specifier: 2.1.6 version: 2.1.6 + '@types/nodemailer': + specifier: ^6.4.15 + version: 6.4.16 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 '@types/react': specifier: 18.3.5 version: 18.3.5 @@ -3503,6 +3551,11 @@ packages: peerDependencies: '@xterm/xterm': ^5.0.0 + '@xterm/addon-clipboard@0.1.0': + resolution: {integrity: sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==} + peerDependencies: + '@xterm/xterm': ^5.4.0 + '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} @@ -5011,6 +5064,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + js-beautify@1.15.1: resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} engines: {node: '>=14'} @@ -9894,6 +9950,11 @@ snapshots: dependencies: '@xterm/xterm': 5.5.0 + '@xterm/addon-clipboard@0.1.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + js-base64: 3.7.7 + '@xterm/xterm@5.5.0': {} '@xtuc/ieee754@1.2.0': {} @@ -11410,6 +11471,8 @@ snapshots: joycon@3.1.1: {} + js-base64@3.7.7: {} + js-beautify@1.15.1: dependencies: config-chain: 1.1.13