Compare commits

...

154 Commits

Author SHA1 Message Date
Mauricio Siu
3ed9da147b Merge pull request #2144 from Marukome0743/sponser
docs: fix broken README.md sponsers images
2025-07-06 21:16:45 -06:00
Marukome0743
636dec4f09 docs: fix broken README.md sponsers images 2025-07-07 10:35:32 +09:00
Mauricio Siu
4dcf6cf4c3 refactor(volume-backups): comment out keepLatestCount field and related logic
- Commented out the keepLatestCount field in the form schema and its usage in the HandleVolumeBackups component.
- Updated related form field rendering to prevent rendering of the keepLatestCount input.
2025-07-06 18:58:26 -06:00
Mauricio Siu
7356d71626 Merge pull request #1866 from Dokploy/1853-external-volume-should-not-be-isolated
refactor: remove unused volume suffix function from collision utility
2025-07-06 18:50:47 -06:00
Mauricio Siu
76603f598c feat(database): add isolatedDeploymentsVolume column to compose table
- Introduced a new boolean column "isolatedDeploymentsVolume" to the "compose" table with a default value of false.
- Updated existing records to set "isolatedDeploymentsVolume" to true.
- Modified related functions to handle the new column for improved deployment isolation management.
2025-07-06 18:46:36 -06:00
Mauricio Siu
e050c218e2 Merge branch 'canary' into 1853-external-volume-should-not-be-isolated 2025-07-06 18:29:00 -06:00
Mauricio Siu
46e0b5df75 Update package.json 2025-07-06 17:57:22 -06:00
Mauricio Siu
5b36503a3f Merge pull request #2134 from andr3i1010/canary
fix: fix environment variable parsing for railpack
2025-07-06 17:09:04 -06:00
Mauricio Siu
b9afc551db Merge pull request #2142 from Dokploy/fix/rollbacks-dns-issue
fix(rollbacks): correct dns issue resolution
2025-07-06 16:44:14 -06:00
Mauricio Siu
078ca19578 feat(rollbacks): enhance rollback functionality with full context support
- Updated the rollbacks schema to include mounts, ports, and optional registry information in the full context.
- Refactored the rollback service to utilize the full context for improved rollback operations, ensuring all necessary configurations are applied.
- Exported the getAuthConfig function for better accessibility in the application context.
2025-07-06 16:43:44 -06:00
Mauricio Siu
b7dc7bbf0c Merge pull request #2141 from Dokploy/1992-gitlab-repository-selection
fix(gitlab): update repository selection to use URL instead of name
2025-07-06 13:57:13 -06:00
Mauricio Siu
b9ee81aa59 fix(gitlab): update repository selection to use URL instead of name
- Changed the repository selection logic in SaveGitlabProvider and SaveGitlabProviderCompose components to use the repository URL for better accuracy.
- Updated the condition for displaying the selected repository in the CheckIcon component to compare against the gitlabPathNamespace instead of the repository name.
- Refactored the getGitlabRepositories function to simplify the group name check logic.
2025-07-06 13:56:58 -06:00
Mauricio Siu
b2d01a2889 Merge pull request #2140 from Dokploy/1187-swarm-service-bind-mounts-not-working-unless-manually-created
feat(volumes): add alert block for bind mount validation in AddVolume…
2025-07-05 17:18:24 -06:00
Mauricio Siu
5ec3d63ab2 feat(volumes): add alert block for bind mount validation in AddVolumes component
- Introduced an AlertBlock to provide users with warnings about host path validity and potential deployment issues when using bind mounts in the AddVolumes component.
- This enhancement improves user experience by ensuring users are informed of important considerations when configuring volumes.
2025-07-05 17:17:51 -06:00
Mauricio Siu
f0ef06ed8c Merge pull request #2139 from Dokploy/1541-register-login-screen-fails-on-chrome
1541 register login screen fails on chrome
2025-07-05 17:07:51 -06:00
Mauricio Siu
75b2c34a13 feat(api): implement lazy WebSocket client for improved connection management
- Introduced a `createLazyWSClient` function to manage WebSocket connections more efficiently by delaying the creation of the client until it's needed.
- Updated the `wsClient` initialization to use the new lazy client, enhancing performance and resource management in the application.
2025-07-05 17:06:39 -06:00
Mauricio Siu
cd4533df9e Merge branch 'canary' into 1541-register-login-screen-fails-on-chrome 2025-07-05 17:03:01 -06:00
Mauricio Siu
65a3d8175a Merge pull request #2138 from Dokploy/2109-reload-results-in-temporary-downtime-despite-healthcheck-zero-downtime-configured
refactor(application): remove redundant service stop logic
2025-07-05 16:48:18 -06:00
Mauricio Siu
d3702d22f2 refactor(application): remove redundant service stop logic
- Eliminated the conditional logic for stopping services based on serverId, streamlining the application status update process.
- This change enhances code clarity and maintains focus on application state management.
2025-07-05 16:47:44 -06:00
Mauricio Siu
d4b74c54da Merge pull request #2116 from Dokploy/394-ability-to-backup-named-volume-to-s3
394 ability to backup named volume to s3
2025-07-05 16:05:49 -06:00
Mauricio Siu
80ede659fb feat(volume-backups): enhance volume backup display with restore functionality
- Wrapped the volume backup handling component with a fragment to include the restore functionality.
- Added a new div for better layout and organization of the volume backup and restore components, improving user experience.
2025-07-05 16:00:01 -06:00
Mauricio Siu
c59ea57814 feat(database): add volume_backup table and update deployment schema
- Introduced a new "volume_backup" table to manage volume backup configurations, including fields for various database IDs and backup settings.
- Updated the "deployment" table to include a foreign key reference to the new "volume_backup" table.
- Added foreign key constraints to ensure data integrity across related tables.
- Updated journal and snapshot files to reflect these changes, enhancing the database schema for better backup management.
2025-07-05 15:40:57 -06:00
Mauricio Siu
381186c9f1 Merge branch 'canary' into 394-ability-to-backup-named-volume-to-s3 2025-07-05 15:40:25 -06:00
Mauricio Siu
05e0031daf refactor(database): remove volume_backup table and associated metadata
- Deleted the "volume_backup" table and its related SQL definitions, including foreign key constraints in the "deployment" table.
- Removed corresponding entries from the journal and snapshot files to ensure consistency and maintain a clean database schema.
- This cleanup improves the organization and maintainability of the database structure.
2025-07-05 15:40:14 -06:00
Mauricio Siu
1bacd42bf5 Merge pull request #2053 from jhon2c/feat/internal-path-routing
feat: add internal path routing and path stripping for domains
2025-07-05 15:34:58 -06:00
Mauricio Siu
2bc12a20ba Merge pull request #2128 from DearTanker/canary
Style: Change Node Applications dialog width, more easy to read.
2025-07-05 14:44:45 -06:00
Mauricio Siu
1943a9e8fa Merge pull request #2137 from Dokploy/2055-bug-report-requests-tab-limited-to-500-entries
fix(settings): update readMonitoringConfig to async and improve log r…
2025-07-05 14:44:34 -06:00
Mauricio Siu
db9109a3be fix(settings): update readMonitoringConfig to async and improve log reading efficiency 2025-07-05 14:43:49 -06:00
Jhonatan Caldeira
2f6084ec8f fix(conflicts): removed conflicted migrations 2025-07-05 16:08:09 -03:00
Jhonatan Caldeira
a62c9f63e1 Merge branch 'canary' into feat/internal-path-routing 2025-07-05 15:50:04 -03:00
autofix-ci[bot]
ee2bbf5e37 [autofix.ci] apply automated fixes 2025-07-05 17:38:16 +00:00
andrei1010
d27dff4906 fix: fix environment variable parsing for railpack 2025-07-05 17:14:28 +02:00
DearTanker
7874445510 Style: Change Node Applications dialog width, more easy to read. 2025-07-05 17:24:11 +08:00
Mauricio Siu
5c73ced500 chore(license): update copyright year to 2025 and remove obsolete dokploy license file 2025-07-05 01:38:37 -06:00
Mauricio Siu
b1450d14ac chore(package): bump version to v0.23.7 2025-07-05 01:36:13 -06:00
Mauricio Siu
c5e3b00990 Merge pull request #2125 from Dokploy/fix/issues
Fix/issues
2025-07-05 01:35:22 -06:00
Mauricio Siu
cf3f44f686 feat(database): add volume_backup table and related foreign key constraints
- Introduced a new table "volume_backup" to manage volume backup configurations.
- Added foreign key constraints linking "volumeBackupId" in the "deployment" table to the new "volume_backup" table.
- Updated the journal and snapshot files to reflect these changes, ensuring proper versioning and tracking.
2025-07-05 00:06:07 -06:00
Mauricio Siu
119883e746 Merge branch 'canary' into 394-ability-to-backup-named-volume-to-s3 2025-07-05 00:05:47 -06:00
Mauricio Siu
2918868166 refactor: remove deprecated volume backup SQL files and snapshots
- Deleted SQL files related to the volume_backup table, including type definitions, constraints, and columns that are no longer in use.
- Removed associated snapshot files to maintain consistency and reduce clutter in the database schema.
- This cleanup enhances the overall organization and maintainability of the database structure.
2025-07-05 00:05:36 -06:00
Mauricio Siu
715e44116d Merge pull request #2030 from s1nyx/fix/traefik-setup-was-not-pulling-image
fix(traefik-setup): now pulling the traefik image to make it sure it's present locally
2025-07-05 00:03:50 -06:00
Mauricio Siu
c15ee721ff feat(setup): add async execution of docker pull for traefik image during setup 2025-07-05 00:01:41 -06:00
Mauricio Siu
71befc3a4b Merge pull request #2115 from zuohuadong/canary
feat: fix OpencloudOS setup
2025-07-04 23:50:24 -06:00
Mauricio Siu
bca32f077b Merge pull request #2084 from Marukome0743/ubuntu
docs: update docker installation method of Ubuntu
2025-07-04 23:45:44 -06:00
Mauricio Siu
1afcecaec0 Merge pull request #2124 from Dokploy/feat/add-ports-publish-mode
Feat/add ports publish mode
2025-07-04 23:44:18 -06:00
Mauricio Siu
80b72dc75e fix: handle potential null values for publishMode in ShowPorts component 2025-07-04 23:42:35 -06:00
Mauricio Siu
5973d7b9b8 Merge branch 'canary' into feat/add-ports-publish-mode 2025-07-04 23:38:37 -06:00
autofix-ci[bot]
ab3a1504cf [autofix.ci] apply automated fixes 2025-07-05 05:37:11 +00:00
Mauricio Siu
f51c51958f feat: add publishMode column to port schema
- Introduced a new ENUM type "publishModeType" with values 'ingress' and 'host'.
- Added "publishMode" column to the "port" table with a default value of 'host'.
- Updated related metadata files to reflect the changes.
2025-07-04 23:33:21 -06:00
Mauricio Siu
f3703e6f5e Merge pull request #2112 from Marukome0743/error
refactor: remove unused catch errors
2025-07-04 23:29:28 -06:00
Mauricio Siu
14cb6cecae Merge pull request #2107 from DearTanker/canary
Style: Make all code editors apply Tailwind CSS font-mono style.
2025-07-04 23:24:01 -06:00
Mauricio Siu
6885b140eb Merge pull request #2108 from Marukome0743/blue
docs: remove blue underlink from README.md
2025-07-04 23:22:26 -06:00
Mauricio Siu
8e8712e33d style: align text in file tree component for improved readability 2025-07-04 23:17:19 -06:00
FelipeVasquez350
56fcaa8ccd Update show-port.tsx 2025-07-04 18:15:43 +02:00
FelipeVasquez350
d229284e5e Update handle-ports.tsx 2025-07-04 18:08:23 +02:00
FelipeVasquez350
3561b5cae6 feat: add option for publishMode in an application port settings 2025-07-04 18:01:13 +02:00
DearTanker
a3192d6584 Merge branch 'Dokploy:canary' into canary 2025-07-03 10:48:52 +08:00
DearTanker
24ea8b7fbd Style: Unspecify the popup list width to make the name appear in full 2025-07-03 10:39:58 +08:00
Marukome0743
e12df7b32e refactor: remove unused catch errors 2025-07-03 08:42:04 +09:00
Marukome0743
8001c9cfc2 docs: remove blue underlink from README.md 2025-07-03 08:41:38 +09:00
Marukome0743
a762b4b4ae docs: update docker installation method of Ubuntu 2025-07-03 08:41:08 +09:00
Jhonatan Caldeira
9cfbd664c5 Merge branch 'Dokploy:canary' into feat/internal-path-routing 2025-07-02 10:54:18 -03:00
huadong zuo
f9972bee60 Merge branch 'Dokploy:canary' into canary 2025-07-02 14:52:42 +08:00
Mauricio Siu
3970cd452b refactor: update volume backup imports for improved organization
- Changed import paths for findVolumeBackupById and findComposeById to enhance clarity and maintainability.
- Updated import structure to align with the latest service organization in the @dokploy/server package.
2025-07-02 00:52:25 -06:00
zuohuadong
6fc51e02a7 Revert "feat(swap): On hosts with less than 2G of memory, increase swap space to prevent installation failure."
This reverts commit 207fe6f477.
2025-07-02 14:51:48 +08:00
zuohuadong
fa7db0dc75 Revert "chore(setup):Increase interactive options"
This reverts commit 0c861585ed.
2025-07-02 14:50:39 +08:00
Mauricio Siu
107cdcee49 feat: extend volume backup functionality with scheduling and management
- Added support for volume backup jobs in the scheduling and removal processes.
- Enhanced job management to include volume backups in the job queue.
- Updated schema to accommodate volume backup data structure.
- Implemented initialization of volume backup jobs based on server status, improving backup management.
2025-07-02 00:45:57 -06:00
Mauricio Siu
6521491e2f feat: enhance volume backup scheduling and management
- Added initVolumeBackupsCronJobs function to initialize scheduled volume backups on server startup.
- Updated volumeBackupsRouter to handle scheduling and removal of volume backup jobs based on user input.
- Improved create and update volume backup logic to include scheduling functionality for both cloud and local environments.
- Introduced utility functions for scheduling and removing volume backup jobs, enhancing overall backup management.
2025-07-02 00:36:46 -06:00
Mauricio Siu
c5311f2a9f feat: implement volume backup and restore functionalities
- Added backupVolume and restoreVolume functions to handle the backup and restoration of volume data.
- Introduced a new index file to streamline exports for volume backup utilities.
- Updated volume backup logic to include scheduling and improved user feedback during operations.
- Refactored existing volume backup utilities for better organization and clarity.
2025-07-02 00:23:27 -06:00
Mauricio Siu
7726c8db21 fix: refine volume backup logic and enhance user feedback
- Updated HandleVolumeBackups to conditionally enable mount retrieval based on volumeBackupType, improving accuracy in fetching mounts.
- Enhanced backupVolume function to provide user feedback upon starting the compose container, ensuring better clarity during operations.
2025-07-02 00:09:07 -06:00
Mauricio Siu
b272f01a18 fix: update volume backup components for improved functionality
- Refactored HandleVolumeBackups to use consistent naming for mount items.
- Added an AlertBlock to RestoreVolumeBackups to inform users about potential volume name conflicts.
- Updated mount retrieval logic in the mount router to enhance accuracy and efficiency in fetching volume mounts.
2025-07-01 23:48:19 -06:00
Mauricio Siu
bddb07898e Merge branch 'canary' into 394-ability-to-backup-named-volume-to-s3 2025-07-01 23:32:50 -06:00
Mauricio Siu
2fe7349889 chore: update version in package.json to v0.23.6 2025-07-01 23:18:44 -06:00
Mauricio Siu
5c1c969873 Merge pull request #2113 from Dokploy/fix/schedules-rm
fix: prevent removal of current directory in deployment logs
2025-07-01 23:18:15 -06:00
Mauricio Siu
2f6f1b19e7 fix: prevent removal of current directory in deployment logs
- Updated the removeLastTenDeployments function to check if logPath is not the current directory before executing the removal command.
- Enhanced the unlink operation to ensure it only attempts to delete valid log paths.
2025-07-01 23:17:27 -06:00
Mauricio Siu
934ec9b16a fix: improve error handling during volume restoration process
- Simplified error handling in the volume restoration logic by removing detailed error messages and stack traces.
- Updated user feedback to provide clearer instructions when a volume is in use, enhancing the user experience during restoration attempts.
- Ensured that the restoration process aborts gracefully with informative messages when prerequisites are not met.
2025-07-01 01:21:54 -06:00
Mauricio Siu
c6d760a904 fix: update RestoreVolumeBackups and ShowVolumeBackups components for improved functionality
- Refactored the RestoreVolumeBackups component to ensure the type prop is required and added serverId handling for better integration.
- Corrected variable naming for destinationId in the form handling to prevent potential issues.
- Enhanced the ShowVolumeBackups component to pass serverId to the RestoreVolumeBackups component, ensuring consistent data flow.
- Improved user interface elements for backup file selection, ensuring better usability and clarity.
2025-07-01 01:12:20 -06:00
Mauricio Siu
4f021a3f79 feat: add restore volume backups component and integrate into show volume backups
- Introduced a new RestoreVolumeBackups component to facilitate the restoration of volume backups from selected files and destinations.
- Integrated the RestoreVolumeBackups component into the ShowVolumeBackups component, enhancing user experience by providing direct access to restoration functionality.
- Updated the restore-backup schema to include validation for destination and backup file selection, ensuring robust user input handling.
2025-06-30 22:50:46 -06:00
zuohuadong
0c861585ed chore(setup):Increase interactive options 2025-07-01 12:19:39 +08:00
Mauricio Siu
d15ccfe505 feat: add runVolumeBackup functionality and update volume backup paths
- Implemented the runVolumeBackup function to facilitate manual execution of volume backups, enhancing user control over backup processes.
- Updated various components to utilize the new VOLUME_BACKUPS_PATH for improved organization and clarity in backup file management.
- Enhanced error handling in the runManually mutation to ensure robust execution and logging of backup operations.
2025-06-30 22:15:45 -06:00
huadong zuo
e158e05ad6 Merge branch 'Dokploy:canary' into canary 2025-07-01 09:40:36 +08:00
Mauricio Siu
e21605030a fix: adjust scaling command in backupVolume function
- Updated the backupVolume function to use the actual number of replicas when scaling services back up after a backup, improving accuracy in service management.
- Enhanced command generation for application service types to ensure proper restoration of service states.
2025-06-30 11:27:18 -06:00
Jhonatan Caldeira
f1c46f0d19 fix(conflicts): redo database migrations broken due to upstream branch conflict 2025-06-30 11:26:33 -03:00
Jhonatan Caldeira
fbf3776548 Merge branch 'canary' into feat/internal-path-routing 2025-06-30 10:56:19 -03:00
Mauricio Siu
46d84eaa71 feat: implement volume restoration functionality
- Added a new restoreVolume function to facilitate the restoration of Docker volumes from backups.
- Integrated service type handling for both application and compose, ensuring proper scaling and management of services during restoration.
- Enhanced command generation for restoring volumes, improving clarity and maintainability of the logic.
2025-06-30 01:38:39 -06:00
Mauricio Siu
2a2b947998 refactor: remove redundant ID assignment in backupVolume command
- Eliminated duplicate ID assignment in the backupVolume function to streamline Docker command generation.
- Improved clarity and maintainability of the command logic by reducing unnecessary lines of code.
2025-06-30 01:35:32 -06:00
Mauricio Siu
9974b2326f refactor: streamline volume backup command generation
- Consolidated Docker command construction for volume backups into a base command to reduce redundancy.
- Enhanced service type handling for application and compose, ensuring proper scaling and stopping of services during backup.
- Improved readability and maintainability of the backup command logic by using template literals and consistent formatting.
2025-06-30 01:33:12 -06:00
Mauricio Siu
c042c8c0c5 feat: implement service-based volume selection for backups
- Added a new query to load mounts by service name, enhancing the volume backup form's functionality.
- Updated the form to allow users to select a service and corresponding volume from a dropdown, improving user experience.
- Retained the option for manual input of volume names, ensuring flexibility in volume selection.
- Refactored the component to streamline the handling of service and volume selections.
2025-06-30 01:25:50 -06:00
Mauricio Siu
819a310d48 feat: add volume selection functionality to volume backup form
- Introduced a new query to fetch named mounts by application ID, enhancing the volume backup form.
- Updated the form to allow users to select a volume from a dropdown, improving user experience.
- Retained the option for manual input of volume names, ensuring flexibility in volume selection.
2025-06-29 23:24:44 -06:00
Mauricio Siu
12860a0736 feat: add appName field to volume_backup schema and enhance deployment volume backup functionality
- Introduced a new appName column in the volume_backup table to improve application identification.
- Updated the volume backup schema to ensure appName is a required field.
- Enhanced the deployment service to support volume backup creation, integrating appName into the deployment process.
- Added validation for volume backup creation to ensure proper handling of appName and related data.
2025-06-29 23:18:12 -06:00
Mauricio Siu
392e2d66ec fix: improve volume backup validation and refactor component props
- Added validation to ensure service name is required when service type is "compose" in the volume backup form schema.
- Refactored the ShowVolumeBackups component to use a more consistent prop name for volume backup type, enhancing clarity and maintainability.
- Updated related components to align with the new prop structure, ensuring seamless integration across the application.
2025-06-29 23:14:53 -06:00
autofix-ci[bot]
1f4ce2daf3 [autofix.ci] apply automated fixes 2025-06-30 05:13:57 +00:00
Mauricio Siu
49edf17463 feat: enhance volume backup functionality and schema
- Added support for volume backups in the deployment management interface by introducing a new volumeBackupId field in the deployment schema.
- Updated the volume backup schema to include relationships with deployments, allowing for better management and tracking of volume backups.
- Enhanced API routes to include application data when querying volume backups, improving the data returned for related entities.
- Updated UI components to reflect the new volume backup features and ensure seamless integration with existing functionalities.
2025-06-29 23:10:49 -06:00
Mauricio Siu
6eea02c098 fix: update default enabled state for volume backups
- Changed the default value of the enabled property in the volume backup handling component from true to false to ensure backups are disabled by default unless explicitly enabled.
2025-06-29 23:07:18 -06:00
Mauricio Siu
a5bba9a11b refactor: update volume backup schema and form handling
- Modified the volume backup schema to enforce non-null constraints on volumeName and added serviceName.
- Removed unnecessary fields (type, hostPath) from the schema and updated related API and form handling.
- Enhanced form validation to ensure required fields are properly checked.
- Updated the UI components to reflect changes in the volume backup management interface.
2025-06-29 23:05:36 -06:00
DearTanker
37c7507507 Style: Make all code editors apply Tailwind CSS font-mono style. 2025-06-30 13:04:47 +08:00
Mauricio Siu
ce88a0a5f2 feat: implement volume backup management functionality
- Added components for handling and displaying volume backups in the dashboard.
- Created API routes for managing volume backups, including create, update, delete, and list operations.
- Introduced database schema for volume backups, including necessary fields and relationships.
- Updated the application to integrate volume backup features into the service management interface.
2025-06-29 22:22:10 -06:00
Mauricio Siu
40f28705cb Merge branch 'canary' into 394-ability-to-backup-named-volume-to-s3 2025-06-29 21:17:05 -06:00
Mauricio Siu
03e04b7bce Merge pull request #2105 from Dokploy/fix/cancel-schedules
feat: add kill process functionality to schedules
2025-06-29 21:13:37 -06:00
Mauricio Siu
b920e7c0f1 fix: handle potential null data in PID extraction for runCommand function 2025-06-29 21:10:11 -06:00
Mauricio Siu
15fde820a3 Merge pull request #2104 from DearTanker/canary
Style: Make Node Applications more readable.
2025-06-29 21:09:29 -06:00
Mauricio Siu
64293fce79 feat: add kill process functionality to deployments
- Implemented a new mutation to kill a running deployment process by its PID.
- Updated the deployment schema to include a PID field.
- Enhanced the deployment service to handle process termination and status updates.
- Modified the deployment scripts to echo PID and schedule ID for better tracking.
- Added error handling for the kill process operation.
2025-06-29 21:08:51 -06:00
DearTanker
526f249d0e Style: Make Node Applications more readable 2025-06-30 10:54:45 +08:00
Mauricio Siu
17c5a42d8e Merge branch 'canary' into 394-ability-to-backup-named-volume-to-s3 2025-06-29 14:11:34 -06:00
autofix-ci[bot]
fac96b5db5 [autofix.ci] apply automated fixes 2025-06-29 20:11:18 +00:00
Mauricio Siu
4796b0cf4e chore: update version in package.json to v0.23.5 2025-06-29 14:09:31 -06:00
Mauricio Siu
159a055bc6 Merge pull request #2102 from Dokploy/2092-preview-link-without-protocol
fix: update preview deployment comment to include protocol in domain URL
2025-06-29 13:47:30 -06:00
Mauricio Siu
cfade317f1 fix: update preview deployment comment to include protocol in domain URL 2025-06-29 13:47:17 -06:00
Mauricio Siu
36ebefff16 Merge pull request #2083 from Marukome0743/models
chore: remove unknown `apps/models` from pnpm-workspace.yaml
2025-06-29 13:24:33 -06:00
Mauricio Siu
7cc0603078 Merge pull request #2101 from SashkaHavr/fix-docker-config-path
fix: change default DOCKER_CONFIG to a config directory instead of config.json file
2025-06-29 13:24:08 -06:00
Mauricio Siu
e0e42ac554 Merge pull request #2100 from DearTanker/canary
style: Remove the width restriction on the server name.
2025-06-29 12:44:12 -06:00
Oleksandr Havrylov
e004d8bd52 fix: change default DOCKER_CONFIG to a config directory instead of config.json file 2025-06-29 17:14:50 +02:00
DearTanker
0c0912f606 Style: Remove the width restriction on the server name so that it can be displayed in full. 2025-06-29 21:13:36 +08:00
Mauricio Siu
8d81440d9b Merge branch 'canary' into 394-ability-to-backup-named-volume-to-s3 2025-06-28 13:23:37 -06:00
Mauricio Siu
9ede3bd71b feat(rollbacks): update backup and restore instructions for N8N data
- Added detailed commands for backing up and restoring N8N data using Docker.
- Included steps for scaling services and managing permissions during the restore process.
- Enhanced clarity of backup commands and added comments for better understanding.
2025-06-28 13:17:34 -06:00
Jhonatan Caldeira
df6a72ea50 Merge branch 'Dokploy:canary' into feat/internal-path-routing 2025-06-28 12:24:01 -03:00
Mauricio Siu
e79f8c4b72 feat: add Tolgee sponsorship banner to README 2025-06-27 00:11:02 -06:00
Mauricio Siu
26e2a24f63 chore: update version in package.json to v0.23.4 2025-06-26 23:57:36 -06:00
Mauricio Siu
830feabd70 Merge pull request #2072 from dscamargo/feat/database-name-database-backup-notification
feat: add database name in database backup notification
2025-06-26 23:55:57 -06:00
Mauricio Siu
122a3d110d Merge pull request #2090 from Dokploy/fix/correct-database-type-on-compose-backups
fix(backups): enhance database type handling in compose backup process
2025-06-26 23:34:33 -06:00
Mauricio Siu
c05edb313f fix(backups): enhance database type handling in compose backup process
- Added a new function to determine the database type based on the backup configuration.
- Updated notifications to use the correct database type instead of a hardcoded value.
2025-06-26 23:32:18 -06:00
Mauricio Siu
1ec2853862 Merge pull request #2089 from Dokploy/2080-invalid-name-when-trying-to-deploy-an-app-with-dockerfile-to-ghcrio
fix(upload): refactor registry tag construction for image uploads
2025-06-26 23:02:36 -06:00
Mauricio Siu
5c2709248c fix(upload): refactor registry tag construction for image uploads
- Updated the logic for constructing the registry tag in the uploadImage and uploadImageRemoteCommand functions to ensure correct formatting.
- Removed unnecessary path imports and streamlined the code for better readability.
2025-06-26 23:00:51 -06:00
autofix-ci[bot]
bc79074441 [autofix.ci] apply automated fixes 2025-06-27 03:13:41 +00:00
Mauricio Siu
3b5428697b feat(rollbacks): add backup and restore instructions for license-named backups
- Introduced a new Backup file containing detailed steps for creating and restoring backups using Docker.
- Included commands for stopping services, removing volumes, and managing backup files.
2025-06-26 21:13:09 -06:00
Mauricio Siu
5ada451916 Merge pull request #2087 from Dokploy/feat/add-dokploy-cloud-endpoints
feat(settings): add query to retrieve Dokploy cloud IPs
2025-06-26 21:01:50 -06:00
Mauricio Siu
6b0d9240dd feat(settings): add query to retrieve Dokploy cloud IPs
- Implemented a new admin procedure to fetch cloud IPs from environment variables.
- Returns an empty array if not in cloud mode.
2025-06-26 21:00:51 -06:00
zuohuadong
475c452451 chore(*): use dnf 2025-06-26 16:05:56 +08:00
Marukome0743
1e31ebb9c2 chore: remove unknown apps/models from pnpm-workspace.yaml 2025-06-26 16:49:44 +09:00
zuohuadong
207fe6f477 feat(swap): On hosts with less than 2G of memory, increase swap space to prevent installation failure. 2025-06-26 10:32:23 +08:00
zuohuadong
0b34676336 chore (opencloud) fix docker install 2025-06-26 10:24:14 +08:00
Douglas Camargo
499022a328 feat: add database name in database backup notification 2025-06-23 22:35:40 -03:00
Mauricio Siu
fb5d2bd5b6 feat(docker-swarm): implement server authorization checks and input validation
- Added server authorization checks to ensure users can only access resources for their organization.
- Enhanced input validation for container and app names using regex to prevent invalid entries.
- Updated multiple query procedures in both docker and swarm routers to include these checks and validations.
2025-06-22 23:57:12 -06:00
Mauricio Siu
e42f6bc610 feat(user-validation): enhance path validation in Traefik config
- Added refined validation for the 'path' field to prevent directory traversal attacks and unauthorized access.
- Implemented checks for null bytes and ensured paths start with the MAIN_TRAEFIK_PATH constant.
2025-06-22 23:57:02 -06:00
Mauricio Siu
61cf426615 feat(user-access): implement access control for user information retrieval
- Added checks to deny access if the user is not found in the organization.
- Implemented authorization logic to allow access only for users requesting their own information or users with owner role in the same organization.
2025-06-22 23:56:13 -06:00
Jhonatan Caldeira
dde12e132a Merge branch 'Dokploy:canary' into feat/internal-path-routing 2025-06-22 17:35:53 -03:00
Jhonatan Caldeira
fd0f679d0f feat(domains): add internal path routing and
strip path functionality to compose

  - Add internalPath field to route requests
  to different paths internally
  - Add stripPath option to remove external
  path prefix before forwarding
  - Improves validation for stripPath (requires
  non-root path) and internalPath (must start
  with /)
2025-06-22 14:55:27 -03:00
Mauricio Siu
2a89be6efc Merge pull request #2069 from Dokploy/2065-rollback-feature-dns-issues
feat(rollbacks): enhance fullContext type and refactor createRollback…
2025-06-22 18:01:21 +02:00
autofix-ci[bot]
412bb9e874 [autofix.ci] apply automated fixes 2025-06-22 16:00:36 +00:00
Mauricio Siu
6290c217f1 feat(rollbacks): add alert for storage usage in rollback settings
- Introduced an AlertBlock component to inform users about increased storage usage when rollbacks are enabled.
- Added cautionary note regarding the potential deletion of rollback images during manual cache cleaning.
2025-06-22 10:00:14 -06:00
Mauricio Siu
4babdd45ea chore: update version in package.json to v0.23.3 2025-06-22 09:58:35 -06:00
Mauricio Siu
24bff96898 feat(rollbacks): enhance fullContext type and refactor createRollback logic
- Updated fullContext type in rollbacks schema to include Application and Project types.
- Refactored createRollback function to separate fullContext from input and handle it more efficiently.
- Integrated environment variable preparation into the rollback process.
2025-06-22 09:56:36 -06:00
Jhonatan Caldeira
df8f1252a0 Merge branch 'Dokploy:canary' into feat/internal-path-routing 2025-06-22 11:54:59 -03:00
autofix-ci[bot]
8599f519a4 [autofix.ci] apply automated fixes 2025-06-22 05:44:21 +00:00
Mauricio Siu
113e4ae4b5 fix(middleware): update domain type and improve middleware configuration
- Changed the type of the `domain` parameter in `createPathMiddlewares` from `any` to `Domain` for better type safety.
- Added missing commas in middleware configuration objects to ensure proper syntax.
2025-06-21 23:43:52 -06:00
Mauricio Siu
7f0bdc7e00 feat(domain): add internalPath and stripPath properties to domain configuration
- Updated test files to include new properties `internalPath` and `stripPath` in domain configurations.
- Removed deprecated `createMultiPathDomain` function from the domain service to streamline the codebase.
2025-06-21 23:42:18 -06:00
Mauricio Siu
b685a817fd feat(domain): add internalPath and stripPath columns to domain schema
- Introduced new columns `internalPath` (text) with a default value of '/' and `stripPath` (boolean) with a default value of false to the domain table.
- Updated journal metadata to reflect the addition of these columns.
2025-06-21 23:39:22 -06:00
Mauricio Siu
6061a443d1 Merge branch 'canary' into feat/internal-path-routing 2025-06-21 23:39:01 -06:00
Mauricio Siu
4c9835d1f3 refactor(domain): remove deprecated columns internalPath and stripPath from domain schema
- Deleted SQL statements for internalPath and stripPath columns in the domain table.
- Updated journal and snapshot metadata to reflect the removal of these columns.
2025-06-21 23:38:39 -06:00
Jhonatan Caldeira
f2671f9369 fix(domain): remove unused ApplicationNested type import 2025-06-20 17:51:05 -03:00
Jhonatan Caldeira
bb904bb011 feat: add internal path routing and path stripping for domains
- Add internalPath and stripPath fields to domain schema
- Implement UI controls for configuring internal path routing
- Create Traefik middleware for path manipulation (addPrefix/stripPrefix)
- Support different external and internal paths for applications
- Enable path stripping for cleaner URL forwarding

This allows applications to be accessed via external paths while maintaining
different internal routing structures, useful for microservices and legacy
applications that expect specific path prefixes.
2025-06-20 16:36:27 -03:00
s1nyx
9fb6ca2b3b feat(traefik-setup): check for existing image before pulling to optimize image management 2025-06-11 08:12:00 +02:00
s1nyx
9f146d7d80 fix(traefik-setup): now pulling the traefik image to make it sure it's present locally 2025-06-11 08:06:13 +02:00
Mauricio Siu
cad628d155 refactor: remove unused volume suffix function from collision utility 2025-05-10 02:58:31 -06:00
Mauricio Siu
cd8230b0e5 refactor(add-domain): remove debug log statement from AddDomain component 2025-04-06 11:14:43 -06:00
141 changed files with 33364 additions and 332 deletions

View File

@@ -16,28 +16,29 @@ Here's how to install docker on different operating systems:
### Ubuntu
```bash
# Uninstall old versions
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Set up stable repository
# Add the repository to Apt sources
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
## Windows

View File

@@ -2,7 +2,7 @@
## Core License (Apache License 2.0)
Copyright 2024 Mauricio Siu.
Copyright 2025 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,20 +1,16 @@
<div align="center">
<div>
<a href="https://dokploy.com" target="_blank" rel="noopener">
<img style="object-fit: cover;" align="center" width="100%"src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." />
</a>
</div>
</br>
<div align="center">
<div>Join us on Discord for help, feedback, and discussions!</div>
<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%" />
</a>
</br>
</br>
<p>Join us on Discord for help, feedback, and discussions!</p>
<a href="https://discord.gg/2tBnJ3jDJc">
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
</a>
</div>
</div>
<br />
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
@@ -61,71 +57,52 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Hero Sponsors 🎖
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
</a>
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
</a>
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
</a>
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
</a>
<div>
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
<a href="https://mandarin3d.com/?ref=dokploy"><img src=".github/sponsors/mandarin.png" alt="Mandarin" width="100"/></a>
<a href="https://lightnode.com/?ref=dokploy"><img src=".github/sponsors/light-node.webp" alt="Lightnode" width="300"/></a>
</div>
<!-- Premium Supporters 🥇 -->
<!-- Add Premium Supporters here -->
### Premium Supporters 🥇
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
</a>
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
</a>
<div>
<a href="https://supafort.com/?ref=dokploy"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="300"/></a>
<a href="https://agentdock.ai/?ref=dokploy"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/></a>
</div>
### Elite Contributors 🥈
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
</a>
</div>
<!-- Elite Contributors 🥈 -->
<!-- Add Elite Contributors here -->
### Supporting Members 🥉
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
### Elite Contributors 🥈
<div>
<a href="https://americancloud.com/?ref=dokploy"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="300"/></a>
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy"><img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/></a>
</div>
### Supporting Members 🥉
<div>
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://cloudblast.io/?ref=dokploy"><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
<a href="https://startupfa.me/?ref=dokploy"><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
<a href="https://itsdb-center.com?ref=dokploy"><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy"><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
</div>
### Community Backers 🤝
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
<div>
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
</div>
#### Organizations:
@@ -140,12 +117,12 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
</a>
</a>
## Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a>
## Contributing

View File

@@ -1,26 +0,0 @@
# License
## Core License (Apache License 2.0)
Copyright 2024 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -19,6 +19,8 @@ describe("createDomainLabels", () => {
path: "/",
createdAt: "",
previewDeploymentId: "",
internalPath: "/",
stripPath: false,
};
it("should create basic labels for web entrypoint", async () => {

View File

@@ -119,6 +119,8 @@ const baseDomain: Domain = {
domainType: "application",
uniqueConfigKey: 1,
previewDeploymentId: "",
internalPath: "/",
stripPath: false,
};
const baseRedirect: Redirect = {

View File

@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
}
try {
return JSON.parse(str);
} catch (_e) {
} catch {
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
return z.NEVER;
}

View File

@@ -107,7 +107,7 @@ export const ShowImport = ({ composeId }: Props) => {
composeId,
});
setShowModal(false);
} catch (_error) {
} catch {
toast.error("Error importing template");
}
};
@@ -126,7 +126,7 @@ export const ShowImport = ({ composeId }: Props) => {
});
setTemplateInfo(result);
setShowModal(true);
} catch (_error) {
} catch {
toast.error("Error processing template");
}
};

View File

@@ -35,6 +35,9 @@ import { z } from "zod";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
publishMode: z.enum(["ingress", "host"], {
required_error: "Publish mode is required",
}),
targetPort: z.number().int().min(1).max(65535),
protocol: z.enum(["tcp", "udp"], {
required_error: "Protocol is required",
@@ -80,6 +83,7 @@ export const HandlePorts = ({
useEffect(() => {
form.reset({
publishedPort: data?.publishedPort ?? 0,
publishMode: data?.publishMode ?? "ingress",
targetPort: data?.targetPort ?? 0,
protocol: data?.protocol ?? "tcp",
});
@@ -165,6 +169,32 @@ export const HandlePorts = ({
</FormItem>
)}
/>
<FormField
control={form.control}
name="publishMode"
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>Published Port Mode</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a publish mode for the port" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"ingress"}>Ingress</SelectItem>
<SelectItem value={"host"}>Host</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="targetPort"

View File

@@ -60,7 +60,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
{data?.ports.map((port) => (
<div key={port.portId}>
<div className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4">
<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-3 lg:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Published Port</span>
<span className="text-sm text-muted-foreground">
@@ -68,7 +68,13 @@ export const ShowPorts = ({ applicationId }: Props) => {
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium"> Target Port</span>
<span className="font-medium">Published Port Mode</span>
<span className="text-sm text-muted-foreground">
{port?.publishMode?.toUpperCase()}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Target Port</span>
<span className="text-sm text-muted-foreground">
{port.targetPort}
</span>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
@@ -169,6 +170,23 @@ export const AddVolumes = ({
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
{type === "bind" && (
<AlertBlock>
<div className="space-y-2">
<p>
Make sure the host path is a valid path and exists in the
host machine.
</p>
<p className="text-sm text-muted-foreground">
<strong>Cluster Warning:</strong> If you're using cluster
features, bind mounts may cause deployment failures since
the path must exist on all worker/manager nodes. Consider
using external tools to distribute the folder across nodes
or use named volumes instead.
</p>
</div>
</AlertBlock>
)}
<FormField
control={form.control}
defaultValue={form.control._defaultValues.type}

View File

@@ -14,7 +14,8 @@ interface Props {
| "schedule"
| "server"
| "backup"
| "previewDeployment";
| "previewDeployment"
| "volumeBackup";
serverId?: string;
refreshToken?: string;
children?: React.ReactNode;

View File

@@ -27,7 +27,8 @@ interface Props {
| "schedule"
| "server"
| "backup"
| "previewDeployment";
| "previewDeployment"
| "volumeBackup";
refreshToken?: string;
serverId?: string;
}
@@ -62,6 +63,8 @@ export const ShowDeployments = ({
const { mutateAsync: rollback, isLoading: isRollingBack } =
api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
api.deployment.killProcess.useMutation();
const [url, setUrl] = React.useState("");
useEffect(() => {
@@ -170,6 +173,32 @@ export const ShowDeployments = ({
</div>
<div className="flex flex-row items-center gap-2">
{deployment.pid && deployment.status === "running" && (
<DialogAction
title="Kill Process"
description="Are you sure you want to kill the process?"
type="default"
onClick={async () => {
await killProcess({
deploymentId: deployment.deploymentId,
})
.then(() => {
toast.success("Process killed successfully");
})
.catch(() => {
toast.error("Error killing process");
});
}}
>
<Button
variant="destructive"
size="sm"
isLoading={isKillingProcess}
>
Kill Process
</Button>
</DialogAction>
)}
<Button
onClick={() => {
setActiveLog(deployment);

View File

@@ -49,6 +49,8 @@ export const domain = z
.object({
host: z.string().min(1, { message: "Add a hostname" }),
path: z.string().min(1).optional(),
internalPath: z.string().optional(),
stripPath: z.boolean().optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
@@ -84,6 +86,29 @@ export const domain = z
message: "Required",
});
}
// Validate stripPath requires a valid path
if (input.stripPath && (!input.path || input.path === "/")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["stripPath"],
message:
"Strip path can only be enabled when a path other than '/' is specified",
});
}
// Validate internalPath starts with /
if (
input.internalPath &&
input.internalPath !== "/" &&
!input.internalPath.startsWith("/")
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["internalPath"],
message: "Internal path must start with '/'",
});
}
});
type Domain = z.infer<typeof domain>;
@@ -162,6 +187,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
defaultValues: {
host: "",
path: undefined,
internalPath: undefined,
stripPath: false,
port: undefined,
https: false,
certificateType: undefined,
@@ -182,6 +209,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
...data,
/* Convert null to undefined */
path: data?.path || undefined,
internalPath: data?.internalPath || undefined,
stripPath: data?.stripPath || false,
port: data?.port || undefined,
certificateType: data?.certificateType || undefined,
customCertResolver: data?.customCertResolver || undefined,
@@ -194,6 +223,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
form.reset({
host: "",
path: undefined,
internalPath: undefined,
stripPath: false,
port: undefined,
https: false,
certificateType: undefined,
@@ -469,6 +500,49 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
}}
/>
<FormField
control={form.control}
name="internalPath"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Internal Path</FormLabel>
<FormDescription>
The path where your application expects to receive
requests internally (defaults to "/")
</FormDescription>
<FormControl>
<Input placeholder={"/"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="stripPath"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Strip Path</FormLabel>
<FormDescription>
Remove the external path from the request before
forwarding to the application
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"

View File

@@ -278,7 +278,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.name}
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -299,7 +299,8 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.url ===
field.value.gitlabPathNamespace
? "opacity-100"
: "opacity-0",
)}

View File

@@ -0,0 +1,108 @@
Backup
# license-namedbackups-abxelc
1. docker ps --filter "label=com.docker.swarm.service.name=license-namedbackups-abxelc" --format "{{.Names}}"
2. docker run --rm \
--volumes-from "license-namedbackups-abxelc.1.m3cxy78ocj3w0zu42kmgamc5y" \
-v $(pwd):/backup \
ubuntu \
tar cvf /backup/backup.tar /var/lib/postgresql/data
# Official Command Backup
1. Backup
docker run --rm \
-v license-namedbackups-abxelc-data:/volume_data \
-v $(pwd):/backup \
ubuntu \
bash -c "cd /volume_data && tar cvf /backup/generic_backup.tar ."
2. Restore
docker service scale license-namedbackups-abxelc=0
docker volume rm license-namedbackups-abxelc-data
2. docker run --rm \
-v license-namedbackups-abxelc-data:/volume_data \
-v $(pwd):/backup \
ubuntu \
bash -c "cd /volume_data && tar xvf /backup/generic_backup.tar ."
docker service scale license-namedbackups-abxelc=1
root@srv594061:~# docker volume inspect n8n_data-data
[
{
"CreatedAt": "2025-06-28T18:07:44Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/n8n_data-data/_data",
"Name": "n8n_data-data",
"Options": null,
"Scope": "local"
}
]
Archivos funcuionando creados por N8N
# root@srv594061:~# cd /var/lib/docker/volumes/n8n_data-data/_data
# root@srv594061:/var/lib/docker/volumes/n8n_data-data/_data# ls
# binaryData config crash.journal database.sqlite git n8nEventLog.log ssh
Luego que intente hacer el backup con el comando de backup
root@srv594061:~# docker run --rm -v n8n_data-data:/volume_data -v $(pwd):/backup ubuntu bash -c "cd /volume_data && tar cvf /backup/generic_backup6.tar ."
./
./config
./crash.journal
./binaryData/
./git/
./database.sqlite
./ssh/
./n8nEventLog.log
root@srv594061:~#
# Paramos la aplicacion
docker service scale n8n=0
# Haciendo el restore
root@srv594061:~# docker volume rm n8n_data-data
n8n_data-data
root@srv594061:~# docker run --rm -v n8n_data-data:/volume_data -v $(pwd):/backup ubuntu bash -c "cd /volume_data && tar xvf /backup/generic_backup6.tar && chown -R 999:999 ."
./
./config
./crash.journal
./binaryData/
./git/
./database.sqlite
./ssh/
./n8nEventLog.log
# Tenemos los archivos en el volumen
root@srv594061:~# ls /var/lib/docker/volumes/n8n_data-data/_data
binaryData config crash.journal database.sqlite git n8nEventLog.log ssh
root@srv594061:~#
docker service scale n8n=1
# Luego en N8N Cuando se que el volumen tiene la data
Permissions 0644 for n8n settings file /home/node/.n8n/config are too wide. This is ignored for now, but in the future n8n will attempt to change the permissions automatically. To automatically enforce correct permissions now set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true (recommended), or turn this check off set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.
User settings loaded from: /home/node/.n8n/config
Last session crashed
Error: EACCES: permission denied, open '/home/node/.n8n/crash.journal'
at open (node:internal/fs/promises:639:25)
at touchFile (/usr/local/lib/node_modules/n8n/dist/crash-journal.js:18:20)
at Object.init (/usr/local/lib/node_modules/n8n/dist/crash-journal.js:32:5)
at Start.initCrashJournal (/usr/local/lib/node_modules/n8n/dist/commands/base-command.js:113:9)
at Start.init (/usr/local/lib/node_modules/n8n/dist/commands/start.js:141:9)
at Start._run (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/command.js:301:13)
at Config.runCommand (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/config/config.js:424:25)
at run (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/main.js:94:16)
at /usr/local/lib/node_modules/n8n/bin/n8n:71:2
TypeError: Cannot read properties of undefined (reading 'error')

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -79,6 +80,11 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
<DialogDescription>
Configure how rollbacks work for this application
</DialogDescription>
<AlertBlock>
Having rollbacks enabled increases storage usage. Be careful with
this option. Note that manually cleaning the cache may delete
rollback images, making them unavailable for future rollbacks.
</AlertBlock>
</DialogHeader>
<Form {...form}>

View File

@@ -166,12 +166,16 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
await runManually({
scheduleId: schedule.scheduleId,
}).then(async () => {
await new Promise((resolve) =>
setTimeout(resolve, 1500),
);
refetchSchedules();
});
})
.then(async () => {
await new Promise((resolve) =>
setTimeout(resolve, 1500),
);
refetchSchedules();
})
.catch(() => {
toast.error("Error running schedule");
});
}}
>
<Play className="size-4 transition-colors" />

View File

@@ -0,0 +1,672 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { CacheType } from "../domains/handle-domain";
import { commonCronExpressions } from "../schedules/handle-schedules";
const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
cronExpression: z.string().min(1, "Cron expression is required"),
volumeName: z.string().min(1, "Volume name is required"),
prefix: z.string(),
// keepLatestCount: z.coerce.number().optional(),
turnOff: z.boolean().default(false),
enabled: z.boolean().default(true),
serviceType: z.enum([
"application",
"compose",
"postgres",
"mariadb",
"mongo",
"mysql",
"redis",
]),
serviceName: z.string(),
destinationId: z.string().min(1, "Destination required"),
})
.superRefine((data, ctx) => {
if (data.serviceType === "compose" && !data.serviceName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Service name is required",
path: ["serviceName"],
});
}
if (data.serviceType === "compose" && !data.serviceName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Service name is required",
path: ["serviceName"],
});
}
});
interface Props {
id?: string;
volumeBackupId?: string;
volumeBackupType?:
| "application"
| "compose"
| "postgres"
| "mariadb"
| "mongo"
| "mysql"
| "redis";
}
export const HandleVolumeBackups = ({
id,
volumeBackupId,
volumeBackupType,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
cronExpression: "",
volumeName: "",
prefix: "",
// keepLatestCount: undefined,
turnOff: false,
enabled: true,
serviceName: "",
serviceType: volumeBackupType,
},
});
const serviceTypeForm = volumeBackupType;
const { data: destinations } = api.destination.all.useQuery();
const { data: volumeBackup } = api.volumeBackups.one.useQuery(
{ volumeBackupId: volumeBackupId || "" },
{ enabled: !!volumeBackupId },
);
const { data: mounts } = api.mounts.allNamedByApplicationId.useQuery(
{ applicationId: id || "" },
{ enabled: !!id && volumeBackupType === "application" },
);
const {
data: services,
isFetching: isLoadingServices,
error: errorServices,
refetch: refetchServices,
} = api.compose.loadServices.useQuery(
{
composeId: id || "",
type: cacheType,
},
{
retry: false,
refetchOnWindowFocus: false,
enabled: !!id && volumeBackupType === "compose",
},
);
const serviceName = form.watch("serviceName");
const { data: mountsByService } = api.compose.loadMountsByService.useQuery(
{
composeId: id || "",
serviceName,
},
{
enabled: !!id && volumeBackupType === "compose" && !!serviceName,
},
);
useEffect(() => {
if (volumeBackupId && volumeBackup) {
form.reset({
name: volumeBackup.name,
cronExpression: volumeBackup.cronExpression,
volumeName: volumeBackup.volumeName || "",
prefix: volumeBackup.prefix,
// keepLatestCount: volumeBackup.keepLatestCount || undefined,
turnOff: volumeBackup.turnOff,
enabled: volumeBackup.enabled || false,
serviceName: volumeBackup.serviceName || "",
destinationId: volumeBackup.destinationId,
serviceType: volumeBackup.serviceType,
});
}
}, [form, volumeBackup, volumeBackupId]);
const { mutateAsync, isLoading } = volumeBackupId
? api.volumeBackups.update.useMutation()
: api.volumeBackups.create.useMutation();
const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (!id && !volumeBackupId) return;
await mutateAsync({
...values,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
...(volumeBackupType === "application" && {
applicationId: id || "",
}),
...(volumeBackupType === "compose" && {
composeId: id || "",
}),
...(volumeBackupType === "postgres" && {
serverId: id || "",
}),
...(volumeBackupType === "postgres" && {
postgresId: id || "",
}),
...(volumeBackupType === "mariadb" && {
mariadbId: id || "",
}),
...(volumeBackupType === "mongo" && {
mongoId: id || "",
}),
...(volumeBackupType === "mysql" && {
mysqlId: id || "",
}),
...(volumeBackupType === "redis" && {
redisId: id || "",
}),
})
.then(() => {
toast.success(
`Volume backup ${volumeBackupId ? "updated" : "created"} successfully`,
);
utils.volumeBackups.list.invalidate({
id,
volumeBackupType,
});
setIsOpen(false);
})
.catch((error) => {
toast.error(
error instanceof Error ? error.message : "An unknown error occurred",
);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{volumeBackupId ? (
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
) : (
<Button>
<PlusCircle className="w-4 h-4 mr-2" />
Add Volume Backup
</Button>
)}
</DialogTrigger>
<DialogContent
className={cn(
"max-h-screen overflow-y-auto",
volumeBackupType === "compose" || volumeBackupType === "application"
? "max-h-[95vh] sm:max-w-2xl"
: " sm:max-w-lg",
)}
>
<DialogHeader>
<DialogTitle>
{volumeBackupId ? "Edit" : "Create"} Volume Backup
</DialogTitle>
<DialogDescription>
Create a volume backup to backup your volume to a destination
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Task Name
</FormLabel>
<FormControl>
<Input placeholder="Daily Database Backup" {...field} />
</FormControl>
<FormDescription>
A descriptive name for your scheduled task
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cronExpression"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="destinationId"
render={({ field }) => (
<FormItem>
<FormLabel>Destination</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a destination" />
</SelectTrigger>
</FormControl>
<SelectContent>
{destinations?.map((destination) => (
<SelectItem
key={destination.destinationId}
value={destination.destinationId}
>
{destination.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose the backup destination where files will be stored
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{serviceTypeForm === "compose" && (
<>
<div className="flex flex-col w-full gap-4">
{errorServices && (
<AlertBlock
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message}
</AlertBlock>
)}
<FormField
control={form.control}
name="serviceName"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
</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>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
{mountsByService && mountsByService.length > 0 && (
<FormField
control={form.control}
name="volumeName"
render={({ field }) => (
<FormItem>
<FormLabel>Volumes</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a volume name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{mountsByService?.map((volume) => (
<SelectItem
key={volume.Name}
value={volume.Name || ""}
>
{volume.Name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose the volume to backup, if you dont see the
volume here, you can type the volume name manually
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
{serviceTypeForm === "application" && (
<FormField
control={form.control}
name="volumeName"
render={({ field }) => (
<FormItem>
<FormLabel>Volumes</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a volume name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{mounts?.map((mount) => (
<SelectItem key={mount.Name} value={mount.Name || ""}>
{mount.Name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose the volume to backup, if you dont see the volume
here, you can type the volume name manually
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="volumeName"
render={({ field }) => (
<FormItem>
<FormLabel>Volume Name</FormLabel>
<FormControl>
<Input placeholder="my-volume-name" {...field} />
</FormControl>
<FormDescription>
The name of the Docker volume to backup
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="prefix"
render={({ field }) => (
<FormItem>
<FormLabel>Backup Prefix</FormLabel>
<FormControl>
<Input placeholder="backup-" {...field} />
</FormControl>
<FormDescription>
Prefix for backup files (optional)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="keepLatestCount"
render={({ field }) => (
<FormItem>
<FormLabel>Keep Latest Count</FormLabel>
<FormControl>
<Input
type="number"
placeholder="5"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value) || undefined)
}
/>
</FormControl>
<FormDescription>
Number of backup files to keep (optional)
</FormDescription>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="turnOff"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
Turn Off Container During Backup
</FormLabel>
<FormDescription className="text-amber-600 dark:text-amber-400">
The container will be temporarily stopped during backup to
prevent file corruption. This ensures data integrity but may
cause temporary service interruption.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
Enabled
</FormLabel>
</FormItem>
)}
/>
<Button type="submit" isLoading={isLoading} className="w-full">
{volumeBackupId ? "Update" : "Create"} Volume Backup
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,411 @@
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { debounce } from "lodash";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { formatBytes } from "../../database/backups/restore-backup";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props {
id: string;
type: "application" | "compose";
serverId?: string;
}
const RestoreBackupSchema = z.object({
destinationId: z
.string({
required_error: "Please select a destination",
})
.min(1, {
message: "Destination is required",
}),
backupFile: z
.string({
required_error: "Please select a backup file",
})
.min(1, {
message: "Backup file is required",
}),
volumeName: z
.string({
required_error: "Please enter a volume name",
})
.min(1, {
message: "Volume name is required",
}),
});
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const { data: destinations = [] } = api.destination.all.useQuery();
const form = useForm<z.infer<typeof RestoreBackupSchema>>({
defaultValues: {
destinationId: "",
backupFile: "",
volumeName: "",
},
resolver: zodResolver(RestoreBackupSchema),
});
const destinationId = form.watch("destinationId");
const volumeName = form.watch("volumeName");
const backupFile = form.watch("backupFile");
const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 350);
const handleSearchChange = (value: string) => {
setSearch(value);
debouncedSetSearch(value);
};
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{
destinationId: destinationId,
search: debouncedSearchTerm,
serverId: serverId ?? "",
},
{
enabled: isOpen && !!destinationId,
},
);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription(
{
id,
serviceType: type,
serverId,
destinationId,
volumeName,
backupFileName: backupFile,
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
if (log === "Restore completed successfully!") {
setIsDeploying(false);
}
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Restore logs error:", error);
setIsDeploying(false);
},
},
);
const onSubmit = async () => {
setIsDeploying(true);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">
<RotateCcw className="mr-2 size-4" />
Restore Volume Backup
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center">
<RotateCcw className="mr-2 size-4" />
Restore Volume Backup
</DialogTitle>
<DialogDescription>
Select a destination and search for volume backup files
</DialogDescription>
<AlertBlock>
Make sure the volume name is not being used by another container.
</AlertBlock>
</DialogHeader>
<Form {...form}>
<form
id="hook-form-restore-backup"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="destinationId"
render={({ field }) => (
<FormItem className="">
<FormLabel>Destination</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{field.value
? destinations.find(
(d) => d.destinationId === field.value,
)?.name
: "Select Destination"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search destinations..."
className="h-9"
/>
<CommandEmpty>No destinations found.</CommandEmpty>
<ScrollArea className="h-64">
<CommandGroup>
{destinations.map((destination) => (
<CommandItem
value={destination.destinationId}
key={destination.destinationId}
onSelect={() => {
form.setValue(
"destinationId",
destination.destinationId,
);
}}
>
{destination.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
destination.destinationId === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="backupFile"
render={({ field }) => (
<FormItem className="">
<FormLabel className="flex items-center">
Search Backup Files
{field.value && (
<Badge variant="outline" className="truncate w-52">
{field.value}
<Copy
className="ml-2 size-4 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
copy(field.value);
toast.success("Backup file copied to clipboard");
}}
/>
</Badge>
)}
</FormLabel>
<Popover modal>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
<span className="truncate text-left flex-1 w-52">
{field.value || "Search and select a backup file"}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search backup files..."
value={search}
onValueChange={handleSearchChange}
className="h-9"
/>
{isLoading ? (
<div className="py-6 text-center text-sm">
Loading backup files...
</div>
) : files.length === 0 && search ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No backup files found for "{search}"
</div>
) : files.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No backup files available
</div>
) : (
<ScrollArea className="h-64">
<CommandGroup className="w-96">
{files?.map((file) => (
<CommandItem
value={file.Path}
key={file.Path}
onSelect={() => {
form.setValue("backupFile", file.Path);
if (file.IsDir) {
setSearch(`${file.Path}/`);
setDebouncedSearchTerm(`${file.Path}/`);
} else {
setSearch(file.Path);
setDebouncedSearchTerm(file.Path);
}
}}
>
<div className="flex w-full flex-col gap-1">
<div className="flex w-full justify-between">
<span className="font-medium">
{file.Path}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file.Path === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
Size: {formatBytes(file.Size)}
</span>
{file.IsDir && (
<span className="text-blue-500">
Directory
</span>
)}
{file.Hashes?.MD5 && (
<span>MD5: {file.Hashes.MD5}</span>
)}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
)}
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="volumeName"
render={({ field }) => (
<FormItem>
<FormLabel>Volume Name</FormLabel>
<FormControl>
<Input placeholder="Enter volume name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isDeploying}
form="hook-form-restore-backup"
type="submit"
// disabled={
// !form.watch("backupFile") ||
// (backupType === "compose" && !form.watch("databaseType"))
// }
>
Restore
</Button>
</DialogFooter>
</form>
</Form>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
// refetch();
}}
filteredLogs={filteredLogs}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,250 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import {
ClipboardList,
DatabaseBackup,
Loader2,
Play,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { HandleVolumeBackups } from "./handle-volume-backups";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { RestoreVolumeBackups } from "./restore-volume-backups";
interface Props {
id: string;
type?: "application" | "compose";
serverId?: string;
}
export const ShowVolumeBackups = ({
id,
type = "application",
serverId,
}: Props) => {
const {
data: volumeBackups,
isLoading: isLoadingVolumeBackups,
refetch: refetchVolumeBackups,
} = api.volumeBackups.list.useQuery(
{
id: id || "",
volumeBackupType: type,
},
{
enabled: !!id,
},
);
const utils = api.useUtils();
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
api.volumeBackups.delete.useMutation();
const { mutateAsync: runManually, isLoading } =
api.volumeBackups.runManually.useMutation();
return (
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
<CardHeader className="px-0">
<div className="flex justify-between items-center">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl font-bold flex items-center gap-2">
Volume Backups
</CardTitle>
<CardDescription>
Schedule volume backups to run automatically at specified
intervals.
</CardDescription>
</div>
<div className="flex items-center gap-2">
{volumeBackups && volumeBackups.length > 0 && (
<>
<HandleVolumeBackups id={id} volumeBackupType={type} />
<div className="flex items-center gap-2">
<RestoreVolumeBackups
id={id}
type={type}
serverId={serverId}
/>
</div>
</>
)}
</div>
</div>
</CardHeader>
<CardContent className="px-0">
{isLoadingVolumeBackups ? (
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
<span className="text-sm text-muted-foreground/70">
Loading volume backups...
</span>
</div>
) : volumeBackups && volumeBackups.length > 0 ? (
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
{volumeBackups.map((volumeBackup) => {
const serverId =
volumeBackup.application?.serverId ||
volumeBackup.postgres?.serverId ||
volumeBackup.mysql?.serverId ||
volumeBackup.mariadb?.serverId ||
volumeBackup.mongo?.serverId ||
volumeBackup.redis?.serverId ||
volumeBackup.compose?.serverId;
return (
<div
key={volumeBackup.volumeBackupId}
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<DatabaseBackup className="size-4 text-primary/70" />
</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium leading-none">
{volumeBackup.name}
</h3>
<Badge
variant={
volumeBackup.enabled ? "default" : "secondary"
}
className="text-[10px] px-1 py-0"
>
{volumeBackup.enabled ? "Enabled" : "Disabled"}
</Badge>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge
variant="outline"
className="font-mono text-[10px] bg-transparent"
>
Cron: {volumeBackup.cronExpression}
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-1.5">
<ShowDeploymentsModal
id={volumeBackup.volumeBackupId}
type="volumeBackup"
serverId={serverId || undefined}
>
<Button variant="ghost" size="icon">
<ClipboardList className="size-4 transition-colors " />
</Button>
</ShowDeploymentsModal>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
isLoading={isLoading}
onClick={async () => {
toast.success("Volume backup run successfully");
await runManually({
volumeBackupId: volumeBackup.volumeBackupId,
})
.then(async () => {
await new Promise((resolve) =>
setTimeout(resolve, 1500),
);
refetchVolumeBackups();
})
.catch(() => {
toast.error("Error running volume backup");
});
}}
>
<Play className="size-4 transition-colors" />
</Button>
</TooltipTrigger>
<TooltipContent>
Run Manual Volume Backup
</TooltipContent>
</Tooltip>
</TooltipProvider>
<HandleVolumeBackups
volumeBackupId={volumeBackup.volumeBackupId}
id={id}
volumeBackupType={type}
/>
<DialogAction
title="Delete Volume Backup"
description="Are you sure you want to delete this volume backup?"
type="destructive"
onClick={async () => {
await deleteVolumeBackup({
volumeBackupId: volumeBackup.volumeBackupId,
})
.then(() => {
utils.volumeBackups.list.invalidate({
id,
volumeBackupType: type,
});
toast.success("Volume backup deleted successfully");
})
.catch(() => {
toast.error("Error deleting volume backup");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isDeleting}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
);
})}
</div>
) : (
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
<p className="text-lg font-medium text-muted-foreground">
No volume backups
</p>
<p className="text-sm text-muted-foreground mt-1">
Create your first volume backup to automate your workflows
</p>
<div className="flex items-center gap-2">
<HandleVolumeBackups id={id} volumeBackupType={type} />
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
</div>
</div>
)}
</CardContent>
</Card>
);
};

View File

@@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
composeId,
});
})
.catch((_e) => {
.catch(() => {
toast.error("Error updating the Compose config");
});
};

View File

@@ -280,7 +280,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.name}
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -301,7 +301,8 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.url ===
field.value.gitlabPathNamespace
? "opacity-100"
: "opacity-0",
)}

View File

@@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
.then(() => {
refetch();
})
.catch((_err) => {});
.catch(() => {});
}
}, [isOpen]);

View File

@@ -199,7 +199,7 @@ const RestoreBackupSchema = z
}
});
const formatBytes = (bytes: number): string => {
export const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
@@ -415,7 +415,7 @@ export const RestoreBackup = ({
<FormLabel className="flex items-center justify-between">
Search Backup Files
{field.value && (
<Badge variant="outline">
<Badge variant="outline" className="truncate">
{field.value}
<Copy
className="ml-2 size-4 cursor-pointer"
@@ -439,7 +439,9 @@ export const RestoreBackup = ({
!field.value && "text-muted-foreground",
)}
>
{field.value || "Search and select a backup file"}
<span className="truncate text-left flex-1 w-52">
{field.value || "Search and select a backup file"}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>

View File

@@ -103,7 +103,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
projectId,
});
})
.catch((_e) => {
.catch(() => {
toast.error("Error creating the service");
});
};

View File

@@ -20,7 +20,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
Show Swarm Nodes
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
<DialogContent className="min-w-[70vw] overflow-y-auto max-h-screen">
<div className="grid w-full gap-1">
<ShowNodes serverId={serverId} />
</div>

View File

@@ -87,7 +87,7 @@ export const ShowNodes = ({ serverId }: Props) => {
</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Hostname</TableHead>
<TableHead className="text-left">Hostname</TableHead>
<TableHead className="text-right">Status</TableHead>
<TableHead className="text-right">Role</TableHead>
<TableHead className="text-right">Availability</TableHead>
@@ -104,7 +104,7 @@ export const ShowNodes = ({ serverId }: Props) => {
const isManager = node.Spec.Role === "manager";
return (
<TableRow key={node.ID}>
<TableCell className="w-[100px]">
<TableCell className="text-left">
{node.Description.Hostname}
</TableCell>
<TableCell className="text-right">

View File

@@ -1063,7 +1063,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
});
}
toast.success("Connection Success");
} catch (_err) {
} catch {
toast.error("Error testing the provider");
}
}}

View File

@@ -63,7 +63,7 @@ export const Disable2FA = () => {
toast.success("2FA disabled successfully");
utils.user.get.invalidate();
setIsOpen(false);
} catch (_error) {
} catch {
form.setError("password", {
message: "Connection error. Please try again.",
});

View File

@@ -36,7 +36,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
await refetch();
}
toast.success("Docker Cleanup updated");
} catch (_error) {
} catch {
toast.error("Docker Cleanup Error");
}
};

View File

@@ -56,7 +56,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
try {
await utils.settings.checkGPUStatus.invalidate({ serverId });
await refetch();
} catch (_error) {
} catch {
toast.error("Failed to refresh GPU status");
} finally {
setIsRefreshing(false);
@@ -74,7 +74,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
try {
await setupGPU.mutateAsync({ serverId });
} catch (_error) {
} catch {
// Error handling is done in mutation's onError
}
};

View File

@@ -141,7 +141,7 @@ export const ShowServers = () => {
</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Name</TableHead>
<TableHead className="text-left">Name</TableHead>
{isCloud && (
<TableHead className="text-center">
Status
@@ -173,7 +173,7 @@ export const ShowServers = () => {
const isActive = server.serverStatus === "active";
return (
<TableRow key={server.serverId}>
<TableCell className="w-[100px]">
<TableCell className="text-left">
{server.name}
</TableCell>
{isCloud && (

View File

@@ -146,7 +146,7 @@ export const ShowInvitations = () => {
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(_e) => {
onSelect={() => {
copy(
`${origin}/invitation?token=${invitation.id}`,
);
@@ -162,7 +162,7 @@ export const ShowInvitations = () => {
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
onSelect={async () => {
const result =
await authClient.organization.cancelInvitation(
{
@@ -189,7 +189,7 @@ export const ShowInvitations = () => {
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
onSelect={async () => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {

View File

@@ -91,7 +91,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
});
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
setOpen(false);
} catch (_error) {}
} catch {}
};
return (

View File

@@ -87,7 +87,7 @@ export const ShowNodeApplications = ({ serverId }: Props) => {
Services
</Button>
</DialogTrigger>
<DialogContent className={"sm:max-w-6xl overflow-y-auto max-h-screen"}>
<DialogContent className={"sm:max-w-10xl overflow-y-auto max-h-screen"}>
<DialogHeader>
<DialogTitle>Node Applications</DialogTitle>
<DialogDescription>

View File

@@ -4,7 +4,7 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {

View File

@@ -246,7 +246,9 @@ const Leaf = React.forwardRef<
aria-hidden="true"
/>
)}
<p className=" text-sm whitespace-normal font-mono">{item.name}</p>
<p className=" text-sm whitespace-normal font-mono text-left">
{item.name}
</p>
</button>
);
});

View File

@@ -17,7 +17,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 w-full rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}

View File

@@ -0,0 +1 @@
ALTER TABLE "deployment" ADD COLUMN "pid" text;

View File

@@ -0,0 +1,2 @@
CREATE TYPE "public"."publishModeType" AS ENUM('ingress', 'host');--> statement-breakpoint
ALTER TABLE "port" ADD COLUMN "publishMode" "publishModeType" DEFAULT 'host' NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "domain" ADD COLUMN "internalPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "stripPath" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1,33 @@
CREATE TABLE "volume_backup" (
"volumeBackupId" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"volumeName" text NOT NULL,
"prefix" text NOT NULL,
"serviceType" "serviceType" DEFAULT 'application' NOT NULL,
"appName" text NOT NULL,
"serviceName" text,
"turnOff" boolean DEFAULT false NOT NULL,
"cronExpression" text NOT NULL,
"keepLatestCount" integer,
"enabled" boolean,
"applicationId" text,
"postgresId" text,
"mariadbId" text,
"mongoId" text,
"mysqlId" text,
"redisId" text,
"composeId" text,
"createdAt" text NOT NULL,
"destinationId" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "volumeBackupId" text;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_postgresId_postgres_postgresId_fk" FOREIGN KEY ("postgresId") REFERENCES "public"."postgres"("postgresId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_mariadbId_mariadb_mariadbId_fk" FOREIGN KEY ("mariadbId") REFERENCES "public"."mariadb"("mariadbId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_mongoId_mongo_mongoId_fk" FOREIGN KEY ("mongoId") REFERENCES "public"."mongo"("mongoId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_mysqlId_mysql_mysqlId_fk" FOREIGN KEY ("mysqlId") REFERENCES "public"."mysql"("mysqlId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_redisId_redis_redisId_fk" FOREIGN KEY ("redisId") REFERENCES "public"."redis"("redisId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_destinationId_destination_destinationId_fk" FOREIGN KEY ("destinationId") REFERENCES "public"."destination"("destinationId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_volumeBackupId_volume_backup_volumeBackupId_fk" FOREIGN KEY ("volumeBackupId") REFERENCES "public"."volume_backup"("volumeBackupId") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "compose" ADD COLUMN "isolatedDeploymentsVolume" boolean DEFAULT false NOT NULL;
UPDATE "compose" SET "isolatedDeploymentsVolume" = true;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -687,6 +687,41 @@
"when": 1750567641441,
"tag": "0097_hard_lizard",
"breakpoints": true
},
{
"idx": 98,
"version": "7",
"when": 1751233265357,
"tag": "0098_conscious_chat",
"breakpoints": true
},
{
"idx": 99,
"version": "7",
"when": 1751693569786,
"tag": "0099_wise_golden_guardian",
"breakpoints": true
},
{
"idx": 100,
"version": "7",
"when": 1751741736144,
"tag": "0100_purple_rogue",
"breakpoints": true
},
{
"idx": 101,
"version": "7",
"when": 1751751631943,
"tag": "0101_moaning_blazing_skull",
"breakpoints": true
},
{
"idx": 102,
"version": "7",
"when": 1751848685503,
"tag": "0102_opposite_grandmaster",
"breakpoints": true
}
]
}

View File

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

View File

@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -390,7 +390,7 @@ const Project = (
break;
}
success++;
} catch (_error) {
} catch {
toast.error(`Error starting service ${serviceId}`);
}
}
@@ -437,7 +437,7 @@ const Project = (
break;
}
success++;
} catch (_error) {
} catch {
toast.error(`Error stopping service ${serviceId}`);
}
}
@@ -1107,7 +1107,7 @@ export async function getServerSideProps(
projectId: params?.projectId,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -14,6 +14,7 @@ import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
@@ -61,7 +62,8 @@ type TabState =
| "deployments"
| "domains"
| "monitoring"
| "preview-deployments";
| "preview-deployments"
| "volume-backups";
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
@@ -234,6 +236,9 @@ const Service = (
Preview Deployments
</TabsTrigger>
<TabsTrigger value="schedules">Schedules</TabsTrigger>
<TabsTrigger value="volume-backups">
Volume Backups
</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
@@ -328,6 +333,15 @@ const Service = (
/>
</div>
</TabsContent>
<TabsContent value="volume-backups" className="w-full pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg">
<ShowVolumeBackups
id={applicationId}
type="application"
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="preview-deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPreviewDeployments applicationId={applicationId} />
@@ -413,7 +427,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -4,6 +4,7 @@ import { ShowDeployments } from "@/components/dashboard/application/deployments/
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
@@ -57,7 +58,8 @@ type TabState =
| "advanced"
| "deployments"
| "domains"
| "monitoring";
| "monitoring"
| "volumeBackups";
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
@@ -214,10 +216,10 @@ const Service = (
className={cn(
"xl:grid xl:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "xl:grid-cols-9"
? "xl:grid-cols-10"
: data?.serverId
? "xl:grid-cols-8"
: "xl:grid-cols-9",
? "xl:grid-cols-9"
: "xl:grid-cols-10",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
@@ -226,6 +228,9 @@ const Service = (
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="schedules">Schedules</TabsTrigger>
<TabsTrigger value="volumeBackups">
Volume Backups
</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
@@ -255,7 +260,15 @@ const Service = (
<ShowSchedules id={composeId} scheduleType="compose" />
</div>
</TabsContent>
<TabsContent value="volumeBackups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowVolumeBackups
id={composeId}
type="compose"
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col border rounded-lg ">
@@ -342,6 +355,7 @@ const Service = (
<ShowDomains id={composeId} type="compose" />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
@@ -409,7 +423,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -338,7 +338,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -340,7 +340,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -324,7 +324,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -321,7 +321,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -328,7 +328,7 @@ export async function getServerSideProps(
activeTab: (activeTab || "general") as TabState,
},
};
} catch (_error) {
} catch {
return {
redirect: {
permanent: false,

View File

@@ -68,7 +68,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -69,7 +69,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -72,7 +72,7 @@ export async function getServerSideProps(
trpcState: helpers.dehydrate(),
},
};
} catch (_error) {
} catch {
return {
props: {},
};

View File

@@ -96,7 +96,7 @@ export default function Home({ IS_CLOUD }: Props) {
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while logging in");
} finally {
setIsLoginLoading(false);
@@ -124,7 +124,7 @@ export default function Home({ IS_CLOUD }: Props) {
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while verifying 2FA code");
} finally {
setIsTwoFactorLoading(false);
@@ -154,7 +154,7 @@ export default function Home({ IS_CLOUD }: Props) {
toast.success("Logged in successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while verifying backup code");
} finally {
setIsBackupCodeLoading(false);
@@ -478,7 +478,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
};
}
} catch (_error) {}
} catch {}
return {
props: {

View File

@@ -133,7 +133,7 @@ const Invitation = ({
toast.success("Account created successfully");
router.push("/dashboard/projects");
} catch (_error) {
} catch {
toast.error("An error occurred while creating your account");
}
};

View File

@@ -4,24 +4,24 @@ import { users_temp } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
(async () => {
try {
const result = await findAdmin();
try {
const result = await findAdmin();
const update = await db
.update(users_temp)
.set({
twoFactorEnabled: false,
})
.where(eq(users_temp.id, result.userId));
const update = await db
.update(users_temp)
.set({
twoFactorEnabled: false,
})
.where(eq(users_temp.id, result.userId));
if (update) {
console.log("2FA reset successful");
} else {
console.log("Password reset failed");
}
if (update) {
console.log("2FA reset successful");
} else {
console.log("Password reset failed");
}
process.exit(0);
} catch (error) {
console.log("Error resetting 2FA", error);
}
process.exit(0);
} catch (error) {
console.log("Error resetting 2FA", error);
}
})();

View File

@@ -37,6 +37,7 @@ import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
import { volumeBackupsRouter } from "./routers/volume-backups";
/**
* This is the primary router for your server.
*
@@ -82,6 +83,7 @@ export const appRouter = createTRPCRouter({
organization: organizationRouter,
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
});
// export type definition of API

View File

@@ -184,12 +184,6 @@ export const applicationRouter = createTRPCRouter({
});
}
if (application.serverId) {
await stopServiceRemote(application.serverId, input.appName);
} else {
await stopService(input.appName);
}
await updateApplicationStatus(input.applicationId, "idle");
await mechanizeDockerContainer(application);
await updateApplicationStatus(input.applicationId, "done");

View File

@@ -32,6 +32,7 @@ import {
findProjectById,
findServerById,
findUserById,
getComposeContainer,
loadServices,
randomizeComposeFile,
randomizeIsolatedDeploymentComposeFile,
@@ -241,6 +242,27 @@ export const composeRouter = createTRPCRouter({
}
return await loadServices(input.composeId, input.type);
}),
loadMountsByService: protectedProcedure
.input(
z.object({
composeId: z.string().min(1),
serviceName: z.string().min(1),
}),
)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
});
}
const container = await getComposeContainer(compose, input.serviceName);
const mounts = container?.Mounts.filter(
(mount) => mount.Type === "volume" && mount.Source !== "",
);
return mounts;
}),
fetchSourceType: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {

View File

@@ -7,16 +7,21 @@ import {
deployments,
} from "@/server/db/schema";
import {
execAsync,
execAsyncRemote,
findAllDeploymentsByApplicationId,
findAllDeploymentsByComposeId,
findAllDeploymentsByServerId,
findApplicationById,
findComposeById,
findDeploymentById,
findServerById,
updateDeploymentStatus,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure
@@ -72,4 +77,30 @@ export const deploymentRouter = createTRPCRouter({
return deploymentsList;
}),
killProcess: protectedProcedure
.input(
z.object({
deploymentId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
const deployment = await findDeploymentById(input.deploymentId);
if (!deployment.pid) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Deployment is not running",
});
}
const command = `kill -9 ${deployment.pid}`;
if (deployment.schedule?.serverId) {
await execAsyncRemote(deployment.schedule.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
}),
});

View File

@@ -1,5 +1,6 @@
import {
containerRestart,
findServerById,
getConfig,
getContainers,
getContainersByAppLabel,
@@ -9,6 +10,9 @@ import {
} from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure
@@ -17,14 +21,23 @@ export const dockerRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getContainers(input.serverId);
}),
restartContainer: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
}),
)
.mutation(async ({ input }) => {
@@ -34,11 +47,20 @@ export const dockerRouter = createTRPCRouter({
getConfig: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getConfig(input.containerId, input.serverId);
}),
@@ -48,11 +70,17 @@ export const dockerRouter = createTRPCRouter({
appType: z
.union([z.literal("stack"), z.literal("docker-compose")])
.optional(),
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getContainersByAppNameMatch(
input.appName,
input.appType,
@@ -63,12 +91,18 @@ export const dockerRouter = createTRPCRouter({
getContainersByAppLabel: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
type: z.enum(["standalone", "swarm"]),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getContainersByAppLabel(
input.appName,
input.type,
@@ -79,22 +113,34 @@ export const dockerRouter = createTRPCRouter({
getStackContainersByAppName: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getStackContainersByAppName(input.appName, input.serverId);
}),
getServiceContainersByAppName: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getServiceContainersByAppName(input.appName, input.serverId);
}),
});

View File

@@ -7,10 +7,13 @@ import {
import {
createMount,
deleteMount,
findApplicationById,
findMountById,
getServiceContainer,
updateMount,
} from "@dokploy/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const mountRouter = createTRPCRouter({
create: protectedProcedure
@@ -33,4 +36,14 @@ export const mountRouter = createTRPCRouter({
.mutation(async ({ input }) => {
return await updateMount(input.mountId, input);
}),
allNamedByApplicationId: protectedProcedure
.input(z.object({ applicationId: z.string().min(1) }))
.query(async ({ input }) => {
const app = await findApplicationById(input.applicationId);
const container = await getServiceContainer(app.appName, app.serverId);
const mounts = container?.Mounts.filter(
(mount) => mount.Type === "volume" && mount.Source !== "",
);
return mounts;
}),
});

View File

@@ -459,6 +459,15 @@ export const settingsRouter = createTRPCRouter({
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async ({ ctx }) => {
@@ -600,14 +609,14 @@ export const settingsRouter = createTRPCRouter({
},
})
.input(apiReadStatsLogs)
.query(({ input }) => {
.query(async ({ input }) => {
if (IS_CLOUD) {
return {
data: [],
totalCount: 0,
};
}
const rawConfig = readMonitoringConfig(
const rawConfig = await readMonitoringConfig(
!!input.dateRange?.start && !!input.dateRange?.end,
);
@@ -643,11 +652,11 @@ export const settingsRouter = createTRPCRouter({
})
.optional(),
)
.query(({ input }) => {
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
const rawConfig = readMonitoringConfig(
const rawConfig = await readMonitoringConfig(
!!input?.dateRange?.start || !!input?.dateRange?.end,
);
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
@@ -837,6 +846,14 @@ export const settingsRouter = createTRPCRouter({
getLogCleanupStatus: adminProcedure.query(async () => {
return getLogCleanupStatus();
}),
getDokployCloudIps: adminProcedure.query(async () => {
if (!IS_CLOUD) {
return [];
}
const ips = process.env.DOKPLOY_CLOUD_IPS?.split(",");
return ips;
}),
});
export const getTraefikPorts = async (serverId?: string) => {

View File

@@ -6,6 +6,9 @@ import {
} from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { findServerById } from "@dokploy/server";
import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({
getNodes: protectedProcedure
@@ -14,12 +17,24 @@ export const swarmRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getSwarmNodes(input.serverId);
}),
getNodeInfo: protectedProcedure
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getNodeInfo(input.nodeId, input.serverId);
}),
getNodeApps: protectedProcedure
@@ -28,17 +43,29 @@ export const swarmRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return getNodeApplications(input.serverId);
}),
getAppInfos: protectedProcedure
.input(
z.object({
appName: z.string(),
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return await getApplicationInfo(input.appName, input.serverId);
}),
});

View File

@@ -75,6 +75,24 @@ export const userRouter = createTRPCRouter({
},
});
// If user not found in the organization, deny access
if (!memberResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found in this organization",
});
}
// Allow access if:
// 1. User is requesting their own information
// 2. User has owner role (admin permissions) AND user is in the same organization
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this user",
});
}
return memberResult;
}),
get: protectedProcedure.query(async ({ ctx }) => {

View File

@@ -0,0 +1,218 @@
import {
IS_CLOUD,
updateVolumeBackup,
removeVolumeBackup,
createVolumeBackup,
runVolumeBackup,
findVolumeBackupById,
restoreVolume,
scheduleVolumeBackup,
removeVolumeBackupJob,
} from "@dokploy/server";
import {
createVolumeBackupSchema,
updateVolumeBackupSchema,
volumeBackups,
} from "@dokploy/server/db/schema";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@dokploy/server/db";
import { eq } from "drizzle-orm";
import { observable } from "@trpc/server/observable";
import {
execAsyncRemote,
execAsyncStream,
} from "@dokploy/server/utils/process/execAsync";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { TRPCError } from "@trpc/server";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
id: z.string().min(1),
volumeBackupType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
}),
)
.query(async ({ input }) => {
return await db.query.volumeBackups.findMany({
where: eq(volumeBackups[`${input.volumeBackupType}Id`], input.id),
with: {
application: true,
postgres: true,
mysql: true,
mariadb: true,
mongo: true,
redis: true,
compose: true,
},
});
}),
create: protectedProcedure
.input(createVolumeBackupSchema)
.mutation(async ({ input }) => {
const newVolumeBackup = await createVolumeBackup(input);
if (newVolumeBackup?.enabled) {
if (IS_CLOUD) {
await schedule({
cronSchedule: newVolumeBackup.cronExpression,
volumeBackupId: newVolumeBackup.volumeBackupId,
type: "volume-backup",
});
} else {
await scheduleVolumeBackup(newVolumeBackup.volumeBackupId);
}
}
return newVolumeBackup;
}),
one: protectedProcedure
.input(
z.object({
volumeBackupId: z.string().min(1),
}),
)
.query(async ({ input }) => {
return await findVolumeBackupById(input.volumeBackupId);
}),
delete: protectedProcedure
.input(
z.object({
volumeBackupId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
return await removeVolumeBackup(input.volumeBackupId);
}),
update: protectedProcedure
.input(updateVolumeBackupSchema)
.mutation(async ({ input }) => {
const updatedVolumeBackup = await updateVolumeBackup(
input.volumeBackupId,
input,
);
if (!updatedVolumeBackup) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Volume backup not found",
});
}
if (IS_CLOUD) {
if (updatedVolumeBackup.enabled) {
await updateJob({
cronSchedule: updatedVolumeBackup.cronExpression,
volumeBackupId: updatedVolumeBackup.volumeBackupId,
type: "volume-backup",
});
} else {
await removeJob({
cronSchedule: updatedVolumeBackup.cronExpression,
volumeBackupId: updatedVolumeBackup.volumeBackupId,
type: "volume-backup",
});
}
} else {
if (updatedVolumeBackup?.enabled) {
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
scheduleVolumeBackup(updatedVolumeBackup.volumeBackupId);
} else {
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
}
}
return updatedVolumeBackup;
}),
runManually: protectedProcedure
.input(z.object({ volumeBackupId: z.string().min(1) }))
.mutation(async ({ input }) => {
try {
return await runVolumeBackup(input.volumeBackupId);
} catch (error) {
console.error(error);
return false;
}
}),
restoreVolumeBackupWithLogs: protectedProcedure
.meta({
openapi: {
enabled: false,
path: "/restore-volume-backup-with-logs",
method: "POST",
override: true,
},
})
.input(
z.object({
backupFileName: z.string().min(1),
destinationId: z.string().min(1),
volumeName: z.string().min(1),
id: z.string().min(1),
serviceType: z.enum(["application", "compose"]),
serverId: z.string().optional(),
}),
)
.subscription(async ({ input }) => {
return observable<string>((emit) => {
const runRestore = async () => {
try {
emit.next("🚀 Starting volume restore process...");
emit.next(`📂 Backup File: ${input.backupFileName}`);
emit.next(`🔧 Volume Name: ${input.volumeName}`);
emit.next(`🏷️ Service Type: ${input.serviceType}`);
emit.next(""); // Empty line for better readability
// Generate the restore command
const restoreCommand = await restoreVolume(
input.id,
input.destinationId,
input.volumeName,
input.backupFileName,
input.serverId || "",
input.serviceType,
);
emit.next("📋 Generated restore command:");
emit.next("▶️ Executing restore...");
emit.next(""); // Empty line
// Execute the restore command with real-time output
if (input.serverId) {
emit.next(`🌐 Executing on remote server: ${input.serverId}`);
await execAsyncRemote(input.serverId, restoreCommand, (data) => {
emit.next(data);
});
} else {
emit.next("🖥️ Executing on local server");
await execAsyncStream(restoreCommand, (data) => {
emit.next(data);
});
}
emit.next("");
emit.next("✅ Volume restore completed successfully!");
emit.next(
"🎉 All containers/services have been restarted with the restored volume.",
);
} catch {
emit.next("");
emit.next("❌ Volume restore failed!");
} finally {
emit.complete();
}
};
// Start the restore process
runRestore();
});
}),
});

View File

@@ -7,6 +7,7 @@ import {
createDefaultTraefikConfig,
initCronJobs,
initSchedules,
initVolumeBackupsCronJobs,
initializeNetwork,
sendDokployRestartNotifications,
setupDirectories,
@@ -51,6 +52,7 @@ void app.prepare().then(async () => {
await migration();
await initCronJobs();
await initSchedules();
await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications();
}

View File

@@ -19,6 +19,11 @@ type QueueJob =
type: "schedule";
cronSchedule: string;
scheduleId: string;
}
| {
type: "volume-backup";
cronSchedule: string;
volumeBackupId: string;
};
export const schedule = async (job: QueueJob) => {
try {

View File

@@ -6,7 +6,7 @@ export const isWSL = async () => {
const { stdout } = await execAsync("uname -r");
const isWSL = stdout.includes("microsoft");
return isWSL;
} catch (_error) {
} catch {
return false;
}
};

View File

@@ -101,7 +101,7 @@ export const setupDeploymentLogsWebSocketServer = (
ws.close();
});
}
} catch (_error) {
} catch {
// @ts-ignore
// const errorMessage = error?.message as unknown as string;
// ws.send(errorMessage);

View File

@@ -12,6 +12,7 @@ import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/setup/setup";
import { execAsync } from "@dokploy/server";
(async () => {
try {
setupDirectories();
@@ -20,6 +21,7 @@ import {
await initializeNetwork();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.1.2");
await initializeTraefik();
await initializeRedis();
await initializePostgres();

View File

@@ -242,3 +242,8 @@
background-color: var(--terminal-paste) !important;
color: currentColor !important;
}
.cm-content,
.cm-lineWrapping {
@apply font-mono;
}

View File

@@ -30,12 +30,41 @@ const getWsUrl = () => {
return `${protocol}${host}/drawer-logs`;
};
const wsClient =
typeof window !== "undefined"
? createWSClient({
url: getWsUrl() || "",
})
: null;
// Create WebSocket client with delayed connection
const createLazyWSClient = () => {
if (typeof window === "undefined") return null;
let actualClient: ReturnType<typeof createWSClient> | null = null;
return {
request: (op: any, callbacks: any) => {
if (!actualClient) {
const wsUrl = getWsUrl();
if (wsUrl) {
actualClient = createWSClient({ url: wsUrl });
}
}
return actualClient?.request(op, callbacks) || (() => {});
},
close: () => {
if (actualClient) {
actualClient.close();
actualClient = null;
}
},
getConnection: () => {
if (!actualClient) {
const wsUrl = getWsUrl();
if (wsUrl) {
actualClient = createWSClient({ url: wsUrl });
}
}
return actualClient!.getConnection();
},
};
};
const wsClient = createLazyWSClient();
/** A set of type-safe react-query hooks for your tRPC API. */
export const api = createTRPCNext<AppRouter>({

View File

@@ -0,0 +1,21 @@
MIT License
Copyright 2025 Mauricio Siu.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -61,6 +61,12 @@ app.post("/update-backup", zValidator("json", jobQueueSchema), async (c) => {
type: "schedule",
cronSchedule: job.pattern,
});
} else if (data.type === "volume-backup") {
result = await removeJob({
volumeBackupId: data.volumeBackupId,
type: "volume-backup",
cronSchedule: job.pattern,
});
}
logger.info({ result }, "Job removed");
}

View File

@@ -42,6 +42,12 @@ export const scheduleJob = (job: QueueJob) => {
pattern: job.cronSchedule,
},
});
} else if (job.type === "volume-backup") {
jobQueue.add(job.volumeBackupId, job, {
repeat: {
pattern: job.cronSchedule,
},
});
}
};
@@ -67,6 +73,13 @@ export const removeJob = async (data: QueueJob) => {
});
return result;
}
if (data.type === "volume-backup") {
const { volumeBackupId, cronSchedule } = data;
const result = await jobQueue.removeRepeatable(volumeBackupId, {
pattern: cronSchedule,
});
return result;
}
return false;
};
@@ -89,6 +102,10 @@ export const getJobRepeatable = async (
const job = repeatableJobs.find((j) => j.name === scheduleId);
return job ? job : null;
}
if (data.type === "volume-backup") {
const { volumeBackupId } = data;
const job = repeatableJobs.find((j) => j.name === volumeBackupId);
return job ? job : null;
}
return null;
};

View File

@@ -16,6 +16,11 @@ export const jobQueueSchema = z.discriminatedUnion("type", [
type: z.literal("schedule"),
scheduleId: z.string(),
}),
z.object({
cronSchedule: z.string(),
type: z.literal("volume-backup"),
volumeBackupId: z.string(),
}),
]);
export type QueueJob = z.infer<typeof jobQueueSchema>;

View File

@@ -5,6 +5,7 @@ import {
findBackupById,
findScheduleById,
findServerById,
findVolumeBackupById,
keepLatestNBackups,
runCommand,
runComposeBackup,
@@ -12,9 +13,15 @@ import {
runMongoBackup,
runMySqlBackup,
runPostgresBackup,
runVolumeBackup,
} from "@dokploy/server";
import { db } from "@dokploy/server/dist/db";
import { backups, schedules, server } from "@dokploy/server/dist/db/schema";
import {
backups,
schedules,
server,
volumeBackups,
} from "@dokploy/server/dist/db/schema";
import { and, eq } from "drizzle-orm";
import { logger } from "./logger.js";
import { scheduleJob } from "./queue.js";
@@ -93,6 +100,12 @@ export const runJobs = async (job: QueueJob) => {
if (schedule.enabled) {
await runCommand(schedule.scheduleId);
}
} else if (job.type === "volume-backup") {
const { volumeBackupId } = job;
const volumeBackup = await findVolumeBackupById(volumeBackupId);
if (volumeBackup.enabled) {
await runVolumeBackup(volumeBackupId);
}
}
} catch (error) {
logger.error(error);
@@ -184,4 +197,44 @@ export const initializeJobs = async () => {
{ Quantity: filteredSchedulesBasedOnServerStatus.length },
"Schedules Initialized",
);
const volumeBackupsResult = await db.query.volumeBackups.findMany({
where: eq(volumeBackups.enabled, true),
with: {
application: {
with: {
server: true,
},
},
compose: {
with: {
server: true,
},
},
},
});
const filteredVolumeBackupsBasedOnServerStatus = volumeBackupsResult.filter(
(volumeBackup) => {
if (volumeBackup.application) {
return volumeBackup.application.server?.serverStatus === "active";
}
if (volumeBackup.compose) {
return volumeBackup.compose.server?.serverStatus === "active";
}
},
);
for (const volumeBackup of filteredVolumeBackupsBasedOnServerStatus) {
scheduleJob({
volumeBackupId: volumeBackup.volumeBackupId,
type: "volume-backup",
cronSchedule: volumeBackup.cronExpression,
});
}
logger.info(
{ Quantity: filteredVolumeBackupsBasedOnServerStatus.length },
"Volume Backups Initialized",
);
};

View File

@@ -24,5 +24,6 @@ export const paths = (isServer = false) => {
MONITORING_PATH: `${BASE_PATH}/monitoring`,
REGISTRY_PATH: `${BASE_PATH}/registry`,
SCHEDULES_PATH: `${BASE_PATH}/schedules`,
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
};
};

View File

@@ -79,6 +79,10 @@ export const compose = pgTable("compose", {
suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false),
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
// Keep this for backward compatibility since we will not add the prefix anymore to volumes
isolatedDeploymentsVolume: boolean("isolatedDeploymentsVolume")
.notNull()
.default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId")

View File

@@ -16,6 +16,7 @@ import { previewDeployments } from "./preview-deployments";
import { schedules } from "./schedule";
import { server } from "./server";
import { rollbacks } from "./rollbacks";
import { volumeBackups } from "./volume-backups";
export const deploymentStatus = pgEnum("deploymentStatus", [
"running",
"done",
@@ -31,6 +32,7 @@ export const deployments = pgTable("deployment", {
description: text("description"),
status: deploymentStatus("status").default("running"),
logPath: text("logPath").notNull(),
pid: text("pid"),
applicationId: text("applicationId").references(
() => applications.applicationId,
{ onDelete: "cascade" },
@@ -63,6 +65,10 @@ export const deployments = pgTable("deployment", {
(): AnyPgColumn => rollbacks.rollbackId,
{ onDelete: "cascade" },
),
volumeBackupId: text("volumeBackupId").references(
(): AnyPgColumn => volumeBackups.volumeBackupId,
{ onDelete: "cascade" },
),
});
export const deploymentsRelations = relations(deployments, ({ one }) => ({
@@ -94,6 +100,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
fields: [deployments.deploymentId],
references: [rollbacks.deploymentId],
}),
volumeBackup: one(volumeBackups, {
fields: [deployments.volumeBackupId],
references: [volumeBackups.volumeBackupId],
}),
}));
const schema = createInsertSchema(deployments, {
@@ -178,6 +188,17 @@ export const apiCreateDeploymentSchedule = schema
scheduleId: z.string().min(1),
});
export const apiCreateDeploymentVolumeBackup = schema
.pick({
title: true,
status: true,
logPath: true,
description: true,
})
.extend({
volumeBackupId: z.string().min(1),
});
export const apiFindAllByApplication = schema
.pick({
applicationId: true,
@@ -215,6 +236,7 @@ export const apiFindAllByType = z
"schedule",
"previewDeployment",
"backup",
"volumeBackup",
]),
})
.required();

View File

@@ -51,6 +51,8 @@ export const domains = pgTable("domain", {
{ onDelete: "cascade" },
),
certificateType: certificateType("certificateType").notNull().default("none"),
internalPath: text("internalPath").default("/"),
stripPath: boolean("stripPath").notNull().default(false),
});
export const domainsRelations = relations(domains, ({ one }) => ({
@@ -82,6 +84,8 @@ export const apiCreateDomain = createSchema.pick({
serviceName: true,
domainType: true,
previewDeploymentId: true,
internalPath: true,
stripPath: true,
});
export const apiFindDomain = createSchema
@@ -112,5 +116,7 @@ export const apiUpdateDomain = createSchema
customCertResolver: true,
serviceName: true,
domainType: true,
internalPath: true,
stripPath: true,
})
.merge(createSchema.pick({ domainId: true }).required());

View File

@@ -33,3 +33,4 @@ export * from "./ai";
export * from "./account";
export * from "./schedule";
export * from "./rollbacks";
export * from "./volume-backups";

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { applications } from "./application";
export const protocolType = pgEnum("protocolType", ["tcp", "udp"]);
export const publishModeType = pgEnum("publishModeType", ["ingress", "host"]);
export const ports = pgTable("port", {
portId: text("portId")
@@ -13,6 +14,7 @@ export const ports = pgTable("port", {
.primaryKey()
.$defaultFn(() => nanoid()),
publishedPort: integer("publishedPort").notNull(),
publishMode: publishModeType("publishMode").notNull().default("host"),
targetPort: integer("targetPort").notNull(),
protocol: protocolType("protocol").notNull(),
@@ -32,6 +34,7 @@ const createSchema = createInsertSchema(ports, {
portId: z.string().min(1),
applicationId: z.string().min(1),
publishedPort: z.number(),
publishMode: z.enum(["ingress", "host"]).default("ingress"),
targetPort: z.number(),
protocol: z.enum(["tcp", "udp"]).default("tcp"),
});
@@ -39,6 +42,7 @@ const createSchema = createInsertSchema(ports, {
export const apiCreatePort = createSchema
.pick({
publishedPort: true,
publishMode: true,
targetPort: true,
protocol: true,
applicationId: true,
@@ -55,6 +59,7 @@ export const apiUpdatePort = createSchema
.pick({
portId: true,
publishedPort: true,
publishMode: true,
targetPort: true,
protocol: true,
})

View File

@@ -4,6 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { deployments } from "./deployment";
import type { Application } from "@dokploy/server/services/application";
import type { Project } from "@dokploy/server/services/project";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Registry } from "@dokploy/server/services/registry";
export const rollbacks = pgTable("rollback", {
rollbackId: text("rollbackId")
@@ -20,7 +25,14 @@ export const rollbacks = pgTable("rollback", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext"),
fullContext: jsonb("fullContext").$type<
Application & {
project: Project;
mounts: Mount[];
ports: Port[];
registry?: Registry | null;
}
>(),
});
export type Rollback = typeof rollbacks.$inferSelect;

View File

@@ -15,6 +15,7 @@ import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
import { paths } from "@dokploy/server/constants";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -236,7 +237,31 @@ export const apiModifyTraefikConfig = z.object({
serverId: z.string().optional(),
});
export const apiReadTraefikConfig = z.object({
path: z.string().min(1),
path: z
.string()
.min(1)
.refine(
(path) => {
// Prevent directory traversal attacks
if (path.includes("../") || path.includes("..\\")) {
return false;
}
const { MAIN_TRAEFIK_PATH } = paths();
if (path.startsWith("/") && !path.startsWith(MAIN_TRAEFIK_PATH)) {
return false;
}
// Prevent null bytes and other dangerous characters
if (path.includes("\0") || path.includes("\x00")) {
return false;
}
return true;
},
{
message:
"Invalid path: path traversal or unauthorized directory access detected",
},
),
serverId: z.string().optional(),
});

View File

@@ -0,0 +1,118 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { serviceType } from "./mount";
import { applications } from "./application";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { redis } from "./redis";
import { compose } from "./compose";
import { postgres } from "./postgres";
import { mariadb } from "./mariadb";
import { destinations } from "./destination";
import { deployments } from "./deployment";
import { generateAppName } from "./utils";
export const volumeBackups = pgTable("volume_backup", {
volumeBackupId: text("volumeBackupId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
volumeName: text("volumeName").notNull(),
prefix: text("prefix").notNull(),
serviceType: serviceType("serviceType").notNull().default("application"),
appName: text("appName")
.notNull()
.$defaultFn(() => generateAppName("volumeBackup")),
serviceName: text("serviceName"),
turnOff: boolean("turnOff").notNull().default(false),
cronExpression: text("cronExpression").notNull(),
keepLatestCount: integer("keepLatestCount"),
enabled: boolean("enabled"),
applicationId: text("applicationId").references(
() => applications.applicationId,
{
onDelete: "cascade",
},
),
postgresId: text("postgresId").references(() => postgres.postgresId, {
onDelete: "cascade",
}),
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
onDelete: "cascade",
}),
mongoId: text("mongoId").references(() => mongo.mongoId, {
onDelete: "cascade",
}),
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
onDelete: "cascade",
}),
redisId: text("redisId").references(() => redis.redisId, {
onDelete: "cascade",
}),
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
destinationId: text("destinationId")
.notNull()
.references(() => destinations.destinationId, { onDelete: "cascade" }),
});
export type VolumeBackup = typeof volumeBackups.$inferSelect;
export const volumeBackupsRelations = relations(
volumeBackups,
({ one, many }) => ({
application: one(applications, {
fields: [volumeBackups.applicationId],
references: [applications.applicationId],
}),
postgres: one(postgres, {
fields: [volumeBackups.postgresId],
references: [postgres.postgresId],
}),
mariadb: one(mariadb, {
fields: [volumeBackups.mariadbId],
references: [mariadb.mariadbId],
}),
mongo: one(mongo, {
fields: [volumeBackups.mongoId],
references: [mongo.mongoId],
}),
mysql: one(mysql, {
fields: [volumeBackups.mysqlId],
references: [mysql.mysqlId],
}),
redis: one(redis, {
fields: [volumeBackups.redisId],
references: [redis.redisId],
}),
compose: one(compose, {
fields: [volumeBackups.composeId],
references: [compose.composeId],
}),
destination: one(destinations, {
fields: [volumeBackups.destinationId],
references: [destinations.destinationId],
}),
deployments: many(deployments),
}),
);
export const createVolumeBackupSchema = createInsertSchema(volumeBackups).omit({
volumeBackupId: true,
});
export const updateVolumeBackupSchema = createVolumeBackupSchema.extend({
volumeBackupId: z.string().min(1),
});
export const apiFindOneVolumeBackup = z.object({
volumeBackupId: z.string().min(1),
});

View File

@@ -4,6 +4,8 @@ export const domain = z
.object({
host: z.string().min(1, { message: "Add a hostname" }),
path: z.string().min(1).optional(),
internalPath: z.string().optional(),
stripPath: z.boolean().optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
@@ -29,12 +31,37 @@ export const domain = z
message: "Required when certificate type is custom",
});
}
// Validate stripPath requires a valid path
if (input.stripPath && (!input.path || input.path === "/")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["stripPath"],
message:
"Strip path can only be enabled when a path other than '/' is specified",
});
}
// Validate internalPath starts with /
if (
input.internalPath &&
input.internalPath !== "/" &&
!input.internalPath.startsWith("/")
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["internalPath"],
message: "Internal path must start with '/'",
});
}
});
export const domainCompose = z
.object({
host: z.string().min(1, { message: "Host is required" }),
path: z.string().min(1).optional(),
internalPath: z.string().optional(),
stripPath: z.boolean().optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
@@ -61,4 +88,27 @@ export const domainCompose = z
message: "Required when certificate type is custom",
});
}
// Validate stripPath requires a valid path
if (input.stripPath && (!input.path || input.path === "/")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["stripPath"],
message:
"Strip path can only be enabled when a path other than '/' is specified",
});
}
// Validate internalPath starts with /
if (
input.internalPath &&
input.internalPath !== "/" &&
!input.internalPath.startsWith("/")
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["internalPath"],
message: "Internal path must start with '/'",
});
}
});

View File

@@ -10,6 +10,7 @@ export * from "./services/mysql";
export * from "./services/backup";
export * from "./services/cluster";
export * from "./services/settings";
export * from "./services/volume-backups";
export * from "./services/docker";
export * from "./services/destination";
export * from "./services/deployment";
@@ -132,5 +133,6 @@ export {
export * from "./utils/schedules/utils";
export * from "./utils/schedules/index";
export * from "./utils/volume-backups/index";
export * from "./lib/logger";

Some files were not shown because too many files have changed in this diff Show More