diff --git a/Dockerfile b/Dockerfile index 8da1db45..8b9d215c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next @@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules # Install docker -RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh +RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash @@ -55,4 +55,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack EXPOSE 3000 -CMD [ "pnpm", "start" ] +CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/apps/docs/content/docs/core/extra/comparison.mdx b/apps/docs/content/docs/core/extra/comparison.mdx index 0036ccbf..028ea6d0 100644 --- a/apps/docs/content/docs/core/extra/comparison.mdx +++ b/apps/docs/content/docs/core/extra/comparison.mdx @@ -1,22 +1,23 @@ --- -title: 'Comparison' -description: 'A comparison of Dokploy, CapRover, Dokku, and Coolify' +title: "Comparison" +description: "A comparison of Dokploy, CapRover, Dokku, and Coolify" --- Comparison of the following deployment tools: -| Feature | Dokploy | CapRover | Dokku | Coolify | -|-----------------------------------|---------------------------------------|--------------------------------------|--------------------------------------|--------------------------------------| -| **User Interface** | ✅ | ✅ | ❌ | ✅ | -| **Docker compose support** | ✅ | ❌ | ❌ | ✅ | -| **API/CLI** | ✅ | ✅ | ✅ | ✅ | -| **Multi node support** | ✅ | ✅ | ❌ | ✅ | -| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ | -| **User Permission Management** | ✅ | ❌ | ❌ | ✅ | -| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ | -| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ | -| **Database Support** | ✅ | ✅ | ❌ | ✅ | -| **Monitoring** | ✅ | ✅ | ❌ | ❌ | -| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ | -| **Open Source** | ✅ | ✅ | ✅ | ✅ | -| **Cloud/Paid Version** | ❌ | ✅ | ❌ | ✅ | +| Feature | Dokploy | CapRover | Dokku | Coolify | +| --------------------------------------- | ------- | --------------------- | --------------------- | ------- | +| **User Interface** | ✅ | ✅ | ❌ | ✅ | +| **Docker compose support** | ✅ | ❌ | ❌ | ✅ | +| **API/CLI** | ✅ | ✅ | ✅ | ✅ | +| **Multi node support** | ✅ | ✅ | ❌ | ✅ | +| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ | +| **User Permission Management** | ✅ | ❌ | ❌ | ✅ | +| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ | +| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ | +| **Database Support** | ✅ | ✅ | ❌ | ✅ | +| **Monitoring** | ✅ | ✅ | ❌ | ❌ | +| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ | +| **Open Source** | ✅ | ✅ | ✅ | ✅ | +| **Multi Server Support** | ✅ | ❌ | ❌ | ✅ | +| **Cloud/Paid Version** | ❌ | ✅ | ✅ | ✅ | diff --git a/apps/docs/content/docs/core/get-started/installation.mdx b/apps/docs/content/docs/core/get-started/installation.mdx index 96e264b3..4bd4490f 100644 --- a/apps/docs/content/docs/core/get-started/installation.mdx +++ b/apps/docs/content/docs/core/get-started/installation.mdx @@ -29,7 +29,7 @@ We have tested on the following Linux Distros: ### Providers -- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://hostinger.com?REFERRALCODE=1SIUMAURICI97) +- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97) - [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get 200$ credits for free with this referral link: [Referral Link](https://m.do.co/c/db24efd43f35) - [Hetzner](https://www.hetzner.com/cloud/) Get 20€ credits for free with this referral link: [Referral Link](https://hetzner.cloud/?ref=vou4fhxJ1W2D) - [Linode](https://www.linode.com/es/pricing/#compute-shared) diff --git a/apps/docs/content/docs/core/meta.json b/apps/docs/content/docs/core/meta.json index a6a8a3c9..b2c3020f 100644 --- a/apps/docs/content/docs/core/meta.json +++ b/apps/docs/content/docs/core/meta.json @@ -64,6 +64,9 @@ "docker/overview", "---Monitoring---", "monitoring/overview", + "---Multi Server---", + "multi-server/overview", + "multi-server/example", "---Cluster---", "cluster/overview", "---Deployments---", diff --git a/apps/docs/content/docs/core/multi-server/example.mdx b/apps/docs/content/docs/core/multi-server/example.mdx new file mode 100644 index 00000000..109722c4 --- /dev/null +++ b/apps/docs/content/docs/core/multi-server/example.mdx @@ -0,0 +1,117 @@ +--- +title: Example +description: "Example to setup a remote server and deploy application in a VPS." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed. + +## Requirements + +1. To install Dokploy UI, follow the [installation guide](en/docs/core/get-started/installation). + +2. Create an SSH key by going to `/dashboard/settings/ssh-keys` and add a new key. Be sure to copy the public key. + + + +3. Decide which remote server to deploy your apps on. We recommend these reliable providers: + +- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% off with this [referral link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97). +- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get $200 credits for free with this [referral link](https://m.do.co/c/db24efd43f35). +- [Hetzner](https://www.hetzner.com/cloud/) Get €20 credits with this [referral link](https://hetzner.cloud/?ref=vou4fhxJ1W2D). +- [Linode](https://www.linode.com/es/pricing/#compute-shared). +- [Vultr](https://www.vultr.com/pricing/#cloud-compute). +- [Scaleway](https://www.scaleway.com/en/pricing/?tags=baremetal,available). +- [Google Cloud](https://cloud.google.com/). +- [AWS](https://aws.amazon.com/ec2/pricing/). + +4. When creating the server, it should ask for SSH keys. Ideally, use your computer's public key and the key you generated in the previous step. Here's how to add the public key in Hostinger: + + + +The steps are similar across other providers. + +5. Copy the server’s IP address and ensure you know the username (often `root`). Fill in all fields and click `Create`. + + + +6. To test connectivity, open the server dropdown and click `Enter Terminal`. If everything is correct, you should be able to interact with the remote server. + +7. Click `Setup Server` to proceed. There are two tabs: SSH Keys and Deployments. This guide explains the easy way, but you can follow the manual process via the Dokploy UI if you prefer. + + + +8. Click `Deployments`, then `Setup Server`. If everything is correct, you should see output similar to this: + + + + + You only need to run this setup once. If Dokploy updates later, check the + release notes to see if rerunning this command is required. + + +9. You're ready to deploy your apps! Let's test it out: + + + +10. To check which server an app belongs to, you’ll see the server name at the top. If no server is selected, it defaults to `Dokploy Server`. Click `Deploy` to start building your app on the remote server. You can check the `Logs` tab to see the build process. For this example, we’ll use a test repo: + Repo: `https://github.com/Dokploy/examples.git` + Branch: `main` + Build Path: `/astro` + + + +11. Once the build is done, go to `Domains` and create a free domain. Just click `Create` and you’re good to go! 🎊 + +{" "} + + diff --git a/apps/docs/content/docs/core/multi-server/overview.mdx b/apps/docs/content/docs/core/multi-server/overview.mdx new file mode 100644 index 00000000..051b314c --- /dev/null +++ b/apps/docs/content/docs/core/multi-server/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Overview +description: "Deploy your apps to multiple servers remotely." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed. + +To use the multi-server feature, you need to have Dokploy UI installed either locally or on a remote server. We recommend using a remote server for better connectivity, security, and isolation, for remote instances we install only a traefik instance. + +If you plan to only deploy apps to remote servers and use Dokploy UI for managing deployments, Dokploy will use around 250 MB of RAM and minimal CPU, so a low-resource server should be sufficient. + +All the features we have documented previously are supported by Dokploy Multi Server. The only feature not supported is remote server monitoring, due to performance reasons. However, all functionalities should work the same as when deploying on the same server where Dokploy UI is installed. + +## Features + +1. **Enter the terminal**: Allows you to access the terminal of the remote server. +2. **Setup Server**: Allows you to configure the remote server. + - **SSH Keys**: Steps to add SSH keys to the remote server. + - **Deployments**: Steps to configure the remote server for deploying applications. +3. **Edit Server**: Allows you to modify the remote server's details, such as SSH key, name, description, IP, etc. +4. **View Actions**: Lets you perform actions like managing the Traefik instance, storage, and activating Docker cleanup. +5. **Show Traefik File System**: Displays the contents of the remote server's directory. +6. **Show Docker Containers**: Shows the Docker containers running on the remote server. + + + Remote server monitoring is not supported due to performance reasons. + diff --git a/apps/docs/content/docs/core/troubleshooting/overview.mdx b/apps/docs/content/docs/core/troubleshooting/overview.mdx index d44d3d66..66a9fe80 100644 --- a/apps/docs/content/docs/core/troubleshooting/overview.mdx +++ b/apps/docs/content/docs/core/troubleshooting/overview.mdx @@ -3,4 +3,90 @@ title: Overview description: Solve the most common problems that occur when using Dokploy. --- -WIP \ No newline at end of file +## Applications Domain Not Working? + +You see the deployment succeeded, and logs are running, but the domain isn't working? Here's what to check: + +1. **Correct Port Mapping**: Ensure the domain is using the correct port for your application. For example, if you're using Next.js, the port should be `3000`, or for Laravel, it should be `8000`. If you change the app port, update the domain to reflect that. +2. **Avoid Using `Ports` in Advanced Settings**: Generally, there's no need to use the `Ports` feature unless you want to access your app via `IP:port`. Leaving this feature enabled may interfere with your domain. + +3. **Let's Encrypt Certificates**: It's crucial to point the domain to your server’s IP **before** adding it in Dokploy. If the domain is added first, the certificate won’t be generated, and you may need to recreate the domain or restart Traefik. + +4. **Listen on 0.0.0.0, Not 127.0.0.1**: If your app is bound to `127.0.0.1` (which is common in Vite apps), switch it to `0.0.0.0` to allow external access. + +## Logs and Monitoring Not Working After Changing Application Placement? + +This is expected behavior. If the application is running on a different node (worker), the UI won’t have access to logs or monitoring, as they're not on the same node. + +## Mounts Are Causing My Application Not to Run? + +Docker Swarm won't run your application if there are invalid mounts, even if the deployment shows as successful. Double-check your mounts to ensure they are valid. + +## Volumes in Docker Compose Not Working? + +For Docker Compose, all file mounts defined in the `volumes` section will be stored in the `files` folder. This is the default directory structure: + +## I added a volume to my docker compose, but is not finding the volume? + +For docker compose all the file mounts you've created in the volumes section will be stored to files folder, this is the default structure of the docker compose. + +``` +/application-name + /code + /files +``` + +So instead of using this invalid way to mount a volume: + +```yaml +volumes: + - "/folder:/path/in/container" ❌ +``` + +You should use this format: + +```yaml +volumes: + - "../files/my-database:/var/lib/mysql" ✅ + - "../files/my-configs:/etc/my-app/config" ✅ +``` + +## Logs Not Loading When Deploying to a Remote Server? + +There are a few potential reasons for this: + +1. **Slow Server:**: If the server is too slow, it may struggle to handle concurrent requests, leading to SSL handshake errors. +2. **Insufficient Disk Space:** If the server doesn't have enough disk space, the logs may not load. + +## Docker Compose Domain Not Working? + +When adding a domain in your Docker Compose file, it’s not necessary to expose the ports directly. Simply specify the port where your app is running. Exposing the ports can lead to conflicts with other applications or ports. + +Example of what not to do: + +```yaml +services: + app: + image: dokploy/dokploy:latest + ports: + - 3000:3000 +``` + +Recommended approach: + +```yaml +services: + app: + image: dokploy/dokploy:latest + ports: + - 3000 + - 80 +``` + +Then, when creating the domain in Dokploy, specify the service name and port, like this: + +```yaml +domain: my-app.com +serviceName: app +port: 3000 +``` diff --git a/apps/docs/public/assets/hostinger-add-sshkey.png b/apps/docs/public/assets/hostinger-add-sshkey.png new file mode 100644 index 00000000..dfbb9a77 Binary files /dev/null and b/apps/docs/public/assets/hostinger-add-sshkey.png differ diff --git a/apps/docs/public/assets/multi-server-add-app.png b/apps/docs/public/assets/multi-server-add-app.png new file mode 100644 index 00000000..d6f83882 Binary files /dev/null and b/apps/docs/public/assets/multi-server-add-app.png differ diff --git a/apps/docs/public/assets/multi-server-add-server.png b/apps/docs/public/assets/multi-server-add-server.png new file mode 100644 index 00000000..48e34601 Binary files /dev/null and b/apps/docs/public/assets/multi-server-add-server.png differ diff --git a/apps/docs/public/assets/multi-server-finish.png b/apps/docs/public/assets/multi-server-finish.png new file mode 100644 index 00000000..3f043010 Binary files /dev/null and b/apps/docs/public/assets/multi-server-finish.png differ diff --git a/apps/docs/public/assets/multi-server-overview.png b/apps/docs/public/assets/multi-server-overview.png new file mode 100644 index 00000000..e1d9c5e6 Binary files /dev/null and b/apps/docs/public/assets/multi-server-overview.png differ diff --git a/apps/docs/public/assets/multi-server-setup-2.png b/apps/docs/public/assets/multi-server-setup-2.png new file mode 100644 index 00000000..2f7f907e Binary files /dev/null and b/apps/docs/public/assets/multi-server-setup-2.png differ diff --git a/apps/docs/public/assets/multi-server-setup-3.png b/apps/docs/public/assets/multi-server-setup-3.png new file mode 100644 index 00000000..66f9cc1e Binary files /dev/null and b/apps/docs/public/assets/multi-server-setup-3.png differ diff --git a/apps/docs/public/assets/multi-server-setup-app.png b/apps/docs/public/assets/multi-server-setup-app.png new file mode 100644 index 00000000..88393cb3 Binary files /dev/null and b/apps/docs/public/assets/multi-server-setup-app.png differ diff --git a/apps/docs/public/assets/multi-server-setup.png b/apps/docs/public/assets/multi-server-setup.png new file mode 100644 index 00000000..c5bca6bf Binary files /dev/null and b/apps/docs/public/assets/multi-server-setup.png differ diff --git a/apps/docs/public/assets/ssh-keys.png b/apps/docs/public/assets/ssh-keys.png new file mode 100644 index 00000000..52ee5c5e Binary files /dev/null and b/apps/docs/public/assets/ssh-keys.png differ diff --git a/apps/dokploy/LICENSE.MD b/apps/dokploy/LICENSE.MD index 9031c94b..59e9d822 100644 --- a/apps/dokploy/LICENSE.MD +++ b/apps/dokploy/LICENSE.MD @@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations ## Additional Terms for Specific Features -The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: +The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: -- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version. -- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. -- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service. +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service. For further inquiries or permissions, please contact us directly. diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 5561999c..c411566a 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -1,6 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { APPLICATIONS_PATH } from "@/server/constants"; +import { paths } from "@/server/constants"; +const { APPLICATIONS_PATH } = paths(); +import type { ApplicationNested } from "@/server/utils/builders"; import { unzipDrop } from "@/server/utils/builders/drop"; import AdmZip from "adm-zip"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; @@ -11,11 +13,84 @@ if (typeof window === "undefined") { globalThis.FileList = undici.FileList as any; } +const baseApp: ApplicationNested = { + applicationId: "", + applicationStatus: "done", + appName: "", + autoDeploy: true, + serverId: "", + branch: null, + dockerBuildStage: "", + buildArgs: null, + buildPath: "/", + gitlabPathNamespace: "", + buildType: "nixpacks", + bitbucketBranch: "", + bitbucketBuildPath: "", + bitbucketId: "", + bitbucketRepository: "", + bitbucketOwner: "", + githubId: "", + gitlabProjectId: 0, + gitlabBranch: "", + gitlabBuildPath: "", + gitlabId: "", + gitlabRepository: "", + gitlabOwner: "", + command: null, + cpuLimit: null, + cpuReservation: null, + createdAt: "", + customGitBranch: "", + customGitBuildPath: "", + customGitSSHKeyId: null, + customGitUrl: "", + description: "", + dockerfile: null, + dockerImage: null, + dropBuildPath: null, + enabled: null, + env: null, + healthCheckSwarm: null, + labelsSwarm: null, + memoryLimit: null, + memoryReservation: null, + modeSwarm: null, + mounts: [], + name: "", + networkSwarm: null, + owner: null, + password: null, + placementSwarm: null, + ports: [], + projectId: "", + publishDirectory: null, + redirects: [], + refreshToken: "", + registry: null, + registryId: null, + replicas: 1, + repository: null, + restartPolicySwarm: null, + rollbackConfigSwarm: null, + security: [], + sourceType: "git", + subtitle: null, + title: null, + updateConfigSwarm: null, + username: null, + dockerContextPath: null, +}; +// vi.mock("@/server/constants", () => ({ - APPLICATIONS_PATH: "./__test__/drop/zips/output", + paths: () => ({ + APPLICATIONS_PATH: "./__test__/drop/zips/output", + }), + // APPLICATIONS_PATH: "./__test__/drop/zips/output", })); describe("unzipDrop using real zip files", () => { + // const { APPLICATIONS_PATH } = paths(); beforeAll(async () => { await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true }); }); @@ -25,39 +100,42 @@ describe("unzipDrop using real zip files", () => { }); it("should correctly extract a zip with a single root folder", async () => { - const appName = "single-file"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "single-file"; + // const appName = "single-file"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); expect(files.some((f) => f.name === "test.txt")).toBe(true); }); it("should correctly extract a zip with a single root folder and a subfolder", async () => { - const appName = "folderwithfile"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "folderwithfile"; + // const appName = "folderwithfile"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); expect(files.some((f) => f.name === "folder1.txt")).toBe(true); }); it("should correctly extract a zip with multiple root folders", async () => { - const appName = "two-folders"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "two-folders"; + // const appName = "two-folders"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/two-folders.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); @@ -66,13 +144,14 @@ describe("unzipDrop using real zip files", () => { }); it("should correctly extract a zip with a single root with a file", async () => { - const appName = "nested"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "nested"; + // const appName = "nested"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/nested.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); @@ -82,13 +161,14 @@ describe("unzipDrop using real zip files", () => { }); it("should correctly extract a zip with a single root with a folder", async () => { - const appName = "folder-with-sibling-file"; - const outputPath = path.join(APPLICATIONS_PATH, appName, "code"); + baseApp.appName = "folder-with-sibling-file"; + // const appName = "folder-with-sibling-file"; + const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip"); const zipBuffer = zip.toBuffer(); const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, appName); + await unzipDrop(file, baseApp); const files = await fs.readdir(outputPath, { withFileTypes: true }); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 7ca9f169..222f8fd7 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -9,6 +9,7 @@ const baseApp: ApplicationNested = { applicationStatus: "done", appName: "", autoDeploy: true, + serverId: "", branch: null, dockerBuildStage: "", buildArgs: null, diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index fd91703b..6750527d 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { {isError && {error?.message}} +
+ + Changing settings such as placements may cause the logs/monitoring + to be unavailable. + +
{ - const { data } = api.application.readTraefikConfig.useQuery( + const { data, isLoading } = api.application.readTraefikConfig.useQuery( { applicationId, }, @@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => { - {data === null ? ( + {isLoading ? ( + + Loading... + + + ) : !data ? (
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index 13a7703b..c24d8781 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -7,7 +7,7 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { AlertTriangle, Package } from "lucide-react"; +import { Package } from "lucide-react"; import React from "react"; import { AddVolumes } from "./add-volumes"; import { DeleteVolume } from "./delete-volume"; diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 4af5c429..2e892523 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -11,8 +11,9 @@ interface Props { logPath: string | null; open: boolean; onClose: () => void; + serverId?: string; } -export const ShowDeployment = ({ logPath, open, onClose }: Props) => { +export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { const [data, setData] = useState(""); const endOfLogsRef = useRef(null); @@ -21,7 +22,7 @@ export const ShowDeployment = ({ logPath, open, onClose }: Props) => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`; + const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`; const ws = new WebSocket(wsUrl); ws.onmessage = (e) => { diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 0458bbbb..c2288bb8 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => { { applicationId }, { enabled: !!applicationId, - refetchInterval: 5000, + refetchInterval: 1000, }, ); const [url, setUrl] = React.useState(""); @@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
)} setActiveLog(null)} logPath={activeLog} diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index d77796d0..43a3cb69 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -175,6 +175,7 @@ export const AddDomain = ({ onClick={() => { generateDomain({ appName: application?.appName || "", + serverId: application?.serverId || "", }) .then((domain) => { field.onChange(domain); @@ -296,11 +297,7 @@ export const AddDomain = ({ - diff --git a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx index d8a33384..f9115c76 100644 --- a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx +++ b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx @@ -45,14 +45,17 @@ export const DeployApplication = ({ applicationId }: Props) => { Cancel { - toast.success("Deploying Application...."); - - await refetch(); await deploy({ applicationId, - }).catch(() => { - toast.error("Error to deploy Application"); - }); + }) + .then(async () => { + toast.success("Application deployed succesfully"); + await refetch(); + }) + + .catch(() => { + toast.error("Error to deploy Application"); + }); await refetch(); }} diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx index 28c34963..4ed9df16 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx @@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => { type="submit" className="w-fit" isLoading={isLoading} - disabled={!zip} + disabled={!zip || isLoading} > Deploy{" "} diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index 870f5d54..277ae1eb 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { ) : ( )} - + - + {data && data?.length > 0 && ( +
+
+ + +
- + )} ); diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx index d8f87f39..876d6838 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -18,10 +18,15 @@ const Terminal = dynamic( interface Props { containerId: string; + serverId?: string; children?: React.ReactNode; } -export const DockerTerminalModal = ({ children, containerId }: Props) => { +export const DockerTerminalModal = ({ + children, + containerId, + serverId, +}: Props) => { return ( @@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => { - + ); diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 03001af7..4008d6fd 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach"; interface Props { id: string; containerId: string; + serverId?: string; } -export const DockerTerminal: React.FC = ({ id, containerId }) => { +export const DockerTerminal: React.FC = ({ + id, + containerId, + serverId, +}) => { const termRef = useRef(null); const [activeWay, setActiveWay] = React.useState("bash"); useEffect(() => { @@ -33,7 +38,7 @@ export const DockerTerminal: React.FC = ({ id, containerId }) => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`; + const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`; const ws = new WebSocket(wsUrl); diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx index d2dc1d78..3dfe9875 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/form"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer< interface Props { path: string; + serverId?: string; } -export const ShowTraefikFile = ({ path }: Props) => { - const { data, refetch } = api.settings.readTraefikFile.useQuery( +export const ShowTraefikFile = ({ path, serverId }: Props) => { + const { + data, + refetch, + isLoading: isLoadingFile, + } = api.settings.readTraefikFile.useQuery( { path, + serverId, }, { enabled: !!path, @@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => { }); useEffect(() => { - if (data) { - form.reset({ - traefikConfig: data || "", - }); - } + form.reset({ + traefikConfig: data || "", + }); }, [form, form.reset, data]); const onSubmit = async (data: UpdateServerMiddlewareConfig) => { @@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => { await mutateAsync({ traefikConfig: data.traefikConfig, path, + serverId, }) .then(async () => { toast.success("Traefik config Updated"); @@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => { className="w-full relative z-[5]" >
- ( - - Traefik config - - {path} - - - + + Loading... + + +
+ ) : ( + ( + + Traefik config + + {path} + + + - + {...field} + /> + -
-										
-									
-
- -
-
- )} - /> +
+											
+										
+
+ +
+ + )} + /> + )}
-
diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx index e3e874c5..0aaf9990 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx @@ -1,20 +1,47 @@ -import React from "react"; - +import { AlertBlock } from "@/components/shared/alert-block"; import { Tree } from "@/components/ui/file-tree"; -import { api } from "@/utils/api"; -import { FileIcon, Folder, Workflow } from "lucide-react"; - import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; +import React from "react"; import { ShowTraefikFile } from "./show-traefik-file"; -export const ShowTraefikSystem = () => { +interface Props { + serverId?: string; +} +export const ShowTraefikSystem = ({ serverId }: Props) => { const [file, setFile] = React.useState(null); - const { data: directories } = api.settings.readDirectories.useQuery(); + const { + data: directories, + isLoading, + error, + isError, + } = api.settings.readDirectories.useQuery( + { + serverId, + }, + { + retry: 2, + }, + ); return (
+ {isError && ( + + {error?.message} + + )} + {isLoading && ( +
+ + Loading... + + +
+ )} {directories?.length === 0 && (
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => { />
{file ? ( - + ) : (
diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx index 44b6e39c..925e213d 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx @@ -36,7 +36,10 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => { ) : ( )} - +
+ ( + + + + + + Select a Server (Optional) + + + + + + If not server is selected, the application will be + deployed on the server where the user is logged in. + + + + + + + + + )} + /> { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); + const { data: servers } = api.server.withSSHKey.useQuery(); const postgresMutation = api.postgres.create.useMutation(); const mongoMutation = api.mongo.create.useMutation(); const redisMutation = api.redis.create.useMutation(); @@ -161,6 +172,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { description: "", databaseName: "", databaseUser: "", + serverId: null, }, resolver: zodResolver(mySchema), }); @@ -183,6 +195,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { appName: data.appName, dockerImage: defaultDockerImage, projectId, + serverId: data.serverId, description: data.description, }; @@ -191,8 +204,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName, + databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], + serverId: data.serverId, }); } else if (data.type === "mongo") { promise = mongoMutation.mutateAsync({ @@ -200,11 +215,13 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], + serverId: data.serverId, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, + serverId: data.serverId, projectId, }); } else if (data.type === "mariadb") { @@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], + serverId: data.serverId, }); } else if (data.type === "mysql") { promise = mysqlMutation.mutateAsync({ @@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], databaseRootPassword: data.databaseRootPassword, + serverId: data.serverId, }); } @@ -342,7 +361,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { {...field} onChange={(e) => { const val = e.target.value?.trim() || ""; - form.setValue("appName", `${slug}-${val}`); + form.setValue( + "appName", + `${slug}-${val.toLowerCase()}`, + ); field.onChange(val); }} /> @@ -352,6 +374,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { )} /> + ( + + Select a Server + + + + )} + /> { const [open, setOpen] = useState(false); const { data } = api.compose.templates.useQuery(); const [selectedTags, setSelectedTags] = useState([]); + const { data: servers } = api.server.withSSHKey.useQuery(); const { data: tags, isLoading: isLoadingTags } = api.compose.getTags.useQuery(); const utils = api.useUtils(); + + const [serverId, setServerId] = useState(undefined); const { mutateAsync, isLoading, error, isError } = api.compose.deployTemplate.useMutation(); @@ -109,7 +129,6 @@ export const AddTemplate = ({ projectId }: Props) => { role="combobox" className={cn( "md:max-w-[15rem] w-full justify-between !bg-input", - // !field.value && "text-muted-foreground", )} > {isLoadingTags @@ -268,30 +287,79 @@ export const AddTemplate = ({ projectId }: Props) => { {template.name} template and add it to your project. + +
+ + + + + + + + If not server is selected, the + application will be deployed on the + server where the user is logged in. + + + + + + +
Cancel { - await mutateAsync({ + const promise = mutateAsync({ projectId, + serverId: serverId || undefined, id: template.id, - }) - .then(async () => { - toast.success( - `Succesfully created ${template.name} application from template`, - ); - + }); + toast.promise(promise, { + loading: "Setting up...", + success: (data) => { utils.project.one.invalidate({ projectId, }); setOpen(false); - }) - .catch(() => { - toast.error( - `Error creating ${template.name} application from template`, - ); - }); + return `${template.name} template created succesfully`; + }, + error: (err) => { + return `Ocurred an error deploying ${template.name} template`; + }, + }); }} > Confirm diff --git a/apps/dokploy/components/dashboard/projects/add.tsx b/apps/dokploy/components/dashboard/projects/add.tsx index 1b9f37f8..bd8f268f 100644 --- a/apps/dokploy/components/dashboard/projects/add.tsx +++ b/apps/dokploy/components/dashboard/projects/add.tsx @@ -17,6 +17,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; + import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; @@ -109,6 +110,7 @@ export const AddProject = () => { )} />
+ { )} - + + + + Actions + + + { + await reloadServer() + .then(async () => { + toast.success("Server Reloaded"); + }) + .catch(() => { + toast.success("Server Reloaded"); + }); + }} + > + Reload + + + Watch logs + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx new file mode 100644 index 00000000..1f7f2d14 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx @@ -0,0 +1,44 @@ +import { CardDescription, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { useState } from "react"; +import { ShowStorageActions } from "./show-storage-actions"; +import { ShowTraefikActions } from "./show-traefik-actions"; +import { ToggleDockerCleanup } from "./toggle-docker-cleanup"; +interface Props { + serverId: string; +} + +export const ShowServerActions = ({ serverId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + return ( + + + e.preventDefault()} + > + View Actions + + + +
+ Web server settings + Reload or clean the web server. +
+ +
+ + + +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx new file mode 100644 index 00000000..b3f9c334 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-storage-actions.tsx @@ -0,0 +1,177 @@ +import { Button } from "@/components/ui/button"; +import React from "react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +interface Props { + serverId?: string; +} +export const ShowStorageActions = ({ serverId }: Props) => { + const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } = + api.settings.cleanAll.useMutation(); + + const { + mutateAsync: cleanDockerBuilder, + isLoading: cleanDockerBuilderIsLoading, + } = api.settings.cleanDockerBuilder.useMutation(); + + const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } = + api.settings.cleanMonitoring.useMutation(); + const { + mutateAsync: cleanUnusedImages, + isLoading: cleanUnusedImagesIsLoading, + } = api.settings.cleanUnusedImages.useMutation(); + + const { + mutateAsync: cleanUnusedVolumes, + isLoading: cleanUnusedVolumesIsLoading, + } = api.settings.cleanUnusedVolumes.useMutation(); + + const { + mutateAsync: cleanStoppedContainers, + isLoading: cleanStoppedContainersIsLoading, + } = api.settings.cleanStoppedContainers.useMutation(); + + return ( + + + + + + Actions + + + { + await cleanUnusedImages({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned images"); + }) + .catch(() => { + toast.error("Error to clean images"); + }); + }} + > + Clean unused images + + { + await cleanUnusedVolumes({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned volumes"); + }) + .catch(() => { + toast.error("Error to clean volumes"); + }); + }} + > + Clean unused volumes + + + { + await cleanStoppedContainers({ + serverId: serverId, + }) + .then(async () => { + toast.success("Stopped containers cleaned"); + }) + .catch(() => { + toast.error("Error to clean stopped containers"); + }); + }} + > + Clean stopped containers + + + { + await cleanDockerBuilder({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned Docker Builder"); + }) + .catch(() => { + toast.error("Error to clean Docker Builder"); + }); + }} + > + Clean Docker Builder & System + + {!serverId && ( + { + await cleanMonitoring() + .then(async () => { + toast.success("Cleaned Monitoring"); + }) + .catch(() => { + toast.error("Error to clean Monitoring"); + }); + }} + > + Clean Monitoring + + )} + + { + await cleanAll({ + serverId: serverId, + }) + .then(async () => { + toast.success("Cleaned all"); + }) + .catch(() => { + toast.error("Error to clean all"); + }); + }} + > + Clean all + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx new file mode 100644 index 00000000..a0ea3f5e --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -0,0 +1,125 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import React from "react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +import { cn } from "@/lib/utils"; +import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; +import { ShowModalLogs } from "../../web-server/show-modal-logs"; + +interface Props { + serverId?: string; +} +export const ShowTraefikActions = ({ serverId }: Props) => { + const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } = + api.settings.reloadTraefik.useMutation(); + + const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } = + api.settings.toggleDashboard.useMutation(); + + const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } = + api.settings.haveTraefikDashboardPortEnabled.useQuery({ + serverId, + }); + + return ( + + + + + + Actions + + + { + await reloadTraefik({ + serverId: serverId, + }) + .then(async () => { + toast.success("Traefik Reloaded"); + }) + .catch(() => { + toast.error("Error to reload the traefik"); + }); + }} + > + Reload + + + Watch logs + + + e.preventDefault()} + className="w-full cursor-pointer space-x-3" + > + Modify Env + + + + { + await toggleDashboard({ + enableDashboard: !haveTraefikDashboardPortEnabled, + serverId: serverId, + }) + .then(async () => { + toast.success( + `${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`, + ); + refetchDashboard(); + }) + .catch(() => { + toast.error( + `${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`, + ); + }); + }} + className="w-full cursor-pointer space-x-3" + > + + {haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard + + + {/* + + e.preventDefault()} + > + Enter the terminal + + */} + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx new file mode 100644 index 00000000..17edaa99 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-docker-cleanup.tsx @@ -0,0 +1,52 @@ +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +interface Props { + serverId?: string; +} +export const ToggleDockerCleanup = ({ serverId }: Props) => { + const { data, refetch } = api.admin.one.useQuery(undefined, { + enabled: !serverId, + }); + + const { data: server, refetch: refetchServer } = api.server.one.useQuery( + { + serverId: serverId || "", + }, + { + enabled: !!serverId, + }, + ); + + const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup; + + const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); + return ( +
+ { + await mutateAsync({ + enableDockerCleanup: e, + serverId: serverId, + }) + .then(async () => { + toast.success("Docker Cleanup Enabled"); + }) + .catch(() => { + toast.error("Docker Cleanup Error"); + }); + + if (serverId) { + refetchServer(); + } else { + refetch(); + } + }} + /> + +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/add-server.tsx b/apps/dokploy/components/dashboard/settings/servers/add-server.tsx new file mode 100644 index 00000000..6bd44dcf --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/add-server.tsx @@ -0,0 +1,253 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const Schema = z.object({ + name: z.string().min(1, { + message: "Name is required", + }), + description: z.string().optional(), + ipAddress: z.string().min(1, { + message: "IP Address is required", + }), + port: z.number().optional(), + username: z.string().optional(), + sshKeyId: z.string().min(1, { + message: "SSH Key is required", + }), +}); + +type Schema = z.infer; + +export const AddServer = () => { + const utils = api.useUtils(); + const [isOpen, setIsOpen] = useState(false); + const { data: sshKeys } = api.sshKey.all.useQuery(); + const { mutateAsync, error, isError } = api.server.create.useMutation(); + const form = useForm({ + defaultValues: { + description: "", + name: "", + ipAddress: "", + port: 22, + username: "root", + sshKeyId: "", + }, + resolver: zodResolver(Schema), + }); + + useEffect(() => { + form.reset({ + description: "", + name: "", + ipAddress: "", + port: 22, + username: "root", + sshKeyId: "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: Schema) => { + await mutateAsync({ + name: data.name, + description: data.description || "", + ipAddress: data.ipAddress || "", + port: data.port || 22, + username: data.username || "root", + sshKeyId: data.sshKeyId || "", + }) + .then(async (data) => { + await utils.server.all.invalidate(); + toast.success("Server Created"); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error to create a server"); + }); + }; + + return ( + + + + + + + Add Server + + Add a server to deploy your applications remotely. + + + {isError && {error?.message}} +
+ +
+ ( + + Name + + + + + + + )} + /> +
+ ( + + Description + +