Compare commits

...

73 Commits

Author SHA1 Message Date
Mauricio Siu
1758655f66 feat(swarm): add OpenAPI metadata for drop-deployment endpoint in swarm router 2025-08-04 00:28:03 -06:00
Mauricio Siu
029eed7755 refactor(dashboard): improve layout and spacing in ShowConvertedCompose component 2025-08-03 18:10:19 -06:00
Mauricio Siu
f017536396 chore(package): bump version to v0.24.7 2025-08-03 18:04:24 -06:00
Mauricio Siu
ba5505cf81 feat(auth): add logger configuration to disable logging in production environment 2025-08-03 18:04:09 -06:00
Mauricio Siu
5ff5da9ff9 Merge pull request #2321 from Dokploy/1738-database-placement-constraints
feat(cluster-settings): Add swarm settings for databases
2025-08-03 17:41:08 -06:00
Mauricio Siu
e96a8ea4ad feat(databases): increment ForceUpdate in TaskTemplate for service updates across MariaDB, MongoDB, MySQL, Postgres, and Redis 2025-08-03 17:39:35 -06:00
Mauricio Siu
42864d2472 feat(docker): update generateConfigContainer to accept Partial<ApplicationNested> and enhance mount checks 2025-08-03 17:21:09 -06:00
Mauricio Siu
ae25ea265c feat(databases): enhance database service configuration by integrating health checks, restart policies, and additional settings across MariaDB, MongoDB, MySQL, Postgres, and Redis 2025-08-03 16:52:43 -06:00
Mauricio Siu
0755f28307 refactor(cluster-settings): update dialog content and improve layout in swarm settings component 2025-08-03 16:45:07 -06:00
Mauricio Siu
3d10d48425 fix(cluster-settings): ensure data validation for registryId in form defaults 2025-08-03 16:40:24 -06:00
Mauricio Siu
6e79183f6a feat(cluster-settings): refactor cluster settings components to support multiple database types and add new swarm configuration fields 2025-08-03 16:36:14 -06:00
Mauricio Siu
2318fb062a Merge pull request #2320 from Dokploy/2004-how-to-pause-log-output-in-doploy
feat(docker-logs): add pause/resume functionality for log streaming
2025-08-03 15:45:54 -06:00
Mauricio Siu
14b4bc9d85 feat(docker-logs): add pause/resume functionality for log streaming with message buffering 2025-08-03 15:43:53 -06:00
Mauricio Siu
4c72f1894c Merge pull request #2319 from Dokploy/1614-sort-project-alphabetically-and-by-other-criteria
feat(show-projects): implement project sorting and filtering function…
2025-08-03 15:25:52 -06:00
Mauricio Siu
ebd632df04 feat(show-projects): implement project sorting and filtering functionality 2025-08-03 15:24:56 -06:00
Mauricio Siu
4878ed2b6f Merge pull request #2318 from Dokploy/2223-local-deployment-doesnt-replace-old-container
feat(handle-ports): add publish mode warning for host mode limitations
2025-08-03 15:15:05 -06:00
Mauricio Siu
1d3ab2bafa feat(handle-ports): add publish mode warning for host mode limitations 2025-08-03 15:13:11 -06:00
Mauricio Siu
7cb7cfa2a8 feat(pr-template): add a pull request template to standardize submissions 2025-08-03 13:38:17 -06:00
Mauricio Siu
6009697710 Merge pull request #2312 from JamBalaya56562/create-pr
ci(create-pr): remove unused id
2025-08-03 13:30:29 -06:00
Mauricio Siu
6be86b49bb Merge pull request #2292 from JamBalaya56562/lint-shared
refactor: lint apps/components/shared files
2025-08-03 13:27:11 -06:00
Mauricio Siu
1e81244e0b docs(contributing): update pull request guidelines and clarify branch roles 2025-08-03 13:21:02 -06:00
Mauricio Siu
f659ea463d Merge pull request #2311 from JamBalaya56562/contributing
docs(contributing): use Alerts syntax of GitHub markdown
2025-08-03 13:17:16 -06:00
JamBalaya56562
caf57276a4 ci(create-pr): remove unused id 2025-08-03 18:09:33 +09:00
JamBalaya56562
15c6c7e657 docs(contributing): use Alerts syntax of GitHub markdown 2025-08-03 17:50:29 +09:00
JamBalaya56562
8be0db385a refactor: lint apps/components/shared files 2025-08-03 17:25:06 +09:00
Mauricio Siu
4259e2533e Merge pull request #2309 from Dokploy/1778-railpack-frontend-version-is-hardcoded
1778 railpack frontend version is hardcoded
2025-08-03 02:22:40 -06:00
Mauricio Siu
a138d12082 chore(tests): set railpackVersion to '0.2.2' in drop test configuration 2025-08-03 02:20:28 -06:00
Mauricio Siu
a2405ddd84 chore(build): add cleanup step to remove builder container after Railpack build completion 2025-08-03 02:14:59 -06:00
Mauricio Siu
e785ad5599 fix(build): set default railpackVersion to '0.2.2' and ensure cleanup in build process
- Updated the ShowBuildChooseForm component to default railpackVersion to '0.2.2' if not specified.
- Added cleanup step in the buildRailpack function to remove the builder container after execution.
- Refactored application router to include railpackVersion in the application schema.
2025-08-03 02:00:10 -06:00
Mauricio Siu
cc6445a8ec feat(application): add railpackVersion field to application schema and update related components
- Introduced a new column `railpackVersion` in the application table with a default value of '0.2.2'.
- Updated the application form to include a field for `railpackVersion` when the build type is set to railpack.
- Adjusted the build process to utilize the specified `railpackVersion` dynamically.
- Enhanced validation schema to accommodate the new field.
2025-08-03 01:54:07 -06:00
Mauricio Siu
b8f27d7b76 Merge pull request #2306 from Dokploy/2170-docker-compose-volume-mount-causes-oci-runtime-exec-failed-unless-container-is-force-recreated
refactor(docker): update docker exec command to set working directory…
2025-08-03 01:48:22 -06:00
Mauricio Siu
32f61b5e9b refactor(docker): update docker exec command to set working directory for terminal sessions 2025-08-03 01:47:31 -06:00
Mauricio Siu
e2d6b5eb8a Merge pull request #2305 from Dokploy/2249-traefik-doesnt-reload-when-installed-as-a-docker-service
refactor(traefik): update Traefik initialization to support standalon…
2025-08-03 01:21:45 -06:00
Mauricio Siu
7413c9484a refactor(settings): remove unused getTraefikPorts function to streamline settings router 2025-08-03 01:19:14 -06:00
Mauricio Siu
607c505c4b refactor(traefik): update Traefik initialization to support standalone and service modes, enhance port handling with protocol specification 2025-08-03 01:18:18 -06:00
Mauricio Siu
42629e83a1 Merge pull request #2304 from Dokploy/1681-application-automated-build-fails-due-connection-http-basic-access-denied
refactor(gitea, gitlab): remove unused parameters and fetch entities …
2025-08-02 21:24:51 -06:00
Mauricio Siu
b9f18cddf7 refactor(gitlab): reorder token refresh and provider fetching for improved clarity 2025-08-02 21:24:33 -06:00
Mauricio Siu
2790895642 refactor(gitea, gitlab): remove unused parameters and fetch entities by ID 2025-08-02 21:20:50 -06:00
Mauricio Siu
c21c88d89f chore(package): bump version from v0.24.5 to v0.24.6 2025-08-02 19:37:10 -06:00
Mauricio Siu
bf6b9c6893 Merge pull request #2302 from Dokploy/2281-url-rewrite-via-domain-routing-not-functioning
refactor(domain): enhance middleware handling for Traefik routers and…
2025-08-02 19:32:33 -06:00
Mauricio Siu
e08fe1dbea test(labels): add comprehensive tests for middleware handling in createDomainLabels function 2025-08-02 19:31:21 -06:00
Mauricio Siu
0b9eaac390 refactor(domain): enhance middleware handling for Traefik routers and improve path validation 2025-08-02 19:24:11 -06:00
Mauricio Siu
5ed49a5ca1 Merge pull request #2301 from Dokploy/2074-keep-latest-is-not-respected
fix(backups): change backup file extension from .dump.gz to .sql.gz f…
2025-08-02 18:41:11 -06:00
Mauricio Siu
1300a6242c fix(backups): change backup file extension from .dump.gz to .sql.gz for consistency 2025-08-02 18:40:40 -06:00
Mauricio Siu
201f07c084 fix(volumes): adjust layout for volume display and improve conditional rendering 2025-08-02 16:13:51 -06:00
Mauricio Siu
c5161f1612 Merge pull request #2300 from Dokploy/2259-file-mounts-not-updating
feat(mount): refactor updateMount logic and add updateFileMount funct…
2025-08-02 16:08:15 -06:00
Mauricio Siu
0755de03c2 feat(mount): refactor updateMount logic and add updateFileMount function for handling file mounts 2025-08-02 16:06:46 -06:00
Mauricio Siu
f376ea5fec Merge pull request #2297 from Dokploy/feat/add-custom-service-field-domains-compose
feat(dashboard): add manual input for service name in domains for docker compose
2025-08-02 13:21:01 -06:00
Mauricio Siu
346eb24926 feat(dashboard): add manual input option for service name selection in domain handling 2025-08-02 13:20:00 -06:00
Mauricio Siu
fe45c69939 Merge pull request #2296 from Dokploy/2291-mise-is-trying-to-verify-the-gpg-signature-of-the-nodejs-binary-for-node22180-but-it-cant-find-the-public-key-used-to-verify-the-signature
chore: update Railpack version to 0.2.2 in Dockerfile and related scr…
2025-08-02 13:14:58 -06:00
Mauricio Siu
39d46a51b3 chore: update Railpack version to 0.2.2 in Dockerfile and related scripts 2025-08-02 13:08:46 -06:00
Mauricio Siu
3e193590cc Merge pull request #2295 from JamBalaya56562/readme
docs: polish `README.md`
2025-08-02 12:50:26 -06:00
Mauricio Siu
c157a353f3 Merge pull request #2279 from A-D-E/fix/gitlab-branches-pagination
The getGitlabBranches function was only returning the first 20 branches
2025-08-02 12:48:32 -06:00
Mauricio Siu
2a14ae0c7f Merge branch 'canary' into fix/gitlab-branches-pagination 2025-08-02 12:44:14 -06:00
JamBalaya56562
144c74e7f7 docs: polish README.md 2025-08-02 20:38:28 +09:00
Mauricio Siu
1d4d766f3a Merge pull request #2229 from danielepintore/canary
feat(dashboard): generate user fallback avatar using user email and allow user to choose default avatar
2025-08-02 00:34:16 -06:00
Mauricio Siu
8532cba638 Merge pull request #2255 from Marukome0743/lint-layouts
refactor: lint apps/components/layouts files
2025-08-02 00:27:09 -06:00
Mauricio Siu
fdb4b176cb Merge pull request #2254 from Marukome0743/lint-test
refactor: lint apps/docker/__test__ files
2025-08-02 00:27:00 -06:00
Mauricio Siu
f2b214f8f0 Merge pull request #2266 from rainwashed/canary
fix: github app creation name conflicting with already existing Dokploy names
2025-08-02 00:23:15 -06:00
autofix-ci[bot]
0bcc59f90f [autofix.ci] apply automated fixes 2025-08-02 06:19:09 +00:00
Mauricio Siu
7ae4bf3215 Merge pull request #2290 from Dokploy/2277-two-copies-of-the-same-volume-backup-are-uploaded-to-s3
refactor(backup): consolidate utility imports and add local backup cl…
2025-08-01 01:28:17 -06:00
Mauricio Siu
0f5cf37757 refactor(backup): consolidate utility imports and add local backup cleanup after S3 upload 2025-08-01 01:27:35 -06:00
Mauricio Siu
a7bde655da Merge pull request #2289 from Dokploy/2282-custom-compose-path-not-being-used-when-manually-restarting-service
refactor(compose): reorganize imports and simplify command execution …
2025-08-01 01:16:33 -06:00
Mauricio Siu
295b6df5e1 refactor(compose): reorganize imports and simplify command execution for starting Docker Compose 2025-08-01 01:15:29 -06:00
Mauricio Siu
b5b63eae4f Merge pull request #2288 from Dokploy/git-fetch-origin-git-checkout-2263-swarm-containers-no-data-found
refactor(application): update application handling to support multipl…
2025-08-01 00:35:32 -06:00
Mauricio Siu
794e03460f refactor(application): update application handling to support multiple app names and improve data structure 2025-08-01 00:34:57 -06:00
A-D-E
e8f36f8ba5 The getGitlabBranches function was only returning the first 20 branches
due to GitLab's default API pagination limit. This prevented users from
accessing branches in repositories with more than 20 branches.

Changes:
- Add pagination loop to fetch all branches across multiple pages
- Set per_page to 100 (GitLab's maximum) for efficiency
- Add safety check using x-total header to prevent unnecessary requests
- Follow the same pagination pattern as validateGitlabProvider function

Fixes issue where branch selection was limited to first 20 branches
in repositories with many branches.
2025-07-30 15:17:14 +02:00
f9210d3165 lint: formatted changes using biome 2025-07-28 23:39:06 +02:00
rainwashed
9bc6411c98 fix: github app creation name conflicting with already existing Dokploy-Time names
appended a 5-char random string to the name creation as to prevent
conflicts with other existing Dokploy GitHub apps.
2025-07-28 17:18:30 -04:00
f8261b5364 feat(dashboard): use username instead of email for the generation of the
fallback avatar image
2025-07-28 19:20:05 +02:00
30c2c7afb0 feat(dashboard): generate user fallback avatar using user email. Allow
user to select the default avatar.
2025-07-28 16:17:52 +02:00
Marukome0743
f26c1c0da6 refactor: lint apps/docker/__test__ files 2025-07-28 20:32:08 +09:00
Marukome0743
d02976476a refactor: lint apps/components/layouts files 2025-07-28 19:56:44 +09:00
100 changed files with 14611 additions and 947 deletions

21
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,21 @@
## What is this PR about?
Please describe in a short paragraph what this PR is about.
## Checklist
Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance.
## Issues related (if applicable)
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
Example: `closes #123`
## Screenshots (if applicable)
If you include a video or screenshot, would be awesome so we can see the changes in action.

View File

@@ -19,17 +19,14 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Get version from package.json - name: Get version from package.json
id: package_version
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
- name: Get latest GitHub tag - name: Get latest GitHub tag
id: latest_tag
run: | run: |
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) 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=$LATEST_TAG" >> $GITHUB_ENV
echo $LATEST_TAG echo $LATEST_TAG
- name: Compare versions - name: Compare versions
id: compare_versions
run: | run: |
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
VERSION_CHANGED="true" VERSION_CHANGED="true"
@@ -42,7 +39,6 @@ jobs:
echo "Latest tag: ${{ env.LATEST_TAG }}" echo "Latest tag: ${{ env.LATEST_TAG }}"
echo "Version changed: $VERSION_CHANGED" echo "Version changed: $VERSION_CHANGED"
- name: Check if a PR already exists - name: Check if a PR already exists
id: check_pr
run: | run: |
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV

View File

@@ -87,7 +87,8 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. > [!NOTE]
> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build ## Build
@@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command
pnpm run reset-password pnpm run reset-password
``` ```
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/)
```bash ```bash
bunx lt --port 3000 pnpm dlx localtunnel --port 3000
``` ```
If you run into permission issues of docker run the following command If you run into permission issues of docker run the following command
@@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
## Pull Request ## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release. - The `canary` branch is the source of truth and should always reflect the latest stable release.
- Create a new branch for each feature or bug fix. - Create a new branch for each feature or bug fix.
- Make sure to add tests for your changes. - Make sure to add tests for your changes.
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. - Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
@@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
- If your pull request fixes an open issue, please reference the issue in the pull request description. - If your pull request fixes an open issue, please reference the issue in the pull request description.
- Once your pull request is merged, you will be automatically added as a contributor to the project. - Once your pull request is merged, you will be automatically added as a contributor to the project.
**Important Considerations for Pull Requests:**
- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects.
- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
Thank you for your contribution! Thank you for your contribution!
## Templates ## Templates

View File

@@ -58,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx && pnpm install -g tsx
# Install Railpack # Install Railpack
ARG RAILPACK_VERSION=0.0.64 ARG RAILPACK_VERSION=0.2.2
RUN curl -sSL https://railpack.com/install.sh | bash RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks # Install buildpacks

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://dokploy.com"> <a href="https://dokploy.com">
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" /> <img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" />
</a> </a>
</br> </br>
</br> </br>
@@ -13,7 +13,7 @@
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features ## Features
Dokploy includes multiple features to make your life easier. Dokploy includes multiple features to make your life easier.
@@ -43,7 +43,7 @@ curl -sSL https://dokploy.com/install.sh | sh
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## Sponsors ## ♥️ Sponsors
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features. 🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
@@ -95,7 +95,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Community Backers 🤝 ### Community Backers 🤝
#### Organizations: #### Organizations:
[Sponsors on Open Collective](https://opencollective.com/dokploy) [Sponsors on Open Collective](https://opencollective.com/dokploy)
@@ -107,15 +106,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Contributors 🤝 ### Contributors 🤝
<a href="https://github.com/dokploy/dokploy/graphs/contributors"> <a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" /> <img src="https://contrib.rocks/image?repo=dokploy/dokploy" alt="Contributors" />
</a> </a>
## Video Tutorial ## 📺 Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw"> <a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/> <img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a> </a>
## Contributing ## 🤝 Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information. Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToConfigsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllConfigs } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -108,4 +108,136 @@ describe("createDomainLabels", () => {
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000", "traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
); );
}); });
it("should add stripPath middleware when stripPath is enabled", async () => {
const stripPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
};
const labels = await createDomainLabels(appName, stripPathDomain, "web");
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1",
);
});
it("should add internalPath middleware when internalPath is set", async () => {
const internalPathDomain = {
...baseDomain,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(
appName,
internalPathDomain,
"web",
);
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Middleware definition should only appear in web entrypoint
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Both routers should reference the middleware
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1",
);
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
});
it("should combine HTTPS redirect with internalPath middleware in correct order", async () => {
const combinedDomain = {
...baseDomain,
https: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, combinedDomain, "web");
const websecureLabels = await createDomainLabels(
appName,
combinedDomain,
"websecure",
);
// Web entrypoint should have both middlewares with redirect first
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1",
);
// Websecure should only have the addprefix middleware
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
// Middleware definition should only appear once (in web)
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
});
it("should combine all middlewares in correct order", async () => {
const fullDomain = {
...baseDomain,
https: true,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, fullDomain, "web");
// Should have all middleware definitions (only in web)
expect(webLabels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Should have middlewares in correct order: redirect, stripprefix, addprefix
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should not add middleware definitions for websecure entrypoint", async () => {
const internalPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Should not contain any middleware definitions
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// But should reference the middlewares
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
}); });

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import { generateRandomHash } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { import {
addSuffixToAllNetworks, addSuffixToAllNetworks,
addSuffixToNetworksRoot,
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToSecretsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import type { ComposeSpecification } from "@dokploy/server";
import { import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@dokploy/server"; } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,9 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllVolumes,
addSuffixToVolumesRoot,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToVolumesInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllVolumes } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
describe("GitHub Webhook Skip CI", () => { describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = { const mockGithubHeaders = {

View File

@@ -1,12 +1,12 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server"; import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server"; import { unzipDrop } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => { vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal(); const actual = await importOriginal();
return { return {
@@ -25,6 +25,7 @@ if (typeof window === "undefined") {
} }
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",
giteaBranch: "", giteaBranch: "",
@@ -142,7 +143,7 @@ describe("unzipDrop using real zip files", () => {
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
console.log(`Output Path: ${outputPath}`); console.log(`Output Path: ${outputPath}`);
const zipBuffer = zip.toBuffer(); const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
const file = new File([zipBuffer], "single.zip"); const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp); await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true }); const files = await fs.readdir(outputPath, { withFileTypes: true });

View File

@@ -1,5 +1,6 @@
import { parseRawConfig, processLogs } from "@dokploy/server"; import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
describe("processLogs", () => { describe("processLogs", () => {

View File

@@ -1,10 +1,9 @@
import type { Domain } from "@dokploy/server"; import type { ApplicationNested, Domain, Redirect } from "@dokploy/server";
import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@dokploy/server";
import { createRouterConfig } from "@dokploy/server"; import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
rollbackActive: false, rollbackActive: false,
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -26,12 +32,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const HealthCheckSwarmSchema = z const HealthCheckSwarmSchema = z
.object({ .object({
@@ -181,21 +181,38 @@ const addSwarmSettings = z.object({
type AddSwarmSettings = z.infer<typeof addSwarmSettings>; type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
interface Props { interface Props {
applicationId: string; id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
} }
export const AddSwarmSettings = ({ applicationId }: Props) => { export const AddSwarmSettings = ({ id, type }: Props) => {
const { data, refetch } = api.application.one.useQuery( const queryMap = {
{ postgres: () =>
applicationId, 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 }),
enabled: !!applicationId, 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 }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync, isError, error, isLoading } = const mutationMap = {
api.application.update.useMutation(); postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isError, error, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddSwarmSettings>({ const form = useForm<AddSwarmSettings>({
defaultValues: { defaultValues: {
@@ -244,7 +261,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddSwarmSettings) => { const onSubmit = async (data: AddSwarmSettings) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
healthCheckSwarm: data.healthCheckSwarm, healthCheckSwarm: data.healthCheckSwarm,
restartPolicySwarm: data.restartPolicySwarm, restartPolicySwarm: data.restartPolicySwarm,
placementSwarm: data.placementSwarm, placementSwarm: data.placementSwarm,
@@ -270,7 +292,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
Swarm Settings Swarm Settings
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-5xl p-0"> <DialogContent className="sm:max-w-5xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Swarm Settings</DialogTitle> <DialogTitle>Swarm Settings</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -278,10 +300,10 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="px-4"> <div>
<AlertBlock type="info"> <AlertBlock type="info">
Changing settings such as placements may cause the logs/monitoring Changing settings such as placements may cause the logs/monitoring,
to be unavailable. backups and other features to be unavailable.
</AlertBlock> </AlertBlock>
</div> </div>
@@ -289,13 +311,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
<form <form
id="hook-form-add-permissions" id="hook-form-add-permissions"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative" className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative mt-4"
> >
<FormField <FormField
control={form.control} control={form.control}
name="healthCheckSwarm" name="healthCheckSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Health Check</FormLabel> <FormLabel>Health Check</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -351,7 +373,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="restartPolicySwarm" name="restartPolicySwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Restart Policy</FormLabel> <FormLabel>Restart Policy</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -405,7 +427,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="placementSwarm" name="placementSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Placement</FormLabel> <FormLabel>Placement</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -471,7 +493,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="updateConfigSwarm" name="updateConfigSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Update Config</FormLabel> <FormLabel>Update Config</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -529,7 +551,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="rollbackConfigSwarm" name="rollbackConfigSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Rollback Config</FormLabel> <FormLabel>Rollback Config</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -587,7 +609,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="modeSwarm" name="modeSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Mode</FormLabel> <FormLabel>Mode</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -650,7 +672,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="networkSwarm" name="networkSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Network</FormLabel> <FormLabel>Network</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -709,7 +731,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="labelsSwarm" name="labelsSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Labels</FormLabel> <FormLabel>Labels</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -26,43 +33,57 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AddSwarmSettings } from "./modify-swarm-settings"; import { AddSwarmSettings } from "./modify-swarm-settings";
interface Props { interface Props {
applicationId: string; id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
} }
const AddRedirectchema = z.object({ const AddRedirectchema = z.object({
replicas: z.number().min(1, "Replicas must be at least 1"), replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(), registryId: z.string().optional(),
}); });
type AddCommand = z.infer<typeof AddRedirectchema>; type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ applicationId }: Props) => { export const ShowClusterSettings = ({ id, type }: Props) => {
const { data } = api.application.one.useQuery( const queryMap = {
{ postgres: () =>
applicationId, api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
}, redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
{ enabled: !!applicationId }, 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 }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { data: registries } = api.registry.all.useQuery(); const { data: registries } = api.registry.all.useQuery();
const utils = api.useUtils(); const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = api.application.update.useMutation(); const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddCommand>({ const form = useForm<AddCommand>({
defaultValues: { defaultValues: {
registryId: data?.registryId || "", ...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1, replicas: data?.replicas || 1,
}, },
resolver: zodResolver(AddRedirectchema), resolver: zodResolver(AddRedirectchema),
@@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
useEffect(() => { useEffect(() => {
if (data?.command) { if (data?.command) {
form.reset({ form.reset({
registryId: data?.registryId || "", ...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1, replicas: data?.replicas || 1,
}); });
} }
@@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddCommand) => { const onSubmit = async (data: AddCommand) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId: id || "",
registryId: postgresId: id || "",
data?.registryId === "none" || !data?.registryId redisId: id || "",
? null mysqlId: id || "",
: data?.registryId, mariadbId: id || "",
mongoId: id || "",
...(type === "application"
? {
registryId:
data?.registryId === "none" || !data?.registryId
? null
: data?.registryId,
}
: {}),
replicas: data?.replicas, replicas: data?.replicas,
}) })
.then(async () => { .then(async () => {
toast.success("Command Updated"); toast.success("Command Updated");
await utils.application.one.invalidate({ await refetch();
applicationId,
});
}) })
.catch(() => { .catch(() => {
toast.error("Error updating the command"); toast.error("Error updating the command");
@@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
<div> <div>
<CardTitle className="text-xl">Cluster Settings</CardTitle> <CardTitle className="text-xl">Cluster Settings</CardTitle>
<CardDescription> <CardDescription>
Add the registry and the replicas of the application Modify swarm settings for the service.
</CardDescription> </CardDescription>
</div> </div>
<AddSwarmSettings applicationId={applicationId} /> <AddSwarmSettings id={id} type={type} />
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<AlertBlock type="info"> <AlertBlock type="info">
@@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
/> />
</div> </div>
{registries && registries?.length === 0 ? ( {type === "application" && (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least a
registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
<> <>
<FormField {registries && registries?.length === 0 ? (
control={form.control} <div className="pt-10">
name="registryId" <div className="flex flex-col items-center gap-3">
render={({ field }) => ( <Server className="size-8 text-muted-foreground" />
<FormItem> <span className="text-base text-muted-foreground">
<FormLabel>Select a registry</FormLabel> To use a cluster feature, you need to configure at least
<Select a registry first. Please, go to{" "}
onValueChange={field.onChange} <Link
defaultValue={field.value} href="/dashboard/settings/cluster"
> className="text-foreground"
<SelectTrigger> >
<SelectValue placeholder="Select a registry" /> Settings
</SelectTrigger> </Link>{" "}
<SelectContent> to do so.
<SelectGroup> </span>
{registries?.map((registry) => ( </div>
<SelectItem </div>
key={registry.registryId} ) : (
value={registry.registryId} <>
> <FormField
{registry.registryName} control={form.control}
</SelectItem> name="registryId"
))} render={({ field }) => (
<SelectItem value={"none"}>None</SelectItem> <FormItem>
<SelectLabel> <FormLabel>Select a registry</FormLabel>
Registries ({registries?.length}) <Select
</SelectLabel> onValueChange={field.onChange}
</SelectGroup> defaultValue={field.value}
</SelectContent> >
</Select> <SelectTrigger>
</FormItem> <SelectValue placeholder="Select a registry" />
)} </SelectTrigger>
/> <SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
</>
)}
</> </>
)} )}

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -26,12 +32,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddPortSchema = z.object({ const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535), publishedPort: z.number().int().min(1).max(65535),
@@ -80,6 +80,11 @@ export const HandlePorts = ({
resolver: zodResolver(AddPortSchema), resolver: zodResolver(AddPortSchema),
}); });
const publishMode = useWatch({
control: form.control,
name: "publishMode",
});
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
publishedPort: data?.publishedPort ?? 0, publishedPort: data?.publishedPort ?? 0,
@@ -253,6 +258,16 @@ export const HandlePorts = ({
</div> </div>
</form> </form>
{publishMode === "host" && (
<AlertBlock type="warning" className="mt-4">
<strong>Host Mode Limitation:</strong> When using Host publish
mode, Docker Swarm has limitations that prevent proper container
updates during deployments. Old containers may not be replaced
automatically. Consider using Ingress mode instead, or be prepared
to manually stop/start the application after deployments.
</AlertBlock>
)}
<DialogFooter> <DialogFooter>
<Button <Button
isLoading={isLoading} isLoading={isLoading}

View File

@@ -1,3 +1,5 @@
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -9,11 +11,10 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources"; import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes"; import { AddVolumes } from "./add-volumes";
import { UpdateVolume } from "./update-volume"; import { UpdateVolume } from "./update-volume";
interface Props { interface Props {
id: string; id: string;
type: ServiceType | "compose"; type: ServiceType | "compose";
@@ -80,7 +81,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
> >
{/* <Package className="size-8 self-center text-muted-foreground" /> */} {/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span> <span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
@@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span> </span>
</div> </div>
)} )}
{mount.type === "file" ? ( {mount.type === "file" && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">File Path</span> <span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{mount.filePath} {mount.filePath}
</span> </span>
</div> </div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)} )}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div> </div>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
<UpdateVolume <UpdateVolume

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -15,12 +21,6 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
export enum BuildType { export enum BuildType {
dockerfile = "dockerfile", dockerfile = "dockerfile",
@@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.2.2"),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.static),
@@ -86,6 +87,7 @@ interface ApplicationData {
herokuVersion?: string | null; herokuVersion?: string | null;
publishDirectory?: string | null; publishDirectory?: string | null;
isStaticSpa?: boolean | null; isStaticSpa?: boolean | null;
railpackVersion?: string | null | undefined;
} }
function isValidBuildType(value: string): value is BuildType { function isValidBuildType(value: string): value is BuildType {
@@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.railpack: case BuildType.railpack:
return { return {
buildType: BuildType.railpack, buildType: BuildType.railpack,
railpackVersion: data.railpackVersion || null,
}; };
default: { default: {
const buildType = data.buildType as BuildType; const buildType = data.buildType as BuildType;
@@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
: null, : null,
isStaticSpa: isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null, data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion:
data.buildType === BuildType.railpack
? data.railpackVersion || "0.2.2"
: null,
}) })
.then(async () => { .then(async () => {
toast.success("Build type saved"); toast.success("Build type saved");
@@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)} )}
/> />
)} )}
{buildType === BuildType.railpack && (
<FormField
control={form.control}
name="railpackVersion"
render={({ field }) => (
<FormItem>
<FormLabel>Railpack Version</FormLabel>
<FormControl>
<Input
placeholder="Railpack Version"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Save Save

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -34,14 +41,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import z from "zod";
export type CacheType = "fetch" | "cache"; export type CacheType = "fetch" | "cache";
@@ -123,6 +122,7 @@ interface Props {
export const AddDomain = ({ id, type, domainId = "", children }: Props) => { export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const [isManualInput, setIsManualInput] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery( const { data, refetch } = api.domain.one.useQuery(
@@ -325,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormItem className="w-full"> <FormItem className="w-full">
<FormLabel>Service Name</FormLabel> <FormLabel>Service Name</FormLabel>
<div className="flex gap-2"> <div className="flex gap-2">
<Select {isManualInput ? (
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl> <FormControl>
<SelectTrigger> <Input
<SelectValue placeholder="Select a service name" /> placeholder="Enter service name manually"
</SelectTrigger> {...field}
className="w-full"
/>
</FormControl> </FormControl>
) : (
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent> <SelectContent>
{services?.map((service, index) => ( {services?.map((service, index) => (
<SelectItem <SelectItem
value={service} value={service}
key={`${service}-${index}`} key={`${service}-${index}`}
> >
{service} {service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem> </SelectItem>
))} </SelectContent>
<SelectItem value="none" disabled> </Select>
Empty )}
</SelectItem> {!isManualInput && (
</SelectContent> <>
</Select> <TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and
load the services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services
from the last deployment/fetch from
the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="secondary"
type="button" type="button"
isLoading={isLoadingServices}
onClick={() => { onClick={() => {
if (cacheType === "fetch") { setIsManualInput(!isManualInput);
refetchServices(); if (!isManualInput) {
} else { field.onChange("");
setCacheType("fetch");
} }
}} }}
> >
<RefreshCw className="size-4 text-muted-foreground" /> {isManualInput ? (
<RefreshCw className="size-4 text-muted-foreground" />
) : (
<span className="text-xs text-muted-foreground">
Manual
</span>
)}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
@@ -373,40 +453,9 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p> <p>
Fetch: Will clone the repository and load {isManualInput
the services ? "Switch to service selection"
</p> : "Enter service name manually"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services from
the last deployment/fetch from the
repository
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -1,3 +1,6 @@
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -10,9 +13,6 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface Props { interface Props {
composeId: string; composeId: string;
@@ -79,7 +79,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
</div> </div>
) : ( ) : (
<> <>
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end my-4">
<Button <Button
variant="secondary" variant="secondary"
isLoading={isLoading} isLoading={isLoading}

View File

@@ -1,13 +1,14 @@
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Download as DownloadIcon, Loader2 } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { LineCountFilter } from "./line-count-filter"; import { LineCountFilter } from "./line-count-filter";
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter"; import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
import { StatusLogsFilter } from "./status-logs-filter"; import { StatusLogsFilter } from "./status-logs-filter";
import { TerminalLine } from "./terminal-line"; import { TerminalLine } from "./terminal-line";
import { type LogLine, getLogType, parseLogs } from "./utils"; import { getLogType, type LogLine, parseLogs } from "./utils";
interface Props { interface Props {
containerId: string; containerId: string;
@@ -61,6 +62,9 @@ export const DockerLogsId: React.FC<Props> = ({
const [showTimestamp, setShowTimestamp] = React.useState(true); const [showTimestamp, setShowTimestamp] = React.useState(true);
const [since, setSince] = React.useState<TimeFilter>("all"); const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<string[]>([]); const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
const [isPaused, setIsPaused] = React.useState(false);
const [messageBuffer, setMessageBuffer] = React.useState<string[]>([]);
const isPausedRef = useRef(false);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
@@ -85,15 +89,38 @@ export const DockerLogsId: React.FC<Props> = ({
const handleLines = (lines: number) => { const handleLines = (lines: number) => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setMessageBuffer([]);
setLines(lines); setLines(lines);
}; };
const handleSince = (value: TimeFilter) => { const handleSince = (value: TimeFilter) => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setMessageBuffer([]);
setSince(value); setSince(value);
}; };
const handlePauseResume = () => {
if (isPaused) {
// Resume: Apply all buffered messages
if (messageBuffer.length > 0) {
const bufferedContent = messageBuffer.join("");
setRawLogs((prev) => {
const updated = prev + bufferedContent;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
setMessageBuffer([]);
}
}
const newPausedState = !isPaused;
setIsPaused(newPausedState);
isPausedRef.current = newPausedState;
};
useEffect(() => { useEffect(() => {
if (!containerId) return; if (!containerId) return;
@@ -102,6 +129,10 @@ export const DockerLogsId: React.FC<Props> = ({
setIsLoading(true); setIsLoading(true);
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setMessageBuffer([]);
// Reset pause state when container changes
setIsPaused(false);
isPausedRef.current = false;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const params = new globalThis.URLSearchParams({ const params = new globalThis.URLSearchParams({
@@ -140,14 +171,22 @@ export const DockerLogsId: React.FC<Props> = ({
ws.onmessage = (e) => { ws.onmessage = (e) => {
if (!isCurrentConnection) return; if (!isCurrentConnection) return;
setRawLogs((prev) => {
const updated = prev + e.data; if (isPausedRef.current) {
const splitLines = updated.split("\n"); // When paused, buffer the messages instead of displaying them
if (splitLines.length > lines) { setMessageBuffer((prev) => [...prev, e.data]);
return splitLines.slice(-lines).join("\n"); } else {
} // When not paused, display messages normally
return updated; setRawLogs((prev) => {
}); const updated = prev + e.data;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
}
setIsLoading(false); setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout); if (noDataTimeout) clearTimeout(noDataTimeout);
}; };
@@ -210,9 +249,15 @@ export const DockerLogsId: React.FC<Props> = ({
}); });
}; };
// Sync isPausedRef with isPaused state
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
useEffect(() => { useEffect(() => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setMessageBuffer([]);
}, [containerId]); }, [containerId]);
useEffect(() => { useEffect(() => {
@@ -260,17 +305,48 @@ export const DockerLogsId: React.FC<Props> = ({
/> />
</div> </div>
<Button <div className="flex gap-2">
variant="outline" <Button
size="sm" variant="outline"
className="h-9 sm:w-auto w-full" size="sm"
onClick={handleDownload} className="h-9"
disabled={filteredLogs.length === 0 || !data?.Name} onClick={handlePauseResume}
> title={isPaused ? "Resume logs" : "Pause logs"}
<DownloadIcon className="mr-2 h-4 w-4" /> >
Download logs {isPaused ? (
</Button> <Play className="mr-2 h-4 w-4" />
) : (
<Pause className="mr-2 h-4 w-4" />
)}
{isPaused ? "Resume" : "Pause"}
</Button>
<Button
variant="outline"
size="sm"
className="h-9 sm:w-auto w-full"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
</div>
</div> </div>
{isPaused && (
<AlertBlock type="warning">
<div className="flex items-center gap-2">
<Pause className="h-4 w-4" />
<span>
Logs paused
{messageBuffer.length > 0 && (
<span className="ml-1 font-medium">
({messageBuffer.length} messages buffered)
</span>
)}
</span>
</div>
</AlertBlock>
)}
<div <div
ref={scrollRef} ref={scrollRef}
onScroll={handleScroll} onScroll={handleScroll}

View File

@@ -1,3 +1,17 @@
import {
AlertTriangle,
ArrowUpDown,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
Search,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
@@ -31,20 +45,14 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { import {
AlertTriangle, Select,
BookIcon, SelectContent,
ExternalLinkIcon, SelectItem,
FolderInput, SelectTrigger,
Loader2, SelectValue,
MoreHorizontalIcon, } from "@/components/ui/select";
Search, import { api } from "@/utils/api";
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { HandleProject } from "./handle-project"; import { HandleProject } from "./handle-project";
import { ProjectEnvironment } from "./project-environment"; import { ProjectEnvironment } from "./project-environment";
@@ -54,15 +62,65 @@ export const ShowProjects = () => {
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation(); const { mutateAsync } = api.project.remove.useMutation();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("projectsSort") || "createdAt-desc";
}
return "createdAt-desc";
});
useEffect(() => {
localStorage.setItem("projectsSort", sortBy);
}, [sortBy]);
const filteredProjects = useMemo(() => { const filteredProjects = useMemo(() => {
if (!data) return []; if (!data) return [];
return data.filter(
// First filter by search query
const filtered = data.filter(
(project) => (project) =>
project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
project.description?.toLowerCase().includes(searchQuery.toLowerCase()), project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
); );
}, [data, searchQuery]);
// Then sort the filtered results
const [field, direction] = sortBy.split("-");
return [...filtered].sort((a, b) => {
let comparison = 0;
switch (field) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "createdAt":
comparison =
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "services": {
const aTotalServices =
a.mariadb.length +
a.mongo.length +
a.mysql.length +
a.postgres.length +
a.redis.length +
a.applications.length +
a.compose.length;
const bTotalServices =
b.mariadb.length +
b.mongo.length +
b.mysql.length +
b.postgres.length +
b.redis.length +
b.applications.length +
b.compose.length;
comparison = aTotalServices - bTotalServices;
break;
}
default:
comparison = 0;
}
return direction === "asc" ? comparison : -comparison;
});
}, [data, searchQuery, sortBy]);
return ( return (
<> <>
@@ -98,14 +156,40 @@ export const ShowProjects = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="w-full relative"> <div className="flex max-sm:flex-col gap-4 items-center w-full">
<Input <div className="flex-1 relative max-sm:w-full">
placeholder="Filter projects..." <Input
value={searchQuery} placeholder="Filter projects..."
onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery}
className="pr-10" onChange={(e) => setSearchQuery(e.target.value)}
/> className="pr-10"
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" /> />
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
<ArrowUpDown className="size-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>
<SelectItem value="createdAt-asc">
Oldest first
</SelectItem>
<SelectItem value="services-desc">
Most services
</SelectItem>
<SelectItem value="services-asc">
Least services
</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
{filteredProjects?.length === 0 && ( {filteredProjects?.length === 0 && (
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4"> <div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">

View File

@@ -24,12 +24,14 @@ export const AddGithubProvider = () => {
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState(""); const [organizationName, setOrganization] = useState("");
const randomString = () => Math.random().toString(36).slice(2, 8);
useEffect(() => { useEffect(() => {
const url = document.location.origin; const url = document.location.origin;
const manifest = JSON.stringify( const manifest = JSON.stringify(
{ {
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`, redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`, name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
url: origin, url: origin,
hook_attributes: { hook_attributes: {
url: `${url}/api/deploy/github`, url: `${url}/api/deploy/github`,

View File

@@ -1,3 +1,4 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -19,7 +20,7 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash } from "@/lib/utils"; import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, User } from "lucide-react"; import { Loader2, User } from "lucide-react";
@@ -257,6 +258,24 @@ export const ProfileForm = () => {
value={field.value} value={field.value}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center" className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
> >
<FormItem key="no-avatar">
<FormLabel className="[&:has([data-state=checked])>.default-avatar]:border-primary [&:has([data-state=checked])>.default-avatar]:border-1 [&:has([data-state=checked])>.default-avatar]:p-px cursor-pointer">
<FormControl>
<RadioGroupItem
value=""
className="sr-only"
/>
</FormControl>
<Avatar className="default-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform">
<AvatarFallback className="rounded-lg">
{getFallbackAvatarInitials(
data?.user?.name,
)}
</AvatarFallback>
</Avatar>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => ( {availableAvatars.map((image) => (
<FormItem key={image}> <FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer"> <FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -1,5 +1,6 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -10,8 +11,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
import { ShowModalLogs } from "../../web-server/show-modal-logs"; import { ShowModalLogs } from "../../web-server/show-modal-logs";

View File

@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import type React from "react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -19,15 +27,15 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import type React from "react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@@ -37,6 +45,7 @@ interface Props {
const PortSchema = z.object({ const PortSchema = z.object({
targetPort: z.number().min(1, "Target port is required"), targetPort: z.number().min(1, "Target port is required"),
publishedPort: z.number().min(1, "Published port is required"), publishedPort: z.number().min(1, "Published port is required"),
protocol: z.enum(["tcp", "udp", "sctp"]),
}); });
const TraefikPortsSchema = z.object({ const TraefikPortsSchema = z.object({
@@ -75,12 +84,17 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
useEffect(() => { useEffect(() => {
if (currentPorts) { if (currentPorts) {
form.reset({ ports: currentPorts }); form.reset({
ports: currentPorts.map((port) => ({
...port,
protocol: port.protocol as "tcp" | "udp" | "sctp",
})),
});
} }
}, [currentPorts, form]); }, [currentPorts, form]);
const handleAddPort = () => { const handleAddPort = () => {
append({ targetPort: 0, publishedPort: 0 }); append({ targetPort: 0, publishedPort: 0, protocol: "tcp" });
}; };
const onSubmit = async (data: TraefikPortsForm) => { const onSubmit = async (data: TraefikPortsForm) => {
@@ -96,7 +110,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
return ( return (
<> <>
<div onClick={() => setOpen(true)}>{children}</div> <button type="button" onClick={() => setOpen(true)}>
{children}
</button>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-3xl"> <DialogContent className="sm:max-w-3xl">
<DialogHeader> <DialogHeader>
@@ -143,8 +159,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<ScrollArea className="h-[400px] pr-4"> <ScrollArea className="h-[400px] pr-4">
<div className="grid gap-4"> <div className="grid gap-4">
{fields.map((field, index) => ( {fields.map((field, index) => (
<Card key={field.id}> <Card key={field.id} className="bg-transparent">
<CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent"> <CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent">
<FormField <FormField
control={form.control} control={form.control}
name={`ports.${index}.targetPort`} name={`ports.${index}.targetPort`}
@@ -168,7 +184,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
); );
}} }}
value={field.value || ""} value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 8080" placeholder="e.g. 8080"
/> />
</FormControl> </FormControl>
@@ -200,7 +215,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
); );
}} }}
value={field.value || ""} value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 80" placeholder="e.g. 80"
/> />
</FormControl> </FormControl>
@@ -208,6 +222,42 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name={`ports.${index}.protocol`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
Protocol
</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["tcp", "udp", "sctp"].map(
(protocol) => (
<SelectItem
key={protocol}
value={protocol}
>
{protocol}
</SelectItem>
),
)}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end"> <div className="flex items-end">
<Button <Button

View File

@@ -1,6 +1,7 @@
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources"; import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command"; import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowClusterSettings } from "../application/advanced/cluster/show-cluster-settings";
import { RebuildDatabase } from "./rebuild-database"; import { RebuildDatabase } from "./rebuild-database";
interface Props { interface Props {
@@ -12,6 +13,7 @@ export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
return ( return (
<div className="flex w-full flex-col gap-5"> <div className="flex w-full flex-col gap-5">
<ShowCustomCommand id={id} type={type} /> <ShowCustomCommand id={id} type={type} />
<ShowClusterSettings id={id} type={type} />
<ShowVolumes id={id} type={type} /> <ShowVolumes id={id} type={type} />
<ShowResources id={id} type={type} /> <ShowResources id={id} type={type} />
<RebuildDatabase id={id} type={type} /> <RebuildDatabase id={id} type={type} />

View File

@@ -1,3 +1,4 @@
import { Layers, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -8,7 +9,6 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Layers, Loader2 } from "lucide-react";
import { type ApplicationList, columns } from "./columns"; import { type ApplicationList, columns } from "./columns";
import { DataTable } from "./data-table"; import { DataTable } from "./data-table";
@@ -20,10 +20,10 @@ export const ShowNodeApplications = ({ serverId }: Props) => {
const { data: NodeApps, isLoading: NodeAppsLoading } = const { data: NodeApps, isLoading: NodeAppsLoading } =
api.swarm.getNodeApps.useQuery({ serverId }); api.swarm.getNodeApps.useQuery({ serverId });
let applicationList = ""; let applicationList: string[] = [];
if (NodeApps && NodeApps.length > 0) { if (NodeApps && NodeApps.length > 0) {
applicationList = NodeApps.map((app) => app.Name).join(" "); applicationList = NodeApps.map((app) => app.Name);
} }
const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } =

View File

@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import type React from "react"; import type React from "react";
import { cn } from "@/lib/utils";
import { GithubIcon } from "../icons/data-tools-icons"; import { GithubIcon } from "../icons/data-tools-icons";
import { Logo } from "../shared/logo"; import { Logo } from "../shared/logo";
import { Button } from "../ui/button"; import { Button } from "../ui/button";

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
import type { inferRouterOutputs } from "@trpc/server";
import { import {
Activity, Activity,
BarChartHorizontalBigIcon, BarChartHorizontalBigIcon,
@@ -29,10 +30,10 @@ import {
User, User,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import type * as React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
@@ -77,10 +78,6 @@ import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { AppRouter } from "@/server/api/root"; import type { AppRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { AddOrganization } from "../dashboard/organization/handle-organization"; import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action"; import { DialogAction } from "../shared/dialog-action";
import { Logo } from "../shared/logo"; import { Logo } from "../shared/logo";
@@ -770,9 +767,7 @@ export default function Page({ children }: Props) {
setIsLoaded(true); setIsLoaded(true);
}, []); }, []);
const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const _currentPath = router.pathname;
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();

View File

@@ -1,8 +1,7 @@
import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index"; import type { IUpdateData } from "@dokploy/server/index";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { api } from "@/utils/api";
import UpdateServer from "../dashboard/settings/web-server/update-server"; import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
@@ -11,6 +10,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "../ui/tooltip"; } from "../ui/tooltip";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UpdateServerButton = () => { export const UpdateServerButton = () => {
@@ -18,7 +18,6 @@ export const UpdateServerButton = () => {
latestVersion: null, latestVersion: null,
updateAvailable: false, updateAvailable: false,
}); });
const _router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: getUpdateData } = const { mutateAsync: getUpdateData } =
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
@@ -26,9 +25,6 @@ export const UpdateServerButton = () => {
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null); const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
if (isCloud) {
return null;
}
useEffect(() => { useEffect(() => {
// Handling of automatic check for server updates // Handling of automatic check for server updates
if (isCloud) { if (isCloud) {
@@ -77,7 +73,7 @@ export const UpdateServerButton = () => {
}; };
}, []); }, []);
return updateData.updateAvailable ? ( return !isCloud && updateData.updateAvailable ? (
<div className="border-t pt-4"> <div className="border-t pt-4">
<UpdateServer <UpdateServer
updateData={updateData} updateData={updateData}

View File

@@ -1,3 +1,5 @@
import { ChevronsUpDown } from "lucide-react";
import { useRouter } from "next/router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
DropdownMenu, DropdownMenu,
@@ -17,10 +19,9 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { Languages } from "@/lib/languages"; import { Languages } from "@/lib/languages";
import { getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import useLocale from "@/utils/hooks/use-locale"; import useLocale from "@/utils/hooks/use-locale";
import { ChevronsUpDown } from "lucide-react";
import { useRouter } from "next/router";
import { ModeToggle } from "../ui/modeToggle"; import { ModeToggle } from "../ui/modeToggle";
import { SidebarMenuButton } from "../ui/sidebar"; import { SidebarMenuButton } from "../ui/sidebar";
@@ -46,7 +47,9 @@ export const UserNav = () => {
src={data?.user?.image || ""} src={data?.user?.image || ""}
alt={data?.user?.image || ""} alt={data?.user?.image || ""}
/> />
<AvatarFallback className="rounded-lg">CN</AvatarFallback> <AvatarFallback className="rounded-lg">
{getFallbackAvatarInitials(data?.user?.name)}
</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Account</span> <span className="truncate font-semibold">Account</span>
@@ -122,18 +125,16 @@ export const UserNav = () => {
)} )}
</> </>
) : ( ) : (
<> data?.role === "owner" && (
{data?.role === "owner" && ( <DropdownMenuItem
<DropdownMenuItem className="cursor-pointer"
className="cursor-pointer" onClick={() => {
onClick={() => { router.push("/dashboard/settings/servers");
router.push("/dashboard/settings/servers"); }}
}} >
> Servers
Servers </DropdownMenuItem>
</DropdownMenuItem> )
)}
</>
)} )}
</DropdownMenuGroup> </DropdownMenuGroup>
{isCloud && data?.role === "owner" && ( {isCloud && data?.role === "owner" && (

View File

@@ -10,7 +10,7 @@ interface ChatwootWidgetProps {
launcherTitle?: string; launcherTitle?: string;
darkMode?: boolean; darkMode?: boolean;
hideMessageBubble?: boolean; hideMessageBubble?: boolean;
placement?: "right" | "left"; placement?: "left" | "right";
showPopoutButton?: boolean; showPopoutButton?: boolean;
widgetStyle?: "standard" | "bubble"; widgetStyle?: "standard" | "bubble";
}; };
@@ -41,7 +41,7 @@ export const ChatwootWidget = ({
position: "right", position: "right",
}; };
(window as any).chatwootSDKReady = () => { window.chatwootSDKReady = () => {
window.chatwootSDK?.run({ websiteToken, baseUrl }); window.chatwootSDK?.run({ websiteToken, baseUrl });
const trySetUser = () => { const trySetUser = () => {
@@ -63,7 +63,7 @@ export const ChatwootWidget = ({
<Script <Script
src={`${baseUrl}/packs/js/sdk.js`} src={`${baseUrl}/packs/js/sdk.js`}
strategy="lazyOnload" strategy="lazyOnload"
onLoad={() => (window as any).chatwootSDKReady?.()} onLoad={() => window.chatwootSDKReady?.()}
/> />
); );
}; };

View File

@@ -1,11 +1,11 @@
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
import { cn } from "@/lib/utils";
interface Props extends React.ComponentPropsWithoutRef<"div"> { interface Props extends React.ComponentPropsWithoutRef<"div"> {
icon?: React.ReactNode; icon?: React.ReactNode;
type?: "info" | "success" | "warning" | "error"; type?: "info" | "success" | "warning" | "error";
} }
import { cn } from "@/lib/utils";
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
const iconMap = { const iconMap = {
info: { info: {
className: "bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400", className: "bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400",

View File

@@ -1,3 +1,5 @@
import Link from "next/link";
import { Fragment } from "react";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
@@ -7,8 +9,6 @@ import {
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarTrigger } from "@/components/ui/sidebar";
import Link from "next/link";
import { Fragment } from "react";
interface Props { interface Props {
list: { list: {
@@ -26,7 +26,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
{list.map((item, _index) => ( {list.map((item, index) => (
<Fragment key={item.name}> <Fragment key={item.name}>
<BreadcrumbItem className="block"> <BreadcrumbItem className="block">
<BreadcrumbLink href={item.href} asChild={!!item.href}> <BreadcrumbLink href={item.href} asChild={!!item.href}>
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
)} )}
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
{_index + 1 < list.length && ( {index + 1 < list.length && (
<BreadcrumbSeparator className="block" /> <BreadcrumbSeparator className="block" />
)} )}
</Fragment> </Fragment>

View File

@@ -1,20 +1,19 @@
import { cn } from "@/lib/utils";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { import {
autocompletion,
type Completion, type Completion,
type CompletionContext, type CompletionContext,
type CompletionResult, type CompletionResult,
autocompletion,
} from "@codemirror/autocomplete"; } from "@codemirror/autocomplete";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { properties } from "@codemirror/legacy-modes/mode/properties"; import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell"; import { shell } from "@codemirror/legacy-modes/mode/shell";
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github"; import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror"; import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
// Docker Compose completion options // Docker Compose completion options
const dockerComposeServices = [ const dockerComposeServices = [
@@ -101,9 +100,7 @@ function dockerComposeComplete(
context: CompletionContext, context: CompletionContext,
): CompletionResult | null { ): CompletionResult | null {
const word = context.matchBefore(/\w*/); const word = context.matchBefore(/\w*/);
if (!word) return null; if (!word || (!word.text && !context.explicit)) return null;
if (!word.text && !context.explicit) return null;
// Check if we're at the root level // Check if we're at the root level
const line = context.state.doc.lineAt(context.pos); const line = context.state.doc.lineAt(context.pos);

View File

@@ -1,3 +1,4 @@
import { format, formatDistanceToNow } from "date-fns";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -5,7 +6,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { format, formatDistanceToNow } from "date-fns";
interface Props { interface Props {
date: string; date: string;

View File

@@ -1,3 +1,5 @@
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@@ -5,8 +7,6 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../dashboard/docker/logs/terminal-line"; import { TerminalLine } from "../dashboard/docker/logs/terminal-line";
import type { LogLine } from "../dashboard/docker/logs/utils"; import type { LogLine } from "../dashboard/docker/logs/utils";
@@ -43,11 +43,11 @@ export const DrawerLogs = ({ isOpen, onClose, filteredLogs }: Props) => {
return ( return (
<Sheet <Sheet
open={!!isOpen} open={!!isOpen}
onOpenChange={(_open) => { onOpenChange={() => {
onClose(); onClose();
}} }}
> >
<SheetContent className="sm:max-w-[740px] flex flex-col"> <SheetContent className="sm:max-w-[740px] flex flex-col">
<SheetHeader> <SheetHeader>
<SheetTitle>Deployment Logs</SheetTitle> <SheetTitle>Deployment Logs</SheetTitle>
<SheetDescription>Details of the request log entry.</SheetDescription> <SheetDescription>Details of the request log entry.</SheetDescription>

View File

@@ -13,10 +13,13 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
setIsPasswordVisible((prevVisibility) => !prevVisibility); setIsPasswordVisible((prevVisibility) => !prevVisibility);
}; };
const inputType = isPasswordVisible ? "text" : "password";
return ( return (
<div className="flex w-full items-center space-x-2"> <div className="flex w-full items-center space-x-2">
<Input ref={inputRef} type={inputType} {...props} /> <Input
ref={inputRef}
type={isPasswordVisible ? "text" : "password"}
{...props}
/>
<Button <Button
variant={"secondary"} variant={"secondary"}
onClick={() => { onClick={() => {
@@ -27,10 +30,10 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
<Clipboard className="size-4 text-muted-foreground" /> <Clipboard className="size-4 text-muted-foreground" />
</Button> </Button>
<Button onClick={togglePasswordVisibility} variant={"secondary"}> <Button onClick={togglePasswordVisibility} variant={"secondary"}>
{inputType === "password" ? ( {isPasswordVisible ? (
<EyeIcon className="size-4 text-muted-foreground" />
) : (
<EyeOffIcon className="size-4 text-muted-foreground" /> <EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)} )}
</Button> </Button>
</div> </div>

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "railpackVersion" text DEFAULT '0.2.2';

View File

@@ -0,0 +1,45 @@
ALTER TABLE "postgres" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -729,6 +729,20 @@
"when": 1752465764072, "when": 1752465764072,
"tag": "0103_cultured_pestilence", "tag": "0103_cultured_pestilence",
"breakpoints": true "breakpoints": true
},
{
"idx": 104,
"version": "7",
"when": 1754207407121,
"tag": "0104_omniscient_randall",
"breakpoints": true
},
{
"idx": 105,
"version": "7",
"when": 1754259281559,
"tag": "0105_clumsy_quicksilver",
"breakpoints": true
} }
] ]
} }

View File

@@ -27,3 +27,14 @@ export function formatTimestamp(timestamp: string | number) {
return "Fecha inválida"; return "Fecha inválida";
} }
} }
export function getFallbackAvatarInitials(
fullName: string | undefined,
): string {
if (typeof fullName === "undefined" || fullName === "") return "CN";
const [name = "", surname = ""] = fullName.split(" ");
if (surname === "") {
return name.substring(0, 2).toUpperCase();
}
return (name.charAt(0) + surname.charAt(0)).toUpperCase();
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.24.5", "version": "v0.24.8",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -1,3 +1,17 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowClusterSettings } from "@/components/dashboard/application/advanced/cluster/show-cluster-settings"; import { ShowClusterSettings } from "@/components/dashboard/application/advanced/cluster/show-cluster-settings";
import { AddCommand } from "@/components/dashboard/application/advanced/general/add-command"; import { AddCommand } from "@/components/dashboard/application/advanced/general/add-command";
import { ShowPorts } from "@/components/dashboard/application/advanced/ports/show-port"; import { ShowPorts } from "@/components/dashboard/application/advanced/ports/show-port";
@@ -39,20 +53,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
type TabState = type TabState =
| "projects" | "projects"
@@ -345,7 +345,10 @@ const Service = (
<TabsContent value="advanced"> <TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} /> <AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} /> <ShowClusterSettings
id={applicationId}
type="application"
/>
<ShowResources id={applicationId} type="application" /> <ShowResources id={applicationId} type="application" />
<ShowVolumes id={applicationId} type="application" /> <ShowVolumes id={applicationId} type="application" />

View File

@@ -1,31 +1,4 @@
import { import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGitProvider,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import {
IS_CLOUD,
addNewService, addNewService,
checkServiceAccess, checkServiceAccess,
createApplication, createApplication,
@@ -34,6 +7,7 @@ import {
findGitProviderById, findGitProviderById,
findProjectById, findProjectById,
getApplicationStats, getApplicationStats,
IS_CLOUD,
mechanizeDockerContainer, mechanizeDockerContainer,
readConfig, readConfig,
readRemoteConfig, readRemoteConfig,
@@ -57,6 +31,32 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiSaveGitProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
export const applicationRouter = createTRPCRouter({ export const applicationRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -364,6 +364,7 @@ export const applicationRouter = createTRPCRouter({
dockerBuildStage: input.dockerBuildStage, dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion, herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa, isStaticSpa: input.isStaticSpa,
railpackVersion: input.railpackVersion,
}); });
return true; return true;

View File

@@ -1,3 +1,54 @@
import {
canAccessToTraefikFiles,
checkGPUStatus,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
DEFAULT_UPDATE_DATA,
execAsync,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
IS_CLOUD,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readEnvironmentVariables,
readMainConfig,
readMonitoringConfig,
readPorts,
recreateDirectory,
reloadDockerResource,
sendDockerCleanupNotifications,
setupGPUSupport,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
writeTraefikSetup,
} from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { z } from "zod";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiAssignDomain, apiAssignDomain,
@@ -11,54 +62,6 @@ import {
apiUpdateDockerCleanup, apiUpdateDockerCleanup,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup"; import { removeJob, schedule } from "@/server/utils/backup";
import {
DEFAULT_UPDATE_DATA,
IS_CLOUD,
canAccessToTraefikFiles,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
execAsync,
execAsyncRemote,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
initializeTraefik,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readMainConfig,
readMonitoringConfig,
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import packageInfo from "../../../package.json"; import packageInfo from "../../../package.json";
import { appRouter } from "../root"; import { appRouter } from "../root";
import { import {
@@ -73,10 +76,7 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
const { stdout } = await execAsync( await reloadDockerResource("dokploy");
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
return true; return true;
}), }),
cleanRedis: adminProcedure.mutation(async () => { cleanRedis: adminProcedure.mutation(async () => {
@@ -101,20 +101,15 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await reloadDockerResource("dokploy-redis");
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true; return true;
}), }),
reloadTraefik: adminProcedure reloadTraefik: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
if (input?.serverId) { await reloadDockerResource("dokploy-traefik", input?.serverId);
await execAsync("docker restart dokploy-traefik");
} else if (!IS_CLOUD) {
await execAsync("docker restart dokploy-traefik");
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -124,17 +119,28 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure toggleDashboard: adminProcedure
.input(apiEnableDashboard) .input(apiEnableDashboard)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const ports = (await getTraefikPorts(input.serverId)).filter( const ports = await readPorts("dokploy-traefik", input.serverId);
(port) => const env = await readEnvironmentVariables(
port.targetPort !== 80 && "dokploy-traefik",
port.targetPort !== 443 && input.serverId,
port.targetPort !== 8080,
); );
await initializeTraefik({ const preparedEnv = prepareEnvironmentVariables(env);
additionalPorts: ports, let newPorts = ports;
enableDashboard: input.enableDashboard, // If receive true, add 8080 to ports
if (input.enableDashboard) {
newPorts.push({
targetPort: 8080,
publishedPort: 8080,
protocol: "tcp",
});
} else {
newPorts = ports.filter((port) => port.targetPort !== 8080);
}
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: newPorts,
serverId: input.serverId, serverId: input.serverId,
force: true,
}); });
return true; return true;
}), }),
@@ -551,29 +557,23 @@ export const settingsRouter = createTRPCRouter({
readTraefikEnv: adminProcedure readTraefikEnv: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
const command = const envVars = await readEnvironmentVariables(
"docker container inspect dokploy-traefik --format '{{json .Config.Env}}'"; "dokploy-traefik",
input?.serverId,
let result = ""; );
if (input?.serverId) { return envVars;
const execResult = await execAsyncRemote(input.serverId, command);
result = execResult.stdout;
} else {
const execResult = await execAsync(command);
result = execResult.stdout;
}
const envVars = JSON.parse(result.trim());
return envVars.join("\n");
}), }),
writeTraefikEnv: adminProcedure writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() })) .input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env); const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({ const ports = await readPorts("dokploy-traefik", input?.serverId);
await writeTraefikSetup({
env: envs, env: envs,
additionalPorts: ports,
serverId: input.serverId, serverId: input.serverId,
force: true,
}); });
return true; return true;
@@ -581,22 +581,8 @@ export const settingsRouter = createTRPCRouter({
haveTraefikDashboardPortEnabled: adminProcedure haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`; const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports.some((port) => port.targetPort === 8080);
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const ports = JSON.parse(stdout.trim());
return Object.entries(ports).some(([containerPort, bindings]) => {
const [port] = containerPort.split("/");
return port === "8080" && bindings && (bindings as any[]).length > 0;
});
}), }),
readStatsLogs: adminProcedure readStatsLogs: adminProcedure
@@ -793,6 +779,7 @@ export const settingsRouter = createTRPCRouter({
z.object({ z.object({
targetPort: z.number(), targetPort: z.number(),
publishedPort: z.number(), publishedPort: z.number(),
protocol: z.enum(["tcp", "udp", "sctp"]),
}), }),
), ),
}), }),
@@ -805,10 +792,16 @@ export const settingsRouter = createTRPCRouter({
message: "Please set a serverId to update Traefik ports", message: "Please set a serverId to update Traefik ports",
}); });
} }
await initializeTraefik({ const env = await readEnvironmentVariables(
serverId: input.serverId, "dokploy-traefik",
input?.serverId,
);
const preparedEnv = prepareEnvironmentVariables(env);
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: input.additionalPorts, additionalPorts: input.additionalPorts,
force: true, serverId: input.serverId,
}); });
return true; return true;
} catch (error) { } catch (error) {
@@ -825,7 +818,8 @@ export const settingsRouter = createTRPCRouter({
getTraefikPorts: adminProcedure getTraefikPorts: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
return await getTraefikPorts(input?.serverId); const ports = await readPorts("dokploy-traefik", input?.serverId);
return ports;
}), }),
updateLogCleanup: adminProcedure updateLogCleanup: adminProcedure
.input( .input(
@@ -855,56 +849,3 @@ export const settingsRouter = createTRPCRouter({
return ips; return ips;
}), }),
}); });
export const getTraefikPorts = async (serverId?: string) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
try {
let stdout = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
}> = [];
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
});
}
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
};

View File

@@ -1,10 +1,10 @@
import { import {
findServerById,
getApplicationInfo, getApplicationInfo,
getNodeApplications, getNodeApplications,
getNodeInfo, getNodeInfo,
getSwarmNodes, getSwarmNodes,
} from "@dokploy/server"; } from "@dokploy/server";
import { findServerById } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -53,9 +53,22 @@ export const swarmRouter = createTRPCRouter({
return getNodeApplications(input.serverId); return getNodeApplications(input.serverId);
}), }),
getAppInfos: protectedProcedure getAppInfos: protectedProcedure
.meta({
openapi: {
path: "/drop-deployment",
method: "POST",
override: true,
enabled: false,
},
})
.input( .input(
z.object({ z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), appName: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid app name.")
.array()
.min(1),
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )

View File

@@ -55,7 +55,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
conn conn
.once("ready", () => { .once("ready", () => {
conn.exec( conn.exec(
`docker exec -it ${containerId} ${activeWay}`, `docker exec -it -w / ${containerId} ${activeWay}`,
{ pty: true }, { pty: true },
(err, stream) => { (err, stream) => {
if (err) throw err; if (err) throw err;
@@ -107,7 +107,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
const shell = getShell(); const shell = getShell();
const ptyProcess = spawn( const ptyProcess = spawn(
shell, shell,
["-c", `docker exec -it ${containerId} ${activeWay}`], ["-c", `docker exec -it -w / ${containerId} ${activeWay}`],
{}, {},
); );

View File

@@ -1,10 +1,3 @@
import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/setup/traefik-setup";
import { execAsync } from "@dokploy/server"; import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
@@ -13,6 +6,13 @@ import {
initializeNetwork, initializeNetwork,
initializeSwarm, initializeSwarm,
} from "@dokploy/server/setup/setup"; } from "@dokploy/server/setup/setup";
import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeStandaloneTraefik,
} from "@dokploy/server/setup/traefik-setup";
(async () => { (async () => {
try { try {
setupDirectories(); setupDirectories();
@@ -22,7 +22,7 @@ import {
createDefaultTraefikConfig(); createDefaultTraefikConfig();
createDefaultServerTraefikConfig(); createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.1.2"); await execAsync("docker pull traefik:v3.1.2");
await initializeTraefik(); await initializeStandaloneTraefik();
await initializeRedis(); await initializeRedis();
await initializePostgres(); await initializePostgres();
} catch (e) { } catch (e) {

View File

@@ -27,7 +27,8 @@
"noUnsafeOptionalChaining": "off", "noUnsafeOptionalChaining": "off",
"noUnusedImports": "error", "noUnusedImports": "error",
"noUnusedFunctionParameters": "error", "noUnusedFunctionParameters": "error",
"noUnusedVariables": "error" "noUnusedVariables": "error",
"useHookAtTopLevel": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off", "noNonNullAssertion": "off",

View File

@@ -24,7 +24,25 @@ import { redirects } from "./redirects";
import { registry } from "./registry"; import { registry } from "./registry";
import { security } from "./security"; import { security } from "./security";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus, certificateType, triggerType } from "./shared"; import {
applicationStatus,
certificateType,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
triggerType,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const sourceType = pgEnum("sourceType", [ export const sourceType = pgEnum("sourceType", [
@@ -46,64 +64,6 @@ export const buildType = pgEnum("buildType", [
"railpack", "railpack",
]); ]);
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
Timeout?: number | undefined;
StartPeriod?: number | undefined;
Retries?: number | undefined;
}
export interface RestartPolicySwarm {
Condition?: string | undefined;
Delay?: number | undefined;
MaxAttempts?: number | undefined;
Window?: number | undefined;
}
export interface PlacementSwarm {
Constraints?: string[] | undefined;
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
MaxReplicas?: number | undefined;
Platforms?:
| Array<{
Architecture: string;
OS: string;
}>
| undefined;
}
export interface UpdateConfigSwarm {
Parallelism: number;
Delay?: number | undefined;
FailureAction?: string | undefined;
Monitor?: number | undefined;
MaxFailureRatio?: number | undefined;
Order: string;
}
export interface ServiceModeSwarm {
Replicated?: { Replicas?: number | undefined } | undefined;
Global?: {} | undefined;
ReplicatedJob?:
| {
MaxConcurrent?: number | undefined;
TotalCompletions?: number | undefined;
}
| undefined;
GlobalJob?: {} | undefined;
}
export interface NetworkSwarm {
Target?: string | undefined;
Aliases?: string[] | undefined;
DriverOpts?: { [key: string]: string } | undefined;
}
export interface LabelsSwarm {
[name: string]: string;
}
export const applications = pgTable("application", { export const applications = pgTable("application", {
applicationId: text("applicationId") applicationId: text("applicationId")
.notNull() .notNull()
@@ -208,6 +168,7 @@ export const applications = pgTable("application", {
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
railpackVersion: text("railpackVersion").default("0.2.2"),
herokuVersion: text("herokuVersion").default("24"), herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"), publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"), isStaticSpa: boolean("isStaticSpa"),
@@ -282,94 +243,6 @@ export const applicationsRelations = relations(
}), }),
); );
const HealthCheckSwarmSchema = z
.object({
Test: z.array(z.string()).optional(),
Interval: z.number().optional(),
Timeout: z.number().optional(),
StartPeriod: z.number().optional(),
Retries: z.number().optional(),
})
.strict();
const RestartPolicySwarmSchema = z
.object({
Condition: z.string().optional(),
Delay: z.number().optional(),
MaxAttempts: z.number().optional(),
Window: z.number().optional(),
})
.strict();
const PreferenceSchema = z
.object({
Spread: z.object({
SpreadDescriptor: z.string(),
}),
})
.strict();
const PlatformSchema = z
.object({
Architecture: z.string(),
OS: z.string(),
})
.strict();
const PlacementSwarmSchema = z
.object({
Constraints: z.array(z.string()).optional(),
Preferences: z.array(PreferenceSchema).optional(),
MaxReplicas: z.number().optional(),
Platforms: z.array(PlatformSchema).optional(),
})
.strict();
const UpdateConfigSwarmSchema = z
.object({
Parallelism: z.number(),
Delay: z.number().optional(),
FailureAction: z.string().optional(),
Monitor: z.number().optional(),
MaxFailureRatio: z.number().optional(),
Order: z.string(),
})
.strict();
const ReplicatedSchema = z
.object({
Replicas: z.number().optional(),
})
.strict();
const ReplicatedJobSchema = z
.object({
MaxConcurrent: z.number().optional(),
TotalCompletions: z.number().optional(),
})
.strict();
const ServiceModeSwarmSchema = z
.object({
Replicated: ReplicatedSchema.optional(),
Global: z.object({}).optional(),
ReplicatedJob: ReplicatedJobSchema.optional(),
GlobalJob: z.object({}).optional(),
})
.strict();
const NetworkSwarmSchema = z.array(
z
.object({
Target: z.string().optional(),
Aliases: z.array(z.string()).optional(),
DriverOpts: z.object({}).optional(),
})
.strict(),
);
const LabelsSwarmSchema = z.record(z.string());
const createSchema = createInsertSchema(applications, { const createSchema = createInsertSchema(applications, {
appName: z.string(), appName: z.string(),
createdAt: z.string(), createdAt: z.string(),
@@ -412,6 +285,7 @@ const createSchema = createInsertSchema(applications, {
"static", "static",
"railpack", "railpack",
]), ]),
railpackVersion: z.string().optional(),
herokuVersion: z.string().optional(), herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(), publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(), isStaticSpa: z.boolean().optional(),
@@ -466,6 +340,7 @@ export const apiSaveBuildType = createSchema
dockerContextPath: true, dockerContextPath: true,
dockerBuildStage: true, dockerBuildStage: true,
herokuVersion: true, herokuVersion: true,
railpackVersion: true,
}) })
.required() .required()
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true })); .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core"; import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus } from "./shared"; import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const mariadb = pgTable("mariadb", { export const mariadb = pgTable("mariadb", {
@@ -38,6 +54,15 @@ export const mariadb = pgTable("mariadb", {
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
.notNull() .notNull()
.default("idle"), .default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -83,6 +108,14 @@ const createSchema = createInsertSchema(mariadb, {
externalPort: z.number(), externalPort: z.number(),
description: z.string().optional(), description: z.string().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
}); });
export const apiCreateMariaDB = createSchema export const apiCreateMariaDB = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core"; import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus } from "./shared"; import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const mongo = pgTable("mongo", { export const mongo = pgTable("mongo", {
@@ -34,6 +50,15 @@ export const mongo = pgTable("mongo", {
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
.notNull() .notNull()
.default("idle"), .default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -79,6 +104,14 @@ const createSchema = createInsertSchema(mongo, {
description: z.string().optional(), description: z.string().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
replicaSets: z.boolean().default(false), replicaSets: z.boolean().default(false),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
}); });
export const apiCreateMongo = createSchema export const apiCreateMongo = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core"; import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus } from "./shared"; import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const mysql = pgTable("mysql", { export const mysql = pgTable("mysql", {
@@ -36,6 +52,15 @@ export const mysql = pgTable("mysql", {
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
.notNull() .notNull()
.default("idle"), .default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -81,6 +106,14 @@ const createSchema = createInsertSchema(mysql, {
externalPort: z.number(), externalPort: z.number(),
description: z.string().optional(), description: z.string().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
}); });
export const apiCreateMySql = createSchema export const apiCreateMySql = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core"; import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus } from "./shared"; import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const postgres = pgTable("postgres", { export const postgres = pgTable("postgres", {
@@ -35,6 +51,16 @@ export const postgres = pgTable("postgres", {
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
.notNull() .notNull()
.default("idle"), .default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -78,6 +104,14 @@ const createSchema = createInsertSchema(postgres, {
createdAt: z.string(), createdAt: z.string(),
description: z.string().optional(), description: z.string().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
}); });
export const apiCreatePostgres = createSchema export const apiCreatePostgres = createSchema

View File

@@ -1,12 +1,28 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core"; import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus } from "./shared"; import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const redis = pgTable("redis", { export const redis = pgTable("redis", {
@@ -35,6 +51,15 @@ export const redis = pgTable("redis", {
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
.notNull() .notNull()
.default("idle"), .default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
projectId: text("projectId") projectId: text("projectId")
.notNull() .notNull()
.references(() => projects.projectId, { onDelete: "cascade" }), .references(() => projects.projectId, { onDelete: "cascade" }),
@@ -73,6 +98,14 @@ const createSchema = createInsertSchema(redis, {
externalPort: z.number(), externalPort: z.number(),
description: z.string().optional(), description: z.string().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
}); });
export const apiCreateRedis = createSchema export const apiCreateRedis = createSchema

View File

@@ -1,4 +1,5 @@
import { pgEnum } from "drizzle-orm/pg-core"; import { pgEnum } from "drizzle-orm/pg-core";
import { z } from "zod";
export const applicationStatus = pgEnum("applicationStatus", [ export const applicationStatus = pgEnum("applicationStatus", [
"idle", "idle",
@@ -14,3 +15,149 @@ export const certificateType = pgEnum("certificateType", [
]); ]);
export const triggerType = pgEnum("triggerType", ["push", "tag"]); export const triggerType = pgEnum("triggerType", ["push", "tag"]);
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
Timeout?: number | undefined;
StartPeriod?: number | undefined;
Retries?: number | undefined;
}
export interface RestartPolicySwarm {
Condition?: string | undefined;
Delay?: number | undefined;
MaxAttempts?: number | undefined;
Window?: number | undefined;
}
export interface PlacementSwarm {
Constraints?: string[] | undefined;
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
MaxReplicas?: number | undefined;
Platforms?:
| Array<{
Architecture: string;
OS: string;
}>
| undefined;
}
export interface UpdateConfigSwarm {
Parallelism: number;
Delay?: number | undefined;
FailureAction?: string | undefined;
Monitor?: number | undefined;
MaxFailureRatio?: number | undefined;
Order: string;
}
export interface ServiceModeSwarm {
Replicated?: { Replicas?: number | undefined } | undefined;
Global?: {} | undefined;
ReplicatedJob?:
| {
MaxConcurrent?: number | undefined;
TotalCompletions?: number | undefined;
}
| undefined;
GlobalJob?: {} | undefined;
}
export interface NetworkSwarm {
Target?: string | undefined;
Aliases?: string[] | undefined;
DriverOpts?: { [key: string]: string } | undefined;
}
export interface LabelsSwarm {
[name: string]: string;
}
export const HealthCheckSwarmSchema = z
.object({
Test: z.array(z.string()).optional(),
Interval: z.number().optional(),
Timeout: z.number().optional(),
StartPeriod: z.number().optional(),
Retries: z.number().optional(),
})
.strict();
export const RestartPolicySwarmSchema = z
.object({
Condition: z.string().optional(),
Delay: z.number().optional(),
MaxAttempts: z.number().optional(),
Window: z.number().optional(),
})
.strict();
export const PreferenceSchema = z
.object({
Spread: z.object({
SpreadDescriptor: z.string(),
}),
})
.strict();
export const PlatformSchema = z
.object({
Architecture: z.string(),
OS: z.string(),
})
.strict();
export const PlacementSwarmSchema = z
.object({
Constraints: z.array(z.string()).optional(),
Preferences: z.array(PreferenceSchema).optional(),
MaxReplicas: z.number().optional(),
Platforms: z.array(PlatformSchema).optional(),
})
.strict();
export const UpdateConfigSwarmSchema = z
.object({
Parallelism: z.number(),
Delay: z.number().optional(),
FailureAction: z.string().optional(),
Monitor: z.number().optional(),
MaxFailureRatio: z.number().optional(),
Order: z.string(),
})
.strict();
export const ReplicatedSchema = z
.object({
Replicas: z.number().optional(),
})
.strict();
export const ReplicatedJobSchema = z
.object({
MaxConcurrent: z.number().optional(),
TotalCompletions: z.number().optional(),
})
.strict();
export const ServiceModeSwarmSchema = z
.object({
Replicated: ReplicatedSchema.optional(),
Global: z.object({}).optional(),
ReplicatedJob: ReplicatedJobSchema.optional(),
GlobalJob: z.object({}).optional(),
})
.strict();
export const NetworkSwarmSchema = z.array(
z
.object({
Target: z.string().optional(),
Aliases: z.array(z.string()).optional(),
DriverOpts: z.object({}).optional(),
})
.strict(),
);
export const LabelsSwarmSchema = z.record(z.string());

View File

@@ -29,6 +29,9 @@ const { handler, api } = betterAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}, },
}, },
logger: {
disabled: process.env.NODE_ENV === "production",
},
...(!IS_CLOUD && { ...(!IS_CLOUD && {
async trustedOrigins() { async trustedOrigins() {
const admin = await db.query.member.findFirst({ const admin = await db.query.member.findFirst({

View File

@@ -1,8 +1,12 @@
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import {
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; type apiCreateCompose,
buildAppName,
cleanAppName,
compose,
} from "@dokploy/server/db/schema";
import { import {
buildCompose, buildCompose,
getBuildComposeCommand, getBuildComposeCommand,
@@ -516,19 +520,20 @@ export const startCompose = async (composeId: string) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
try { try {
const { COMPOSE_PATH } = paths(!!compose.serverId); const { COMPOSE_PATH } = paths(!!compose.serverId);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const path =
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
if (compose.composeType === "docker-compose") { if (compose.composeType === "docker-compose") {
if (compose.serverId) { if (compose.serverId) {
await execAsyncRemote( await execAsyncRemote(
compose.serverId, compose.serverId,
`cd ${join( `cd ${projectPath} && ${baseCommand}`,
COMPOSE_PATH,
compose.appName,
"code",
)} && docker compose -p ${compose.appName} up -d`,
); );
} else { } else {
await execAsync(`docker compose -p ${compose.appName} up -d`, { await execAsync(baseCommand, {
cwd: join(COMPOSE_PATH, compose.appName, "code"), cwd: projectPath,
}); });
} }
} }

View File

@@ -441,13 +441,13 @@ export const getNodeApplications = async (serverId?: string) => {
}; };
export const getApplicationInfo = async ( export const getApplicationInfo = async (
appName: string, appNames: string[],
serverId?: string, serverId?: string,
) => { ) => {
try { try {
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`; const command = `docker service ps ${appNames.join(" ")} --format '{{json .}}' --no-trunc`;
if (serverId) { if (serverId) {
const result = await execAsyncRemote(serverId, command); const result = await execAsyncRemote(serverId, command);

View File

@@ -2,18 +2,22 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {
type ServiceType,
type apiCreateMount, type apiCreateMount,
mounts, mounts,
type ServiceType,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { import {
createFile, createFile,
encodeBase64,
getCreateFileCommand, getCreateFileCommand,
} from "@dokploy/server/utils/docker/utils"; } from "@dokploy/server/utils/docker/utils";
import { removeFileOrDirectory } from "@dokploy/server/utils/filesystem/directory"; import { removeFileOrDirectory } from "@dokploy/server/utils/filesystem/directory";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { type SQL, eq, sql } from "drizzle-orm"; import { eq, type SQL, sql } from "drizzle-orm";
export type Mount = typeof mounts.$inferSelect; export type Mount = typeof mounts.$inferSelect;
@@ -123,7 +127,7 @@ export const updateMount = async (
mountId: string, mountId: string,
mountData: Partial<Mount>, mountData: Partial<Mount>,
) => { ) => {
return await db.transaction(async (tx) => { const mount = await db.transaction(async (tx) => {
const mount = await tx const mount = await tx
.update(mounts) .update(mounts)
.set({ .set({
@@ -140,13 +144,13 @@ export const updateMount = async (
}); });
} }
if (mount.type === "file") {
await deleteFileMount(mountId);
await createFileMount(mountId);
}
return await findMountById(mountId); return await findMountById(mountId);
}); });
if (mount.type === "file") {
await updateFileMount(mountId);
}
return mount;
}; };
export const findMountsByApplicationId = async ( export const findMountsByApplicationId = async (
@@ -198,6 +202,26 @@ export const deleteMount = async (mountId: string) => {
return deletedMount[0]; return deletedMount[0];
}; };
export const updateFileMount = async (mountId: string) => {
const mount = await findMountById(mountId);
if (!mount || !mount.filePath) return;
const basePath = await getBaseFilesPath(mountId);
const fullPath = path.join(basePath, mount.filePath);
try {
const serverId = await getServerId(mount);
const encodedContent = encodeBase64(mount.content || "");
const command = `echo "${encodedContent}" | base64 -d > ${fullPath}`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} catch {
console.log("Error updating file mount");
}
};
export const deleteFileMount = async (mountId: string) => { export const deleteFileMount = async (mountId: string) => {
const mount = await findMountById(mountId); const mount = await findMountById(mountId);
if (!mount.filePath) return; if (!mount.filePath) return;

View File

@@ -5,6 +5,11 @@ import {
execAsync, execAsync,
execAsyncRemote, execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import {
initializeStandaloneTraefik,
initializeTraefikService,
type TraefikOptions,
} from "../setup/traefik-setup";
export interface IUpdateData { export interface IUpdateData {
latestVersion: string | null; latestVersion: string | null;
@@ -243,3 +248,165 @@ export const cleanupFullDocker = async (serverId?: string | null) => {
console.log(error); console.log(error);
} }
}; };
export const getDockerResourceType = async (
resourceName: string,
serverId?: string,
) => {
let result = "";
const command = `
RESOURCE_NAME="${resourceName}"
if docker service inspect "$RESOURCE_NAME" &>/dev/null; then
echo "service"
exit 0
fi
if docker inspect "$RESOURCE_NAME" &>/dev/null; then
echo "standalone"
exit 0
fi
echo "unknown"
exit 0
`;
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "service") {
return "service";
}
if (result === "standalone") {
return "standalone";
}
return "unknown";
};
export const reloadDockerResource = async (
resourceName: string,
serverId?: string,
) => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service update --force ${resourceName}`;
} else {
command = `docker restart ${resourceName}`;
}
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
};
export const readEnvironmentVariables = async (
resourceName: string,
serverId?: string,
) => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}'`;
} else {
command = `docker container inspect ${resourceName} --format '{{json .Config.Env}}'`;
}
let result = "";
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "null") {
return "";
}
return JSON.parse(result)?.join("\n");
};
export const readPorts = async (
resourceName: string,
serverId?: string,
): Promise<
{ targetPort: number; publishedPort: number; protocol?: string }[]
> => {
const resourceType = await getDockerResourceType(resourceName, serverId);
let command = "";
if (resourceType === "service") {
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
} else {
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
}
let result = "";
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, command);
result = stdout.trim();
} else {
const { stdout } = await execAsync(command);
result = stdout.trim();
}
if (result === "null") {
return [];
}
const parsedResult = JSON.parse(result);
if (resourceType === "service") {
return parsedResult
.map((port: any) => ({
targetPort: port.TargetPort,
publishedPort: port.PublishedPort,
protocol: port.Protocol,
}))
.filter((port: any) => port.targetPort !== 80 && port.targetPort !== 443);
}
const ports: {
targetPort: number;
publishedPort: number;
protocol?: string;
}[] = [];
for (const key in parsedResult) {
if (Object.hasOwn(parsedResult, key)) {
const containerPortMapppings = parsedResult[key];
const protocol = key.split("/")[1];
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
containerPortMapppings.forEach((mapping: any) => {
ports.push({
targetPort: targetPort,
publishedPort: Number.parseInt(mapping.HostPort, 10),
protocol: protocol,
});
});
}
}
return ports.filter(
(port: any) => port.targetPort !== 80 && port.targetPort !== 443,
);
};
export const writeTraefikSetup = async (
input: TraefikOptions,
serverId?: string,
) => {
const resourceType = await getDockerResourceType("dokploy-traefik", serverId);
if (resourceType === "service") {
await initializeTraefikService({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: serverId,
});
} else {
await initializeStandaloneTraefik({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: serverId,
});
}
};

View File

@@ -6,18 +6,17 @@ import {
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import { findServerById } from "@dokploy/server/services/server"; import { findServerById } from "@dokploy/server/services/server";
import { import {
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
TRAEFIK_HTTP3_PORT, TRAEFIK_HTTP3_PORT,
TRAEFIK_PORT, TRAEFIK_PORT,
TRAEFIK_SSL_PORT, TRAEFIK_SSL_PORT,
TRAEFIK_VERSION, TRAEFIK_VERSION,
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
} from "@dokploy/server/setup/traefik-setup"; } from "@dokploy/server/setup/traefik-setup";
import slug from "slugify";
import { Client } from "ssh2"; import { Client } from "ssh2";
import { recreateDirectory } from "../utils/filesystem/directory"; import { recreateDirectory } from "../utils/filesystem/directory";
import slug from "slugify";
export const slugify = (text: string | undefined) => { export const slugify = (text: string | undefined) => {
if (!text) { if (!text) {
return ""; return "";
@@ -609,7 +608,7 @@ const installRailpack = () => `
if command_exists railpack; then if command_exists railpack; then
echo "Railpack already installed ✅" echo "Railpack already installed ✅"
else else
export RAILPACK_VERSION=0.0.64 export RAILPACK_VERSION=0.2.2
bash -c "$(curl -fsSL https://railpack.com/install.sh)" bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅" echo "Railpack version $RAILPACK_VERSION installed ✅"
fi fi

View File

@@ -1,6 +1,6 @@
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import type { ContainerCreateOptions } from "dockerode"; import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { paths } from "../constants"; import { paths } from "../constants";
import { getRemoteDocker } from "../utils/servers/remote-docker"; import { getRemoteDocker } from "../utils/servers/remote-docker";
@@ -15,23 +15,20 @@ export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2"; export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
interface TraefikOptions { export interface TraefikOptions {
enableDashboard?: boolean;
env?: string[]; env?: string[];
serverId?: string; serverId?: string;
additionalPorts?: { additionalPorts?: {
targetPort: number; targetPort: number;
publishedPort: number; publishedPort: number;
protocol?: string;
}[]; }[];
force?: boolean;
} }
export const initializeTraefik = async ({ export const initializeStandaloneTraefik = async ({
enableDashboard = false,
env, env,
serverId, serverId,
additionalPorts = [], additionalPorts = [],
force = false,
}: TraefikOptions = {}) => { }: TraefikOptions = {}) => {
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId); const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const imageName = `traefik:v${TRAEFIK_VERSION}`; const imageName = `traefik:v${TRAEFIK_VERSION}`;
@@ -51,13 +48,17 @@ export const initializeTraefik = async ({
], ],
}; };
const enableDashboard = additionalPorts.some(
(port) => port.targetPort === 8080,
);
if (enableDashboard) { if (enableDashboard) {
exposedPorts["8080/tcp"] = {}; exposedPorts["8080/tcp"] = {};
portBindings["8080/tcp"] = [{ HostPort: "8080" }]; portBindings["8080/tcp"] = [{ HostPort: "8080" }];
} }
for (const port of additionalPorts) { for (const port of additionalPorts) {
const portKey = `${port.targetPort}/tcp`; const portKey = `${port.targetPort}/${port.protocol ?? "tcp"}`;
exposedPorts[portKey] = {}; exposedPorts[portKey] = {};
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }]; portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
} }
@@ -87,68 +88,117 @@ export const initializeTraefik = async ({
const docker = await getRemoteDocker(serverId); const docker = await getRemoteDocker(serverId);
try { try {
try {
const service = docker.getService("dokploy-traefik");
await service?.remove({ force: true });
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
try {
await docker.listServices({
filters: { name: ["dokploy-traefik"] },
});
console.log("Waiting for service cleanup...");
await new Promise((resolve) => setTimeout(resolve, 5000));
attempts++;
} catch {
break;
}
}
} catch {
console.log("No existing service to remove");
}
// Then try to remove any existing container
const container = docker.getContainer(containerName); const container = docker.getContainer(containerName);
try { try {
const inspect = await container.inspect();
if (inspect.State.Status === "running" && !force) {
console.log("Traefik already running");
return;
}
await container.remove({ force: true }); await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
} catch {
console.log("No existing container to remove");
}
// Create and start the new container
try {
await docker.createContainer(settings); await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName); const newContainer = docker.getContainer(containerName);
await newContainer.start(); await newContainer.start();
console.log("Traefik container started successfully"); console.log("Traefik Started ✅");
} catch (error: any) { } catch (error) {
if (error?.json?.message?.includes("port is already allocated")) { console.error("Error in initializeStandaloneTraefik", error);
console.log("Ports still in use, waiting longer for cleanup...");
await new Promise((resolve) => setTimeout(resolve, 10000));
// Try one more time
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully after retry");
} else {
throw error;
}
} }
} catch (error) { } catch (error) {
console.error("Failed to initialize Traefik:", error); await docker.createContainer(settings);
console.error("Error in initializeStandaloneTraefik", error);
throw error; throw error;
} }
}; };
export const initializeTraefikService = async ({
env,
additionalPorts = [],
serverId,
}: TraefikOptions) => {
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const imageName = `traefik:v${TRAEFIK_VERSION}`;
const appName = "dokploy-traefik";
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: imageName,
Env: env,
Mounts: [
{
Type: "bind",
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
Target: "/etc/traefik/traefik.yml",
},
{
Type: "bind",
Source: DYNAMIC_TRAEFIK_PATH,
Target: "/etc/dokploy/traefik/dynamic",
},
{
Type: "bind",
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
},
],
},
Networks: [{ Target: "dokploy-network" }],
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Ports: [
{
TargetPort: 443,
PublishedPort: TRAEFIK_SSL_PORT,
PublishMode: "host",
Protocol: "tcp",
},
{
TargetPort: 443,
PublishedPort: TRAEFIK_SSL_PORT,
PublishMode: "host",
Protocol: "udp",
},
{
TargetPort: 80,
PublishedPort: TRAEFIK_PORT,
PublishMode: "host",
Protocol: "tcp",
},
...additionalPorts.map((port) => ({
TargetPort: port.targetPort,
PublishedPort: port.publishedPort,
Protocol: port.protocol as "tcp" | "udp" | "sctp" | undefined,
PublishMode: "host" as const,
})),
],
},
};
const docker = await getRemoteDocker(serverId);
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
console.log("Traefik Updated ✅");
} catch {
await docker.createService(settings);
console.log("Traefik Started ✅");
}
};
export const createDefaultServerTraefikConfig = () => { export const createDefaultServerTraefikConfig = () => {
const { DYNAMIC_TRAEFIK_PATH } = paths(); const { DYNAMIC_TRAEFIK_PATH } = paths();
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml"); const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");

View File

@@ -17,7 +17,7 @@ export const runComposeBackup = async (
const project = await findProjectById(projectId); const project = await findProjectById(projectId);
const { prefix, databaseType } = backup; const { prefix, databaseType } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({ const deployment = await createDeploymentBackup({
backupId: backup.backupId, backupId: backup.backupId,

View File

@@ -1,7 +1,11 @@
import path from "node:path"; import path from "node:path";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server"; import { getAllServers } from "@dokploy/server/services/server";
import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { db } from "../../db/index"; import { db } from "../../db/index";
import { startLogCleanup } from "../access-log/handler";
import { import {
cleanUpDockerBuilder, cleanUpDockerBuilder,
cleanUpSystemPrune, cleanUpSystemPrune,
@@ -11,11 +15,6 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils"; import { getS3Credentials, scheduleBackup } from "./utils";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => { export const initCronJobs = async () => {
console.log("Setting up cron jobs...."); console.log("Setting up cron jobs....");

View File

@@ -14,7 +14,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const project = await findProjectById(projectId); const project = await findProjectById(projectId);
const { prefix } = backup; const { prefix } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({ const deployment = await createDeploymentBackup({
backupId: backup.backupId, backupId: backup.backupId,

View File

@@ -1,7 +1,6 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs"; import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import { import {
parseEnvironmentKeyValuePair, parseEnvironmentKeyValuePair,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -9,6 +8,7 @@ import {
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
const calculateSecretsHash = (envVariables: string[]): string => { const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256"); const hash = createHash("sha256");
@@ -75,7 +75,7 @@ export const buildRailpack = async (
] ]
: []), : []),
"--build-arg", "--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64", `BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f", "-f",
`${buildAppDirectory}/railpack-plan.json`, `${buildAppDirectory}/railpack-plan.json`,
"--output", "--output",
@@ -110,6 +110,8 @@ export const buildRailpack = async (
return true; return true;
} catch (e) { } catch (e) {
throw e; throw e;
} finally {
await execAsync("docker buildx rm builder-containerd");
} }
}; };
@@ -155,7 +157,7 @@ export const getRailpackCommand = (
] ]
: []), : []),
"--build-arg", "--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64", `BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f", "-f",
`${buildAppDirectory}/railpack-plan.json`, `${buildAppDirectory}/railpack-plan.json`,
"--output", "--output",
@@ -194,6 +196,7 @@ docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
exit 1; exit 1;
} }
echo "✅ Railpack build completed." >> ${logPath}; echo "✅ Railpack build completed." >> ${logPath};
docker buildx rm builder-containerd
`; `;
return bashCommand; return bashCommand;

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import { import {
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer,
generateFileMounts, generateFileMounts,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -34,6 +35,17 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
const defaultMariadbEnv = `MARIADB_DATABASE="${databaseName}"\nMARIADB_USER="${databaseUser}"\nMARIADB_PASSWORD="${databasePassword}"\nMARIADB_ROOT_PASSWORD="${databaseRootPassword}"${ const defaultMariadbEnv = `MARIADB_DATABASE="${databaseName}"\nMARIADB_USER="${databaseUser}"\nMARIADB_PASSWORD="${databasePassword}"\nMARIADB_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : "" env ? `\n${env}` : ""
}`; }`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(mariadb);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
memoryReservation, memoryReservation,
@@ -54,6 +66,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Name: appName, Name: appName,
TaskTemplate: { TaskTemplate: {
ContainerSpec: { ContainerSpec: {
HealthCheck,
Image: dockerImage, Image: dockerImage,
Env: envVariables, Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -63,20 +76,17 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Args: ["-c", command], Args: ["-c", command],
} }
: {}), : {}),
Labels,
}, },
Networks: [{ Target: "dokploy-network" }], Networks,
RestartPolicy,
Placement,
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
}, },
Mode,
RollbackConfig,
EndpointSpec: { EndpointSpec: {
Mode: "dnsrr", Mode: "dnsrr",
Ports: externalPort Ports: externalPort
@@ -90,6 +100,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
] ]
: [], : [],
}, },
UpdateConfig,
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);
@@ -97,6 +108,10 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
await service.update({ await service.update({
version: Number.parseInt(inspect.Version.Index), version: Number.parseInt(inspect.Version.Index),
...settings, ...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
}); });
} catch { } catch {
await docker.createService(settings); await docker.createService(settings);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import { import {
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer,
generateFileMounts, generateFileMounts,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -81,6 +82,17 @@ ${command ?? "wait $MONGOD_PID"}`;
env ? `\n${env}` : "" env ? `\n${env}` : ""
}`; }`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(mongo);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
memoryReservation, memoryReservation,
@@ -102,6 +114,7 @@ ${command ?? "wait $MONGOD_PID"}`;
Name: appName, Name: appName,
TaskTemplate: { TaskTemplate: {
ContainerSpec: { ContainerSpec: {
HealthCheck,
Image: dockerImage, Image: dockerImage,
Env: envVariables, Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -116,20 +129,17 @@ ${command ?? "wait $MONGOD_PID"}`;
Args: ["-c", command], Args: ["-c", command],
}), }),
}), }),
Labels,
}, },
Networks: [{ Target: "dokploy-network" }], Networks,
RestartPolicy,
Placement,
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
}, },
Mode,
RollbackConfig,
EndpointSpec: { EndpointSpec: {
Mode: "dnsrr", Mode: "dnsrr",
Ports: externalPort Ports: externalPort
@@ -143,6 +153,7 @@ ${command ?? "wait $MONGOD_PID"}`;
] ]
: [], : [],
}, },
UpdateConfig,
}; };
try { try {
@@ -151,6 +162,10 @@ ${command ?? "wait $MONGOD_PID"}`;
await service.update({ await service.update({
version: Number.parseInt(inspect.Version.Index), version: Number.parseInt(inspect.Version.Index),
...settings, ...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
}); });
} catch { } catch {
await docker.createService(settings); await docker.createService(settings);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import { import {
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer,
generateFileMounts, generateFileMounts,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -40,6 +41,17 @@ export const buildMysql = async (mysql: MysqlNested) => {
: `MYSQL_DATABASE="${databaseName}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${ : `MYSQL_DATABASE="${databaseName}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : "" env ? `\n${env}` : ""
}`; }`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(mysql);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
memoryReservation, memoryReservation,
@@ -60,6 +72,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
Name: appName, Name: appName,
TaskTemplate: { TaskTemplate: {
ContainerSpec: { ContainerSpec: {
HealthCheck,
Image: dockerImage, Image: dockerImage,
Env: envVariables, Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -69,20 +82,17 @@ export const buildMysql = async (mysql: MysqlNested) => {
Args: ["-c", command], Args: ["-c", command],
} }
: {}), : {}),
Labels,
}, },
Networks: [{ Target: "dokploy-network" }], Networks,
RestartPolicy,
Placement,
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
}, },
Mode,
RollbackConfig,
EndpointSpec: { EndpointSpec: {
Mode: "dnsrr", Mode: "dnsrr",
Ports: externalPort Ports: externalPort
@@ -96,6 +106,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
] ]
: [], : [],
}, },
UpdateConfig,
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);
@@ -103,6 +114,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
await service.update({ await service.update({
version: Number.parseInt(inspect.Version.Index), version: Number.parseInt(inspect.Version.Index),
...settings, ...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
}); });
} catch { } catch {
await docker.createService(settings); await docker.createService(settings);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import { import {
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer,
generateFileMounts, generateFileMounts,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -33,6 +34,17 @@ export const buildPostgres = async (postgres: PostgresNested) => {
const defaultPostgresEnv = `POSTGRES_DB="${databaseName}"\nPOSTGRES_USER="${databaseUser}"\nPOSTGRES_PASSWORD="${databasePassword}"${ const defaultPostgresEnv = `POSTGRES_DB="${databaseName}"\nPOSTGRES_USER="${databaseUser}"\nPOSTGRES_PASSWORD="${databasePassword}"${
env ? `\n${env}` : "" env ? `\n${env}` : ""
}`; }`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(postgres);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
memoryReservation, memoryReservation,
@@ -53,6 +65,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Name: appName, Name: appName,
TaskTemplate: { TaskTemplate: {
ContainerSpec: { ContainerSpec: {
HealthCheck,
Image: dockerImage, Image: dockerImage,
Env: envVariables, Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -62,20 +75,17 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Args: ["-c", command], Args: ["-c", command],
} }
: {}), : {}),
Labels,
}, },
Networks: [{ Target: "dokploy-network" }], Networks,
RestartPolicy,
Placement,
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
}, },
Mode,
RollbackConfig,
EndpointSpec: { EndpointSpec: {
Mode: "dnsrr", Mode: "dnsrr",
Ports: externalPort Ports: externalPort
@@ -89,6 +99,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
] ]
: [], : [],
}, },
UpdateConfig,
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);
@@ -96,6 +107,10 @@ export const buildPostgres = async (postgres: PostgresNested) => {
await service.update({ await service.update({
version: Number.parseInt(inspect.Version.Index), version: Number.parseInt(inspect.Version.Index),
...settings, ...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
}); });
} catch (error) { } catch (error) {
console.log("error", error); console.log("error", error);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import { import {
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer,
generateFileMounts, generateFileMounts,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -31,6 +32,17 @@ export const buildRedis = async (redis: RedisNested) => {
const defaultRedisEnv = `REDIS_PASSWORD="${databasePassword}"${ const defaultRedisEnv = `REDIS_PASSWORD="${databasePassword}"${
env ? `\n${env}` : "" env ? `\n${env}` : ""
}`; }`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(redis);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
memoryReservation, memoryReservation,
@@ -51,6 +63,7 @@ export const buildRedis = async (redis: RedisNested) => {
Name: appName, Name: appName,
TaskTemplate: { TaskTemplate: {
ContainerSpec: { ContainerSpec: {
HealthCheck,
Image: dockerImage, Image: dockerImage,
Env: envVariables, Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -59,20 +72,17 @@ export const buildRedis = async (redis: RedisNested) => {
"-c", "-c",
command ? command : `redis-server --requirepass ${databasePassword}`, command ? command : `redis-server --requirepass ${databasePassword}`,
], ],
Labels,
}, },
Networks: [{ Target: "dokploy-network" }], Networks,
RestartPolicy,
Placement,
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
}, },
Mode,
RollbackConfig,
EndpointSpec: { EndpointSpec: {
Mode: "dnsrr", Mode: "dnsrr",
Ports: externalPort Ports: externalPort
@@ -86,6 +96,7 @@ export const buildRedis = async (redis: RedisNested) => {
] ]
: [], : [],
}, },
UpdateConfig,
}; };
try { try {
@@ -94,6 +105,10 @@ export const buildRedis = async (redis: RedisNested) => {
await service.update({ await service.update({
version: Number.parseInt(inspect.Version.Index), version: Number.parseInt(inspect.Version.Index),
...settings, ...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
}); });
} catch { } catch {
await docker.createService(settings); await docker.createService(settings);

View File

@@ -313,40 +313,46 @@ export const createDomainLabels = (
`traefik.http.routers.${routerName}.service=${routerName}`, `traefik.http.routers.${routerName}.service=${routerName}`,
]; ];
// Validate stripPath - it should only be used when path is defined and not "/" // Collect middlewares for this router
if (stripPath) { const middlewares: string[] = [];
if (!path || path === "/") {
console.warn( // Add HTTPS redirect for web entrypoint (must be first)
`stripPath is enabled but path is not defined or is "/" for domain ${host}`, if (entrypoint === "web" && https) {
); middlewares.push("redirect-to-https@file");
} else { }
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Add stripPath middleware if needed
if (stripPath && path && path !== "/") {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Only define middleware once (on web entrypoint)
if (entrypoint === "web") {
labels.push( labels.push(
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`, `traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
); );
} }
middlewares.push(middlewareName);
} }
// Validate internalPath - ensure it's a valid path format // Add internalPath middleware if needed
if (internalPath && internalPath !== "/") { if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
if (!internalPath.startsWith("/")) { const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
console.warn( // Only define middleware once (on web entrypoint)
`internalPath "${internalPath}" should start with "/" and not be empty for domain ${host}`, if (entrypoint === "web") {
);
} else {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
labels.push( labels.push(
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`, `traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
); );
} }
middlewares.push(middlewareName);
} }
if (entrypoint === "web" && https) { // Apply middlewares to router if any exist
if (middlewares.length > 0) {
labels.push( labels.push(
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`, `traefik.http.routers.${routerName}.middlewares=${middlewares.join(",")}`,
); );
} }
// Add TLS configuration for websecure
if (entrypoint === "websecure") { if (entrypoint === "websecure") {
if (certificateType === "letsencrypt") { if (certificateType === "letsencrypt") {
labels.push( labels.push(

View File

@@ -348,7 +348,9 @@ export const calculateResources = ({
}; };
}; };
export const generateConfigContainer = (application: ApplicationNested) => { export const generateConfigContainer = (
application: Partial<ApplicationNested>,
) => {
const { const {
healthCheckSwarm, healthCheckSwarm,
restartPolicySwarm, restartPolicySwarm,
@@ -362,7 +364,7 @@ export const generateConfigContainer = (application: ApplicationNested) => {
networkSwarm, networkSwarm,
} = application; } = application;
const haveMounts = mounts.length > 0; const haveMounts = mounts && mounts.length > 0;
return { return {
...(healthCheckSwarm && { ...(healthCheckSwarm && {

View File

@@ -3,8 +3,8 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose"; import type { Compose } from "@dokploy/server/services/compose";
import { import {
type Gitea,
findGiteaById, findGiteaById,
type Gitea,
updateGitea, updateGitea,
} from "@dokploy/server/services/gitea"; } from "@dokploy/server/services/gitea";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
@@ -118,7 +118,6 @@ export const getGiteaCloneCommand = async (
giteaOwner, giteaOwner,
giteaRepository, giteaRepository,
serverId, serverId,
gitea,
enableSubmodules, enableSubmodules,
} = entity; } = entity;
@@ -145,6 +144,7 @@ export const getGiteaCloneCommand = async (
// Use paths(true) for remote operations // Use paths(true) for remote operations
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGiteaToken(giteaId); await refreshGiteaToken(giteaId);
const gitea = await findGiteaById(giteaId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");

View File

@@ -4,8 +4,8 @@ import { paths } from "@dokploy/server/constants";
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema"; import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose"; import type { Compose } from "@dokploy/server/services/compose";
import { import {
type Gitlab,
findGitlabById, findGitlabById,
type Gitlab,
updateGitlab, updateGitlab,
} from "@dokploy/server/services/gitlab"; } from "@dokploy/server/services/gitlab";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
@@ -112,7 +112,6 @@ export const cloneGitlabRepository = async (
appName, appName,
gitlabBranch, gitlabBranch,
gitlabId, gitlabId,
gitlab,
gitlabPathNamespace, gitlabPathNamespace,
enableSubmodules, enableSubmodules,
} = entity; } = entity;
@@ -125,6 +124,7 @@ export const cloneGitlabRepository = async (
} }
await refreshGitlabToken(gitlabId); await refreshGitlabToken(gitlabId);
const gitlab = await findGitlabById(gitlabId);
const requirements = getErrorCloneRequirements(entity); const requirements = getErrorCloneRequirements(entity);
@@ -187,7 +187,6 @@ export const getGitlabCloneCommand = async (
gitlabBranch, gitlabBranch,
gitlabId, gitlabId,
serverId, serverId,
gitlab,
enableSubmodules, enableSubmodules,
} = entity; } = entity;
@@ -235,6 +234,7 @@ export const getGitlabCloneCommand = async (
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGitlabToken(gitlabId); await refreshGitlabToken(gitlabId);
const gitlab = await findGitlabById(gitlabId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
@@ -310,22 +310,43 @@ export const getGitlabBranches = async (input: {
const gitlabProvider = await findGitlabById(input.gitlabId); const gitlabProvider = await findGitlabById(input.gitlabId);
const branchesResponse = await fetch( const allBranches = [];
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches`, let page = 1;
{ const perPage = 100; // GitLab's max per page is 100
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!branchesResponse.ok) { while (true) {
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`); const branchesResponse = await fetch(
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!branchesResponse.ok) {
throw new Error(
`Failed to fetch branches: ${branchesResponse.statusText}`,
);
}
const branches = await branchesResponse.json();
if (branches.length === 0) {
break;
}
allBranches.push(...branches);
page++;
// Check if we've reached the total using headers (optional optimization)
const total = branchesResponse.headers.get("x-total");
if (total && allBranches.length >= Number.parseInt(total)) {
break;
}
} }
const branches = await branchesResponse.json(); return allBranches as {
return branches as {
id: string; id: string;
name: string; name: string;
commit: { commit: {
@@ -350,9 +371,9 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
}); });
} }
const gitlabProvider = await findGitlabById(gitlabId);
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths();
await refreshGitlabToken(gitlabId); await refreshGitlabToken(gitlabId);
const gitlabProvider = await findGitlabById(gitlabId);
const basePath = COMPOSE_PATH; const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
@@ -398,9 +419,9 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
message: "Gitlab Provider not found", message: "Gitlab Provider not found",
}); });
} }
const gitlabProvider = await findGitlabById(gitlabId);
const { COMPOSE_PATH } = paths(true); const { COMPOSE_PATH } = paths(true);
await refreshGitlabToken(gitlabId); await refreshGitlabToken(gitlabId);
const gitlabProvider = await findGitlabById(gitlabId);
const basePath = COMPOSE_PATH; const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace); const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);

View File

@@ -81,7 +81,7 @@ const getMongoSpecificCommand = (
backupFile: string, backupFile: string,
): string => { ): string => {
const tempDir = "/tmp/dokploy-restore"; const tempDir = "/tmp/dokploy-restore";
const fileName = backupFile.split("/").pop() || "backup.dump.gz"; const fileName = backupFile.split("/").pop() || "backup.sql.gz";
const decompressedName = fileName.replace(".gz", ""); const decompressedName = fileName.replace(".gz", "");
return ` return `
rm -rf ${tempDir} && \ rm -rf ${tempDir} && \

View File

@@ -2,8 +2,7 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils"; import { getS3Credentials, normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils";
export const backupVolume = async ( export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>, volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
@@ -37,6 +36,9 @@ export const backupVolume = async (
echo "Starting upload to S3..." echo "Starting upload to S3..."
${rcloneCommand} ${rcloneCommand}
echo "Upload to S3 done ✅" echo "Upload to S3 done ✅"
echo "Cleaning up local backup file..."
rm "${volumeBackupPath}/${backupFileName}"
echo "Local backup file cleaned up ✅"
`; `;
if (!turnOff) { if (!turnOff) {