or provider-specific PR refs.
+- *(ApplicationPreview)* Change null check to empty check for fqdn in generate_preview_fqdn method
+- *(email notifications)* Enhance EmailChannel to validate team membership for recipients and handle errors gracefully
+- *(service api)* Separate create and update service functionalities
+- *(templates)* Added a category tag for the docs service filter
+- *(application)* Clear Docker Compose specific data when switching away from dockercompose
+- *(database)* Conditionally set started_at only if the database is running
+- *(ui)* Handle null values in postgres metrics (#6388)
+- Disable env sorting by default
+- *(proxy)* Filter host network from default proxy (#6383)
+- *(modal)* Enhance confirmation text handling
+- *(notification)* Update unread count display and improve HTML rendering
+- *(select)* Remove unnecessary sanitization for logo rendering
+- *(tags)* Update tag display to limit name length and adjust styling
+- *(init)* Improve error handling for deployment and template pulling processes
+- *(settings-dropdown)* Adjust unread count badge size and display logic for better consistency
+- *(sanitization)* Enhance DOMPurify hook to remove Alpine.js directives for improved XSS protection
+- *(servercheck)* Properly check server statuses with and without Sentinel
+- *(errors)* Update error pages to provide navigation options
+- *(github-deploy-key)* Update background color for selected private keys in deployment key selection UI
+- *(auth)* Enhance authorization checks in application management
+
+### 💼 Other
+
+- *(settings-dropdown)* Add icons to buttons for improved UI in settings dropdown
+- *(ui)* Introduce task for simplifying resource operations UI by replacing boxes with dropdown selections to enhance user experience and streamline interactions
+
+### 🚜 Refactor
+
+- *(jobs)* Remove logging for ScheduledJobManager and ServerResourceManager start and completion
+- *(services)* Update validation rules to be optional
+- *(service)* Improve langfuse
+- *(service)* Improve openpanel template
+- *(service)* Improve librechat
+- *(public-git-repository)* Enhance form structure and add autofocus to repository URL input
+- *(public-git-repository)* Remove commented-out code for cleaner template
+- *(templates)* Update service template file handling to use dynamic file name from constants
+- *(parsers)* Streamline domain handling in applicationParser and improve DNS validation logic
+- *(templates)* Replace SERVICE_FQDN variables with SERVICE_URL in compose files for consistency
+- *(links)* Replace inline SVGs with reusable external link component for consistency and improved maintainability
+- *(previews)* Improve layout and add deployment/application logs links for previews
+- *(docker compose)* Remove deprecated newParser function and associated test file to streamline codebase
+- *(shared helpers)* Remove unused parseServiceVolumes function to clean up codebase
+- *(parsers)* Update volume parsing logic to use beforeLast and afterLast for improved accuracy
+- *(validation)* Implement centralized validation patterns across components
+- *(jobs)* Rename job classes to indicate deprecation status
+- Update check frequency logic for cloud and self-hosted environments; streamline server task scheduling and timezone handling
+- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility
+
+### 📚 Documentation
+
+- *(claude)* Clarify that artisan commands should only be run inside the "coolify" container during development
+- Add AGENTS.md for project guidance and development instructions
+- Update changelog
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Improve matrix service
+- *(service)* Format runner service
+- *(service)* Improve sequin
+- *(service)* Add `NOT_SECURED` env to Postiz (#6243)
+- *(service)* Improve evolution-api environment variables (#6283)
+- *(service)* Update Langfuse template to v3 (#6301)
+- *(core)* Remove unused argument
+- *(deletion)* Rename isDeleteOperation to deleteConnectedNetworks
+- *(docker)* Remove unused arguments on StopService
+- *(service)* Homebox formatting
+- Clarify usage of custom redis configuration (#6321)
+- *(changelogs)* Add .gitignore for changelogs directory and remove outdated changelog files for May, June, and July 2025
+- *(service)* Change affine images (#6366)
+- Elasticsearch URL, fromatting and add category
+- Update service-templates json files
+- *(docs)* Remove AGENTS.md file; enhance CLAUDE.md with detailed form authorization patterns and service configuration examples
+- *(cleanup)* Remove unused GitLab view files for change, new, and show pages
+- *(workflows)* Add backlog directory to build triggers for production and staging workflows
+- *(config)* Disable auto_commit in backlog configuration to prevent automatic commits
+- *(versions)* Update coolify version to 4.0.0-beta.420.8 and nightly version to 4.0.0-beta.420.9 in versions.json and constants.php
+- *(docker)* Update soketi image version to 1.0.10 in production and Windows configurations
+
+### ◀️ Revert
+
+- *(parser)* Enhance FQDN generation logic for services and applications
+
+## [4.0.0-beta.420.6] - 2025-07-18
+
+### 🚀 Features
+
+- *(service)* Enable password protection for the Wireguard Ul
+- *(queues)* Improve Horizon config to reduce CPU and RAM usage (#6212)
+- *(service)* Add Gowa service (#6164)
+- *(container)* Add updatedSelectedContainer method to connect to non-default containers and update wire:model for improved reactivity
+- *(application)* Implement environment variable updates for Docker Compose applications, including creation, updating, and deletion of SERVICE_FQDN and SERVICE_URL variables
+
+### 🐛 Bug Fixes
+
+- *(installer)* Public IPv4 link does not work
+- *(composer)* Version constraint of prompts
+- *(service)* Budibase secret keys (#6205)
+- *(service)* Wg-easy host should be just the FQDN
+- *(ui)* Search box overlaps the sidebar navigation (#6176)
+- *(webhooks)* Exclude webhook routes from CSRF protection (#6200)
+- *(services)* Update environment variable naming convention to use underscores instead of dashes for SERVICE_FQDN and SERVICE_URL
+
+### 🚜 Refactor
+
+- *(service)* Improve gowa
+- *(previews)* Streamline preview domain generation logic in ApplicationDeploymentJob for improved clarity and maintainability
+- *(services)* Simplify environment variable updates by using updateOrCreate and add cleanup for removed FQDNs
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Update Nitropage template (#6181)
+- *(versions)* Update all version
+- *(bump)* Update composer deps
+- *(version)* Bump Coolify version to 4.0.0-beta.420.6
+
+## [4.0.0-beta.420.4] - 2025-07-08
+
+### 🚀 Features
+
+- *(scheduling)* Add command to manually run scheduled database backups and tasks with options for chunking, delays, and dry runs
+- *(scheduling)* Add frequency filter option for manual execution of scheduled jobs
+- *(logging)* Implement scheduled logs command and enhance backup/task scheduling with cron checks
+- *(logging)* Add frequency filters for scheduled logs command to support hourly, daily, weekly, and monthly job views
+- *(scheduling)* Introduce ScheduledJobManager and ServerResourceManager for enhanced job scheduling and resource management
+- *(previews)* Implement soft delete and cleanup for ApplicationPreview, enhancing resource management in DeleteResourceJob
+
+### 🐛 Bug Fixes
+
+- *(service)* Update Postiz compose configuration for improved server availability
+- *(install.sh)* Use IPV4_PUBLIC_IP variable in output instead of repeated curl
+- *(env)* Generate literal env variables better
+- *(deployment)* Update x-data initialization in deployment view for improved functionality
+- *(deployment)* Enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility
+- *(deployment)* Improve docker-compose domain handling and environment variable generation
+- *(deployment)* Refactor domain parsing and environment variable generation using Spatie URL library
+- *(deployment)* Update COOLIFY_URL and COOLIFY_FQDN generation to use Spatie URL library for improved accuracy
+- *(scheduling)* Change redis cleanup command frequency from hourly to weekly for better resource management
+- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.5 and 4.0.0-beta.420.6
+- *(database)* Ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy
+- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.6 and 4.0.0-beta.420.7
+- *(scheduling)* Remove unnecessary padding from scheduled task form layout for improved UI consistency
+- *(horizon)* Update queue configuration to use environment variable for dynamic queue management
+- *(horizon)* Add silenced jobs
+- *(application)* Sanitize service names for HTML form binding and ensure original names are stored in docker compose domains
+- *(previews)* Adjust padding for rate limit message in application previews
+- *(previews)* Order application previews by pull request ID in descending order
+- *(previews)* Add unique wire keys for preview containers and services based on pull request ID
+- *(previews)* Enhance domain generation logic for application previews, ensuring unique domains are created when none are set
+- *(previews)* Refine preview domain generation for Docker Compose applications, ensuring correct method usage based on build pack type
+- *(ui)* Typo on proxy request handler tooltip (#6192)
+- *(backups)* Large database backups are not working (#6217)
+- *(backups)* Error message if there is no exception
+
+### 🚜 Refactor
+
+- *(previews)* Streamline preview URL generation by utilizing application method
+- *(application)* Adjust layout and spacing in general application view for improved UI
+- *(postgresql)* Improve layout and spacing in SSL and Proxy configuration sections for better UI consistency
+- *(scheduling)* Replace deprecated job checks with ScheduledJobManager and ServerResourceManager for improved scheduling efficiency
+- *(previews)* Move preview domain generation logic to ApplicationPreview model for better encapsulation and consistency across webhook handlers
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+## [4.0.0-beta.420.3] - 2025-07-03
+
+### 📚 Documentation
+
+- Update changelog
+
+## [4.0.0-beta.420.2] - 2025-07-03
+
+### 🚀 Features
+
+- *(template)* Added excalidraw (#6095)
+- *(template)* Add excalidraw service configuration with documentation and tags
+
+### 🐛 Bug Fixes
+
+- *(terminal)* Ensure shell execution only uses valid shell if available in terminal command
+- *(ui)* Improve destination selection description for clarity in resource segregation
+- *(jobs)* Update middleware to use expireAfter for WithoutOverlapping in multiple job classes
+- Removing eager loading (#6071)
+- *(template)* Adjust health check interval and retries for excalidraw service
+- *(ui)* Env variable settings wrong order
+- *(service)* Ensure configuration changes are properly tracked and dispatched
+
+### 🚜 Refactor
+
+- *(ui)* Enhance project cloning interface with improved table layout for server and resource selection
+- *(terminal)* Simplify command construction for SSH execution
+- *(settings)* Streamline instance admin checks and initialization of settings in Livewire components
+- *(policy)* Optimize team membership checks in S3StoragePolicy
+- *(popup)* Improve styling and structure of the small popup component
+- *(shared)* Enhance FQDN generation logic for services in newParser function
+- *(redis)* Enhance CleanupRedis command with dry-run option and improved key deletion logic
+- *(init)* Standardize method naming conventions and improve command structure in Init.php
+- *(shared)* Improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml
+- *(database)* Improve error handling for unsupported database types in StartDatabaseProxy
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively
+- *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively
+
+## [4.0.0-beta.420.1] - 2025-06-26
+
+### 🐛 Bug Fixes
+
+- *(server)* Prepend 'mux_' to UUID in muxFilename method for consistent naming
+- *(ui)* Enhance terminal access messaging to clarify server functionality and terminal status
+- *(database)* Proxy ssl port if ssl is enabled
+
+### 🚜 Refactor
+
+- *(ui)* Separate views for instance settings to separate paths to make it cleaner
+- *(ui)* Remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files
+
+## [4.0.0-beta.420] - 2025-06-26
+
+### 🚀 Features
+
+- *(service)* Add Miniflux service (#5843)
+- *(service)* Add Pingvin Share service (#5969)
+- *(auth)* Add Discord OAuth Provider (#5552)
+- *(auth)* Add Clerk OAuth Provider (#5553)
+- *(auth)* Add Zitadel OAuth Provider (#5490)
+- *(core)* Set custom API rate limit (#5984)
+- *(service)* Enhance service status handling and UI updates
+- *(cleanup)* Add functionality to delete teams with no members or servers in CleanupStuckedResources command
+- *(ui)* Add heart icon and enhance popup messaging for sponsorship support
+- *(settings)* Add sponsorship popup toggle and corresponding database migration
+- *(migrations)* Add optimized indexes to activity_log for improved query performance
+
+### 🐛 Bug Fixes
+
+- *(service)* Audiobookshelf healthcheck command (#5993)
+- *(service)* Downgrade Evolution API phone version (#5977)
+- *(service)* Pingvinshare-with-clamav
+- *(ssh)* Scp requires square brackets for ipv6 (#6001)
+- *(github)* Changing github app breaks the webhook. it does not anymore
+- *(parser)* Improve FQDN generation and update environment variable handling
+- *(ui)* Enhance status refresh buttons with loading indicators
+- *(ui)* Update confirmation button text for stopping database and service
+- *(routes)* Update middleware for deploy route to use 'api.ability:deploy'
+- *(ui)* Refine API token creation form and update helper text for clarity
+- *(ui)* Adjust layout of deployments section for improved alignment
+- *(ui)* Adjust project grid layout and refine server border styling for better visibility
+- *(ui)* Update border styling for consistency across components and enhance loading indicators
+- *(ui)* Add padding to section headers in settings views for improved spacing
+- *(ui)* Reduce gap between input fields in email settings for better alignment
+- *(docker)* Conditionally enable gzip compression in Traefik labels based on configuration
+- *(parser)* Enable gzip compression conditionally for Pocketbase images and streamline service creation logic
+- *(ui)* Update padding for trademarks policy and enhance spacing in advanced settings section
+- *(ui)* Correct closing tag for sponsorship link in layout popups
+- *(ui)* Refine wording in sponsorship donation prompt in layout popups
+- *(ui)* Update navbar icon color and enhance popup layout for sponsorship support
+- *(ui)* Add target="_blank" to sponsorship links in layout popups for improved user experience
+- *(models)* Refine comment wording in User model for clarity on user deletion criteria
+- *(models)* Improve user deletion logic in User model to handle team member roles and prevent deletion if user is alone in root team
+- *(ui)* Update wording in sponsorship prompt for clarity and engagement
+- *(shared)* Refactor gzip handling for Pocketbase in newParser function for improved clarity
+
+### 🚜 Refactor
+
+- *(service)* Update Hoarder to their new name karakeep (#5964)
+- *(service)* Karakeep naming and formatting
+- *(service)* Improve miniflux
+- *(core)* Rename API rate limit ENV
+- *(ui)* Simplify container selection form in execute-container-command view
+- *(email)* Streamline SMTP and resend settings logic for improved clarity
+- *(invitation)* Rename methods for consistency and enhance invitation deletion logic
+- *(user)* Streamline user deletion process and enhance team management logic
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Update Evolution API image to the official one (#6031)
+- *(versions)* Bump coolify versions to v4.0.0-beta.420 and v4.0.0-beta.421
+- *(dependencies)* Update composer dependencies to latest versions including resend-laravel to ^0.19.0 and aws-sdk-php to 3.347.0
+- *(versions)* Update Coolify version to 4.0.0-beta.420.1 and add new services (karakeep, miniflux, pingvinshare) to service templates
+
+## [4.0.0-beta.419] - 2025-06-17
+
+### 🚀 Features
+
+- *(core)* Add 'postmarketos' to supported OS list
+- *(service)* Add memos service template (#5032)
+- *(ui)* Upgrade to Tailwind v4 (#5710)
+- *(service)* Add Navidrome service template (#5022)
+- *(service)* Add Passbolt service (#5769)
+- *(service)* Add Vert service (#5663)
+- *(service)* Add Ryot service (#5232)
+- *(service)* Add Marimo service (#5559)
+- *(service)* Add Diun service (#5113)
+- *(service)* Add Observium service (#5613)
+- *(service)* Add Leantime service (#5792)
+- *(service)* Add Limesurvey service (#5751)
+- *(service)* Add Paymenter service (#5809)
+- *(service)* Add CodiMD service (#4867)
+- *(modal)* Add dispatchAction property to confirmation modal
+- *(security)* Implement server patching functionality
+- *(service)* Add Typesense service (#5643)
+- *(service)* Add Yamtrack service (#5845)
+- *(service)* Add PG Back Web service (#5079)
+- *(service)* Update Maybe service and adjust it for the new release (#5795)
+- *(oauth)* Set redirect uri as optional and add default value (#5760)
+- *(service)* Add apache superset service (#4891)
+- *(service)* Add One Time Secret service (#5650)
+- *(service)* Add Seafile service (#5817)
+- *(service)* Add Netbird-Client service (#5873)
+- *(service)* Add OrangeHRM and Grist services (#5212)
+- *(rules)* Add comprehensive documentation for Coolify architecture and development practices for AI tools, especially for cursor
+- *(server)* Implement server patch check notifications
+- *(api)* Add latest query param to Service restart API (#5881)
+- *(api)* Add connect_to_docker_network setting to App creation API (#5691)
+- *(routes)* Restrict backup download access to team admins and owners
+- *(destination)* Update confirmation modal text and add persistent storage warning for server deployment
+- *(terminal-access)* Implement terminal access control for servers and containers, including UI updates and backend logic
+- *(ca-certificate)* Add CA certificate management functionality with UI integration and routing
+- *(security-patches)* Add update check initialization and enhance notification messaging in UI
+- *(previews)* Add force deploy without cache functionality and update deploy method to accept force rebuild parameter
+- *(security-patterns)* Expand sensitive patterns list to include additional security-related variables
+- *(database-backup)* Add MongoDB credential extraction and backup handling to DatabaseBackupJob
+- *(activity-monitor)* Implement auto-scrolling functionality and dynamic content observation for improved user experience
+- *(utf8-handling)* Implement UTF-8 sanitization for command outputs and enhance error handling in logs processing
+- *(navbar)* Add Traefik dashboard availability check and server IP handling; refactor dynamic configurations loading
+- *(proxy-dashboard)* Implement ProxyDashboardCacheService to manage Traefik dashboard cache; clear cache on configuration changes and proxy actions
+- *(terminal-connection)* Enhance terminal connection handling with auto-connect feature and improved status messaging
+- *(terminal)* Implement resize handling with ResizeObserver for improved terminal responsiveness
+- *(migration)* Add is_sentinel_enabled column to server_settings with default true
+- *(seeder)* Dispatch StartProxy action for each server in ProductionSeeder
+- *(seeder)* Add CheckAndStartSentinelJob dispatch for each server in ProductionSeeder
+- *(seeder)* Conditionally dispatch StartProxy action based on proxy check result
+- *(service)* Update Changedetection template (#5937)
+
+### 🐛 Bug Fixes
+
+- *(constants)* Adding 'fedora-asahi-remix' as a supported OS (#5646)
+- *(authentik)* Update docker-compose configuration for authentik service
+- *(api)* Allow nullable destination_uuid (#5683)
+- *(service)* Fix documenso startup and mail (#5737)
+- *(docker)* Fix production dockerfile
+- *(service)* Navidrome service
+- *(service)* Passbolt
+- *(service)* Add missing ENVs to NTFY service (#5629)
+- *(service)* NTFY is behind a proxy
+- *(service)* Vert logo and ENVs
+- *(service)* Add platform to Observium service
+- *(ActivityMonitor)* Prevent multiple event dispatches during polling
+- *(service)* Convex ENVs and update image versions (#5827)
+- *(service)* Paymenter
+- *(ApplicationDeploymentJob)* Ensure correct COOLIFY_FQDN/COOLIFY_URL values (#4719)
+- *(service)* Snapdrop no matching manifest error (#5849)
+- *(service)* Use the same volume between chatwoot and sidekiq (#5851)
+- *(api)* Validate docker_compose_raw input in ApplicationsController
+- *(api)* Enhance validation for docker_compose_raw in ApplicationsController
+- *(select)* Update PostgreSQL versions and titles in resource selection
+- *(database)* Include DatabaseStatusChanged event in activityMonitor dispatch
+- *(css)* Tailwind v5 things
+- *(service)* Diun ENV for consistency
+- *(service)* Memos service name
+- *(css)* 8+ issue with new tailwind v4
+- *(css)* `bg-coollabs-gradient` not working anymore
+- *(ui)* Add back missing service navbar components
+- *(deploy)* Update resource timestamp handling in deploy_resource method
+- *(patches)* DNF reboot logic is flipped
+- *(deployment)* Correct syntax for else statement in docker compose build command
+- *(shared)* Remove unused relation from queryDatabaseByUuidWithinTeam function
+- *(deployment)* Correct COOLIFY_URL and COOLIFY_FQDN assignments based on parsing version in preview deployments
+- *(docker)* Ensure correct parsing of environment variables by limiting explode to 2 parts
+- *(project)* Update selected environment handling to use environment name instead of UUID
+- *(ui)* Update server status display and improve server addition layout
+- *(service)* Neon WS Proxy service not working on ARM64 (#5887)
+- *(server)* Enhance error handling in server patch check notifications
+- *(PushServerUpdateJob)* Add null checks before updating application and database statuses
+- *(environment-variables)* Update label text for build variable checkboxes to improve clarity
+- *(service-management)* Update service stop and restart messages for improved clarity and formatting
+- *(preview-form)* Update helper text formatting in preview URL template input for better readability
+- *(application-management)* Improve stop messages for application, database, and service to enhance clarity and formatting
+- *(application-configuration)* Prevent access to preview deployments for deploy_key applications and update menu visibility accordingly
+- *(select-component)* Handle exceptions during parameter retrieval and environment selection in the mount method
+- *(previews)* Escape container names in stopContainers method to prevent shell injection vulnerabilities
+- *(docker)* Add protection against empty container queries in GetContainersStatus to prevent unnecessary updates
+- *(modal-confirmation)* Decode HTML entities in confirmation text to ensure proper display
+- *(select-component)* Enhance user interaction by adding cursor styles and disabling selection during processing
+- *(deployment-show)* Remove unnecessary fixed positioning for button container to improve layout responsiveness
+- *(email-notifications)* Change notify method to notifyNow for immediate test email delivery
+- *(service-templates)* Update Convex service configuration to use FQDN variables
+- *(database-heading)* Simplify stop database message for clarity
+- *(navbar)* Remove unnecessary x-init directive for loading proxy configuration
+- *(patches)* Add padding to loading message for better visibility during update checks
+- *(terminal-connection)* Improve error handling and stability for auto-connection; enhance component readiness checks and retry logic
+- *(terminal)* Add unique wire:key to terminal component for improved reactivity and state management
+- *(css)* Adjust utility classes in utilities.css for consistent application of Tailwind directives
+- *(css)* Refine utility classes in utilities.css for proper Tailwind directive application
+- *(install)* Update Docker installation script to use dynamic OS_TYPE and correct installation URL
+- *(cloudflare)* Add error handling to automated Cloudflare configuration script
+- *(navbar)* Add error handling for proxy status check to improve user feedback
+- *(web)* Update user team retrieval method for consistent authentication handling
+- *(cloudflare)* Update refresh method to correctly set Cloudflare tunnel status and improve user notification on IP address update
+- *(service)* Update service template for affine and add migration service for improved deployment process
+- *(supabase)* Update Supabase service images and healthcheck methods for improved reliability
+- *(terminal)* Now it should work
+- *(degraded-status)* Remove unnecessary whitespace in badge element for cleaner HTML
+- *(routes)* Add name to security route for improved route management
+- *(migration)* Update default value handling for is_sentinel_enabled column in server_settings
+- *(seeder)* Conditionally dispatch CheckAndStartSentinelJob based on server's sentinel status
+- *(service)* Disable healthcheck logging for Gotenberg (#6005)
+- *(service)* Joplin volume name (#5930)
+- *(server)* Update sentinelUpdatedAt assignment to use server's sentinel_updated_at property
+
+### 💼 Other
+
+- Add support for postmarketOS (#5608)
+- *(core)* Simplify events for app/db/service status changes
+
+### 🚜 Refactor
+
- *(service)* Observium
- *(service)* Improve leantime
- *(service)* Imporve limesurvey
@@ -4698,761 +1556,1454 @@ ### 🚜 Refactor
- *(ui)* Terminal
- *(ui)* Remove terminal header from execute-container-command view
- *(ui)* Remove unnecessary padding from deployment, backup, and logs sections
-- *(service)* Update Hoarder to their new name karakeep (#5964)
-- *(service)* Karakeep naming and formatting
-- *(service)* Improve miniflux
-- *(core)* Rename API rate limit ENV
-- *(ui)* Simplify container selection form in execute-container-command view
-- *(email)* Streamline SMTP and resend settings logic for improved clarity
-- *(invitation)* Rename methods for consistency and enhance invitation deletion logic
-- *(user)* Streamline user deletion process and enhance team management logic
-- *(ui)* Separate views for instance settings to separate paths to make it cleaner
-- *(ui)* Remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code
-- *(ui)* Enhance project cloning interface with improved table layout for server and resource selection
-- *(terminal)* Simplify command construction for SSH execution
-- *(settings)* Streamline instance admin checks and initialization of settings in Livewire components
-- *(policy)* Optimize team membership checks in S3StoragePolicy
-- *(popup)* Improve styling and structure of the small popup component
-- *(shared)* Enhance FQDN generation logic for services in newParser function
-- *(redis)* Enhance CleanupRedis command with dry-run option and improved key deletion logic
-- *(init)* Standardize method naming conventions and improve command structure in Init.php
-- *(shared)* Improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml
-- *(database)* Improve error handling for unsupported database types in StartDatabaseProxy
-- *(previews)* Streamline preview URL generation by utilizing application method
-- *(application)* Adjust layout and spacing in general application view for improved UI
-- *(postgresql)* Improve layout and spacing in SSL and Proxy configuration sections for better UI consistency
-- *(scheduling)* Replace deprecated job checks with ScheduledJobManager and ServerResourceManager for improved scheduling efficiency
-- *(previews)* Move preview domain generation logic to ApplicationPreview model for better encapsulation and consistency across webhook handlers
-- *(service)* Improve gowa
-- *(previews)* Streamline preview domain generation logic in ApplicationDeploymentJob for improved clarity and maintainability
-- *(services)* Simplify environment variable updates by using updateOrCreate and add cleanup for removed FQDNs
-- *(jobs)* Remove logging for ScheduledJobManager and ServerResourceManager start and completion
-- *(services)* Update validation rules to be optional
-- *(service)* Improve langfuse
-- *(service)* Improve openpanel template
-- *(service)* Improve librechat
-- *(public-git-repository)* Enhance form structure and add autofocus to repository URL input
-- *(public-git-repository)* Remove commented-out code for cleaner template
-- *(templates)* Update service template file handling to use dynamic file name from constants
-- *(parsers)* Streamline domain handling in applicationParser and improve DNS validation logic
-- *(templates)* Replace SERVICE_FQDN variables with SERVICE_URL in compose files for consistency
-- *(links)* Replace inline SVGs with reusable external link component for consistency and improved maintainability
-- *(previews)* Improve layout and add deployment/application logs links for previews
-- *(docker compose)* Remove deprecated newParser function and associated test file to streamline codebase
-- *(shared helpers)* Remove unused parseServiceVolumes function to clean up codebase
-- *(parsers)* Update volume parsing logic to use beforeLast and afterLast for improved accuracy
-- *(validation)* Implement centralized validation patterns across components
-- *(jobs)* Rename job classes to indicate deprecation status
-- Update check frequency logic for cloud and self-hosted environments; streamline server task scheduling and timezone handling
-- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility
-- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility
-- *(git)* Improve submodule cloning
-- *(parsers)* Remove unnecessary hyphen-to-underscore replacement for service names in serviceParser function
-- *(urls)* Replace generateFqdn with generateUrl for consistent URL generation across applications
-- *(domains)* Rename check_domain_usage to checkDomainUsage and update references across the application
-- *(auth)* Simplify access control logic in CanAccessTerminal and ServerPolicy by allowing all users to perform actions
-- *(policy)* Simplify ServiceDatabasePolicy methods to always return true and add manageBackups method
-- *(jobs)* Pull github changelogs from cdn instead of github
-- *(command)* Streamline database deletion process to handle multiple database types and improve user experience
-- *(command)* Improve database collection logic for deletion command by using unique identifiers and enhancing user experience
-- *(command)* Remove InitChangelog command as it is no longer needed
-- *(command)* Streamline Init command by removing unnecessary options and enhancing error handling for various operations
-- *(webhook)* Replace direct forceDelete calls with DeleteResourceJob dispatch for application previews
-- *(command)* Replace forceDelete calls with DeleteResourceJob dispatch for all stuck resources in cleanup process
-- *(command)* Simplify SSH command retry logic by removing unnecessary logging and improving delay calculation
-- *(ssh)* Enhance error handling in SSH command execution and improve connection validation logging
-- *(backlog)* Remove outdated guidelines and project manager agent files to streamline task management documentation
-- *(error-handling)* Remove ray debugging statements from CheckUpdates and shared helper functions to clean up error reporting
-- *(file-transfer)* Replace base64 encoding with direct file transfer method across multiple database actions for improved clarity and efficiency
-- *(remoteProcess)* Remove debugging statement from transfer_file_to_server function to clean up code
-- *(dns-validation)* Rename DNS validation functions for consistency and clarity, and remove unused code
-- *(file-transfer)* Replace base64 encoding with direct file transfer method in various components for improved clarity and efficiency
-- *(private-key)* Remove debugging statement from storeInFileSystem method for cleaner code
-- *(github-webhook)* Restructure application processing by grouping applications by server for improved deployment handling
-- *(deployment)* Enhance queuing logic to support concurrent deployments by including pull request ID in checks
-- *(remoteProcess)* Remove debugging statement from transfer_file_to_container function for cleaner code
-- *(deployment)* Streamline next deployment queuing logic by repositioning queue_next_deployment call
-- *(deployment)* Add validation for pull request existence in deployment process to enhance error handling
-- *(database)* Remove volume_configuration_dir and streamline configuration directory usage in MongoDB and PostgreSQL handlers
-- *(application-source)* Improve layout and accessibility of Git repository links in the application source view
-- *(models)* Remove 'is_readonly' attribute from multiple database models for consistency
-- *(webhook)* Remove Webhook model and related logic; add migrations to drop webhooks and kubernetes tables
-- *(clone)* Consolidate application cloning logic into a dedicated function for improved maintainability and readability
-- *(clone)* Integrate preview cloning logic directly into application cloning function for improved clarity and maintainability
-- *(application)* Enhance environment variable retrieval in configuration change check for improved accuracy
-- *(clone)* Enhance application cloning by separating production and preview environment variable handling
-- *(deployment)* Add environment variable copying logic to Docker build commands for pull requests
-- *(environment)* Standardize service name formatting by replacing '-' and '.' with '_' in environment variable keys
-- *(deployment)* Update environment file handling in Docker commands to use '/artifacts/' path and streamline variable management
-- *(openapi)* Remove 'is_build_time' attribute from environment variable definitions to streamline configuration
-- *(environment)* Remove 'is_build_time' attribute from environment variable handling across the application to simplify configuration
-- *(environment)* Streamline environment variable handling by replacing sorting methods with direct property access and enhancing query ordering for improved performance
-- *(stripe-jobs)* Comment out internal notification calls and add subscription status verification before sending failure notifications
-- *(deployment)* Streamline environment variable handling for dockercompose and improve sorting of runtime variables
-- *(remoteProcess)* Remove command log comments for file transfers to simplify code
-- *(remoteProcess)* Remove file transfer handling from remote_process and instant_remote_process functions to simplify code
-- *(deployment)* Update environment file paths in docker compose commands to use working directory for improved consistency
-- *(server)* Remove debugging ray call from validateConnection method for cleaner code
-- *(deployment)* Conditionally cleanup build secrets based on Docker BuildKit support and remove redundant calls for improved efficiency
-- *(deployment)* Remove redundant environment variable documentation from Dockerfile comments to streamline the deployment process
-- *(deployment)* Streamline Docker BuildKit detection and environment variable handling for enhanced security during application deployment
-- *(deployment)* Optimize BuildKit capabilities detection and remove unnecessary comments for cleaner deployment logic
-- *(deployment)* Rename method for modifying Dockerfile to improve clarity and streamline build secrets integration
-- *(environment)* Conditionally render Docker Build Secrets checkbox based on build pack type
-- *(search)* Optimize cache clearing logic to only trigger on searchable field changes
-- *(environment)* Streamline rendering of Docker Build Secrets checkbox and adjust layout for environment variable settings
-- *(proxy)* Streamline proxy configuration form layout and improve button placements
-- *(remoteProcess)* Remove redundant file transfer functions for improved clarity
-- *(github)* Enhance API request handling and validation
-- *(databases)* Remove deprecated backup parameters from API documentation
-- *(databases)* Streamline backup queries to use team context
-- *(databases)* Update backup queries to use team-specific method
-- *(server)* Update dispatch messages and streamline data synchronization
-- *(cache)* Update team retrieval method in ClearsGlobalSearchCache trait
-- *(database-backup)* Move unique UUID generation for backup execution to database loop
-- *(cloud-commands)* Consolidate and enhance subscription management commands
-- *(toast-component)* Improve layout and icon handling in toast notifications
-- *(private-key-update)* Implement transaction for private key association and connection validation
-- *(installer)* Improve install script
-- *(upgrade)* Improve upgrade script
-- *(installer, upgrade)* Enhance environment variable management
-- *(upgrade)* Enhance logging and quoting in upgrade scripts
-- *(upgrade)* Replace warning div with a callout component for better UI consistency
-- *(ui)* Replace warning and error divs with callout components for improved consistency and readability
-- *(ui)* Improve styling and consistency in environment variable warning and docker cleanup components
-- *(security)* Streamline update check functionality and improve UI button interactions in patches view
-- *(tests)* Simplify matchWatchPaths tests and update implementation for better clarity
-- *(deployment)* Improve environment variable handling in ApplicationDeploymentJob
-- *(deployment)* Remove commented-out code and streamline environment variable handling in ApplicationDeploymentJob
-- *(application)* Improve handling of docker compose domains by normalizing keys and ensuring valid JSON structure
-- *(forms)* Update wire:model bindings to use 'blur' instead of 'blur-sm' for input fields across multiple views
-- *(global-search)* Change event listener to window level for global search modal
-- *(dashboard)* Remove deployment loading logic and introduce DeploymentsIndicator component for better UI management
-- *(dashboard)* Replace project navigation method with direct link in UI
-- *(global-search)* Improve event handling and cleanup in global search component
-- *(environment-variables)* Adjust ordering logic for environment variables
-- Update ente photos configuration for improved service management
-- *(deployment)* Streamline environment variable generation in ApplicationDeploymentJob
-- *(deployment)* Enhance deployment data retrieval and relationships
-- *(deployment)* Standardize environment variable handling in ApplicationDeploymentJob
-- *(deployment)* Update environment variable handling for Docker builds
-- *(navbar, app)* Improve layout and styling for better responsiveness
-- *(switch-team)* Remove label from team selection component for cleaner UI
-- *(global-search, environment)* Streamline environment retrieval with new query method
-- *(backup)* Make backup_log_uuid initialization lazy
-- *(checkbox, utilities, global-search)* Enhance focus styles for better accessibility
-- *(forms)* Simplify wire:dirty class bindings for input, select, and textarea components
-- Replace direct SslCertificate queries with server relationship methods for consistency
-- *(ui)* Improve cloud-init script save checkbox visibility and styling
-- Enable cloud-init save checkbox at all times with backend validation
-- Improve cloud-init script UX and remove description field
-- Improve cloud-init script management UI and cache control
-- Remove debug sleep from global search modal
-- Reduce cloud-init label width for better layout
-- Remove SendsWebhook interface
-- Reposition POST badge as button
-- Migrate database components from legacy model binding to explicit properties
-- Volumes set back to ./pds-data:/pds
-- *(campfire)* Streamline environment variable definitions in Docker Compose file
-- Improve validation error handling and coding standards
-- Preserve exception chain in validation error handling
-- Harden and deduplicate validateShellSafePath
-- Replace random ID generation with Cuid2 for unique HTML IDs in form components
### 📚 Documentation
-- Contribution guide
-- How to add new services
-- Update
-- Update
-- Update Plunk documentation link in compose/plunk.yaml
-- Update link to deploy api docs
-- Add TECH_STACK.md (#4883)
-- *(services)* Reword nitropage url and slogan
-- *(readme)* Add Convex to special sponsors section
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
- Update changelog
- *(service)* Add new docs link for zipline (#5912)
- Update changelog
- Update changelog
- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- *(claude)* Clarify that artisan commands should only be run inside the "coolify" container during development
-- Add AGENTS.md for project guidance and development instructions
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- *(testing-patterns)* Add important note to always run tests inside the `coolify` container for clarity
-- Update changelog
-- Update changelog
-- Update changelog
-- *(claude)* Update testing guidelines and add note on Application::team relationship
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- *(tests)* Update testing guidelines for unit and feature tests
-- *(sync)* Create AI Instructions Synchronization Guide and update CLAUDE.md references
-- *(database-patterns)* Add critical note on mass assignment protection for new columns
-- Clarify cloud-init script compatibility
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
-- Update changelog
### 🎨 Styling
-- Linting
- *(css)* Update padding utility for password input and add newline in app.css
- *(css)* Refine badge utility styles in utilities.css
- *(css)* Enhance badge utility styles in utilities.css
-- *(environment-variable)* Adjust SVG icon margin for improved layout in locked state
-- *(proxy)* Adjust padding in proxy configuration form for better visual alignment
-- *(campfire)* Format environment variables for better readability in Docker Compose file
-- *(campfire)* Update comment for DISABLE_SSL environment variable for clarity
-
-### 🧪 Testing
-
-- Native binary target
-- Dockerfile
-- Remove prisma
-- More tests
-- Setup database for upcoming tests
-- Improve Git ls-remote parsing tests with uppercase SHA and negative cases
-- Add coverage for newline and tab rejection in volume strings
### ⚙️ Miscellaneous Tasks
-- Version bump
-- Version
-- Version
-- Version++
-- Version++
-- Version++
-- Version++
+- *(versions)* Update coolify version to 4.0.0-beta.419 and nightly version to 4.0.0-beta.420 in configuration files
+- *(service)* Rename hoarder server to karakeep (#5607)
+- *(service)* Update Supabase services (#5708)
+- *(service)* Remove unused documenso env
+- *(service)* Formatting and cleanup of ryot
+- *(docs)* Remove changelog and add it to gitignore
+- *(versions)* Update version to 4.0.0-beta.419
+- *(service)* Diun formatting
+- *(docs)* Update CHANGELOG.md
+- *(service)* Switch convex vars
+- *(service)* Pgbackweb formatting and naming update
+- *(service)* Remove typesense default API key
+- *(service)* Format yamtrack healthcheck
+- *(core)* Remove unused function
+- *(ui)* Remove unused stopEvent code
+- *(service)* Remove unused env
+- *(tests)* Update test environment database name and add new feature test for converting container environment variables to array
+- *(service)* Update Immich service (#5886)
+- *(service)* Remove unused logo
+- *(api)* Update API docs
+- *(dependencies)* Update package versions in composer.json and composer.lock for improved compatibility and performance
+- *(dependencies)* Update package versions in package.json and package-lock.json for improved stability and features
+- *(version)* Update coolify-realtime to version 1.0.9 in docker-compose and versions files
+- *(version)* Update coolify version to 4.0.0-beta.420 and nightly version to 4.0.0-beta.421
+- *(service)* Changedetection remove unused code
+
+## [4.0.0-beta.417] - 2025-05-07
+
+### 🐛 Bug Fixes
+
+- *(select)* Update fallback logo path to use absolute URL for improved reliability
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update coolify version to 4.0.0-beta.418
+
+## [4.0.0-beta.416] - 2025-05-05
+
+### 🚀 Features
+
+- *(migration)* Add 'is_migrated' and 'custom_type' columns to service_applications and service_databases tables
+- *(backup)* Implement custom database type selection and enhance scheduled backups management
+- *(README)* Add Gozunga and Macarne to sponsors list
+- *(redis)* Add scheduled cleanup command for Redis keys and enhance cleanup logic
+
+### 🐛 Bug Fixes
+
+- *(service)* Graceful shutdown of old container (#5731)
+- *(ServerCheck)* Enhance proxy container check to ensure it is running before proceeding
+- *(applications)* Include pull_request_id in deployment queue check to prevent duplicate deployments
+- *(database)* Update label for image input field to improve clarity
+- *(ServerCheck)* Set default proxy status to 'exited' to handle missing container state
+- *(database)* Reduce container stop timeout from 300 to 30 seconds for improved responsiveness
+- *(ui)* System theming for charts (#5740)
+- *(dev)* Mount points?!
+- *(dev)* Proxy mount point
+- *(ui)* Allow adding scheduled backups for non-migrated databases
+- *(DatabaseBackupJob)* Escape PostgreSQL password in backup command (#5759)
+- *(ui)* Correct closing div tag in service index view
+
+### 🚜 Refactor
+
+- *(Database)* Streamline container shutdown process and reduce timeout duration
+- *(core)* Streamline container stopping process and reduce timeout duration; update related methods for consistency
+- *(database)* Update DB facade usage for consistency across service files
+- *(database)* Enhance application conversion logic and add existence checks for databases and applications
+- *(actions)* Standardize method naming for network and configuration deletion across application and service classes
+- *(logdrain)* Consolidate log drain stopping logic to reduce redundancy
+- *(StandaloneMariadb)* Add type hint for destination method to improve code clarity
+- *(DeleteResourceJob)* Streamline resource deletion logic and improve conditional checks for database types
+- *(jobs)* Update middleware to prevent job release after expiration for CleanupInstanceStuffsJob, RestartProxyJob, and ServerCheckJob
+- *(jobs)* Unify middleware configuration to prevent job release after expiration for DockerCleanupJob and PushServerUpdateJob
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(seeder)* Update git branch from 'main' to 'v4.x' for multiple examples in ApplicationSeeder
+- *(versions)* Update coolify version to 4.0.0-beta.417 and nightly version to 4.0.0-beta.418
+
+## [4.0.0-beta.415] - 2025-04-29
+
+### 🐛 Bug Fixes
+
+- *(ui)* Remove required attribute from image input in service application view
+- *(ui)* Change application image validation to be nullable in service application view
+- *(Server)* Correct proxy path formatting for Traefik proxy type
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update coolify version to 4.0.0-beta.416 and nightly version to 4.0.0-beta.417 in configuration files; fix links in deployment view
+
+## [4.0.0-beta.414] - 2025-04-28
+
+### 🐛 Bug Fixes
+
+- *(ui)* Disable livewire navigate feature (causing spam of setInterval())
+
+## [4.0.0-beta.413] - 2025-04-28
+
+### 💼 Other
+
+- Adjust Workflows for v5 (#5689)
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(workflows)* Adjust workflow for announcement
+
+## [4.0.0-beta.411] - 2025-04-23
+
+### 🚀 Features
+
+- *(deployment)* Add repository_project_id handling for private GitHub apps and clean up unused Caddy label logic
+- *(api)* Enhance OpenAPI specifications with token variable and additional key attributes
+- *(docker)* Add HTTP Basic Authentication support and enhance hostname parsing in Docker run conversion
+- *(api)* Add HTTP Basic Authentication fields to OpenAPI specifications and enhance PrivateKey model descriptions
+- *(README)* Add InterviewPal sponsorship link and corresponding SVG icon
+
+### 🐛 Bug Fixes
+
+- *(backup-edit)* Conditionally enable S3 checkbox based on available validated S3 storage
+- *(source)* Update no sources found message for clarity
+- *(api)* Correct middleware for service update route to ensure proper permissions
+- *(api)* Handle JSON response in service creation and update methods for improved error handling
+- Add 201 json code to servers validate api response
+- *(docker)* Ensure password hashing only occurs when HTTP Basic Authentication is enabled
+- *(docker)* Enhance hostname and GPU option validation in Docker run to compose conversion
+- *(terminal)* Enhance WebSocket client verification with authorized IPs in terminal server
+- *(ApplicationDeploymentJob)* Ensure source is an object before checking GitHub app properties
+
+### 🚜 Refactor
+
+- *(jobs)* Comment out unused Caddy label handling in ApplicationDeploymentJob and simplify proxy path logic in Server model
+- *(database)* Simplify database type checks in ServiceDatabase and enhance image validation in Docker helper
+- *(shared)* Remove unused ray debugging statement from newParser function
+- *(applications)* Remove redundant error response in create_env method
+- *(api)* Restructure routes to include versioning and maintain existing feedback endpoint
+- *(api)* Remove token variable from OpenAPI specifications for clarity
+- *(environment-variables)* Remove protected variable checks from delete methods for cleaner logic
+- *(http-basic-auth)* Rename 'http_basic_auth_enable' to 'http_basic_auth_enabled' across application files for consistency
+- *(docker)* Remove debug statement and enhance hostname handling in Docker run conversion
+- *(server)* Simplify proxy path logic and remove unnecessary conditions
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update coolify version to 4.0.0-beta.411 and nightly version to 4.0.0-beta.412 in configuration files
+- *(versions)* Update coolify version to 4.0.0-beta.412 and nightly version to 4.0.0-beta.413 in configuration files
+- *(versions)* Update coolify version to 4.0.0-beta.413 and nightly version to 4.0.0-beta.414 in configuration files
+- *(versions)* Update realtime version to 1.0.8 in versions.json
+- *(versions)* Update realtime version to 1.0.8 in versions.json
+- *(docker)* Update soketi image version to 1.0.8 in production configuration files
+- *(versions)* Update coolify version to 4.0.0-beta.414 and nightly version to 4.0.0-beta.415 in configuration files
+
+## [4.0.0-beta.410] - 2025-04-18
+
+### 🚀 Features
+
+- Add HTTP Basic Authentication
+- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README
+- *(core)* Enable magic env variables for compose based applications
+
+### 🐛 Bug Fixes
+
+- *(application)* Append base directory to git branch URLs for improved path handling
+- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON
+- *(navbar)* Update error message link to use route for environment variables navigation
+- Unsend template
+- Replace ports with expose
+- *(templates)* Update Unsend compose configuration for improved service integration
+
+### 🚜 Refactor
+
+- *(jobs)* Update WithoutOverlapping middleware to use expireAfter for better queue management
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files
+- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service
+
+## [4.0.0-beta.409] - 2025-04-16
+
+### 🐛 Bug Fixes
+
+- *(parser)* Transform associative array labels into key=value format for better compatibility
+- *(redis)* Update username and password input handling to clarify database sync requirements
+- *(source)* Update connected source display to handle cases with no source connected
+
+### 🚜 Refactor
+
+- *(source)* Conditionally display connected source and change source options based on private key presence
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files
+
+## [4.0.0-beta.408] - 2025-04-14
+
+### 🚀 Features
+
+- *(OpenApi)* Enhance OpenAPI specifications by adding UUID parameters for application, project, and service updates; improve deployment listing with pagination parameters; update command signature for OpenApi generation
+- *(subscription)* Enhance subscription management with loading states and Stripe status checks
+
+### 🐛 Bug Fixes
+
+- *(pre-commit)* Correct input redirection for /dev/tty and add OpenAPI generation command
+- *(pricing-plans)* Adjust grid class for improved layout consistency in subscription pricing plans
+- *(migrations)* Make stripe_comment field nullable in subscriptions table
+- *(mongodb)* Also apply custom config when SSL is enabled
+- *(templates)* Correct casing of denoKV references in service templates and YAML files
+- *(deployment)* Handle missing destination in deployment process to prevent errors
+
+### 💼 Other
+
+- Add missing openapi items to PrivateKey
+
+### 🚜 Refactor
+
+- *(commands)* Reorganize OpenAPI and Services generation commands into a new namespace for better structure; remove old command files
+- *(Dockerfile)* Remove service generation command from the build process to streamline Dockerfile and improve build efficiency
+- *(navbar-delete-team)* Simplify modal confirmation layout and enhance button styling for better user experience
+- *(Server)* Remove debug logging from isReachableChanged method to clean up code and improve performance
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update nightly version to 4.0.0-beta.410
+- *(pre-commit)* Remove OpenAPI generation command from pre-commit hook
+- *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json
+
+## [4.0.0-beta.407] - 2025-04-09
+
+### 📚 Documentation
+
+- Update changelog
+
+## [4.0.0-beta.406] - 2025-04-05
+
+### 🚀 Features
+
+- *(Deploy)* Add info dispatch for proxy check initiation
+- *(EnvironmentVariable)* Add handling for Redis credentials in the environment variable component
+- *(EnvironmentVariable)* Implement protection for critical environment variables and enhance deletion logic
+- *(Application)* Add networkAliases attribute for handling network aliases as JSON or comma-separated values
+- *(GithubApp)* Update default events to include 'pull_request' and streamline event handling
+- *(CleanupDocker)* Add support for realtime image management in Docker cleanup process
+- *(Deployment)* Enhance queue_application_deployment to handle existing deployments and return appropriate status messages
+- *(SourceManagement)* Add functionality to change Git source and display current source in the application settings
+
+### 🐛 Bug Fixes
+
+- *(CheckProxy)* Update port conflict check to ensure accurate grep matching
+- *(CheckProxy)* Refine port conflict detection with improved grep patterns
+- *(CheckProxy)* Enhance port conflict detection by adjusting ss command for better output
+- *(api)* Add back validateDataApplications (#5539)
+- *(CheckProxy, Status)* Prevent proxy checks when force_stop is active; remove debug statement in General
+- *(Status)* Conditionally check proxy status and refresh button based on force_stop state
+- *(General)* Change redis_password property to nullable string
+- *(DeployController)* Update request handling to use input method and enhance OpenAPI description for deployment endpoint
+
+### 💼 Other
+
+- Add missing UUID to openapi spec
+
+### 🚜 Refactor
+
+- *(Server)* Use data_get for safer access to settings properties in isFunctional method
+- *(Application)* Rename network_aliases to custom_network_aliases across the application for clarity and consistency
+- *(ApplicationDeploymentJob)* Streamline environment variable handling by introducing generate_coolify_env_variables method and consolidating logic for pull request and main branch scenarios
+- *(ApplicationDeploymentJob, ApplicationDeploymentQueue)* Improve deployment status handling and log entry management with transaction support
+- *(SourceManagement)* Sort sources by name and improve UI for changing Git source with better error handling
+- *(Email)* Streamline SMTP and resend settings handling in copyFromInstanceSettings method
+- *(Email)* Enhance error handling in SMTP and resend methods by passing context to handleError function
+- *(DynamicConfigurations)* Improve handling of dynamic configuration content by ensuring fallback to empty string when content is null
+- *(ServicesGenerate)* Update command signature from 'services:generate' to 'generate:services' for consistency; update Dockerfile to run service generation during build; update Odoo image version to 18 and add extra addons volume in compose configuration
+- *(Dockerfile)* Streamline RUN commands for improved readability and maintainability by adding line continuations
+- *(Dockerfile)* Reintroduce service generation command in the build process for consistency and ensure proper asset compilation
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Bump version to 406
+- *(versions)* Bump version to 407 and 408 for coolify and nightly
+- *(versions)* Bump version to 408 for coolify and 409 for nightly
+
+## [4.0.0-beta.405] - 2025-04-04
+
+### 🚀 Features
+
+- *(api)* Update OpenAPI spec for services (#5448)
+- *(proxy)* Enhance proxy handling and port conflict detection
+
+### 🐛 Bug Fixes
+
+- *(api)* Used ssh keys can be deleted
+- *(email)* Transactional emails not sending
+
+### 🚜 Refactor
+
+- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Bump version to 406
+- *(versions)* Bump version to 407
+
+## [4.0.0-beta.404] - 2025-04-03
+
+### 🚀 Features
+
+- *(lang)* Added Azerbaijani language updated turkish language. (#5497)
+- *(lang)* Added Portuguese from Brazil language (#5500)
+- *(lang)* Add Indonesian language translations (#5513)
+
+### 🐛 Bug Fixes
+
+- *(docs)* Comment out execute for now
+- *(installation)* Mount the docker config
+- *(installation)* Path to config file for docker login
+- *(service)* Add health check to Bugsink service (#5512)
+- *(email)* Emails are not sent in multiple cases
+- *(deployments)* Use graceful shutdown instead of `rm`
+- *(docs)* Contribute service url (#5517)
+- *(proxy)* Proxy restart does not work on domain
+- *(ui)* Only show copy button on https
+- *(database)* Custom config for MongoDB (#5471)
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Remove unused code in Bugsink service
+- *(versions)* Update version to 404
+- *(versions)* Bump version to 403 (#5520)
+- *(versions)* Bump version to 404
+
+## [4.0.0-beta.402] - 2025-04-01
+
+### 🚀 Features
+
+- *(deployments)* Add list application deployments api route
+- *(deploy)* Add pull request ID parameter to deploy endpoint
+- *(api)* Add pull request ID parameter to applications endpoint
+- *(api)* Add endpoints for retrieving application logs and deployments
+- *(lang)* Added Norwegian language (#5280)
+- *(dep)* Bump all dependencies
+
+### 🐛 Bug Fixes
+
+- Only get apps for the current team
+- *(DeployController)* Cast 'pr' query parameter to integer
+- *(deploy)* Validate team ID before deployment
+- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424)
+- *(ui)* Instance Backup settings
+
+### 🚜 Refactor
+
+- *(dev)* Remove OpenAPI generation functionality
+- *(migration)* Enhance local file volumes migration with logging
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Update minecraft service ENVs
+- *(service)* Add more vars to infisical.yaml (#5418)
+- *(service)* Add google variables to plausible.yaml (#5429)
+- *(service)* Update authentik.yaml versions (#5373)
+- *(core)* Remove redocs
+- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
+
+## [4.0.0-beta.401] - 2025-03-28
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+## [4.0.0-beta.400] - 2025-03-27
+
+### 🚀 Features
+
+- *(database)* Disable MongoDB SSL by default in migration
+- *(database)* Add CA certificate generation for database servers
+- *(application)* Add SPA configuration and update Nginx generation logic
+
+### 🐛 Bug Fixes
+
+- *(file-storage)* Double save on compose volumes
+- *(parser)* Add logging support for applications in services
+
+### 🚜 Refactor
+
+- *(proxy)* Improve port availability checks with multiple methods
+- *(database)* Update MongoDB SSL configuration for improved security
+- *(database)* Enhance SSL configuration handling for various databases
+- *(notifications)* Update Telegram button URL for staging environment
+- *(models)* Remove unnecessary cloud check in isEnabled method
+- *(database)* Streamline event listeners in Redis General component
+- *(database)* Remove redundant database status display in MongoDB view
+- *(database)* Update import statements for Auth in database components
+- *(database)* Require PEM key file for SSL certificate regeneration
+- *(database)* Change MySQL daemon command to MariaDB daemon
+- *(nightly)* Update version numbers and enhance upgrade script
+- *(versions)* Update version numbers for coolify and nightly
+- *(email)* Validate team membership for email recipients
+- *(shared)* Simplify deployment status check logic
+- *(shared)* Add logging for running deployment jobs
+- *(shared)* Enhance job status check to include 'reserved'
+- *(email)* Improve error handling by passing context to handleError
+- *(email)* Streamline email sending logic and improve configuration handling
+- *(email)* Remove unnecessary whitespace in email sending logic
+- *(email)* Allow custom email recipients in email sending logic
+- *(email)* Enhance sender information formatting in email logic
+- *(proxy)* Remove redundant stop call in restart method
+- *(file-storage)* Add loadStorageOnServer method for improved error handling
+- *(docker)* Parse and sanitize YAML compose file before encoding
+- *(file-storage)* Improve layout and structure of input fields
+- *(email)* Update label for test email recipient input
+- *(database-backup)* Remove existing Docker container before backup upload
+- *(database)* Improve decryption and deduplication of local file volumes
+- *(database)* Remove debug output from volume update process
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update version numbers for coolify and nightly
+
+### ◀️ Revert
+
+- Encrypting mount and fs_path
+
+## [4.0.0-beta.399] - 2025-03-25
+
+### 🚀 Features
+
+- *(service)* Neon
+- *(migration)* Add `ssl_certificates` table and model
+- *(migration)* Add ssl setting to `standalone_postgresqls` table
+- *(ui)* Add ssl settings to Postgres ui
+- *(db)* Add ssl mode to Postgres URLs
+- *(db)* Setup ssl during Postgres start
+- *(migration)* Encrypt local file volumes content and paths
+- *(ssl)* Ssl generation helper
+- *(ssl)* Migrate to `ECC`certificates using `secp521r1`
+- *(ssl)* Improve SSL helper
+- *(ssl)* Add a Coolify CA Certificate to all servers
+- *(seeder)* Call CA SSL seeder in prod and dev
+- *(ssl)* Add Coolify CA Certificate when adding a new server
+- *(installer)* Create CA folder during installation
+- *(ssl)* Improve SSL helper
+- *(ssl)* Use new improved helper for SSL generation
+- *(ui)* Add CA cert UI
+- *(ui)* New copy button component
+- *(ui)* Use new copy button component everywhere
+- *(ui)* Improve server advanced view
+- *(migration)* Add CN and alternative names to DB
+- *(databases)* Add CA SSL crt location to Postgres URLs
+- *(ssl)* Improve ssl generation
+- *(ssl)* Regenerate SSL certs job
+- *(ssl)* Regenerate certificate and valid until UI
+- *(ssl)* Regenerate CA cert and all other certs logic
+- *(ssl)* Add full MySQL SSL Support
+- *(ssl)* Add full MariaDB SSL support
+- *(ssl)* Add `openssl.conf` to configure SSL extension properly
+- *(ssl)* Improve SSL generation and security a lot
+- *(ssl)* Check for SSL renewal twice daily
+- *(ssl)* Add SSL relationships to all DBs
+- Add full SSL support to MongoDB
+- *(ssl)* Fix some issues and improve ssl generation helper
+- *(ssl)* Ability to create `.pem` certs and add `clientAuth` to `extendedKeyUsage`
+- *(ssl)* New modes for MongoDB and get `caCert` and `mountPath` correctly
+- *(ssl)* Full SSL support for Redis
+- New mode implementation for MongoDB
+- *(ssl)* Improve Redis and remove modes
+- Full SSL support for DrangonflyDB
+- SSL notification
+- *(github-source)* Enhance GitHub App configuration with manual and private key support
+- *(ui)* Improve GitHub repository selection and styling
+- *(database)* Implement two-step confirmation for database deletion
+- *(assets)* Add new SVG logo for Coolify
+- *(install)* Enhance Docker address pool configuration and validation
+- *(install)* Improve Docker address pool management and service restart logic
+- *(install)* Add missing env variable to install script
+- *(LocalFileVolume)* Add binary file detection and update UI logic
+- *(templates)* Change glance for v0.7
+- *(templates)* Add Freescout service template
+- *(service)* Add Evolution API template
+- *(service)* Add evolution-api and neon-ws-proxy templates
+- *(svg)* Add coolify and evolution-api SVG logos
+- *(api)* Add api to create custom services
+- *(api)* Separate create and one-click routes
+- *(api)* Update Services api routes and handlers
+- *(api)* Unify service creation endpoint and enhance validation
+- *(notifications)* Add discord ping functionality and settings
+- *(user)* Implement session deletion on password reset
+- *(github)* Enhance repository loading and validation in applications
+
+### 🐛 Bug Fixes
+
+- *(api)* Docker compose based apps creationg through api
+- *(database)* Improve database type detection for Supabase Postgres images
+- *(ssl)* Permission of ssl crt and key inside the container
+- *(ui)* Make sure file mounts do not showing the encrypted values
+- *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert
+- *(ui)* Select component should not always uses title case
+- *(db)* SSL certificates table and model
+- *(migration)* Ssl certificates table
+- *(databases)* Fix database name users new `uuid` instead of DB one
+- *(database)* Fix volume and file mounts and naming
+- *(migration)* Store subjectAlternativeNames as a json array in the db
+- *(ssl)* Make sure the subjectAlternativeNames are unique and stored correctly
+- *(ui)* Certificate expiration data is null before starting the DB
+- *(deletion)* Fix DB deletion
+- *(ssl)* Improve SSL cert file mounts
+- *(ssl)* Always create ca crt on disk even if it is already there
+- *(ssl)* Use mountPath parameter not a hardcoded path
+- *(ssl)* Use 1 instead of on for mysql
+- *(ssl)* Do not remove SSL directory
+- *(ssl)* Wrong ssl cert is loaded to the server and UI error when regenerating SSL
+- *(ssl)* Make sure when regenerating the CA cert it is not overwritten with a server cert
+- *(ssl)* Regenerating certs for a specific DB
+- *(ssl)* Fix MariaDB and MySQL need CA cert
+- *(ssl)* Add mount path to DB to fix regeneration of certs
+- *(ssl)* Fix SSL regeneration to sign with CA cert and use mount path
+- *(ssl)* Get caCert correctly
+- *(ssl)* Remove caCert even if it is a folder by accident
+- *(ssl)* Ger caCert and `mountPath` correctly
+- *(ui)* Only show Regenerate SSL Certificates button when there is a cert
+- *(ssl)* Server id
+- *(ssl)* When regenerating SSL certs the cert is not singed with the new CN
+- *(ssl)* Adjust ca paths for MySQL
+- *(ssl)* Remove mode selection for MariaDB as it is not supported
+- *(ssl)* Permission issue with MariDB cert and key and paths
+- *(ssl)* Rename Redis mode to verify-ca as it is not verify-full
+- *(ui)* Remove unused mode for MongoDB
+- *(ssl)* KeyDB port and caCert args are missing
+- *(ui)* Enable SSL is not working correctly for KeyDB
+- *(ssl)* Add `--tls` arg to DrangflyDB
+- *(notification)* Always send SSL notifications
+- *(database)* Change default value of enable_ssl to false for multiple tables
+- *(ui)* Correct grammatical error in 404 page
+- *(seeder)* Update GitHub app name in GithubAppSeeder
+- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration
+- *(domain)* Dispatch refreshStatus event after successful domain update
+- *(database)* Correct container name generation for service databases
+- *(database)* Limit container name length for database proxy
+- *(database)* Handle unsupported database types in StartDatabaseProxy
+- *(database)* Simplify container name generation in StartDatabaseProxy
+- *(install)* Handle potential errors in Docker address pool configuration
+- *(backups)* Retention settings
+- *(redis)* Set default redis_username for new instances
+- *(core)* Improve instantSave logic and error handling
+- *(general)* Correct link to framework specific documentation
+- *(core)* Redirect healthcheck route for dockercompose applications
+- *(api)* Use name from request payload
+- *(issue#4746)* Do not use setGitImportSettings inside of generateGitLsRemoteCommands
+- Correct some spellings
+- *(service)* Replace deprecated credentials env variables on keycloak service
+- *(keycloak)* Update keycloak image version to 26.1
+- *(console)* Handle missing root user in password reset command
+- *(ssl)* Handle missing CA certificate in SSL regeneration job
+- *(copy-button)* Ensure text is safely passed to clipboard
+
+### 💼 Other
+
+- Bump Coolify to 4.0.0-beta.400
+- *(migration)* Add SSL fields to database tables
+- SSL Support for KeyDB
+
+### 🚜 Refactor
+
+- *(ui)* Unhide log toggle in application settings
+- *(nginx)* Streamline default Nginx configuration and improve error handling
+- *(install)* Clean up install script and enhance Docker installation logic
+- *(ScheduledTask)* Clean up code formatting and remove unused import
+- *(app)* Remove unused MagicBar component and related code
+- *(database)* Streamline SSL configuration handling across database types
+- *(application)* Streamline healthcheck parsing from Dockerfile
+- *(notifications)* Standardize getRecipients method signatures
+- *(configuration)* Centralize configuration management in ConfigurationRepository
+- *(docker)* Update image references to use centralized registry URL
+- *(env)* Add centralized registry URL to environment configuration
+- *(storage)* Simplify file storage iteration in Blade template
+- *(models)* Add is_directory attribute to LocalFileVolume model
+- *(modal)* Add ignoreWire attribute to modal-confirmation component
+- *(invite-link)* Adjust layout for better responsiveness in form
+- *(invite-link)* Enhance form layout for improved responsiveness
+- *(network)* Enhance docker network creation with ipv6 fallback
+- *(network)* Check for existing coolify network before creation
+- *(database)* Enhance encryption process for local file volumes
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(migration)* Remove unused columns
+- *(ssl)* Improve code in ssl helper
+- *(migration)* Ssl cert and key should not be nullable
+- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts
+- Rename ca crt folder to ssl
+- *(ui)* Improve valid until handling
+- Improve code quality suggested by code rabbit
+- *(supabase)* Update Supabase service template and Postgres image version
+- *(versions)* Update version numbers for coolify and nightly
+
+## [4.0.0-beta.398] - 2025-03-01
+
+### 🚀 Features
+
+- *(billing)* Add Stripe past due subscription status tracking
+- *(ui)* Add past due subscription warning banner
+
+### 🐛 Bug Fixes
+
+- *(billing)* Restrict Stripe subscription status update to 'active' only
+
+### 💼 Other
+
+- Bump Coolify to 4.0.0-beta.398
+
+### 🚜 Refactor
+
+- *(billing)* Enhance Stripe subscription status handling and notifications
+
+## [4.0.0-beta.397] - 2025-02-28
+
+### 🐛 Bug Fixes
+
+- *(billing)* Handle 'past_due' subscription status in Stripe processing
+- *(revert)* Label parsing
+- *(helpers)* Initialize command variable in parseCommandFromMagicEnvVariable
+
+### 📚 Documentation
+
+- Update changelog
+
+## [4.0.0-beta.396] - 2025-02-28
+
+### 🚀 Features
+
+- *(ui)* Add wire:key to two-step confirmation settings
+- *(database)* Add index to scheduled task executions for improved query performance
+- *(database)* Add index to scheduled database backup executions
+
+### 🐛 Bug Fixes
+
+- *(core)* Production dockerfile
+- *(ui)* Update storage configuration guidance link
+- *(ui)* Set default SMTP encryption to starttls
+- *(notifications)* Correct environment URL path in application notifications
+- *(config)* Update default PostgreSQL host to coolify-db instead of postgres
+- *(docker)* Improve Docker compose file validation process
+- *(ui)* Restrict service retrieval to current team
+- *(core)* Only validate custom compose files
+- *(mail)* Set default mailer to array when not specified
+- *(ui)* Correct redirect routes after task deletion
+- *(core)* Adding a new server should not try to make the default docker network
+- *(core)* Clean up unnecessary files during application image build
+- *(core)* Improve label generation and merging for applications and services
+
+### 💼 Other
+
+- Bump all dependencies (#5216)
+
+### 🚜 Refactor
+
+- *(ui)* Simplify file storage modal confirmations
+- *(notifications)* Improve transactional email settings handling
+- *(scheduled-tasks)* Improve scheduled task creation and management
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- Bump helper and realtime version
+
+## [4.0.0-beta.395] - 2025-02-22
+
+### 📚 Documentation
+
+- Update changelog
+
+## [4.0.0-beta.394] - 2025-02-17
+
+### 📚 Documentation
+
+- Update changelog
+
+## [4.0.0-beta.393] - 2025-02-15
+
+### 📚 Documentation
+
+- Update changelog
+
+## [4.0.0-beta.392] - 2025-02-13
+
+### 🚀 Features
+
+- *(ui)* Add top padding to pricing plans view
+- *(core)* Add error logging and cron parsing to docker/server schedules
+- *(core)* Prevent using servers with existing resources as build servers
+- *(ui)* Add textarea switching option in service compose editor
+
+### 🐛 Bug Fixes
+
+- Pull latest image from registry when using build server
+- *(deployment)* Improve server selection for deployment cancellation
+- *(deployment)* Improve log line rendering and formatting
+- *(s3-storage)* Optimize team admin notification query
+- *(core)* Improve connection testing with dynamic disk configuration for s3 backups
+- *(core)* Update service status refresh event handling
+- *(ui)* Adjust polling intervals for database and service status checks
+- *(service)* Update Fider service template healthcheck command
+- *(core)* Improve server selection error handling in Docker component
+- *(core)* Add server functionality check before dispatching container status
+- *(ui)* Disable sticky scroll in Monaco editor
+- *(ui)* Add literal and multiline env support to services.
+- *(services)* Owncloud docs link
+- *(template)* Remove db-migration step from `infisical.yaml` (#5209)
+- *(service)* Penpot (#5047)
+
+### 🚜 Refactor
+
+- Use pull flag on docker compose up
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- Rollback Coolify version to 4.0.0-beta.392
+- Bump Coolify version to 4.0.0-beta.393
+- Bump Coolify version to 4.0.0-beta.394
+- Bump Coolify version to 4.0.0-beta.395
+- Bump Coolify version to 4.0.0-beta.396
+- *(services)* Update zipline to use new Database env var. (#5210)
+- *(service)* Upgrade authentik service
+- *(service)* Remove unused env from zipline
+
+## [4.0.0-beta.391] - 2025-02-04
+
+### 🚀 Features
+
+- Add application api route
+- Container logs
+- Remove ansi color from log
+- Add lines query parameter
+- *(changelog)* Add git cliff for automatic changelog generation
+- *(workflows)* Improve changelog generation and workflows
+- *(ui)* Add periodic status checking for services
+- *(deployment)* Ensure private key is stored in filesystem before deployment
+- *(slack)* Show message title in notification previews (#5063)
+- *(i18n)* Add Arabic translations (#4991)
+- *(i18n)* Add French translations (#4992)
+- *(services)* Update `service-templates.json`
+
+### 🐛 Bug Fixes
+
+- *(core)* Improve deployment failure Slack notification formatting
+- *(core)* Update Slack notification formatting to use bold correctly
+- *(core)* Enhance Slack deployment success notification formatting
+- *(ui)* Simplify service templates loading logic
+- *(ui)* Align title and add button vertically in various views
+- Handle pullrequest:updated for reliable preview deployments
+- *(ui)* Fix typo on team page (#5105)
+- Cal.com documentation link give 404 (#5070)
+- *(slack)* Notification settings URL in `HighDiskUsage` message (#5071)
+- *(ui)* Correct typo in Storage delete dialog (#5061)
+- *(lang)* Add missing italian translations (#5057)
+- *(service)* Improve duplicati.yaml (#4971)
+- *(service)* Links in homepage service (#5002)
+- *(service)* Added SMTP credentials to getoutline yaml template file (#5011)
+- *(service)* Added `KEY` Variable to Beszel Template (#5021)
+- *(cloudflare-tunnels)* Dead links to docs (#5104)
+- System-wide GitHub apps (#5114)
+
+### 🚜 Refactor
+
+- Simplify service start and restart workflows
+
+### 📚 Documentation
+
+- *(services)* Reword nitropage url and slogan
+- *(readme)* Add Convex to special sponsors section
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(config)* Increase default PHP memory limit to 256M
+- Add openapi response
+- *(workflows)* Make naming more clear and remove unused code
+- Bump Coolify version to 4.0.0-beta.392/393
+- *(ci)* Update changelog generation workflow to target 'next' branch
+- *(ci)* Update changelog generation workflow to target main branch
+
+## [4.0.0-beta.390] - 2025-01-28
+
+### 🚀 Features
+
+- *(template)* Add Open Web UI
+- *(templates)* Add Open Web UI service template
+- *(ui)* Update GitHub source creation advanced section label
+- *(core)* Add dynamic label reset for application settings
+- *(ui)* Conditionally enable advanced application settings based on label readonly status
+- *(env)* Added COOLIFY_RESOURCE_UUID environment variable
+- *(vite)* Add Cloudflare async script and style tag attributes
+- *(meta)* Add comprehensive SEO and social media meta tags
+- *(core)* Add name to default proxy configuration
+
+### 🐛 Bug Fixes
+
+- *(ui)* Update database control UI to check server functionality before displaying actions
+- *(ui)* Typo in upgrade message
+- *(ui)* Cloudflare tunnel configuration should be an info, not a warning
+- *(s3)* DigitalOcean storage buckets do not work
+- *(ui)* Correct typo in container label helper text
+- Disable certain parts if readonly label is turned off
+- Cleanup old scheduled_task_executions
+- Validate cron expression in Scheduled Task update
+- *(core)* Check cron expression on save
+- *(database)* Detect more postgres database image types
+- *(templates)* Update service templates
+- Remove quotes in COOLIFY_CONTAINER_NAME
+- *(templates)* Update Trigger.dev service templates with v3 configuration
+- *(database)* Adjust MongoDB restore command and import view styling
+- *(core)* Improve public repository URL parsing for branch and base directory
+- *(core)* Increase HTTP/2 max concurrent streams to 250 (default)
+- *(ui)* Update docker compose file helper text to clarify repository modification
+- *(ui)* Skip SERVICE_FQDN and SERVICE_URL variables during update
+- *(core)* Stopping database is not disabling db proxy
+- *(core)* Remove --remove-orphans flag from proxy startup command to prevent other proxy deletions (db)
+- *(api)* Domain check when updating domain
+- *(ui)* Always redirect to dashboard after team switch
+- *(backup)* Escape special characters in database backup commands
+
+### 💼 Other
+
+- Trigger.dev templates - wrong key length issue
+- Trigger.dev template - missing ports and wrong env usage
+- Trigger.dev template - fixed otel config
+- Trigger.dev template - fixed otel config
+- Trigger.dev template - fixed port config
+
+### 🚜 Refactor
+
+- *(s3)* Improve S3 bucket endpoint formatting
+- *(vite)* Improve environment variable handling in Vite configuration
+- *(ui)* Simplify GitHub App registration UI and layout
+
+### ⚙️ Miscellaneous Tasks
+
+- *(version)* Bump Coolify version to 4.0.0-beta.391
+
+### ◀️ Revert
+
+- Remove Cloudflare async tag attributes
+
+## [4.0.0-beta.389] - 2025-01-23
+
+### 🚀 Features
+
+- *(docs)* Update tech stack
+- *(terminal)* Show terminal unavailable if the container does not have a shell on the global terminal UI
+- *(ui)* Improve deployment UI
+
+### 🐛 Bug Fixes
+
+- *(service)* Infinite loading and lag with invoiceninja service (#4876)
+- *(service)* Invoiceninja service
+- *(workflows)* `Waiting for changes` label should also be considered and improved messages
+- *(workflows)* Remove tags only if the PR has been merged into the main branch
+- *(terminal)* Terminal shows that it is not available, even though it is
+- *(labels)* Docker labels do not generated correctly
+- *(helper)* Downgrade Nixpacks to v1.29.0
+- *(labels)* Generate labels when they are empty not when they are already generated
+- *(storage)* Hetzner storage buckets not working
+
+### 📚 Documentation
+
+- Add TECH_STACK.md (#4883)
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update coolify versions to v4.0.0-beta.389
+- *(core)* EnvironmentVariable Model now extends BaseModel to remove duplicated code
+- *(versions)* Update coolify versions to v4.0.0-beta.3909
+
+## [4.0.0-beta.388] - 2025-01-22
+
+### 🚀 Features
+
+- *(core)* Add SOURCE_COMMIT variable to build environment in ApplicationDeploymentJob
+- *(service)* Update affine.yaml with AI environment variables (#4918)
+- *(service)* Add new service Flipt (#4875)
+
+### 🐛 Bug Fixes
+
+- *(core)* Update environment variable generation logic in ApplicationDeploymentJob to handle different build packs
+- *(env)* Shared variables can not be updated
+- *(ui)* Metrics stuck in loading state
+- *(ui)* Use `wire:navigate` to navigate to the server settings page
+- *(service)* Plunk API & health check endpoint (#4925)
+
+## [4.0.0-beta.386] - 2025-01-22
+
+### 🐛 Bug Fixes
+
+- *(redis)* Update environment variable keys from standalone_redis_id to resourceable_id
+- *(routes)* Local API docs not available on domain or IP
+- *(routes)* Local API docs not available on domain or IP
+- *(core)* Update application_id references to resourable_id and resourable_type for Nixpacks configuration
+- *(core)* Correct spelling of 'resourable' to 'resourceable' in Nixpacks configuration for ApplicationDeploymentJob
+- *(ui)* Traefik dashboard url not working
+- *(ui)* Proxy status badge flashing during navigation
+
+### 🚜 Refactor
+
+- *(workflows)* Replace jq with PHP script for version retrieval in workflows
+
+### ⚙️ Miscellaneous Tasks
+
+- *(dep)* Bump helper version to 1.0.5
+- *(docker)* Add blank line for readability in Dockerfile
+- *(versions)* Update coolify versions to v4.0.0-beta.388
+- *(versions)* Update coolify versions to v4.0.0-beta.389 and add helper version retrieval script
+
+## [4.0.0-beta.385] - 2025-01-21
+
+### 🚀 Features
+
+- *(core)* Wip version of coolify.json
+
+### 🐛 Bug Fixes
+
+- *(email)* Transactional email sending
+- *(ui)* Add missing save button for new Docker Cleanup page
+- *(ui)* Show preview deployment environment variables
+- *(ui)* Show error on terminal if container has no shell (bash/sh)
+- *(parser)* Resource URL should only be parsed if there is one
+- *(core)* Compose parsing for apps
+
+### ⚙️ Miscellaneous Tasks
+
+- *(dep)* Bump nixpacks version
+- *(dep)* Version++
+
+## [4.0.0-beta.384] - 2025-01-21
+
+### 🐛 Bug Fixes
+
+- *(ui)* Backups link should not redirected to general
+- Envs with special chars during build
+- *(db)* `finished_at` timestamps are not set for existing deployments
+- Load service templates on cloud
+
+## [4.0.0-beta.383] - 2025-01-20
+
+### 🐛 Bug Fixes
+
+- *(service)* Add healthcheck to Cloudflared service (#4859)
+- Remove wire:navigate from import backups
+
+## [4.0.0-beta.382] - 2025-01-17
+
+### 🚀 Features
+
+- Add log file check message in upgrade script for better troubleshooting
+- Add root user details to install script
+
+### 🐛 Bug Fixes
+
+- Create the private key before the server in the prod seeder
+- Update ProductionSeeder to check for private key instead of server's private key
+- *(ui)* Missing underline for docs link in the Swarm section (#4860)
+- *(service)* Change chatwoot service postgres image from `postgres:12` to `pgvector/pgvector:pg12`
+- Docker image parser
+- Add public key attribute to privatekey model
+- Correct service update logic in Docker Compose parser
+- Update CDN URL in install script to point to nightly version
+
+### 🚜 Refactor
+
+- Comment out RootUserSeeder call in ProductionSeeder for clarity
+- Streamline ProductionSeeder by removing debug logs and unnecessary checks, while ensuring essential seeding operations remain intact
+- Remove debug echo statements from Init command to clean up output and improve readability
+
+## [4.0.0-beta.381] - 2025-01-17
+
+### 🚀 Features
+
+- Able to import full db backups for pg/mysql/mariadb
+- Restore backup from server file
+- Docker volume data cloning
+- Move volume data cloning to a Job
+- Volume cloning for ResourceOperations
+- Remote server volume cloning
+- Add horizon server details to queue
+- Enhance horizon:manage command with worker restart check
+- Add is_coolify_host to the server api responses
+- DB migration for Backup retention
+- UI for backup retention settings
+- New global s3 and local backup deletion function
+- Use new backup deletion functions
+- Add calibre-web service
+- Add actual-budget service
+- Add rallly service
+- Template for Gotenberg, a Docker-powered stateless API for PDF files
+- Enhance import command options with additional guidance and improved checkbox label
+- Purify for better sanitization
+- Move docker cleanup to its own tab
+- DB and Model for docker cleanup executions
+- DockerCleanupExecutions relationship
+- DockerCleanupDone event
+- Get command and output for logs from CleanupDocker
+- New sidebar menu and order
+- Docker cleanup executions UI
+- Add execution log to dockerCleanupJob
+- Improve deployment UI
+- Root user envs and seeding
+- Email, username and password validation when they are set via envs
+- Improved error handling and log output
+- Add root user configuration variables to production environment
+
+### 🐛 Bug Fixes
+
+- Compose envs
+- Scheduled tasks and backups are executed by server timezone.
+- Show backup timezone on the UI
+- Disappearing UI after livewire event received
+- Add default vector db for anythingllm
+- We need XSRF-TOKEN for terminal
+- Prevent default link behavior for resource and settings actions in dashboard
+- Increase default php memory limit
+- Show if only build servers are added to your team
+- Update Livewire button click method to use camelCase
+- Local dropzonejs
+- Import backups due to js stuff should not be navigated
+- Install inetutils on Arch Linux
+- Use ip in place of hostname from inetutils in arch
+- Update import command to append file redirection for database restoration
+- Ui bug on pw confirmation
+- Exclude system and computed fields from model replication
+- Service cloning on a separate server
+- Application cloning
+- `Undefined variable $fs_path` for databases
+- Service and database cloning and label generation
+- Labels and URL generation when cloning
+- Clone naming for different database data volumes
+- Implement all the cloneMe changes for ResourceOperations as well
+- Volume and fileStorages cloning
+- View text and helpers
+- Teable
+- Trigger with external db
+- Set `EXPERIMENTAL_FEATURES` to false for labelstudio
+- Monaco editor disabled state
+- Edge case where executions could be null
+- Create destination properly
+- Getcontainer status should timeout after 30s
+- Enable response for temporary unavailability in sentinel push endpoint
+- Use timeout in cleanup resources
+- Add timeout to sentinel process checks for improved reliability
+- Horizon job checker
+- Update response message for sentinel push route
+- Add own servers on cloud
+- Application deployment
+- Service update statsu
+- If $SERVICE found in the service specific configuration, then search for it in the db
+- Instance wide GitHub apps are not available on other teams then the source team
+- Function calls
+- UI
+- Deletion of single backup
+- Backup job deletion - delete all backups from s3 and local
+- Use new removeOldBackups function
+- Retention functions and folder deletion for local backups
+- Storage retention setting
+- Db without s3 should still backup
+- Wording
+- `Undefined variable $service` when creating a new service
+- Nodebb service
+- Calibre-web service
+- Rallly and actualbudget service
+- Removed container_name
+- Added healthcheck for gotenberg template
+- Gotenberg
+- *(template)* Gotenberg healthcheck, use /health instead of /version
+- Use wire:navigate on sidebar
+- Use wire:navigate on dashboard
+- Use wire:navigate on projects page
+- More wire:navigate
+- Even more wire:navigate
+- Service navigation
+- Logs icons everywhere + terminal
+- Redis DB should use the new resourceable columns
+- Joomla service
+- Add back letters to prod password requirement
+- Check System and GitHub time and throw and error if it is over 50s out of sync
+- Error message and server time getting
+- Error rendering
+- Render html correctly now
+- Indent
+- Potential fix for permissions update
+- Expiration time claim ('exp') must be a numeric value
+- Sanitize html error messages
+- Production password rule and cleanup code
+- Use json as it is just better than string for huge amount of logs
+- Use `wire:navigate` on server sidebar
+- Use finished_at for the end time instead of created_at
+- Cancelled deployments should not show end and duration time
+- Redirect to server index instead of show on error in Advanced and DockerCleanup components
+- Disable registration after creating the root user
+- RootUserSeeder
+- Regex username validation
+- Add spacing around echo outputs
+- Success message
+- Silent return if envs are empty or not set.
+
+### 💼 Other
+
+- Arrrrr
+- Dep
+- Docker dep
+
+### 🚜 Refactor
+
+- Rename parameter in DatabaseBackupJob for clarity
+- Improve checkbox component accessibility and styling
+- Remove unused tags method from ApplicationDeploymentJob
+- Improve deployment status check in isAnyDeploymentInprogress function
+- Extend HorizonServiceProvider from HorizonApplicationServiceProvider
+- Streamline job status retrieval and clean up repository interface
+- Enhance ApplicationDeploymentJob and HorizonServiceProvider for improved job handling
+- Remove commented-out unsubscribe route from API
+- Update redirect calls to use a consistent navigation method in deployment functions
+- AppServiceProvider
+- Github.php
+- Improve data formatting and UI
+
+### ⚙️ Miscellaneous Tasks
+
+- Improve Penpot healthchecks
+- Switch up readonly lables to make more sense
+- Remove unused computed fields
+- Use the new job dispatch
+- Disable volume data cloning for now
+- Improve code
+- Lowcoder service naming
+- Use new functions
+- Improve error styling
+- Css
+- More css as it still looks like shit
+- Final css touches
+- Ajust time to 50s (tests done)
+- Remove debug log, finally found it
+- Remove more logging
+- Remove limit on commit message
+- Remove dayjs
+- Remove unused code and fix import
+
+## [4.0.0-beta.380] - 2024-12-27
+
+### 🚀 Features
+
+- New ServerReachabilityChanged event
+- Use new ServerReachabilityChanged event instead of isDirty
+- Add infomaniak oauth
+- Add server disk usage check frequency
+- Add environment_uuid support and update API documentation
+- Add service/resource/project labels
+- Add coolify.environment label
+- Add database subtype
+- Migrate to new encryption options
+- New encryption options
+
+### 🐛 Bug Fixes
+
+- Render html on error page correctly
+- Invalid API response on missing project
+- Applications API response code + schema
+- Applications API writing to unavailable models
+- If an init script is renamed the old version is still on the server
+- Oauthseeder
+- Compose loading seq
+- Resource clone name + volume name generation
+- Update Dockerfile entrypoint path to /etc/entrypoint.d
+- Debug mode
+- Unreachable notifications
+- Remove duplicated ServerCheckJob call
+- Few fixes and use new ServerReachabilityChanged event
+- Use serverStatus not just status
+- Oauth seeder
+- Service ui structure
+- Check port 8080 and fallback to 80
+- Refactor database view
+- Always use docker cleanup frequency
+- Advanced server UI
+- Html css
+- Fix domain being override when update application
+- Use nixpacks predefined build variables, but still could update the default values from Coolify
+- Use local monaco-editor instead of Cloudflare
+- N8n timezone
+- Smtp encryption
+- Bind() to 0.0.0.0:80 failed
+- Oauth seeder
+- Unreachable notifications
+- Instance settings migration
+- Only encrypt instance email settings if there are any
+- Error message
+- Update healthcheck and port configurations to use port 8080
+
+### 🚜 Refactor
+
+- Rename `coolify.environment` to `coolify.environmentName`
+
+### ⚙️ Miscellaneous Tasks
+
+- Regenerate API spec, removing notification fields
+- Remove ray debugging
- Version ++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version ++
-- Version++
-- Version++
-- Version++
-- Fixed typo on New Git Source view
-- Version++
-- Version++
-- Version++
-- Version++
-- Lock file + fix packages
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Update packages
-- Version++
-- Update build scripts
-- Update build packages
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Add .pnpm-store in .gitignore
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Minor changes
-- Minor changes
-- Minor changes
-- Whoops
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Update staging release
-- Version++
-- Version++
-- Add jda icon for lavalink service
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Version++
-- Update version to 4.0.0-beta.275
-- Update DNS server validation helper text
-- Dark mode should be the default
-- Improve menu item styling and spacing in service configuration and index views
-- Improve menu item styling and spacing in service configuration and index views
-- Improve menu item styling and spacing in project index and show views
-- Remove docker compose versions
-- Add Listmonk service template and logo
-- Refactor GetContainersStatus.php for improved readability and maintainability
-- Refactor ApplicationDeploymentJob.php for improved readability and maintainability
-- Add metrics and logs directories to installation script
-- Update sentinel version to 0.0.2 in versions.json
-- Update permissions on metrics and logs directories
-- Comment out server sentinel check in ServerStatusJob
-- Update version numbers to 4.0.0-beta.278
-- Update hover behavior and cursor style in scheduled task executions view
-- Refactor scheduled task view to improve code readability and maintainability
-- Skip scheduled tasks if application or service is not running
-- Remove debug logging statements in Kernel.php
-- Handle invalid cron strings in Kernel.php
-- Refactor Service.php to handle missing admin user in extraFields() method
-- Update twenty CRM template with environment variables and dependencies
-- Refactor applications.php to remove unused imports and improve code readability
-- Refactor deployment index.blade.php for improved readability and rollback handling
-- Refactor GitHub app selection UI in project creation form
-- Update ServerLimitCheckJob.php to handle missing serverLimit value
-- Remove unnecessary code for saving commit message
-- Update DOCKER_VERSION to 26.0 in install.sh script
-- Update Docker and Docker Compose versions in Dockerfiles
-- Update version numbers to 4.0.0-beta.279
-- Limit commit message length to 50 characters in ApplicationDeploymentJob
-- Update version to 4.0.0-beta.283
-- Change pre and post deployment command length in applications table
-- Refactor container name logic in GetContainersStatus.php and ForcePasswordReset.php
-- Remove unnecessary content from Docker Compose file
-- Update Sentry release version to 4.0.0-beta.287
-- Add Thompson Edolo as a sponsor
-- Add null checks for team in Stripe webhook
-- Update Sentry release version to 4.0.0-beta.288
-- Update for version 289
-- Fix formatting issue in deployment index.blade.php file
-- Remove unnecessary wire:navigate attribute in breadcrumbs.blade.php
-- Rename docker dirs
-- Update laravel/socialite to version v5.14.0 and livewire/livewire to version 3.4.9
-- Update modal styles for better user experience
-- Update deployment index.blade.php script for better performance
-- Update version numbers to 4.0.0-beta.290
-- Update version numbers to 4.0.0-beta.291
-- Update version numbers to 4.0.0-beta.292
-- Update version numbers to 4.0.0-beta.293
-- Add upgrade guide link to upgrade.blade.php
-- Improve upgrade.blade.php with clearer instructions and formatting
-- Update version numbers to 4.0.0-beta.294
-- Add Lightspeed.run as a sponsor
-- Update Dockerfile to install vim
-- Update Dockerfile with latest versions of Docker, Docker Compose, Docker Buildx, Pack, and Nixpacks
-- Update version numbers to 4.0.0-beta.295
-- Update supported OS list with almalinux
-- Update install.sh to support PopOS
-- Update install.sh script to version 1.3.2 and handle Linux Mint as Ubuntu
-- Update page title in resource index view
-- Update logo file path in logto.yaml
-- Update logo file path in logto.yaml
-- Remove commented out code for docker container removal
-- Add isAnyDeploymentInprogress function to check if any deployments are in progress
-- Add ApplicationDeploymentJob and pint.json
-- Update version numbers to 4.0.0-beta.298
-- Switch to database sessions from redis
-- Update dependencies and remove unused code
-- Update tailwindcss and vue versions in package.json
-- Update service template URL in constants.php
-- Update sentinel version to 0.0.8
-- Update chart styling and loading text
-- Update sentinel version to 0.0.9
-- Update Spanish translation for failed authentication messages
-- Add portuguese traslation
-- Add Turkish translations
-- Add Vietnamese translate
-- Add Treive logo to donations section
-- Update README.md with latest release version badge
-- Update latest release version badge in README.md
-- Update version to 4.0.0-beta.299
-- Move server delete component to the bottom of the page
-- Update version to 4.0.0-beta.301
-- Update version to 4.0.0-beta.302
-- Update version to 4.0.0-beta.303
-- Update version to 4.0.0-beta.305
-- Update version to 4.0.0-beta.306
-- Add log1x/laravel-webfonts package
-- Update version to 4.0.0-beta.307
-- Refactor ServerStatusJob constructor formatting
-- Update Monaco Editor for Docker Compose and Proxy Configuration
-- More details
-- Refactor shared.php helper functions
-- Update Plausible docker compose template to Plausible 2.1.0
-- Update Plausible docker compose template to Plausible 2.1.0
-- Update livewire/livewire dependency to version 3.4.9
-- Refactor checkIfDomainIsAlreadyUsed function
-- Update storage.blade.php view for livewire project service
-- Update version to 4.0.0-beta.310
-- Update composer dependencies
-- Add new logo for Latitude
-- Bump version to 4.0.0-beta.311
-- Update version to 4.0.0-beta.315
-- Update version to 4.0.0-beta.316
-- Update bug report template
-- Update repository form with simplified URL input field
-- Update width of container in general.blade.php
-- Update checkbox labels in general.blade.php
-- Update general page of apps
-- Handle JSON parsing errors in format_docker_command_output_to_json
-- Update Traefik image version to v2.11
-- Update version to 4.0.0-beta.317
-- Update version to 4.0.0-beta.318
-- Update helper message with link to documentation
-- Disable health check by default
-- Remove commented out code for sending internal notification
-- Update APP_BASE_URL to use SERVICE_FQDN_PLANE
-- Update resource-limits.blade.php with improved input field helpers
-- Update version numbers to 4.0.0-beta.319
-- Remove commented out code for docker image pruning
-- Collect/create/update volumes in parseDockerComposeFile function
-- Update version to 4.0.0-beta.320
-- Add pull_request image builds to GH actions
-- Add comment explaining the purpose of disconnecting the network in cleanup_unused_network_from_coolify_proxy()
-- Update formbricks template
-- Update registration view to display a notice for first user that it will be an admin
-- Update server form to use password input for IP Address/Domain field
-- Update navbar to include service status check
-- Update navbar and configuration to improve service status check functionality
-- Update workflows to include PR build and merge manifest steps
-- Update UpdateCoolifyJob timeout to 10 minutes
-- Update UpdateCoolifyJob to dispatch CheckForUpdatesJob synchronously
-- Update version to 4.0.0-beta.321
-- Update version to 4.0.0-beta.322
-- Update version to 4.0.0-beta.323
-- Update version to 4.0.0-beta.324
-- New compose parser with tests
-- Update version to 1.3.4 in install.sh and 1.0.6 in upgrade.sh
-- Update memory limit to 64MB in horizon configuration
-- Update php packages
-- Update axios npm dependency to version 1.7.5
-- Update Coolify version to 4.0.0-beta.324 and fix file paths in upgrade script
-- Update Coolify version to 4.0.0-beta.324
-- Update Coolify version to 4.0.0-beta.325
-- Update Coolify version to 4.0.0-beta.326
-- Add cd command to change directory before removing .env file
-- Update Coolify version to 4.0.0-beta.327
-- Update Coolify version to 4.0.0-beta.328
-- Update sponsor links in README.md
-- Update version.json to versions.json in GitHub workflow
-- Cleanup stucked resources and scheduled backups
-- Update GitHub workflow to use versions.json instead of version.json
-- Update GitHub workflow to use versions.json instead of version.json
-- Update GitHub workflow to use versions.json instead of version.json
-- Update GitHub workflow to use jq container for version extraction
-- Update GitHub workflow to use jq container for version extraction
-- Update UI for displaying no executions found in scheduled task list
-- Update UI for displaying deployment status in deployment list
-- Update UI for displaying deployment status in deployment list
-- Ignore unnecessary files in production build workflow
-- Update server form layout and settings
-- Update Dockerfile with latest versions of PACK and NIXPACKS
-- Update coolify-helper.yml to get version from versions.json
-- Disable Ray by default
-- Enable Ray by default and update Dockerfile with latest versions of PACK and NIXPACKS
-- Update Ray configuration and Dockerfile
-- Add middleware for updating environment variables by UUID in `api.php` routes
-- Expose port 3000 in browserless.yaml template
-- Update Ray configuration and Dockerfile
-- Update coolify version to 4.0.0-beta.331
-- Update versions.json and sentry.php to 4.0.0-beta.332
-- Update version to 4.0.0-beta.332
-- Update DATABASE_URL in plunk.yaml to use plunk database
-- Add coolify.managed=true label to Docker image builds
-- Update docker image pruning command to exclude managed images
-- Update docker cleanup schedule to run daily at midnight
-- Update versions.json to version 1.0.1
-- Update coolify-helper.yml to include "next" branch in push trigger
-- Set timeout for ServerCheckJob to 60 seconds
-- Update appwrite.yaml to include OpenSSL key variable assignment
-- Update version numbers to 4.0.0-beta.333
-- Copy .env file to .env-{DATE} if it exists
-- Update .env file with new values
-- Update server check job middleware to use server ID instead of UUID
-- Add reminder to backup .env file before running install script again
-- Copy .env file to backup location during installation script
-- Add reminder to backup .env file during installation script
-- Update permissions in pr-build.yml and version numbers
-- Add minio/mc command to Dockerfile
-- Remove itsgoingd/clockwork from require-dev in composer.json
-- Update 'key' value of gitlab in Service.php to use environment variable
-- Update release version to 4.0.0-beta.335
-- Update constants.ssh.mux_enabled in remoteProcess.php
-- Update listeners and proxy settings in server form and new server components
-- Remove unnecessary null check for proxy_type in generate_default_proxy_configuration
-- Remove unnecessary SSH command execution time logging
-- Update release version to 4.0.0-beta.336
-- Update coolify environment variable assignment with double quotes
-- Update shared.php to fix issues with source and network variables
-- Update terminal styling for better readability
-- Update button text for container connection form
-- Update Dockerfile and workflow for Coolify Realtime (v4)
-- Remove unused entrypoint script and update volume mapping
-- Update .env file and docker-compose configuration
-- Update APP_NAME environment variable in docker-compose.prod.yml
-- Update WebSocket URL in terminal.blade.php
-- Update Dockerfile and workflow for Coolify Realtime (v4)
-- Update Dockerfile and workflow for Coolify Realtime (v4)
-- Update Dockerfile and workflow for Coolify Realtime (v4)
-- Rename Command Center to Terminal in code and views
-- Update branch restriction for push event in coolify-helper.yml
-- Update terminal button text and layout in application heading view
-- Refactor terminal component and select form layout
-- Update coolify nightly version to 4.0.0-beta.335
-- Update helper version to 1.0.1
-- Fix syntax error in versions.json
-- Update version numbers to 4.0.0-beta.337
-- Update Coolify installer and scripts to include a function for fetching programming jokes
-- Update docker network connection command in ApplicationDeploymentJob.php
-- Add validation to prevent selecting 'default' server or container in RunCommand.php
-- Update versions.json to reflect latest version of realtime container
-- Update soketi image to version 1.0.1
-- Nightly - Update soketi image to version 1.0.1 and versions.json to reflect latest version of realtime container
-- Update version numbers to 4.0.0-beta.339
-- Update version numbers to 4.0.0-beta.340
-- Update version numbers to 4.0.0-beta.341
-- Update version numbers to 4.0.0-beta.342
-- Update remove-labels-and-assignees-on-close.yml
-- Add SSH key for localhost in ProductionSeeder
-- Update SSH key generation in install.sh script
-- Update ProductionSeeder to call OauthSettingSeeder and PopulateSshKeysDirectorySeeder
-- Update install.sh to support Asahi Linux
-- Update install.sh version to 1.6
-- Remove unused middleware and uniqueId method in DockerCleanupJob
-- Refactor DockerCleanupJob to remove unused middleware and uniqueId method
-- Remove unused migration file for populating SSH keys and clearing mux directory
-- Add modified files to the commit
-- Refactor pre-commit hook to improve performance and readability
-- Update CONTRIBUTING.md with troubleshooting note about database migrations
-- Refactor pre-commit hook to improve performance and readability
-- Update cleanup command to use Redis instead of queue
-- Update Docker commands to start proxy
-- Update version numbers to 4.0.0-beta.343
-- Update version numbers to 4.0.0-beta.344
-- Update version numbers to 4.0.0-beta.345
-- Update version numbers to 4.0.0-beta.346
-- Add autocomplete attribute to input fields
-- Refactor API Tokens component to use isApiEnabled flag
-- Update versions.json file
-- Remove unused .env.development.example file
-- Update API Tokens view to include link to Settings menu
-- Update web.php to cast server port as integer
-- Update backup deletion labels to use language files
-- Update database startup heading title
-- Update database startup heading title
-- Custom vite envs
-- Update version numbers to 4.0.0-beta.348
-- Refactor code to improve SSH key handling and storage
-- Update Mailpit logo to use SVG format
-- Fix docs link in running state
-- Update Coolify Realtime workflow to only trigger on the main branch
-- Refactor instanceSettings() function to improve code readability
-- Update Coolify Realtime image to version 1.0.2
-- Remove unnecessary code in DatabaseBackupJob.php
-- Add "Not Usable" indicator for storage items
-- Refactor instanceSettings() function and improve code readability
-- Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350
-- Update version numbers to 4.0.0-beta.350 in configuration files
-- Update command signature and description for cleanup application deployment queue
-- Add missing import for Attribute class in ApplicationDeploymentQueue model
-- Update modal input in server form to prevent closing on outside click
-- Remove unnecessary command from SshMultiplexingHelper
-- Remove commented out code for uploading to S3 in DatabaseBackupJob
-- Update soketi service image to version 1.0.3
-- Update version to 4.0.0-beta.352
-- Refactor DatabaseBackupJob to handle missing team
-- Update version to 4.0.0-beta.353
-- Update service application view
-- Update version to 4.0.0-beta.354
-- Remove debug statement in Service model
-- Remove commented code in Server model
-- Fix application deployment queue filter logic
-- Refactor modal-confirmation component
-- Update it-tools service template and port configuration
-- Update homarr service template and remove unnecessary code
-- Update homarr service template and remove unnecessary code
-- Update version to 4.0.0-beta.355
-- Update version to 4.0.0-beta.356
-- Remove commented code for shared variable type validation
-- Update MariaDB image to version 11 and fix service environment variable orders
-- Update anythingllm.yaml volumes configuration
-- Update proxy configuration paths for Caddy and Nginx in dev
-- Update password form submission in modal-confirmation component
-- Update project query to order by name in uppercase
-- Update project query to order by name in lowercase
-- Update select.blade.php with improved search functionality
-- Add Nitropage service template and logo
-- Bump coolify-helper version to 1.0.2
-- Refactor loadServices2 method and remove unused code
-- Update version to 4.0.0-beta.357
-- Update service names and volumes in windmill.yaml
-- Update version to 4.0.0-beta.358
-- Ignore .ignition.json files in Docker and Git
-- Add mattermost logo as svg
-- Add mattermost svg to compose
-- Update version to 4.0.0-beta.357
-- Fix form submission and keydown event handling in modal-confirmation.blade.php
-- Update version numbers to 4.0.0-beta.359 in configuration files
-- Disable adding default environment variables in shared.php
-- Update laravel/horizon dependency to version 5.29.1
-- Update service extra fields to use dynamic keys
-- Update livewire/livewire dependency to version 3.4.9
-- Add transmission template desc
-- Update transmission docs link
-- Update version numbers to 4.0.0-beta.360 in configuration files
-- Update AWS environment variable names in unsend.yaml
-- Update AWS environment variable names in unsend.yaml
-- Update livewire/livewire dependency to version 3.4.9
-- Update version to 4.0.0-beta.361
-- Update Docker build and push actions to v6
-- Update Docker build and push actions to v6
-- Update Docker build and push actions to v6
-- Sync coolify-helper to dockerhub as well
-- Push realtime to dockerhub
-- Sync coolify-realtime to dockerhub
-- Rename workflows
-- Rename development to staging build
-- Sync coolify-testing-host to dockerhbu
-- Sync coolify prod image to dockerhub as well
-- Update Docker version to 26.0
-- Update project resource index page
-- Update project service configuration view
-- Edit www helper
-- Update dep
+
+## [4.0.0-beta.378] - 2024-12-13
+
+### 🐛 Bug Fixes
+
+- Monaco editor light and dark mode switching
+- Service status indicator + oauth saving
+- Socialite for azure and authentik
+- Saving oauth
+- Fallback for copy button
+- Copy the right text
+- Maybe fallback is now working
+- Only show copy button on secure context
+
+## [4.0.0-beta.377] - 2024-12-13
+
+### 🚀 Features
+
+- Add deploy-only token permission
+- Able to deploy without cache on every commit
+- Update private key nam with new slug as well
+- Allow disabling default redirect, set status to 503
+- Add TLS configuration for default redirect in Server model
+- Slack notifications
+- Introduce root permission
+- Able to download schedule task logs
+- Migrate old email notification settings from the teams table
+- Migrate old discord notification settings from the teams table
+- Migrate old telegram notification settings from the teams table
+- Add slack notifications to a new table
+- Enable success messages again
+- Use new notification stuff inside team model
+- Some more notification settings and better defaults
+- New email notification settings
+- New shared function name `is_transactional_emails_enabled()`
+- New shared notifications functions
+- Email Notification Settings Model
+- Telegram notification settings Model
+- Discord notification settings Model
+- Slack notification settings Model
+- New Discord notification UI
+- New Slack notification UI
+- New telegram UI
+- Use new notification event names
+- Always sent notifications
+- Scheduled task success notification
+- Notification trait
+- Get discord Webhook form new table
+- Get Slack Webhook form new table
+- Use new table or instance settings for email
+- Use new place for settings and topic IDs for telegram
+- Encrypt instance email settings
+- Use encryption in instance settings model
+- Scheduled task success and failure notifications
+- Add docker cleanup success and failure notification settings columns
+- UI for docker cleanup success and failure notification
+- Docker cleanup email views
+- Docker cleanup success and failure notification files
+- Scheduled task success email
+- Send new docker cleanup notifications
+- :passport_control: integrate Authentik authentication with Coolify
+- *(notification)* Add Pushover
+- Add seeder command and configuration for database seeding
+- Add new password magic env with symbols
+- Add documenso service
+
+### 🐛 Bug Fixes
+
+- Resolve undefined searchInput reference in Alpine.js component
+- URL and sync new app name
+- Typos and naming
+- Client and webhook secret disappear after sync
+- Missing `mysql_password` API property
+- Incorrect MongoDB init API property
+- Old git versions does not have --cone implemented properly
+- Don't allow editing traefik config
+- Restart proxy
+- Dev mode
+- Ui
+- Display actual values for disk space checks in installer script
+- Proxy change behaviour
+- Add warning color
+- Import NotificationSlack correctly
+- Add middleware to new abilities, better ux for selecting permissions, etc.
+- Root + read:sensive could read senstive data with a middlewarew
+- Always have download logs button on scheduled tasks
+- Missing css
+- Development image
+- Dockerignore
+- DB migration error
+- Drop all unused smtp columns
+- Backward compatibility
+- Email notification channel enabled function
+- Instance email settins
+- Make sure resend is false if SMTP is true and vice versa
+- Email Notification saving
+- Slack and discord url now uses text filed because encryption makes the url very long
+- Notification trait
+- Encryption fixes
+- Docker cleanup email template
+- Add missing deployment notifications to telegram
+- New docker cleanup settings are now saved to the DB correctly
+- Ui + migrations
+- Docker cleanup email notifications
+- General notifications does not go through email channel
+- Test notifications to only send it to the right channel
+- Remove resale_license from db as well
+- Nexus service
+- Fileflows volume names
+- --cone
+- Provider error
+- Database migration
+- Seeder
+- Migration call
+- Slack helper
+- Telegram helper
+- Discord helper
+- Telegram topic IDs
+- Make pushover settings more clear
+- Typo in pushover user key
+- Use Livewire refresh method and lock properties
+- Create pushover settings for existing teams
+- Update token permission check from 'write' to 'root'
+- Pushover
+- Oauth seeder
+- Correct heading display for OAuth settings in settings-oauth.blade.php
+- Adjust spacing in login form for improved layout
+- Services env values should be sensitive
+- Documenso
+- Dolibarr
+- Typo
+- Update OauthSettingSeeder to handle new provider definitions and ensure authentik is recreated if missing
+- Improve OauthSettingSeeder to correctly delete non-existent providers and ensure proper handling of provider definitions
+- Encrypt resend API key in instance settings
+- Resend api key is already a text column
+
+### 💼 Other
+
+- Test rename GitHub app
+- Checkmate service and fix prowlar slogan (too long)
+
+### 🚜 Refactor
+
+- Update Traefik configuration for improved security and logging
+- Improve proxy configuration and code consistency in Server model
+- Rename name method to sanitizedName in BaseModel for clarity
+- Improve migration command and enhance application model with global scope and status checks
+- Unify notification icon
+- Remove unused Azure and Authentik service configurations from services.php
+- Change email column types in instance_settings migration from string to text
+- Change OauthSetting creation to updateOrCreate for better handling of existing records
+
+### ⚙️ Miscellaneous Tasks
+
- Regenerate openapi spec
- Composer dep bump
- Dep bump
@@ -5483,205 +3034,6043 @@ ### ⚙️ Miscellaneous Tasks
- Reorder navbar
- Rename topicID to threadId like in the telegram API response
- Update PHP configuration to set memory limit using environment variable
-- Regenerate API spec, removing notification fields
-- Remove ray debugging
-- Version ++
-- Improve Penpot healthchecks
-- Switch up readonly lables to make more sense
-- Remove unused computed fields
-- Use the new job dispatch
-- Disable volume data cloning for now
-- Improve code
-- Lowcoder service naming
-- Use new functions
-- Improve error styling
-- Css
-- More css as it still looks like shit
-- Final css touches
-- Ajust time to 50s (tests done)
-- Remove debug log, finally found it
-- Remove more logging
-- Remove limit on commit message
-- Remove dayjs
-- Remove unused code and fix import
-- *(dep)* Bump nixpacks version
-- *(dep)* Version++
-- *(dep)* Bump helper version to 1.0.5
-- *(docker)* Add blank line for readability in Dockerfile
-- *(versions)* Update coolify versions to v4.0.0-beta.388
-- *(versions)* Update coolify versions to v4.0.0-beta.389 and add helper version retrieval script
-- *(versions)* Update coolify versions to v4.0.0-beta.389
-- *(core)* EnvironmentVariable Model now extends BaseModel to remove duplicated code
-- *(versions)* Update coolify versions to v4.0.0-beta.3909
-- *(version)* Bump Coolify version to 4.0.0-beta.391
-- *(config)* Increase default PHP memory limit to 256M
-- Add openapi response
-- *(workflows)* Make naming more clear and remove unused code
-- Bump Coolify version to 4.0.0-beta.392/393
-- *(ci)* Update changelog generation workflow to target 'next' branch
-- *(ci)* Update changelog generation workflow to target main branch
-- Rollback Coolify version to 4.0.0-beta.392
-- Bump Coolify version to 4.0.0-beta.393
-- Bump Coolify version to 4.0.0-beta.394
-- Bump Coolify version to 4.0.0-beta.395
-- Bump Coolify version to 4.0.0-beta.396
-- *(services)* Update zipline to use new Database env var. (#5210)
-- *(service)* Upgrade authentik service
-- *(service)* Remove unused env from zipline
-- Bump helper and realtime version
-- *(migration)* Remove unused columns
-- *(ssl)* Improve code in ssl helper
-- *(migration)* Ssl cert and key should not be nullable
-- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts
-- Rename ca crt folder to ssl
-- *(ui)* Improve valid until handling
-- Improve code quality suggested by code rabbit
-- *(supabase)* Update Supabase service template and Postgres image version
-- *(versions)* Update version numbers for coolify and nightly
-- *(versions)* Update version numbers for coolify and nightly
-- *(service)* Update minecraft service ENVs
-- *(service)* Add more vars to infisical.yaml (#5418)
-- *(service)* Add google variables to plausible.yaml (#5429)
-- *(service)* Update authentik.yaml versions (#5373)
-- *(core)* Remove redocs
-- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
-- *(service)* Remove unused code in Bugsink service
-- *(versions)* Update version to 404
-- *(versions)* Bump version to 403 (#5520)
-- *(versions)* Bump version to 404
-- *(versions)* Bump version to 406
-- *(versions)* Bump version to 407
-- *(versions)* Bump version to 406
-- *(versions)* Bump version to 407 and 408 for coolify and nightly
-- *(versions)* Bump version to 408 for coolify and 409 for nightly
-- *(versions)* Update nightly version to 4.0.0-beta.410
-- *(pre-commit)* Remove OpenAPI generation command from pre-commit hook
-- *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json
-- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files
-- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files
-- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service
-- *(versions)* Update coolify version to 4.0.0-beta.411 and nightly version to 4.0.0-beta.412 in configuration files
-- *(versions)* Update coolify version to 4.0.0-beta.412 and nightly version to 4.0.0-beta.413 in configuration files
-- *(versions)* Update coolify version to 4.0.0-beta.413 and nightly version to 4.0.0-beta.414 in configuration files
-- *(versions)* Update realtime version to 1.0.8 in versions.json
-- *(versions)* Update realtime version to 1.0.8 in versions.json
-- *(docker)* Update soketi image version to 1.0.8 in production configuration files
-- *(versions)* Update coolify version to 4.0.0-beta.414 and nightly version to 4.0.0-beta.415 in configuration files
-- *(workflows)* Adjust workflow for announcement
-- *(versions)* Update coolify version to 4.0.0-beta.416 and nightly version to 4.0.0-beta.417 in configuration files; fix links in deployment view
-- *(seeder)* Update git branch from 'main' to 'v4.x' for multiple examples in ApplicationSeeder
-- *(versions)* Update coolify version to 4.0.0-beta.417 and nightly version to 4.0.0-beta.418
-- *(versions)* Update coolify version to 4.0.0-beta.418
-- *(versions)* Update coolify version to 4.0.0-beta.419 and nightly version to 4.0.0-beta.420 in configuration files
-- *(service)* Rename hoarder server to karakeep (#5607)
-- *(service)* Update Supabase services (#5708)
-- *(service)* Remove unused documenso env
-- *(service)* Formatting and cleanup of ryot
-- *(docs)* Remove changelog and add it to gitignore
-- *(versions)* Update version to 4.0.0-beta.419
-- *(service)* Diun formatting
-- *(docs)* Update CHANGELOG.md
-- *(service)* Switch convex vars
-- *(service)* Pgbackweb formatting and naming update
-- *(service)* Remove typesense default API key
-- *(service)* Format yamtrack healthcheck
-- *(core)* Remove unused function
-- *(ui)* Remove unused stopEvent code
-- *(service)* Remove unused env
-- *(tests)* Update test environment database name and add new feature test for converting container environment variables to array
-- *(service)* Update Immich service (#5886)
-- *(service)* Remove unused logo
-- *(api)* Update API docs
-- *(dependencies)* Update package versions in composer.json and composer.lock for improved compatibility and performance
-- *(dependencies)* Update package versions in package.json and package-lock.json for improved stability and features
-- *(version)* Update coolify-realtime to version 1.0.9 in docker-compose and versions files
-- *(version)* Update coolify version to 4.0.0-beta.420 and nightly version to 4.0.0-beta.421
-- *(service)* Changedetection remove unused code
-- *(service)* Update Evolution API image to the official one (#6031)
-- *(versions)* Bump coolify versions to v4.0.0-beta.420 and v4.0.0-beta.421
-- *(dependencies)* Update composer dependencies to latest versions including resend-laravel to ^0.19.0 and aws-sdk-php to 3.347.0
-- *(versions)* Update Coolify version to 4.0.0-beta.420.1 and add new services (karakeep, miniflux, pingvinshare) to service templates
-- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files
-- *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively
-- *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively
-- *(service)* Update Nitropage template (#6181)
-- *(versions)* Update all version
-- *(bump)* Update composer deps
-- *(version)* Bump Coolify version to 4.0.0-beta.420.6
-- *(service)* Improve matrix service
-- *(service)* Format runner service
-- *(service)* Improve sequin
-- *(service)* Add `NOT_SECURED` env to Postiz (#6243)
-- *(service)* Improve evolution-api environment variables (#6283)
-- *(service)* Update Langfuse template to v3 (#6301)
-- *(core)* Remove unused argument
-- *(deletion)* Rename isDeleteOperation to deleteConnectedNetworks
-- *(docker)* Remove unused arguments on StopService
-- *(service)* Homebox formatting
-- Clarify usage of custom redis configuration (#6321)
-- *(changelogs)* Add .gitignore for changelogs directory and remove outdated changelog files for May, June, and July 2025
-- *(service)* Change affine images (#6366)
-- Elasticsearch URL, fromatting and add category
-- Update service-templates json files
-- *(docs)* Remove AGENTS.md file; enhance CLAUDE.md with detailed form authorization patterns and service configuration examples
-- *(cleanup)* Remove unused GitLab view files for change, new, and show pages
-- *(workflows)* Add backlog directory to build triggers for production and staging workflows
-- *(config)* Disable auto_commit in backlog configuration to prevent automatic commits
-- *(versions)* Update coolify version to 4.0.0-beta.420.8 and nightly version to 4.0.0-beta.420.9 in versions.json and constants.php
-- *(docker)* Update soketi image version to 1.0.10 in production and Windows configurations
-- *(core)* Update version
-- *(core)* Update version
-- *(versions)* Update coolify version to 4.0.0-beta.421 and nightly version to 4.0.0-beta.422
-- Update version
-- Update development node version
-- Update coolify version to 4.0.0-beta.423 and nightly version to 4.0.0-beta.424
-- Update coolify version to 4.0.0-beta.424 and nightly version to 4.0.0-beta.425
-- Update coolify version to 4.0.0-beta.425 and nightly version to 4.0.0-beta.426
-- Update coolify version to 4.0.0-beta.426 and nightly version to 4.0.0-beta.427
-- Update coolify version to 4.0.0-beta.427 and nightly version to 4.0.0-beta.428
-- Use main value then fallback to service_ values
-- Remove webhooks table cleanup
-- *(cleanup)* Remove deprecated ServerCheck and related job classes to streamline codebase
-- *(versions)* Update sentinel version from 0.0.15 to 0.0.16 in versions.json files
-- *(constants)* Update realtime_version from 1.0.10 to 1.0.11
-- *(versions)* Increment coolify version to 4.0.0-beta.428 and update realtime_version to 1.0.10
-- *(docker)* Add a blank line for improved readability in Dockerfile
-- *(versions)* Bump coolify version to 4.0.0-beta.429 and nightly version to 4.0.0-beta.430
-- Change order of runtime and buildtime
-- *(docker-compose)* Update soketi image version to 1.0.10 in production and Windows configurations
-- *(versions)* Update coolify version numbers to 4.0.0-beta.430 and 4.0.0-beta.431 in configuration files
-- *(versions)* Increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files
-- *(versions)* Update coolify version numbers to 4.0.0-beta.432 and 4.0.0-beta.433 in configuration files
-- Remove unused files
-- Adjust wording
-- *(workflow)* Update pull request trigger to pull_request_target and refine permissions for enhanced security
-- *(application)* Remove debugging statement from loadComposeFile method
-- *(workflows)* Update Claude GitHub Action configuration to support new event types and improve permissions
-- *(versions)* Update coolify version to 4.0.0-beta.433 and nightly version to 4.0.0-beta.434 in configuration files
-- *(versions)* Update version numbers for Coolify releases
-- *(versions)* Bump Coolify stable version to 4.0.0-beta.434
-- *(versions)* Update Coolify version numbers to 4.0.0-beta.435 and 4.0.0-beta.436
-- Update package-lock.json
-- *(service)* Update convex template and image
-- *(signoz)* Remove unused ports
-- *(signoz)* Bump version to 0.77.0
-- *(signoz)* Bump version to 0.78.1
-- Add category field to siyuan.yaml
-- Update siyuan category in service templates
+
+## [4.0.0-beta.376] - 2024-12-07
+
+### 🐛 Bug Fixes
+
+- Api endpoint
+
+## [4.0.0-beta.374] - 2024-12-03
+
+### 🐛 Bug Fixes
+
+- Application view loading
+- Postiz service
+- Only able to select the right keys
+- Test email should not be required
+- A few inputs
+
+### 🧪 Testing
+
+- Setup database for upcoming tests
+
+## [4.0.0-beta.372] - 2024-11-26
+
+### 🚀 Features
+
+- Add MacOS template
+- Add Windows template
+- *(service)* :sparkles: add mealie
+- Add hex magic env var
+
+### 🐛 Bug Fixes
+
+- Service generate includes yml files as well (haha)
+- ServercheckJob should run every 5 minutes on cloud
+- New resource icons
+- Search should be more visible on scroll on new resource
+- Logdrain settings
+- Ui
+- Email should be retried with backoff
+- Alpine in body layout
+
+### 💼 Other
+
+- Caddy docker labels do not honor "strip prefix" option
+
+## [4.0.0-beta.371] - 2024-11-22
+
+### 🐛 Bug Fixes
+
+- Improve helper text for metrics input fields
+- Refine helper text for metrics input fields
+- If mux conn fails, still use it without mux + save priv key with better logic
+- Migration
+- Always validate ssh key
+- Make sure important jobs/actions are running on high prio queue
+- Do not send internal notification for backups and status jobs
+- Validateconnection
+- View issue
+- Heading
+- Remove mux cleanup
+- Db backup for services
+- Version should come from constants + fix stripe webhook error reporting
+- Undefined variable
+- Remove version.php as everything is coming from constants.php
+- Sentry error
+- Websocket connections autoreconnect
+- Sentry error
+- Sentry
+- Empty server API response
+- Incorrect server API patch response
+- Missing `uuid` parameter on server API patch
+- Missing `settings` property on servers API
+- Move servers API `delete_unused_*` properties
+- Servers API returning `port` as a string -> integer
+- Only return server uuid on server update
+
+## [4.0.0-beta.370] - 2024-11-15
+
+### 🐛 Bug Fixes
+
+- Modal (+ add) on dynamic config was not opening, removed x-cloak
+- AUTOUPDATE + checkbox opacity
+
+## [4.0.0-beta.369] - 2024-11-15
+
+### 🐛 Bug Fixes
+
+- Modal-input
+
+## [4.0.0-beta.368] - 2024-11-15
+
+### 🚀 Features
+
+- Check local horizon scheduler deployments
+- Add internal api docs to /docs/api with auth
+- Add proxy type change to create/update apis
+
+### 🐛 Bug Fixes
+
+- Show proper error message on invalid Git source
+- Convert HTTP to SSH source when using deploy key on GitHub
+- Cloud + stripe related
+- Terminal view loading in async
+- Cool 500 error (thanks hugodos)
+- Update schema in code decorator
+- Openapi docs
+- Add tests for git url converts
+- Minio / logto url generation
+- Admin view
+- Min docker version 26
+- Pull latest service-templates.json on init
+- Workflow files for coolify build
+- Autocompletes
+- Timezone settings validation
+- Invalid tz should not prevent other jobs to be executed
+- Testing-host should be built locally
+- Poll with modal issue
+- Terminal opening issue
+- If service img not found, use github as a source
+- Fallback to local coolify.png
+- Gather private ips
+- Cf tunnel menu should be visible when server is not validated
+- Deployment optimizations
+- Init script + optimize laravel
+- Default docker engine version + fix install script
+- Pull helper image on init
+- SPA static site default nginx conf
+
+### 💼 Other
+
+- Https://github.com/coollabsio/coolify/issues/4186
+- Separate resources by type in projects view
+- Improve s3 add view
+
+### ⚙️ Miscellaneous Tasks
+
+- Update dep
+
+## [4.0.0-beta.365] - 2024-11-11
+
+### 🚀 Features
+
+- Custom nginx configuration for static deployments + fix 404 redirects in nginx conf
+
+### 🐛 Bug Fixes
+
+- Trigger.dev db host & sslmode=disable
+- Manual update should be executed only once + better UX
+- Upgrade.sh
+- Missing privateKey
+
+## [4.0.0-beta.364] - 2024-11-08
+
+### 🐛 Bug Fixes
+
+- Define separate volumes for mattermost service template
+- Github app name is too long
+- ServerTimezone update
+
+### ⚙️ Miscellaneous Tasks
+
+- Edit www helper
+
+## [4.0.0-beta.363] - 2024-11-08
+
+### 🚀 Features
+
+- Add Firefox template
+- Add template for Wiki.js
+- Add upgrade logs to /data/coolify/source
+
+### 🐛 Bug Fixes
+
+- Saving resend api key
+- Wildcard domain save
+- Disable cloudflare tunnel on "localhost"
+
+## [4.0.0-beta.362] - 2024-11-08
+
+### 🐛 Bug Fixes
+
+- Notifications ui
+- Disable wire:navigate
+- Confirmation Settings css for light mode
+- Server wildcard
+
+## [4.0.0-beta.361] - 2024-11-08
+
+### 🚀 Features
+
+- Add Transmission template
+- Add transmission healhcheck
+- Add zipline template
+- Dify template
+- Required envs
+- Add EdgeDB
+- Show warning if people would like to use sslip with https
+- Add is shared to env variables
+- Variabel sync and support shared vars
+- Add notification settings to server_disk_usage
+- Add coder service tamplate and logo
+- Debug mode for sentinel
+- Add jitsi template
+- Add --gpu support for custom docker command
+
+### 🐛 Bug Fixes
+
+- Make sure caddy is not removed by cleanup
+- Libretranslate
+- Do not allow to change number of lines when streaming logs
+- Plunk
+- No manual timezones
+- Helper push
+- Format
+- Add port metadata and Coolify magic to generate the domain
+- Sentinel
+- Metrics
+- Generate sentinel url
+- Only enable Sentinel for new servers
+- Is_static through API
+- Allow setting standalone redis variables via ENVs (team variables...)
+- Check for username separately form password
+- Encrypt all existing redis passwords
+- Pull helper image on helper_version change
+- Redis database user and password
+- Able to update ipv4 / ipv6 instance settings
+- Metrics for dbs
+- Sentinel start fixed
+- Validate sentinel custom URL when enabling sentinel
+- Should be able to reset labels in read-only mode with manual click
+- No sentinel for swarm yet
+- Charts ui
+- Volume
+- Sentinel config changes restarts sentinel
+- Disable sentinel for now
+- Disable Sentinel temporarily
+- Disable Sentinel temporarily for non-dev environments
+- Access team's github apps only
+- Admins should now invite owner
+- Add experimental flag
+- GenerateSentinelUrl method
+- NumberOfLines could be null
+- Login / register view
+- Restart sentinel once a day
+- Changing private key manually won't trigger a notification
+- Grammar for helper
+- Fix my own grammar
+- Add telescope only in dev mode
+- New way to update container statuses
+- Only run server storage every 10 mins if sentinel is not active
+- Cloud admin view
+- Queries in kernel.php
+- Lower case emails only
+- Change emails to lowercase on init
+- Do not error on update email
+- Always authenticate with lowercase emails
+- Dashboard refactor
+- Add min/max length to input/texarea
+- Remove livewire legacy from help view
+- Remove unnecessary endpoints (magic)
+- Transactional email livewire
+- Destinations livewire refactor
+- Refactor destination/docker view
+- Logdrains validation
+- Reworded
+- Use Auth(), add new db proxy stop event refactor clickhouse view
+- Add user/pw to db view
+- Sort servers by name
+- Keydb view
+- Refactor tags view / remove obsolete one
+- Send discord/telegram notifications on high job queue
+- Server view refresh on validation
+- ShowBoarding
+- Show docker installation logs & ubuntu 24.10 notification
+- Do not overlap servercheckjob
+- Server limit check
+- Server validation
+- Clear route / view
+- Only skip docker installation on 24.10 if its not installed
+- For --gpus device support
+- Db/service start should be on high queue
+- Do not stop sentinel on Coolify restart
+- Run resourceCheck after new serviceCheckJob
+- Mongodb in dev
+- Better invitation errors
+- Loading indicator for db proxies
+- Do not execute gh workflow on template changes
+- Only use sentry in cloud
+- Update packagejson of coolify-realtime + add lock file
+- Update last online with old function
+- Seeder should not start sentinel
+- Start sentinel on seeder
+
+### 💼 Other
+
+- Add peppermint
+- Loggy
+- Add UI for redis password and username
+- Wireguard-easy template
+
+### 📚 Documentation
+
+- Update link to deploy api docs
+
+### ⚙️ Miscellaneous Tasks
+
+- Add transmission template desc
+- Update transmission docs link
+- Update version numbers to 4.0.0-beta.360 in configuration files
+- Update AWS environment variable names in unsend.yaml
+- Update AWS environment variable names in unsend.yaml
+- Update livewire/livewire dependency to version 3.4.9
+- Update version to 4.0.0-beta.361
+- Update Docker build and push actions to v6
+- Update Docker build and push actions to v6
+- Update Docker build and push actions to v6
+- Sync coolify-helper to dockerhub as well
+- Push realtime to dockerhub
+- Sync coolify-realtime to dockerhub
+- Rename workflows
+- Rename development to staging build
+- Sync coolify-testing-host to dockerhbu
+- Sync coolify prod image to dockerhub as well
+- Update Docker version to 26.0
+- Update project resource index page
+- Update project service configuration view
+
+## [4.0.0-beta.360] - 2024-10-11
+
+### ⚙️ Miscellaneous Tasks
+
+- Update livewire/livewire dependency to version 3.4.9
+
+## [4.0.0-beta.359] - 2024-10-11
+
+### 🐛 Bug Fixes
+
+- Use correct env variable for invoice ninja password
+
+### ⚙️ Miscellaneous Tasks
+
+- Update laravel/horizon dependency to version 5.29.1
+- Update service extra fields to use dynamic keys
+
+## [4.0.0-beta.358] - 2024-10-10
+
+### 🚀 Features
+
+- Add customHelper to stack-form
+- Add cloudbeaver template
+- Add ntfy template
+- Add qbittorrent template
+- Add Homebox template
+- Add owncloud service and logo
+- Add immich service
+- Auto generate url
+- Refactored to work with coolify auto env vars
+- Affine service template and logo
+- Add LibreTranslate template
+- Open version in a new tab
+
+### 🐛 Bug Fixes
+
+- Signup
+- Application domains should be http and https only
+- Validate and sanitize application domains
+- Sanitize and validate application domains
+
+### 💼 Other
+
+- Other DB options for freshrss
+- Nextcloud MariaDB and MySQL versions
+
+### ⚙️ Miscellaneous Tasks
+
+- Fix form submission and keydown event handling in modal-confirmation.blade.php
+- Update version numbers to 4.0.0-beta.359 in configuration files
+- Disable adding default environment variables in shared.php
+
+## [4.0.0-beta.357] - 2024-10-08
+
+### 🚀 Features
+
+- Add Mautic 4 and 5 to service templates
+- Add keycloak template
+- Add onedev template
+- Improve search functionality in project selection
+
+### 🐛 Bug Fixes
+
+- Update mattermost image tag and add default port
+- Remove env, change timezone
+- Postgres healthcheck
+- Azimutt template - still not working haha
+- New parser with SERVICE_URL_ envs
+- Improve service template readability
+- Update password variables in Service model
+- Scheduled database server
+- Select server view
+
+### 💼 Other
+
+- Keycloak
+
+### ⚙️ Miscellaneous Tasks
+
+- Add mattermost logo as svg
+- Add mattermost svg to compose
+- Update version to 4.0.0-beta.357
+
+## [4.0.0-beta.356] - 2024-10-07
+
+### 🚀 Features
+
+- Add Argilla service configuration to Service model
+- Add Invoice Ninja service configuration to Service model
+- Project search on frontend
+- Add ollama service with open webui and logo
+- Update setType method to use slug value for type
+- Refactor setType method to use slug value for type
+- Refactor setType method to use slug value for type
+- Add Supertokens template
+- Add easyappointments service template
+- Add dozzle template
+- Adds forgejo service with runners
+
+### 🐛 Bug Fixes
+
+- Reset description and subject fields after submitting feedback
+- Tag mass redeployments
+- Service env orders, application env orders
+- Proxy conf in dev
+- One-click services
+- Use local service-templates in dev
+- New services
+- Remove not used extra host
+- Chatwoot service
+- Directus
+- Database descriptions
+- Update services
+- Soketi
+- Select server view
+
+### 💼 Other
+
+- Update helper version
+- Outline
+- Directus
+- Supertokens
+- Supertokens json
+- Rabbitmq
+- Easyappointments
+- Soketi
+- Dozzle
+- Windmill
+- Coolify.json
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.356
+- Remove commented code for shared variable type validation
+- Update MariaDB image to version 11 and fix service environment variable orders
+- Update anythingllm.yaml volumes configuration
+- Update proxy configuration paths for Caddy and Nginx in dev
+- Update password form submission in modal-confirmation component
+- Update project query to order by name in uppercase
+- Update project query to order by name in lowercase
+- Update select.blade.php with improved search functionality
+- Add Nitropage service template and logo
+- Bump coolify-helper version to 1.0.2
+- Refactor loadServices2 method and remove unused code
+- Update version to 4.0.0-beta.357
+- Update service names and volumes in windmill.yaml
+- Update version to 4.0.0-beta.358
+- Ignore .ignition.json files in Docker and Git
+
+## [4.0.0-beta.355] - 2024-10-03
+
+### 🐛 Bug Fixes
+
+- Scheduled backup for services view
+- Parser, espacing container labels
+
+### ⚙️ Miscellaneous Tasks
+
+- Update homarr service template and remove unnecessary code
+- Update version to 4.0.0-beta.355
+
+## [4.0.0-beta.354] - 2024-10-03
+
+### 🚀 Features
+
+- Add it-tools service template and logo
+- Add homarr service tamplate and logo
+
+### 🐛 Bug Fixes
+
+- Parse proxy config and check the set ports usage
+- Update FQDN
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.354
+- Remove debug statement in Service model
+- Remove commented code in Server model
+- Fix application deployment queue filter logic
+- Refactor modal-confirmation component
+- Update it-tools service template and port configuration
+- Update homarr service template and remove unnecessary code
+
+## [4.0.0-beta.353] - 2024-10-03
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.353
+- Update service application view
+
+## [4.0.0-beta.352] - 2024-10-03
+
+### 🐛 Bug Fixes
+
+- Service application view
+- Add new supported database images
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.352
+- Refactor DatabaseBackupJob to handle missing team
+
+## [4.0.0-beta.351] - 2024-10-03
+
+### 🚀 Features
+
+- Add strapi template
+
+### 🐛 Bug Fixes
+
+- Able to support more database dynamically from Coolify's UI
+- Strapi template
+- Bitcoin core template
+- Api useBuildServer
+
+## [4.0.0-beta.349] - 2024-10-01
+
+### 🚀 Features
+
+- Add command to check application deployment queue
+- Support Hetzner S3
+- Handle HTTPS domain in ConfigureCloudflareTunnels
+- Backup all databases for mysql,mariadb,postgresql
+- Restart service without pulling the latest image
+
+### 🐛 Bug Fixes
+
+- Remove autofocuses
+- Ipv6 scp should use -6 flag
+- Cleanup stucked applicationdeploymentqueue
+- Realtime watch in development mode
+- Able to select root permission easier
+
+### 💼 Other
+
+- Show backup button on supported db service stacks
+
+### 🚜 Refactor
+
+- Remove deployment queue when deleting an application
+- Improve SSH command generation in Terminal.php and terminal-server.js
+- Fix indentation in modal-confirmation.blade.php
+- Improve parsing of commands for sudo in parseCommandsByLineForSudo
+- Improve popup component styling and button behavior
+- Encode delimiter in SshMultiplexingHelper
+- Remove inactivity timer in terminal-server.js
+- Improve socket reconnection interval in terminal.js
+- Remove unnecessary watch command from soketi service entrypoint
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.350 in configuration files
+- Update command signature and description for cleanup application deployment queue
+- Add missing import for Attribute class in ApplicationDeploymentQueue model
+- Update modal input in server form to prevent closing on outside click
+- Remove unnecessary command from SshMultiplexingHelper
+- Remove commented out code for uploading to S3 in DatabaseBackupJob
+- Update soketi service image to version 1.0.3
+
+## [4.0.0-beta.348] - 2024-10-01
+
+### 🚀 Features
+
+- Update resource deletion job to allow configurable options through API
+- Add query parameters for deleting configurations, volumes, docker cleanup, and connected networks
+
+### 🐛 Bug Fixes
+
+- In dev mode do not ask confirmation on delete
+- Mixpost
+- Handle deletion of 'hello' in confirmation modal for dev environment
+
+### 💼 Other
+
+- Server storage check
+
+### 🚜 Refactor
+
+- Update search input placeholder in resource index view
+
+### ⚙️ Miscellaneous Tasks
+
+- Fix docs link in running state
+- Update Coolify Realtime workflow to only trigger on the main branch
+- Refactor instanceSettings() function to improve code readability
+- Update Coolify Realtime image to version 1.0.2
+- Remove unnecessary code in DatabaseBackupJob.php
+- Add "Not Usable" indicator for storage items
+- Refactor instanceSettings() function and improve code readability
+- Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350
+
+## [4.0.0-beta.347] - 2024-09-28
+
+### 🚀 Features
+
+- Allow specify use_build_server when creating/updating an application
+- Add support for `use_build_server` in API endpoints for creating/updating applications
+- Add Mixpost template
+
+### 🐛 Bug Fixes
+
+- Filebrowser template
+- Edit is_build_server_enabled upon creating application on other application type
+- Save settings after assigning value
+
+### 💼 Other
+
+- Remove memlock as it caused problems for some users
+
+### ⚙️ Miscellaneous Tasks
+
+- Update Mailpit logo to use SVG format
+
+## [4.0.0-beta.346] - 2024-09-27
+
+### 🚀 Features
+
+- Add ContainerStatusTypes enum for managing container status
+
+### 🐛 Bug Fixes
+
+- Proxy fixes
+- Proxy
+- *(templates)* Filebrowser FQDN env variable
+- Handle edge case when build variables and env variables are in different format
+- Compose based terminal
+
+### 💼 Other
+
+- Manual cleanup button and unused volumes and network deletion
+- Force helper image removal
+- Use the new confirmation flow
+- Typo
+- Typo in install script
+- If API is disabeled do not show API token creation stuff
+- Disable API by default
+- Add debug bar
+
+### 🚜 Refactor
+
+- Update environment variable name for uptime-kuma service
+- Improve start proxy script to handle existing containers gracefully
+- Update delete server confirmation modal buttons
+- Remove unnecessary code
+
+### ⚙️ Miscellaneous Tasks
+
+- Add autocomplete attribute to input fields
+- Refactor API Tokens component to use isApiEnabled flag
+- Update versions.json file
+- Remove unused .env.development.example file
+- Update API Tokens view to include link to Settings menu
+- Update web.php to cast server port as integer
+- Update backup deletion labels to use language files
+- Update database startup heading title
+- Update database startup heading title
+- Custom vite envs
+- Update version numbers to 4.0.0-beta.348
+- Refactor code to improve SSH key handling and storage
+
+## [4.0.0-beta.343] - 2024-09-25
+
+### 🐛 Bug Fixes
+
+- Parser
+- Exited services statuses
+- Make sure to reload window if app status changes
+- Deploy key based deployments
+
+### 🚜 Refactor
+
+- Remove commented out code and improve environment variable handling in newParser function
+- Improve label positioning in input and checkbox components
+- Group and sort fields in StackForm by service name and password status
+- Improve layout and add checkbox for task enablement in scheduled task form
+- Update checkbox component to support full width option
+- Update confirmation label in danger.blade.php template
+- Fix typo in execute-container-command.blade.php
+- Update OS_TYPE for Asahi Linux in install.sh script
+- Add localhost as Server if it doesn't exist and not in cloud environment
+- Add localhost as Server if it doesn't exist and not in cloud environment
+- Update ProductionSeeder to fix issue with coolify_key assignment
+- Improve modal confirmation titles and button labels
+- Update install.sh script to remove redirection of upgrade output to /dev/null
+- Fix modal input closeOutside prop in configuration.blade.php
+- Add support for IPv6 addresses in sslip function
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.343
+- Update version numbers to 4.0.0-beta.344
+- Update version numbers to 4.0.0-beta.345
+- Update version numbers to 4.0.0-beta.346
+
+## [4.0.0-beta.342] - 2024-09-24
+
+### 🚀 Features
+
+- Add nullable constraint to 'fingerprint' column in private_keys table
+- *(api)* Add an endpoint to execute a command
+- *(api)* Add endpoint to execute a command
+
+### 🐛 Bug Fixes
+
+- Proxy status
+- Coolify-db should not be in the managed resources
+- Store original root key in the original location
+- Logto service
+- Cloudflared service
+- Migrations
+- Cloudflare tunnel configuration, ui, etc
+
+### 💼 Other
+
+- Volumes on development environment
+- Clean new volume name for dev volumes
+- Persist DBs, services and so on stored in data/coolify
+- Add SSH Key fingerprint to DB
+- Add a fingerprint to every private key on save, create...
+- Make sure invalid private keys can not be added
+- Encrypt private SSH keys in the DB
+- Add is_sftp and is_server_ssh_key coloums
+- New ssh key file name on disk
+- Store all keys on disk by default
+- Populate SSH key folder
+- Populate SSH keys in dev
+- Use new function names and logic everywhere
+- Create a Multiplexing Helper
+- SSH multiplexing
+- Remove unused code form multiplexing
+- SSH Key cleanup job
+- Private key with ID 2 on dev
+- Move more functions to the PrivateKey Model
+- Add ssh key fingerprint and generate one for existing keys
+- ID issues on dev seeders
+- Server ID 0
+- Make sure in use private keys are not deleted
+- Do not delete SSH Key from disk during server validation error
+- UI bug, do not write ssh key to disk in server dialog
+- SSH Multiplexing for Jobs
+- SSH algorhytm text
+- Few multiplexing things
+- Clear mux directory
+- Multiplexing do not write file manually
+- Integrate tow step process in the modal component WIP
+- Ability to hide labels
+- DB start, stop confirm
+- Del init script
+- General confirm
+- Preview deployments and typos
+- Service confirmation
+- Confirm file storage
+- Stop service confirm
+- DB image cleanup
+- Confirm ressource operation
+- Environment variabel deletion
+- Confirm scheduled tasks
+- Confirm API token
+- Confirm private key
+- Confirm server deletion
+- Confirm server settings
+- Proxy stop and restart confirmation
+- GH app deletion confirmation
+- Redeploy all confirmation
+- User deletion confirmation
+- Team deletion confirmation
+- Backup job confirmation
+- Delete volume confirmation
+- More conformations and fixes
+- Delete unused private keys button
+- Ray error because port is not uncommented
+- #3322 deploy DB alterations before updating
+- Css issue with advanced settings and remove cf tunnel in onboarding
+- New cf tunnel install flow
+- Made help text more clear
+- Cloudflare tunnel
+- Make helper text more clean to use a FQDN and not an URL
+
+### 🚜 Refactor
+
+- Update Docker cleanup label in Heading.php and Navbar.php
+- Remove commented out code in Navbar.php
+- Remove CleanupSshKeysJob from schedule in Kernel.php
+- Update getAJoke function to exclude offensive jokes
+- Update getAJoke function to use HTTPS for API request
+- Update CleanupHelperContainersJob to use more efficient Docker command
+- Update PrivateKey model to improve code readability and maintainability
+- Remove unnecessary code in PrivateKey model
+- Update PrivateKey model to use ownedByCurrentTeam() scope for cleanupUnusedKeys()
+- Update install.sh script to check if coolify-db volume exists before generating SSH key
+- Update ServerSeeder and PopulateSshKeysDirectorySeeder
+- Improve attribute sanitization in Server model
+- Update confirmation button text for deletion actions
+- Remove unnecessary code in shared.php file
+- Update environment variables for services in compose files
+- Update select.blade.php to improve trademarks policy display
+- Update select.blade.php to improve trademarks policy display
+- Fix typo in subscription URLs
+- Add Postiz service to compose file (disabled for now)
+- Update shared.php to include predefined ports for services
+- Simplify SSH key synchronization logic
+- Remove unused code in DatabaseBackupStatusJob and PopulateSshKeysDirectorySeeder
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.342
+- Update remove-labels-and-assignees-on-close.yml
+- Add SSH key for localhost in ProductionSeeder
+- Update SSH key generation in install.sh script
+- Update ProductionSeeder to call OauthSettingSeeder and PopulateSshKeysDirectorySeeder
+- Update install.sh to support Asahi Linux
+- Update install.sh version to 1.6
+- Remove unused middleware and uniqueId method in DockerCleanupJob
+- Refactor DockerCleanupJob to remove unused middleware and uniqueId method
+- Remove unused migration file for populating SSH keys and clearing mux directory
+- Add modified files to the commit
+- Refactor pre-commit hook to improve performance and readability
+- Update CONTRIBUTING.md with troubleshooting note about database migrations
+- Refactor pre-commit hook to improve performance and readability
+- Update cleanup command to use Redis instead of queue
+- Update Docker commands to start proxy
+
+## [4.0.0-beta.341] - 2024-09-18
+
+### 🚀 Features
+
+- Add buddy logo
+
+## [4.0.0-beta.336] - 2024-09-16
+
+### 🚀 Features
+
+- Make coolify full width by default
+- Fully functional terminal for command center
+- Custom terminal host
+
+### 🐛 Bug Fixes
+
+- Keep-alive ws connections
+- Add build.sh to debug logs
+- Update Coolify installer
+- Terminal
+- Generate https for minio
+- Install script
+- Handle WebSocket connection close in terminal.blade.php
+- Able to open terminal to any containers
+- Refactor run-command
+- If you exit a container manually, it should close the underlying tty as well
+- Move terminal to separate view on services
+- Only update helper image in DB
+- Generated fqdn for SERVICE_FQDN_APP_3000 magic envs
+
+### 💼 Other
+
+- Remove labels and assignees on issue close
+- Make sure this action is also triggered on PR issue close
+
+### 🚜 Refactor
+
+- Remove unnecessary code in ExecuteContainerCommand.php
+- Improve Docker network connection command in StartService.php
+- Terminal / run command
+- Add authorization check in ExecuteContainerCommand mount method
+- Remove unnecessary code in Terminal.php
+- Remove unnecessary code in Terminal.blade.php
+- Update WebSocket connection initialization in terminal.blade.php
+- Remove unnecessary console.log statements in terminal.blade.php
+
+### ⚙️ Miscellaneous Tasks
+
+- Update release version to 4.0.0-beta.336
+- Update coolify environment variable assignment with double quotes
+- Update shared.php to fix issues with source and network variables
+- Update terminal styling for better readability
+- Update button text for container connection form
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Remove unused entrypoint script and update volume mapping
+- Update .env file and docker-compose configuration
+- Update APP_NAME environment variable in docker-compose.prod.yml
+- Update WebSocket URL in terminal.blade.php
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Rename Command Center to Terminal in code and views
+- Update branch restriction for push event in coolify-helper.yml
+- Update terminal button text and layout in application heading view
+- Refactor terminal component and select form layout
+- Update coolify nightly version to 4.0.0-beta.335
+- Update helper version to 1.0.1
+- Fix syntax error in versions.json
+- Update version numbers to 4.0.0-beta.337
+- Update Coolify installer and scripts to include a function for fetching programming jokes
+- Update docker network connection command in ApplicationDeploymentJob.php
+- Add validation to prevent selecting 'default' server or container in RunCommand.php
+- Update versions.json to reflect latest version of realtime container
+- Update soketi image to version 1.0.1
+- Nightly - Update soketi image to version 1.0.1 and versions.json to reflect latest version of realtime container
+- Update version numbers to 4.0.0-beta.339
+- Update version numbers to 4.0.0-beta.340
+- Update version numbers to 4.0.0-beta.341
+
+### ◀️ Revert
+
+- Databasebackup
+
+## [4.0.0-beta.335] - 2024-09-12
+
+### 🐛 Bug Fixes
+
+- Cloudflare tunnel with new multiplexing feature
+
+### 💼 Other
+
+- SSH Multiplexing on docker desktop on Windows
+
+### ⚙️ Miscellaneous Tasks
+
+- Update release version to 4.0.0-beta.335
+- Update constants.ssh.mux_enabled in remoteProcess.php
+- Update listeners and proxy settings in server form and new server components
+- Remove unnecessary null check for proxy_type in generate_default_proxy_configuration
+- Remove unnecessary SSH command execution time logging
+
+## [4.0.0-beta.334] - 2024-09-12
+
+### ⚙️ Miscellaneous Tasks
+
+- Remove itsgoingd/clockwork from require-dev in composer.json
+- Update 'key' value of gitlab in Service.php to use environment variable
+
+## [4.0.0-beta.333] - 2024-09-11
+
+### 🐛 Bug Fixes
+
+- Disable mux_enabled during server validation
+- Move mc command to coolify image from helper
+- Keydb. add `:` delimiter for connection string
+
+### 💼 Other
+
+- Remote servers with port and user
+- Do not change localhost server name on revalidation
+- Release.md file
+
+### 🚜 Refactor
+
+- Improve handling of environment variable merging in upgrade script
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.333
+- Copy .env file to .env-{DATE} if it exists
+- Update .env file with new values
+- Update server check job middleware to use server ID instead of UUID
+- Add reminder to backup .env file before running install script again
+- Copy .env file to backup location during installation script
+- Add reminder to backup .env file during installation script
+- Update permissions in pr-build.yml and version numbers
+- Add minio/mc command to Dockerfile
+
+## [4.0.0-beta.332] - 2024-09-10
+
+### 🚀 Features
+
+- Expose project description in API response
+- Add elixir finetunes to the deployment job
+
+### 🐛 Bug Fixes
+
+- Reenable overlapping servercheckjob
+- Appwrite template + parser
+- Don't add `networks` key if `network_mode` is used
+- Remove debug statement in shared.php
+- Scp through cloudflare
+- Delete older versions of the helper image other than the latest one
+- Update remoteProcess.php to handle null values in logItem properties
+
+### 💼 Other
+
+- Set a default server timezone
+- Implement SSH Multiplexing
+- Enabel mux
+- Cleanup stale multiplexing connections
+
+### 🚜 Refactor
+
+- Improve environment variable handling in shared.php
+
+### ⚙️ Miscellaneous Tasks
+
+- Set timeout for ServerCheckJob to 60 seconds
+- Update appwrite.yaml to include OpenSSL key variable assignment
+
+## [4.0.0-beta.330] - 2024-09-06
+
+### 🐛 Bug Fixes
+
+- Parser
+- Plunk NEXT_PUBLIC_API_URI
+
+### 💼 Other
+
+- Pull helper image if not available otherwise s3 backup upload fails
+
+### 🚜 Refactor
+
+- Improve handling of server timezones in scheduled backups and tasks
+- Improve handling of server timezones in scheduled backups and tasks
+- Improve handling of server timezones in scheduled backups and tasks
+- Update cleanup schedule to run daily at midnight
+- Skip returning volume if driver type is cifs or nfs
+
+### ⚙️ Miscellaneous Tasks
+
+- Update coolify-helper.yml to get version from versions.json
+- Disable Ray by default
+- Enable Ray by default and update Dockerfile with latest versions of PACK and NIXPACKS
+- Update Ray configuration and Dockerfile
+- Add middleware for updating environment variables by UUID in `api.php` routes
+- Expose port 3000 in browserless.yaml template
+- Update Ray configuration and Dockerfile
+- Update coolify version to 4.0.0-beta.331
+- Update versions.json and sentry.php to 4.0.0-beta.332
+- Update version to 4.0.0-beta.332
+- Update DATABASE_URL in plunk.yaml to use plunk database
+- Add coolify.managed=true label to Docker image builds
+- Update docker image pruning command to exclude managed images
+- Update docker cleanup schedule to run daily at midnight
+- Update versions.json to version 1.0.1
+- Update coolify-helper.yml to include "next" branch in push trigger
+
+## [4.0.0-beta.326] - 2024-09-03
+
+### 🚀 Features
+
+- Update server_settings table to force docker cleanup
+- Update Docker Compose file with DB_URL environment variable
+- Refactor shared.php to improve environment variable handling
+
+### 🐛 Bug Fixes
+
+- Wrong executions order
+- Handle project not found error in environment_details API endpoint
+- Deployment running for - without "ago"
+- Update helper image pulling logic to only pull if the version is newer
+
+### 💼 Other
+
+- Plunk svg
+
+### 📚 Documentation
+
+- Update Plunk documentation link in compose/plunk.yaml
+
+### ⚙️ Miscellaneous Tasks
+
+- Update UI for displaying no executions found in scheduled task list
+- Update UI for displaying deployment status in deployment list
+- Update UI for displaying deployment status in deployment list
+- Ignore unnecessary files in production build workflow
+- Update server form layout and settings
+- Update Dockerfile with latest versions of PACK and NIXPACKS
+
+## [4.0.0-beta.324] - 2024-09-02
+
+### 🚀 Features
+
+- Preserve git repository with advanced file storages
+- Added Windmill template
+- Added Budibase template
+- Add shm-size for custom docker commands
+- Add custom docker container options to all databases
+- Able to select different postgres database
+- Add new logos for jobscollider and hostinger
+- Order scheduled task executions
+- Add Code Server environment variables to Service model
+- Add coolify build env variables to building phase
+- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid
+- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid
+
+### 🐛 Bug Fixes
+
+- Timezone not updated when systemd is missing
+- If volumes + file mounts are defined, should merge them together in the compose file
+- All mongo v4 backups should use the different backup command
+- Database custom environment variables
+- Connect compose apps to the right predefined network
+- Docker compose destination network
+- Server status when there are multiple servers
+- Sync fqdn change on the UI
+- Pr build names in case custom name is used
+- Application patch request instant_deploy
+- Canceling deployment on build server
+- Backup of password protected postgresql database
+- Docker cleanup job
+- Storages with preserved git repository
+- Parser parser parser
+- New parser only in dev
+- Parser parser
+- Numberoflines should be number
+- Docker cleanup job
+- Fix directory and file mount headings in file-storage.blade.php
+- Preview fqdn generation
+- Revert a few lines
+- Service ui sync bug
+- Setup script doesn't work on rhel based images with some curl variant already installed
+- Let's wait for healthy container during installation and wait an extra 20 seconds (for migrations)
+- Infra files
+- Log drain only for Applications
+- Copy large compose files through scp (not ssh)
+- Check if array is associative or not
+- Openapi endpoint urls
+- Convert environment variables to one format in shared.php
+- Logical volumes could be overwritten with new path
+- Env variable in value parsed
+- Pull coolify image only when the app needs to be updated
+
+### 💼 Other
+
+- Actually update timezone on the server
+- Cron jobs are executed based on the server timezone
+- Server timezone seeder
+- Recent backups UI
+- Use apt-get instead of apt
+- Typo
+- Only pull helper image if the version is newer than the one
+
+### 🚜 Refactor
+
+- Update event listeners in Show components
+- Refresh application to get latest database changes
+- Update RabbitMQ configuration to use environment variable for port
+- Remove debug statement in parseDockerComposeFile function
+- ParseServiceVolumes
+- Update OpenApi command to generate documentation
+- Remove unnecessary server status check in destination view
+- Remove unnecessary admin user email and password in budibase.yaml
+- Improve saving of custom internal name in Advanced.php
+- Add conditional check for volumes in generate_compose_file()
+- Improve storage mount forms in add.blade.php
+- Load environment variables based on resource type in sortEnvironmentVariables()
+- Remove unnecessary network cleanup in Init.php
+- Remove unnecessary environment variable checks in parseDockerComposeFile()
+- Add null check for docker_compose_raw in parseCompose()
+- Update dockerComposeParser to use YAML data from $yaml instead of $compose
+- Convert service variables to key-value pairs in parseDockerComposeFile function
+- Update database service name from mariadb to mysql
+- Remove unnecessary code in DatabaseBackupJob and BackupExecutions
+- Update Docker Compose parsing function to convert service variables to key-value pairs
+- Update Docker Compose parsing function to convert service variables to key-value pairs
+- Remove unused server timezone seeder and related code
+- Remove unused server timezone seeder and related code
+- Remove unused PullCoolifyImageJob from schedule
+- Update parse method in Advanced, All, ApplicationPreview, General, and ApplicationDeploymentJob classes
+- Remove commented out code for getIptables() in Dashboard.php
+- Update .env file path in install.sh script
+- Update SELF_HOSTED environment variable in docker-compose.prod.yml
+- Remove unnecessary code for creating coolify network in upgrade.sh
+- Update environment variable handling in StartClickhouse.php and ApplicationDeploymentJob.php
+- Improve handling of COOLIFY_URL in shared.php
+- Update build_args property type in ApplicationDeploymentJob
+- Update background color of sponsor section in README.md
+- Update Docker Compose location handling in PublicGitRepository
+- Upgrade process of Coolify
+
+### 🧪 Testing
+
+- More tests
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.324
+- New compose parser with tests
+- Update version to 1.3.4 in install.sh and 1.0.6 in upgrade.sh
+- Update memory limit to 64MB in horizon configuration
+- Update php packages
+- Update axios npm dependency to version 1.7.5
+- Update Coolify version to 4.0.0-beta.324 and fix file paths in upgrade script
+- Update Coolify version to 4.0.0-beta.324
+- Update Coolify version to 4.0.0-beta.325
+- Update Coolify version to 4.0.0-beta.326
+- Add cd command to change directory before removing .env file
+- Update Coolify version to 4.0.0-beta.327
+- Update Coolify version to 4.0.0-beta.328
+- Update sponsor links in README.md
+- Update version.json to versions.json in GitHub workflow
+- Cleanup stucked resources and scheduled backups
+- Update GitHub workflow to use versions.json instead of version.json
+- Update GitHub workflow to use versions.json instead of version.json
+- Update GitHub workflow to use versions.json instead of version.json
+- Update GitHub workflow to use jq container for version extraction
+- Update GitHub workflow to use jq container for version extraction
+
+## [4.0.0-beta.323] - 2024-08-08
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.323
+
+## [4.0.0-beta.322] - 2024-08-08
+
+### 🐛 Bug Fixes
+
+- Manual update process
+
+### 🚜 Refactor
+
+- Update Server model getContainers method to use collect() for containers and containerReplicates
+- Import ProxyTypes enum and use TRAEFIK instead of TRAEFIK_V2
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.322
+
+## [4.0.0-beta.321] - 2024-08-08
+
+### 🐛 Bug Fixes
+
+- Scheduledbackup not found
+
+### 🚜 Refactor
+
+- Update StandalonePostgresql database initialization and backup handling
+- Update cron expressions and add helper text for scheduled tasks
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.321
+
+## [4.0.0-beta.320] - 2024-08-08
+
+### 🚀 Features
+
+- Delete team in cloud without subscription
+- Coolify init should cleanup stuck networks in proxy
+- Add manual update check functionality to settings page
+- Update auto update and update check frequencies in settings
+- Update Upgrade component to check for latest version of Coolify
+- Improve homepage service template
+- Support map fields in Directus
+- Labels by proxy type
+- Able to generate only the required labels for resources
+
+### 🐛 Bug Fixes
+
+- Only append docker network if service/app is running
+- Remove lazy load from scheduled tasks
+- Plausible template
+- Service_url should not have a trailing slash
+- If usagebefore cannot be determined, cleanup docker with force
+- Async remote command
+- Only run logdrain if necessary
+- Remove network if it is only connected to coolify proxy itself
+- Dir mounts should have proper dirs
+- File storages (dir/file mount) handled properly
+- Do not use port exposes on docker compose buildpacks
+- Minecraft server template fixed
+- Graceful shutdown
+- Stop resources gracefully
+- Handle null and empty disk usage in DockerCleanupJob
+- Show latest version on manual update view
+- Empty string content should be saved as a file
+- Update Traefik labels on init
+- Add missing middleware for server check job
+
+### 🚜 Refactor
+
+- Update CleanupDatabase.php to adjust keep_days based on environment
+- Adjust keep_days in CleanupDatabase.php based on environment
+- Remove commented out code for cleaning up networks in CleanupDocker.php
+- Update livewire polling interval in heading.blade.php
+- Remove unused code for checking server status in Heading.php
+- Simplify log drain installation in ServerCheckJob
+- Remove unnecessary debug statement in ServerCheckJob
+- Simplify log drain installation and stop log drain if necessary
+- Cleanup unnecessary dynamic proxy configuration in Init command
+- Remove unnecessary debug statement in ApplicationDeploymentJob
+- Update timeout for graceful_shutdown_container in ApplicationDeploymentJob
+- Remove unused code and optimize CheckForUpdatesJob
+- Update ProxyTypes enum values to use TRAEFIK instead of TRAEFIK_V2
+- Update Traefik labels on init and cleanup unnecessary dynamic proxy configuration
+
+### 🎨 Styling
+
+- Linting
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.320
+- Add pull_request image builds to GH actions
+- Add comment explaining the purpose of disconnecting the network in cleanup_unused_network_from_coolify_proxy()
+- Update formbricks template
+- Update registration view to display a notice for first user that it will be an admin
+- Update server form to use password input for IP Address/Domain field
+- Update navbar to include service status check
+- Update navbar and configuration to improve service status check functionality
+- Update workflows to include PR build and merge manifest steps
+- Update UpdateCoolifyJob timeout to 10 minutes
+- Update UpdateCoolifyJob to dispatch CheckForUpdatesJob synchronously
+
+## [4.0.0-beta.319] - 2024-07-26
+
+### 🐛 Bug Fixes
+
+- Parse docker composer
+- Service env parsing
+- Service env variables
+- Activity type invalid
+- Update env on ui
+
+### 💼 Other
+
+- Service env parsing
+
+### ⚙️ Miscellaneous Tasks
+
+- Collect/create/update volumes in parseDockerComposeFile function
+
+## [4.0.0-beta.318] - 2024-07-24
+
+### 🚀 Features
+
+- Create/delete project endpoints
+- Add patch request to projects
+- Add server api endpoints
+- Add branddev logo to README.md
+- Update API endpoint summaries
+- Update Caddy button label in proxy.blade.php
+- Check custom internal name through server's applications.
+- New server check job
+
+### 🐛 Bug Fixes
+
+- Preview deployments should be stopped properly via gh webhook
+- Deleting application should delete preview deployments
+- Plane service images
+- Fix issue with deployment start command in ApplicationDeploymentJob
+- Directory will be created by default for compose host mounts
+- Restart proxy does not work + status indicator on the UI
+- Uuid in api docs type
+- Raw compose deployment .env not found
+- Api -> application patch endpoint
+- Remove pull always when uploading backup to s3
+- Handle array env vars
+- Link in task failed job notifications
+- Random generated uuid will be full length (not 7 characters)
+- Gitlab service
+- Gitlab logo
+- Bitbucket repository url
+- By default volumes that we cannot determine if they are directories or files are treated as directories
+- Domain update on services on the UI
+- Update SERVICE_FQDN/URL env variables when you change the domain
+- Several shared environment variables in one value, parsed correctly
+- Members of root team should not see instance admin stuff
+
+### 💼 Other
+
+- Formbricks template add required CRON_SECRET
+- Add required CRON_SECRET to Formbricks template
+
+### ⚙️ Miscellaneous Tasks
+
+- Update APP_BASE_URL to use SERVICE_FQDN_PLANE
+- Update resource-limits.blade.php with improved input field helpers
+- Update version numbers to 4.0.0-beta.319
+- Remove commented out code for docker image pruning
+
+## [4.0.0-beta.314] - 2024-07-15
+
+### 🚀 Features
+
+- Improve error handling in loadComposeFile method
+- Add readonly labels
+- Preserve git repository
+- Force cleanup server
+
+### 🐛 Bug Fixes
+
+- Typo in is_literal helper
+- Env is_literal helper text typo
+- Update docker compose pull command with --policy always
+- Plane service template
+- Vikunja
+- Docmost template
+- Drupal
+- Improve github source creation
+- Tag deployments
+- New docker compose parsing
+- Handle / in preselecting branches
+- Handle custom_internal_name check in ApplicationDeploymentJob.php
+- If git limit reached, ignore it and continue with a default selection
+- Backup downloads
+- Missing input for api endpoint
+- Volume detection (dir or file) is fixed
+- Supabase
+- Create file storage even if content is empty
+
+### 💼 Other
+
+- Add basedir + compose file in new compose based apps
+
+### 🚜 Refactor
+
+- Remove unused code and fix storage form layout
+- Update Docker Compose build command to include --pull flag
+- Update DockerCleanupJob to handle nullable usageBefore property
+- Server status job and docker cleanup job
+- Update DockerCleanupJob to use server settings for force cleanup
+- Update DockerCleanupJob to use server settings for force cleanup
+- Disable health check for Rust applications during deployment
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.315
+- Update version to 4.0.0-beta.316
+- Update bug report template
+- Update repository form with simplified URL input field
+- Update width of container in general.blade.php
+- Update checkbox labels in general.blade.php
+- Update general page of apps
+- Handle JSON parsing errors in format_docker_command_output_to_json
+- Update Traefik image version to v2.11
+- Update version to 4.0.0-beta.317
+- Update version to 4.0.0-beta.318
+- Update helper message with link to documentation
+- Disable health check by default
+- Remove commented out code for sending internal notification
+
+### ◀️ Revert
+
+- Pull policy
+- Advanced dropdown
+
+## [4.0.0-beta.308] - 2024-07-11
+
+### 🚀 Features
+
+- Cleanup unused docker networks from proxy
+- Compose parser v2
+- Display time interval for rollback images
+- Add security and storage access key env to twenty template
+- Add new logo for Latitude
+- Enable legacy model binding in Livewire configuration
+
+### 🐛 Bug Fixes
+
+- Do not overwrite hardcoded variables if they rely on another variable
+- Remove networks when deleting a docker compose based app
+- Api
+- Always set project name during app deployments
+- Remove volumes as well
+- Gitea pr previews
+- Prevent instance fqdn persisting to other servers dynamic proxy configs
+- Better volume cleanups
+- Cleanup parameter
+- Update redirect URL in unauthenticated exception handler
+- Respect top-level configs and secrets
+- Service status changed event
+- Disable sentinel until a few bugs are fixed
+- Service domains and envs are properly updated
+- *(reactive-resume)* New healthcheck command for MinIO
+- *(MinIO)* New command healthcheck
+- Update minio hc in services
+- Add validation for missing docker compose file
+
+### 🚜 Refactor
+
+- Add force parameter to StartProxy handle method
+- Comment out unused code for network cleanup
+- Reset default labels when docker_compose_domains is modified
+- Webhooks view
+- Tags view
+- Only get instanceSettings once from db
+- Update Dockerfile to set CI environment variable to true
+- Remove unnecessary code in AppServiceProvider.php
+- Update Livewire configuration views
+- Update Webhooks.php to use nullable type for webhook URLs
+- Add lazy loading to tags in Livewire configuration view
+- Update metrics.blade.php to improve alert message clarity
+- Update version numbers to 4.0.0-beta.312
+- Update version numbers to 4.0.0-beta.314
+
+### ⚙️ Miscellaneous Tasks
+
+- Update Plausible docker compose template to Plausible 2.1.0
+- Update Plausible docker compose template to Plausible 2.1.0
+- Update livewire/livewire dependency to version 3.4.9
+- Refactor checkIfDomainIsAlreadyUsed function
+- Update storage.blade.php view for livewire project service
+- Update version to 4.0.0-beta.310
+- Update composer dependencies
+- Add new logo for Latitude
+- Bump version to 4.0.0-beta.311
+
+### ◀️ Revert
+
+- Instancesettings
+
+## [4.0.0-beta.301] - 2024-06-24
+
+### 🚀 Features
+
+- Local fonts
+- More API endpoints
+- Bulk env update api endpoint
+- Update server settings metrics history days to 7
+- New app API endpoint
+- Private gh deployments through api
+- Lots of api endpoints
+- Api api api api api api
+- Rename CloudCleanupSubs to CloudCleanupSubscriptions
+- Early fraud warning webhook
+- Improve internal notification message for early fraud warning webhook
+- Add schema for uuid property in app update response
+
+### 🐛 Bug Fixes
+
+- Run user commands on high prio queue
+- Load js locally
+- Remove lemon + paddle things
+- Run container commands on high priority
+- Image logo
+- Remove both option for api endpoints. it just makes things complicated
+- Cleanup subs in cloud
+- Show keydbs/dragonflies/clickhouses
+- Only run cloud clean on cloud + remove root team
+- Force cleanup on busy servers
+- Check domain on new app via api
+- Custom container name will be the container name, not just internal network name
+- Api updates
+- Yaml everywhere
+- Add newline character to private key before saving
+- Add validation for webhook endpoint selection
+- Database input validators
+- Remove own app from domain checks
+- Return data of app update
+
+### 💼 Other
+
+- Update process
+- Glances service
+- Glances
+- Able to update application
+
+### 🚜 Refactor
+
+- Update Service model's saveComposeConfigs method
+- Add default environment to Service model's saveComposeConfigs method
+- Improve handling of default environment in Service model's saveComposeConfigs method
+- Remove commented out code in Service model's saveComposeConfigs method
+- Update stack-form.blade.php to include wire:target attribute for submit button
+- Update code to use str() instead of Str::of() for string manipulation
+- Improve formatting and readability of source.blade.php
+- Add is_build_time property to nixpacks_php_fallback_path and nixpacks_php_root_dir
+- Simplify code for retrieving subscription in Stripe webhook
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.302
+- Update version to 4.0.0-beta.303
+- Update version to 4.0.0-beta.305
+- Update version to 4.0.0-beta.306
+- Add log1x/laravel-webfonts package
+- Update version to 4.0.0-beta.307
+- Refactor ServerStatusJob constructor formatting
+- Update Monaco Editor for Docker Compose and Proxy Configuration
+- More details
+- Refactor shared.php helper functions
+
+## [4.0.0-beta.298] - 2024-06-24
+
+### 🚀 Features
+
+- Spanish translation
+- Cancelling a deployment will check if new could be started.
+- Add supaguide logo to donations section
+- Nixpacks now could reach local dbs internally
+- Add Tigris logo to other/logos directory
+- COOLIFY_CONTAINER_NAME predefined variable
+- Charts
+- Sentinel + charts
+- Container metrics
+- Add high priority queue
+- Add metrics warning for servers without Sentinel enabled
+- Add blacksmith logo to donations section
+- Preselect server and destination if only one found
+- More api endpoints
+- Add API endpoint to update application by UUID
+- Update statusnook logo filename in compose template
+
+### 🐛 Bug Fixes
+
+- Stripprefix middleware correctly labeled to http
+- Bitbucket link
+- Compose generator
+- Do no truncate repositories wtih domain (git) in it
+- In services should edit compose file for volumes and envs
+- Handle laravel deployment better
+- Db proxy status shown better in the UI
+- Show commit message on webhooks + prs
+- Metrics parsing
+- Charts
+- Application custom labels reset after saving
+- Static build with new nixpacks build process
+- Make server charts one livewire component with one interval selector
+- You can now add env variable from ui to services
+- Update compose environment with UI defined variables
+- Refresh deployable compose without reload
+- Remove cloud stripe notifications
+- App deployment should be in high queue
+- Remove zoom from modals
+- Get envs before sortby
+- MB is % lol
+- Projects with 0 envs
+
+### 💼 Other
+
+- Unnecessary notification
+
+### 🚜 Refactor
+
+- Update text color for stderr output in deployment show view
+- Update text color for stderr output in deployment show view
+- Remove debug code for saving environment variables
+- Update Docker build commands for better performance and flexibility
+- Update image sizes and add new logos to README.md
+- Update README.md with new logos and fix styling
+- Update shared.php to use correct key for retrieving sentinel version
+- Update container name assignment in Application model
+- Remove commented code for docker container removal
+- Update Application model to include getDomainsByUuid method
+- Update Project/Show component to sort environments by created_at
+- Update profile index view to display 2FA QR code in a centered container
+- Update dashboard.blade.php to use project's default environment for redirection
+- Update gitCommitLink method to handle null values in source.html_url
+- Update docker-compose generation to use multi-line literal block
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.298
+- Switch to database sessions from redis
+- Update dependencies and remove unused code
+- Update tailwindcss and vue versions in package.json
+- Update service template URL in constants.php
+- Update sentinel version to 0.0.8
+- Update chart styling and loading text
+- Update sentinel version to 0.0.9
+- Update Spanish translation for failed authentication messages
+- Add portuguese traslation
+- Add Turkish translations
+- Add Vietnamese translate
+- Add Treive logo to donations section
+- Update README.md with latest release version badge
+- Update latest release version badge in README.md
+- Update version to 4.0.0-beta.299
+- Move server delete component to the bottom of the page
+- Update version to 4.0.0-beta.301
+
+## [4.0.0-beta.297] - 2024-06-11
+
+### 🚀 Features
+
+- Easily redirect between www-and-non-www domains
+- Add logos for new sponsors
+- Add homepage template
+- Update homepage.yaml with environment variables and volumes
+
+### 🐛 Bug Fixes
+
+- Multiline build args
+- Setup script doesnt link to the correct source code file
+- Install.sh do not reinstall packages on arch
+- Just restart
+
+### 🚜 Refactor
+
+- Replaces duplications in code with a single function
+
+### ⚙️ Miscellaneous Tasks
+
+- Update page title in resource index view
+- Update logo file path in logto.yaml
+- Update logo file path in logto.yaml
+- Remove commented out code for docker container removal
+- Add isAnyDeploymentInprogress function to check if any deployments are in progress
+- Add ApplicationDeploymentJob and pint.json
+
+## [4.0.0-beta.295] - 2024-06-10
+
+### 🚀 Features
+
+- Able to change database passwords on the UI. It won't sync to the database.
+- Able to add several domains to compose based previews
+- Add bounty program link to bug report template
+- Add titles
+- Db proxy logs
+
+### 🐛 Bug Fixes
+
+- Custom docker compose commands, add project dir if needed
+- Autoupdate process
+- Backup executions view
+- Handle previously defined compose previews
+- Sort backup executions
+- Supabase service, newest versions
+- Set default name for Docker volumes if it is null
+- Multiline variable should be literal + should be multiline in bash with \
+- Gitlab merge request should close PR
+
+### 💼 Other
+
+- Rocketchat
+- New services based git apps
+
+### 🚜 Refactor
+
+- Append utm_source parameter to documentation URL
+- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview
+- Update deployment previews heading to "Deployments"
+- Remove unused variables and improve code readability
+- Initialize null properties in Github Change component
+- Improve pre and post deployment command inputs
+- Improve handling of Docker volumes in parseDockerComposeFile function
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.295
+- Update supported OS list with almalinux
+- Update install.sh to support PopOS
+- Update install.sh script to version 1.3.2 and handle Linux Mint as Ubuntu
+
+## [4.0.0-beta.294] - 2024-06-04
+
+### ⚙️ Miscellaneous Tasks
+
+- Update Dockerfile with latest versions of Docker, Docker Compose, Docker Buildx, Pack, and Nixpacks
+
+## [4.0.0-beta.289] - 2024-05-29
+
+### 🚀 Features
+
+- Add PHP memory limit environment variable to docker-compose.prod.yml
+- Add manual update option to UpdateCoolify handle method
+- Add port configuration for Vaultwarden service
+
+### 🐛 Bug Fixes
+
+- Sync upgrade process
+- Publish horizon
+- Add missing team model
+- Test new upgrade process?
+- Throw exception
+- Build server dirs not created on main server
+- Compose load with non-root user
+- Able to redeploy dockerfile based apps without cache
+- Compose previews does have env variables
+- Fine-tune cdn pulls
+- Spamming :D
+- Parse docker version better
+- Compose issues
+- SERVICE_FQDN has source port in it
+- Logto service
+- Allow invitations via email
+- Sort by defined order + fixed typo
+- Only ignore volumes with driver_opts
+- Check env in args for compose based apps
+
+### 🚜 Refactor
+
+- Update destination.blade.php to add group class for better styling
+- Applicationdeploymentjob
+- Improve code structure in ApplicationDeploymentJob.php
+- Remove unnecessary debug statement in ApplicationDeploymentJob.php
+- Remove unnecessary debug statements and improve code structure in RunRemoteProcess.php and ApplicationDeploymentJob.php
+- Remove unnecessary logging statements from UpdateCoolify
+- Update storage form inputs in show.blade.php
+- Improve Docker Compose parsing for services
+- Remove unnecessary port appending in updateCompose function
+- Remove unnecessary form class in profile index.blade.php
+- Update form layout in invite-link.blade.php
+- Add log entry when starting new application deployment
+- Improve Docker Compose parsing for services
+- Update Docker Compose parsing for services
+- Update slogan in shlink.yaml
+- Improve display of deployment time in index.blade.php
+- Remove commented out code for clearing Ray logs
+- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview
+
+### ⚙️ Miscellaneous Tasks
+
+- Update for version 289
+- Fix formatting issue in deployment index.blade.php file
+- Remove unnecessary wire:navigate attribute in breadcrumbs.blade.php
+- Rename docker dirs
+- Update laravel/socialite to version v5.14.0 and livewire/livewire to version 3.4.9
+- Update modal styles for better user experience
+- Update deployment index.blade.php script for better performance
+- Update version numbers to 4.0.0-beta.290
+- Update version numbers to 4.0.0-beta.291
+- Update version numbers to 4.0.0-beta.292
+- Update version numbers to 4.0.0-beta.293
+- Add upgrade guide link to upgrade.blade.php
+- Improve upgrade.blade.php with clearer instructions and formatting
+- Update version numbers to 4.0.0-beta.294
+- Add Lightspeed.run as a sponsor
+- Update Dockerfile to install vim
+
+## [4.0.0-beta.288] - 2024-05-28
+
+### 🐛 Bug Fixes
+
+- Do not allow service storage mount point modifications
+- Volume adding
+
+### ⚙️ Miscellaneous Tasks
+
+- Update Sentry release version to 4.0.0-beta.288
+
+## [4.0.0-beta.287] - 2024-05-27
+
+### 🚀 Features
+
+- Handle incomplete expired subscriptions in Stripe webhook
+- Add more persistent storage types
+
+### 🐛 Bug Fixes
+
+- Force load services from cdn on reload list
+
+### ⚙️ Miscellaneous Tasks
+
+- Update Sentry release version to 4.0.0-beta.287
+- Add Thompson Edolo as a sponsor
+- Add null checks for team in Stripe webhook
+
+## [4.0.0-beta.286] - 2024-05-27
+
+### 🚀 Features
+
+- If the time seems too long it remains at 0s
+- Improve Docker Engine start logic in ServerStatusJob
+- If proxy stopped manually, it won't start back again
+- Exclude_from_hc magic
+- Gitea manual webhooks
+- Add container logs in case the container does not start healthy
+
+### 🐛 Bug Fixes
+
+- Wrong time during a failed deployment
+- Removal of the failed deployment condition, addition of since started instead of finished time
+- Use local versions + service templates and query them every 10 minutes
+- Check proxy functionality before removing unnecessary coolify.yaml file and checking Docker Engine
+- Show first 20 users only in admin view
+- Add subpath for services
+- Ghost subdir
+- Do not pull templates in dev
+- Templates
+- Update error message for invalid token to mention invalid signature
+- Disable containerStopped job for now
+- Disable unreachable/revived notifications for now
+- JSON_UNESCAPED_UNICODE
+- Add wget to nixpacks builds
+- Pre and post deployment commands
+- Bitbucket commits link
+- Better way to add curl/wget to nixpacks
+- Root team able to download backups
+- Build server should not have a proxy
+- Improve build server functionalities
+- Sentry issue
+- Sentry
+- Sentry error + livewire downgrade
+- Sentry
+- Sentry
+- Sentry error
+- Sentry
+
+### 🚜 Refactor
+
+- Update edit-domain form in project service view
+- Add Huly services to compose file
+- Remove redundant heading in backup settings page
+- Add isBuildServer method to Server model
+- Update docker network creation in ApplicationDeploymentJob
+
+### ⚙️ Miscellaneous Tasks
+
+- Change pre and post deployment command length in applications table
+- Refactor container name logic in GetContainersStatus.php and ForcePasswordReset.php
+- Remove unnecessary content from Docker Compose file
+
+## [4.0.0-beta.285] - 2024-05-21
+
+### 🚀 Features
+
+- Add SerpAPI as a Github Sponsor
+- Admin view for deleting users
+- Scheduled task failed notification
+
+### 🐛 Bug Fixes
+
+- Optimize new resource creation
+- Show it docker compose has syntax errors
+
+### 💼 Other
+
+- Responsive here and there
+
+## [4.0.0-beta.284] - 2024-05-19
+
+### 🚀 Features
+
+- Add hc logs to healthchecks
+
+### ◀️ Revert
+
+- Hc return code check
+
+## [4.0.0-beta.283] - 2024-05-17
+
+### 🚀 Features
+
+- Update healthcheck test in StartMongodb action
+- Add pull_request_id filter to get_last_successful_deployment method in Application model
+
+### 🐛 Bug Fixes
+
+- PR deployments have good predefined envs
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.283
+
+## [4.0.0-beta.281] - 2024-05-17
+
+### 🚀 Features
+
+- Shows the latest deployment commit + message on status
+- New manual update process + remove next_channel
+- Add lastDeploymentInfo and lastDeploymentLink props to breadcrumbs and status components
+- Sort envs alphabetically and creation date
+- Improve sorting of environment variables in the All component
+
+### 🐛 Bug Fixes
+
+- Hc from localhost to 127.0.0.1
+- Use rc in hc
+- Telegram group chat notifications
+
+## [4.0.0-beta.280] - 2024-05-16
+
+### 🐛 Bug Fixes
+
+- Commit message length
+
+## [4.0.0-beta.279] - 2024-05-16
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.279
+- Limit commit message length to 50 characters in ApplicationDeploymentJob
+
+## [4.0.0-beta.278] - 2024-05-16
+
+### 🚀 Features
+
+- Adding new COOLIFY_ variables
+- Save commit message and better view on deployments
+- Toggle label escaping mechanism
+
+### 🐛 Bug Fixes
+
+- Use commit hash on webhooks
+
+### ⚙️ Miscellaneous Tasks
+
+- Refactor Service.php to handle missing admin user in extraFields() method
+- Update twenty CRM template with environment variables and dependencies
+- Refactor applications.php to remove unused imports and improve code readability
+- Refactor deployment index.blade.php for improved readability and rollback handling
+- Refactor GitHub app selection UI in project creation form
+- Update ServerLimitCheckJob.php to handle missing serverLimit value
+- Remove unnecessary code for saving commit message
+- Update DOCKER_VERSION to 26.0 in install.sh script
+- Update Docker and Docker Compose versions in Dockerfiles
+
+## [4.0.0-beta.277] - 2024-05-10
+
+### 🚀 Features
+
+- Add AdminRemoveUser command to remove users from the database
+
+### 🐛 Bug Fixes
+
+- Color for resource operation server and project name
+- Only show realtime error on non-cloud instances
+- Only allow push and mr gitlab events
+- Improve scheduled task adding/removing
+- Docker compose dependencies for pr previews
+- Properly populating dependencies
+
+### 💼 Other
+
+- Fix a few boxes here and there
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version numbers to 4.0.0-beta.278
+- Update hover behavior and cursor style in scheduled task executions view
+- Refactor scheduled task view to improve code readability and maintainability
+- Skip scheduled tasks if application or service is not running
+- Remove debug logging statements in Kernel.php
+- Handle invalid cron strings in Kernel.php
+
+## [4.0.0-beta.275] - 2024-05-06
+
+### 🚀 Features
+
+- Add container name to network aliases in ApplicationDeploymentJob
+- Add lazy loading for images in General.php and improve Docker Compose file handling in Application.php
+- Experimental sentinel
+- Start Sentinel on servers.
+- Pull new sentinel image and restart container
+- Init metrics
+
+### 🐛 Bug Fixes
+
+- Typo in tags.blade.php
+- Install.sh error
+- Env file
+- Comment out internal notification in email_verify method
+- Confirmation for custom labels
+- Change permissions on newly created dirs
+
+### 💼 Other
+
+- Fix tag view
+
+### 🚜 Refactor
+
+- Add SCHEDULER environment variable to StartSentinel.php
+
+### ⚙️ Miscellaneous Tasks
+
+- Dark mode should be the default
+- Improve menu item styling and spacing in service configuration and index views
+- Improve menu item styling and spacing in service configuration and index views
+- Improve menu item styling and spacing in project index and show views
+- Remove docker compose versions
+- Add Listmonk service template and logo
+- Refactor GetContainersStatus.php for improved readability and maintainability
+- Refactor ApplicationDeploymentJob.php for improved readability and maintainability
+- Add metrics and logs directories to installation script
+- Update sentinel version to 0.0.2 in versions.json
+- Update permissions on metrics and logs directories
+- Comment out server sentinel check in ServerStatusJob
+
+## [4.0.0-beta.273] - 2024-05-03
+
+### 🐛 Bug Fixes
+
+- Formbricks image origin
+- Add port even if traefik is used
+
+### ⚙️ Miscellaneous Tasks
+
+- Update version to 4.0.0-beta.275
+- Update DNS server validation helper text
+
+## [4.0.0-beta.267] - 2024-04-26
+
+### 🚀 Features
+
+- Initial datalist
+- Update service contribution docs URL
+- The final pricing plan, pay-as-you-go
+
+### 🐛 Bug Fixes
+
+- Move s3 storages to separate view
+- Mongo db backup
+- Backups
+- Autoupdate
+- Respect start period and chekc interval for hc
+- Parse HEALTHCHECK from dockerfile
+- Make s3 name and endpoint required
+- Able to update source path for predefined volumes
+- Get logs with non-root user
+- Mongo 4.0 db backup
+
+### 💼 Other
+
+- Update resource operations view
+
+### ◀️ Revert
+
+- Variable parsing
+
+## [4.0.0-beta.266] - 2024-04-24
+
+### 🐛 Bug Fixes
+
+- Refresh public ips on start
+
+## [4.0.0-beta.259] - 2024-04-17
+
+### 🚀 Features
+
+- Literal env variables
+- Lazy load stuffs + tell user if compose based deployments have missing envs
+- Can edit file/dir volumes from ui in compose based apps
+- Upgrade Appwrite service template to 1.5
+- Upgrade Appwrite service template to 1.5
+- Add db name to backup notifications
+
+### 🐛 Bug Fixes
+
+- Helper image only pulled if required, not every 10 mins
+- Make sure that confs when checking if it is changed sorted
+- Respect .env file (for default values)
+- Remove temporary cloudflared config
+- Remove lazy loading until bug figured out
+- Rollback feature
+- Base64 encode .env
+- $ in labels escaped
+- .env saved to deployment server, not to build server
+- Do no able to delete gh app without deleting resources
+- 500 error on edge case
+- Able to select server when creating new destination
+- N8n template
+
+### 💼 Other
+
+- Non-root user for remote servers
+- Non-root
+
+## [4.0.0-beta.258] - 2024-04-12
+
+### 🚀 Features
+
+- Dynamic mux time
+
+### 🐛 Bug Fixes
+
+- Check each required binaries one-by-one
+
+## [4.0.0-beta.256] - 2024-04-12
+
+### 🚀 Features
+
+- Upload large backups
+- Edit domains easier for compose
+- Able to delete configuration from server
+- Configuration checker for all resources
+- Allow tab in textarea
+
+### 🐛 Bug Fixes
+
+- Service config hash update
+- Redeploy if image not found in restart only mode
+
+### 💼 Other
+
+- New pricing
+- Fix allowTab logic
+- Use 2 space instead of tab
+
+## [4.0.0-beta.252] - 2024-04-09
+
+### 🚀 Features
+
+- Add amazon linux 2023
+
+### 🐛 Bug Fixes
+
+- Git submodule update
+- Unintended left padding on sidebar
+- Hashed random delimeter in ssh commands + make sure to remove the delimeter from the command
+
+## [4.0.0-beta.250] - 2024-04-05
+
+### 🚀 Features
+
+- *(application)* Update submodules after git checkout
+
+## [4.0.0-beta.249] - 2024-04-03
+
+### 🚀 Features
+
+- Able to make rsa/ed ssh keys
+
+### 🐛 Bug Fixes
+
+- Warning if you use multiple domains for a service
+- New github app creation
+- Always rebuild Dockerfile / dockerimage buildpacks
+- Do not rebuild dockerfile based apps twice
+- Make sure if envs are changed, rebuild is needed
+- Members cannot manage subscriptions
+- IsMember
+- Storage layout
+- How to update docker-compose, environment variables and fqdns
+
+### 💼 Other
+
+- Light buttons
+- Multiple server view
+
+## [4.0.0-beta.242] - 2024-03-25
+
+### 🚀 Features
+
+- Change page width
+- Watch paths
+
+### 🐛 Bug Fixes
+
+- Compose env has SERVICE, but not defined for Coolify
+- Public service database
+- Make sure service db proxy restarted
+- Restart service db proxies
+- Two factor
+- Ui for tags
+- Update resources view
+- Realtime connection check
+- Multline env in dev mode
+- Scheduled backup for other service databases (supabase)
+- PR deployments should not be distributed to 2 servers
+- Name/from address required for resend
+- Autoupdater
+- Async service loads
+- Disabled inputs are not trucated
+- Duplicated generated fqdns are now working
+- Uis
+- Ui for cftunnels
+- Search services
+- Trial users subscription page
+- Async public key loading
+- Unfunctional server should see resources
+
+### 💼 Other
+
+- Run cleanup every day
+- Fix
+- Fix log outputs
+- Automatic cloudflare tunnels
+- Backup executions
+
+## [4.0.0-beta.241] - 2024-03-20
+
+### 🚀 Features
+
+- Able to run scheduler/horizon programatically
+
+### 🐛 Bug Fixes
+
+- Volumes for prs
+- Shared env variable parsing
+
+### 💼 Other
+
+- Redesign
+- Redesign
+
+## [4.0.0-beta.240] - 2024-03-18
+
+### 🐛 Bug Fixes
+
+- Empty get logs number of lines
+- Only escape envs after v239+
+- 0 in env value
+- Consistent container name
+- Custom ip address should turn off rolling update
+- Multiline input
+- Raw compose deployment
+- Dashboard view if no project found
+
+## [4.0.0-beta.239] - 2024-03-14
+
+### 🐛 Bug Fixes
+
+- Duplicate dockerfile
+- Multiline env variables
+- Server stopped, service page not reachable
+
+## [4.0.0-beta.237] - 2024-03-14
+
+### 🚀 Features
+
+- Domains api endpoint
+- Resources api endpoint
+- Team api endpoint
+- Add deployment details to deploy endpoint
+- Add deployments api
+- Experimental caddy support
+- Dynamic configuration for caddy
+- Reset password
+- Show resources on source page
+
+### 🐛 Bug Fixes
+
+- Deploy api messages
+- Fqdn null in case docker compose bp
+- Reload caddy issue
+- /realtime endpoint
+- Proxy switch
+- Service ports for services + caddy
+- Failed deployments should send failed email/notification
+- Consider custom healthchecks in dockerfile
+- Create initial files async
+- Docker compose validation
+
+## [4.0.0-beta.235] - 2024-03-05
+
+### 🐛 Bug Fixes
+
+- Should note delete personal teams
+- Make sure to show some buttons
+- Sort repositories by name
+
+## [4.0.0-beta.224] - 2024-02-23
+
+### 🚀 Features
+
+- Custom server limit
+- Delay container/server jobs
+- Add static ipv4 ipv6 support
+- Server disabled by overflow
+- Preview deployment logs
+- Collect webhooks during maintenance
+- Logs and execute commands with several servers
+
+### 🐛 Bug Fixes
+
+- Subscription / plan switch, etc
+- Firefly service
+- Force enable/disable server in case ultimate package quantity decreases
+- Server disabled
+- Custom dockerfile location always checked
+- Import to mysql and mariadb
+- Resource tab not loading if server is not reachable
+- Load unmanaged async
+- Do not show n/a networsk
+- Service container status updates
+- Public prs should not be commented
+- Pull request deployments + build servers
+- Env value generation
+- Sentry error
+- Service status updated
+
+### 💼 Other
+
+- Change + icon to hamburger.
+
+## [4.0.0-beta.222] - 2024-02-22
+
+### 🚀 Features
+
+- Able to add dynamic configurations from proxy dashboard
+
+### 🐛 Bug Fixes
+
+- Connections being stuck and not processed until proxy restarts
+- Use latest image if nothing is specified
+- No coolify.yaml found
+- Server validation
+- Statuses
+- Unknown image of service until it is uploaded
+
+## [4.0.0-beta.220] - 2024-02-19
+
+### 🚀 Features
+
+- Save github app permission locally
+- Minversion for services
+
+### 🐛 Bug Fixes
+
+- Add openbsd ssh server check
+- Resources
+- Empty build variables
+- *(server)* Revalidate server button not showing in server's page
+- Fluent bit ident level
+- Submodule cloning
+- Database status
+- Permission change updates from webhook
+- Server validation
+
+### 💼 Other
+
+- Updates
+
+## [4.0.0-beta.213] - 2024-02-12
+
+### 🚀 Features
+
+- Magic for traefik redirectregex in services
+- Revalidate server
+- Disable gzip compression on service applications
+
+### 🐛 Bug Fixes
+
+- Cleanup scheduled tasks
+- Padding left on input boxes
+- Use ls / command instead ls
+- Do not add the same server twice
+- Only show redeployment required if status is not exited
+
+## [4.0.0-beta.212] - 2024-02-08
+
+### 🚀 Features
+
+- Cleanup queue
+
+### 🐛 Bug Fixes
+
+- New menu on navbar
+- Make sure resources are deleted in async mode
+- Go to prod env from dashboard if there is no other envs defined
+- User proper image_tag, if set
+- New menu ui
+- Lock logdrain configuration when one of them are enabled
+- Add docker compose check during server validation
+- Get service stack as uuid, not name
+- Menu
+- Flex wrap deployment previews
+- Boolean docker options
+- Only add 'networks' key if 'network_mode' is absent
+
+## [4.0.0-beta.206] - 2024-02-05
+
+### 🚀 Features
+
+- Clone to env
+- Multi deployments
+
+### 🐛 Bug Fixes
+
+- Wrap tags and avoid horizontal overflow
+- Stripe webhooks
+- Feedback from self-hosted envs to discord
+
+### 💼 Other
+
+- Specific about newrelic logdrains
+
+## [4.0.0-beta.201] - 2024-01-29
+
+### 🚀 Features
+
+- Added manual webhook support for bitbucket
+- Add initial support for custom docker run commands
+- Cleanup unreachable servers
+- Tags and tag deploy webhooks
+
+### 🐛 Bug Fixes
+
+- Bitbucket manual deployments
+- Webhooks for multiple apps
+- Unhealthy deployments should be failed
+- Add env variables for wordpress template without database
+- Service deletion function
+- Service deletion fix
+- Dns validation + duplicated fqdns
+- Validate server navbar upated
+- Regenerate labels on application clone
+- Service deletion
+- Not able to use other shared envs
+- Sentry fix
+- Sentry
+- Sentry error
+- Sentry
+- Sentry error
+- Create dynamic directory
+- Migrate to new modal
+- Duplicate domain check
+- Tags
+
+### 💼 Other
+
+- New modal component
+
+## [4.0.0-beta.188] - 2024-01-11
+
+### 🚀 Features
+
+- Search between resources
+- Move resources between projects / environments
+- Clone any resource
+- Shared environments
+- Concurrent builds / server
+- Able to deploy multiple resources with webhook
+- Add PR comments
+- Dashboard live deployment view
+
+### 🐛 Bug Fixes
+
+- Preview deployments with nixpacks
+- Cleanup docker stuffs before upgrading
+- Service deletion command
+- Cpuset limits was determined in a way that apps only used 1 CPU max, ehh, sorry.
+- Service stack view
+- Change proxy view
+- Checkbox click
+- Git pull command for deploy key based previews
+- Server status job
+- Service deletion bug!
+- Links
+- Redis custom conf
+- Sentry error
+- Restrict concurrent deployments per server
+- Queue
+- Change env variable length
+
+### 💼 Other
+
+- Send notification email if payment
+
+### 🚜 Refactor
+
+- Compose file and install script
+
+## [4.0.0-beta.186] - 2024-01-11
+
+### 🚀 Features
+
+- Import backups
+
+### 🐛 Bug Fixes
+
+- Do not include thegameplan.json into build image
+- Submit error on postgresql
+- Email verification / forgot password
+- Escape build envs properly for nixpacks + docker build
+- Undead endpoint
+- Upload limit on ui
+- Save cmd output propely (merge)
+- Load profile on remote commands
+- Load profile and set envs on remote cmd
+- Restart should not update config hash
+
+## [4.0.0-beta.184] - 2024-01-09
+
+### 🐛 Bug Fixes
+
+- Healthy status
+- Show framework based notification in build logs
+- Traefik labels
+- Use ip for sslip in dev if remote server is used
+- Service labels without ports (unknown ports)
+- Sort and rename (unique part) of labels
+- Settings menu
+- Remove traefik debug in dev mode
+- Php pgsql to 8.2
+- Static buildpack should set port 80
+- Update navbar on build_pack change
+
+## [4.0.0-beta.183] - 2024-01-06
+
+### 🚀 Features
+
+- Add www-non-www redirects to traefik
+
+### 🐛 Bug Fixes
+
+- Database env variables
+
+## [4.0.0-beta.182] - 2024-01-04
+
+### 🐛 Bug Fixes
+
+- File storage save
+
+## [4.0.0-beta.181] - 2024-01-03
+
+### 🐛 Bug Fixes
+
+- Nixpacks buildpack
+
+## [4.0.0-beta.180] - 2024-01-03
+
+### 🐛 Bug Fixes
+
+- Nixpacks cache
+- Only add restart policy if its empty (compose)
+
+## [4.0.0-beta.179] - 2024-01-02
+
+### 🐛 Bug Fixes
+
+- Set deployment failed if new container is not healthy
+
+## [4.0.0-beta.177] - 2024-01-02
+
+### 🚀 Features
+
+- Raw docker compose deployments
+
+### 🐛 Bug Fixes
+
+- Duplicate compose variable
+
+## [4.0.0-beta.176] - 2023-12-31
+
+### 🐛 Bug Fixes
+
+- Horizon
+
+## [4.0.0-beta.175] - 2023-12-30
+
+### 🚀 Features
+
+- Add environment description + able to change name
+
+### 🐛 Bug Fixes
+
+- Sub
+- Wrong env variable parsing
+- Deploy key + docker compose
+
+## [4.0.0-beta.174] - 2023-12-27
+
+### 🐛 Bug Fixes
+
+- Restore falsely deleted coolify-db-backup
+
+## [4.0.0-beta.173] - 2023-12-27
+
+### 🐛 Bug Fixes
+
+- Cpu limit to float from int
+- Add source commit to final envs
+- Routing, switch back to old one
+- Deploy instead of restart in case swarm is used
+- Button title
+
+## [4.0.0-beta.163] - 2023-12-15
+
+### 🚀 Features
+
+- Custom docker compose commands
+
+### 🐛 Bug Fixes
+
+- Domains for compose bp
+- No action in webhooks
+- Add debug output to gitlab webhooks
+- Do not push dockerimage
+- Add alpha to swarm
+- Server not found
+- Do not autovalidate server on mount
+- Server update schedule
+- Swarm support ui
+- Server ready
+- Get swarm service logs
+- Docker compose apps env rewritten
+- Storage error on dbs
+- Why?!
+- Stay tuned
+
+### 💼 Other
+
+- Swarm
+- Swarm
+
+## [4.0.0-beta.155] - 2023-12-11
+
+### 🚀 Features
+
+- Autoupdate env during seed
+- Disable autoupdate
+- Randomly sleep between executions
+- Pull latest images for services
+
+### 🐛 Bug Fixes
+
+- Do not send telegram noti on intent payment failed
+- Database ui is realtime based
+- Live mode for github webhooks
+- Ui
+- Realtime connection popup could be disabled
+- Realtime check
+- Add new destination
+- Proxy logs
+- Db status check
+- Pusher host
+- Add ipv6
+- Realtime connection?!
+- Websocket
+- Better handling of errors with install script
+- Install script parse version
+- Only allow to modify in .env file if AUTOUPDATE is set
+- Is autoupdate not null
+- Run init command after production seeder
+- Init
+- Comma in traefik custom labels
+- Ignore if dynamic config could not be set
+- Service env variable ovewritten if it has a default value
+- Labelling
+- Non-ascii chars in labels
+- Labels
+- Init script echos
+- Update Coolify script
+- Null notify
+- Check queued deployments as well
+- Copy invitation
+- Password reset / invitation link requests
+- Add catch all route
+- Revert random container job delay
+- Backup executions view
+- Only check server status in container status job
+- Improve server status check times
+- Handle other types of generated values
+- Server checking status
+- Ui for adding new destination
+- Reset domains on compose file change
+
+### 💼 Other
+
+- Fix for comma in labels
+- Add image name to service stack + better options visibility
+
+### 🚜 Refactor
+
+- Service logs are now on one page
+- Application status changed realtime
+- Custom labels
+- Clone project
+
+## [4.0.0-beta.154] - 2023-12-07
+
+### 🚀 Features
+
+- Execute command in container
+
+### 🐛 Bug Fixes
+
+- Container selection
+- Service navbar using new realtime events
+- Do not create duplicated networks
+- Live event
+- Service start + event
+- Service deletion job
+- Double ws connection
+- Boarding view
+
+### 💼 Other
+
+- Env vars
+- Migrate to livewire 3
+
+## [4.0.0-beta.124] - 2023-11-13
+
+### 🚀 Features
+
+- Log drain (wip)
+- Enable/disable log drain by service
+- Log drainer container check
+- Add docker engine support install script to rhel based systems
+- Save timestamp configuration for logs
+- Custom log drain endpoints
+- Auto-restart tcp proxies for databases
+
+### 🐛 Bug Fixes
+
+- *(fider template)* Use the correct docs url
+- Fqdn for minio
+- Generate service fields
+- Mariadb backups
+- When to pull image
+- Do not allow to enter local ip addresses
+- Reset password
+- Only report nonruntime errors
+- Handle different label formats in services
+- Server adding process
+- Show defined resources in server tab, so you will know what you need to delete before you can delete the server.
+- Lots of regarding git + docker compose deployments
+- Pull request build variables
+- Double default password length
+- Do not remove deployment in case compose based failed
+- No container servers
+- Sentry issue
+- Dockercompose save ./ volumes under /data/coolify
+- Server view for link()
+- Default value do not overwrite existing env value
+- Use official install script with rancher (one will work for sure)
+- Add cf tunnel to boarding server view
+- Prevent autorefresh of proxy status
+- Missing docker image thing
+- Add hc for soketi
+- Deploy the right compose file
+- Bind volumes for compose bp
+- Use hc port 80 in case of static build
+- Switching to static build
+
+### 💼 Other
+
+- New deployment jobs
+- Compose based apps
+- Swarm
+- Swarm
+- Swarm
+- Swarm
+- Disable trial
+- Meilisearch
+- Broadcast
+- 🌮
+
+### 🚜 Refactor
+
+- Env variable generator
+
+### ◀️ Revert
+
+- Wip
+
+## [4.0.0-beta.109] - 2023-11-06
+
+### 🚀 Features
+
+- Deployment logs fullscreen
+- Service database backups
+- Make service databases public
+
+### 🐛 Bug Fixes
+
+- Missing environment variables prevewi on service
+- Invoice.paid should sleep for 5 seconds
+- Local dev repo
+- Deployments ui
+- Dockerfile build pack fix
+- Set labels on generate domain
+- Network service parse
+- Notification url in containerstatusjob
+- Gh webhook response 200 to installation_repositories
+- Delete destination
+- No id found
+- Missing $mailMessage
+- Set default from/sender names
+- No environments
+- Telegram text
+- Private key not found error
+- UI
+- Resourcesdelete command
+- Port number should be int
+- Separate delete with validation of server
+- Add nixpacks info
+- Remove filter
+- Container logs are now followable in full-screen and sorted by timestamp
+- Ui for labels
+- Ui
+- Deletions
+- Build_image not found
+- Github source view
+- Github source view
+- Dockercleanupjob should be released back
+- Ui
+- Local ip address
+- Revert workdir to basedir
+- Container status jobs for old pr deployments
+- Service updates
+
+## [4.0.0-beta.99] - 2023-10-24
+
+### 🚀 Features
+
+- Improve deployment time by a lot
+
+### 🐛 Bug Fixes
+
+- Space in build args
+- Lock SERVICE_FQDN envs
+- If user is invited, that means its email is verified
+- Force password reset on invited accounts
+- Add ssh options to git ls-remote
+- Git ls-remote
+- Remove coolify labels from ui
+
+### 💼 Other
+
+- Fix subs
+
+## [4.0.0-beta.97] - 2023-10-20
+
+### 🚀 Features
+
+- Standalone mongodb
+- Cloning project
+- Api tokens + deploy webhook
+- Start all kinds of things
+- Simple search functionality
+- Mysql, mariadb
+- Lock environment variables
+- Download local backups
+
+### 🐛 Bug Fixes
+
+- Service docs links
+- Add PGUSER to prevent HC warning
+- Preselect s3 storage if available
+- Port exposes change, shoud regenerate label
+- Boarding
+- Clone to with the same environment name
+- Cleanup stucked resources on start
+- Do not allow to delete env if a resource is defined
+- Service template generator + appwrite
+- Mongodb backup
+- Make sure coolfiy network exists on install
+- Syncbunny command
+- Encrypt mongodb password
+- Mongodb healtcheck command
+- Rate limit for api + add mariadb + mysql
+- Server settings guarded
+
+### 💼 Other
+
+- Generate services
+- Mongodb backup
+- Mongodb backup
+- Updates
+
+## [4.0.0-beta.93] - 2023-10-18
+
+### 🚀 Features
+
+- Able to customize docker labels on applications
+- Show if config is not applied
+
+### 🐛 Bug Fixes
+
+- Setup:dev script & contribution guide
+- Do not show configuration changed if config_hash is null
+- Add config_hash if its null (old deployments)
+- Label generation
+- Labels
+- Email channel no recepients
+- Limit horizon processes to 2 by default
+- Add custom port as ssh option to deploy_key based commands
+- Remove custom port from git repo url
+- ContainerStatus job
+
+### 💼 Other
+
+- PAT by team
+
+## [4.0.0-beta.92] - 2023-10-17
+
+### 🐛 Bug Fixes
+
+- Proxy start process
+
+## [4.0.0-beta.91] - 2023-10-17
+
+### 🐛 Bug Fixes
+
+- Always start proxy if not NONE is selected
+
+### 💼 Other
+
+- Add helper to service domains
+
+## [4.0.0-beta.90] - 2023-10-17
+
+### 🐛 Bug Fixes
+
+- Only include config.json if its exists and a file
+
+### 💼 Other
+
+- Wordpress
+
+## [4.0.0-beta.89] - 2023-10-17
+
+### 🐛 Bug Fixes
+
+- Noindex meta tag
+- Show docker build logs
+
+## [4.0.0-beta.88] - 2023-10-17
+
+### 🚀 Features
+
+- Use docker login credentials from server
+
+## [4.0.0-beta.87] - 2023-10-17
+
+### 🐛 Bug Fixes
+
+- Service status check is a bit better
+- Generate fqdn if you deleted a service app, but it requires fqdn
+- Cancel any deployments + queue next
+- Add internal domain names during build process
+
+## [4.0.0-beta.86] - 2023-10-15
+
+### 🐛 Bug Fixes
+
+- Build image before starting dockerfile buildpacks
+
+## [4.0.0-beta.85] - 2023-10-14
+
+### 🐛 Bug Fixes
+
+- Redis URL generated
+
+## [4.0.0-beta.83] - 2023-10-13
+
+### 🐛 Bug Fixes
+
+- Docker hub URL
+
+## [4.0.0-beta.70] - 2023-10-09
+
+### 🚀 Features
+
+- Add email verification for cloud
+- Able to deploy docker images
+- Add dockerfile location
+- Proxy logs on the ui
+- Add custom redis conf
+
+### 🐛 Bug Fixes
+
+- Server validation process
+- Fqdn could be null
+- Small
+- Server unreachable count
+- Do not reset unreachable count
+- Contact docs
+- Check connection
+- Server saving
+- No env goto envs from dashboard
+- Goto
+- Tcp proxy for dbs
+- Database backups
+- Only send email if transactional email set
+- Backupfailed notification is forced
+- Use port exposed for reverse proxy
+- Contact link
+- Use only ip addresses for servers
+- Deleted team and it is the current one
+- Add new team button
+- Transactional email link
+- Dashboard goto link
+- Only require registry image in case of dockerimage bp
+- Instant save build pack change
+- Public git
+- Cannot remove localhost
+- Check localhost connection
+- Send unreachable/revived notifications
+- Boarding + verification
+- Make sure proxy wont start in NONE mode
+- Service check status 10 sec
+- IsCloud in production seeder
+- Make sure to use IP address
+- Dockerfile location feature
+- Server ip could be hostname in self-hosted
+- Urls should be password fields
+- No backup for redis
+- Show database logs in case of its not healthy and running
+- Proxy check for ports, do not kill anything listening on port 80/443
+- Traefik dashboard ip
+- Db labels
+- Docker cleanup jobs
+- Timeout for instant remote processes
+- Dev containerjobs
+- Backup database one-by-one.
+- Turn off static deployment if you switch buildpacks
+
+### 💼 Other
+
+- Dockerimage
+- Updated dashboard
+- Fix
+- Fix
+- Coolify proxy access logs exposed in dev
+- Able to select environment on new resource
+- Delete server
+- Redis
+
+## [4.0.0-beta.58] - 2023-10-02
+
+### 🚀 Features
+
+- Reset root password
+- Attach Coolify defined networks to services
+- Delete resource command
+- Multiselect removable resources
+- Disable service, required version
+- Basedir / monorepo initial support
+- Init version of any git deployment
+- Deploy private repo with ssh key
+
+### 🐛 Bug Fixes
+
+- If waitlist is disabled, redirect to register
+- Add destination to new services
+- Predefined content for files
+- Move /data to ./_data in dev
+- UI
+- Show all storages in one place for services
+- Ui
+- Add _data to vite ignore
+- Only use _ in volume names for services
+- Volume names in services
+- Volume names
+- Service logs visible if the whole service stack is not running
+- Ui
+- Compose magic
+- Compose parser updated
+- Dev compose files
+- Traefik labels for multiport deployments
+- Visible version number
+- Remove SERVICE_ from deployable compose
+- Delete event to deleting
+- Move dev data to volumes to prevent permission issues
+- Traefik labelling in case of several http and https domain added
+- PR deployments use the first fqdn as base
+- Email notifications subscription fixed
+- Services - do not remove unnecessary things for now
+- Decrease max horizon processes to get lower memory usage
+- Test emails only available for user owned smtp/resend
+- Ui for self-hosted email settings
+- Set smtp notifications on by default
+- Select branch on other git
+- Private repository
+- Contribution guide
+- Public repository names
+- *(create)* Flex wrap on server & network selection
+- Better unreachable/revived server statuses
+- Able to set base dir for Dockerfile build pack
+
+### 💼 Other
+
+- Uptime kume hc updated
+- Switch back to /data (volume errors)
+- Notifications
+- Add shared email option to everyone
+
+## [4.0.0-beta.57] - 2023-10-02
+
+### 🚀 Features
+
+- Container logs
+
+### 🐛 Bug Fixes
+
+- Always pull helper image in dev
+- Only show last 1000 lines
+- Service status
+
+## [4.0.0-beta.47] - 2023-09-28
+
+### 🐛 Bug Fixes
+
+- Next helper image
+- Service templates
+- Sync:bunny
+- Update process if server has been renamed
+- Reporting handler
+- Localhost privatekey update
+- Remove private key in case you removed a github app
+- Only show manually added private keys on server view
+- Show source on all type of applications
+- Docker cleanup should be a job by server
+- File/dir based volumes are now read from the server
+- Respect server fqdn
+- If public repository does not have a main branch
+- Preselect branc on private repos
+- Deploykey branch
+- Backups are now working again
+- Not found base_branch in git webhooks
+- Coolify db backup
+- Preview deployments name, status etc
+- Services should have destination as well
+- Dockerfile expose is not overwritten
+- If app settings is not saved to db
+- Do not show subscription cancelled noti
+- Show real volume names
+- Only parse expose in dockerfiles if ports_exposes is empty
+- Add uuid to volume names
+- New volumes for services should have - instead of _
+
+### 💼 Other
+
+- Fix previews to preview
+
+## [4.0.0-beta.46] - 2023-09-28
+
+### 🐛 Bug Fixes
+
+- Containerstatusjob
+- Aaaaaaaaaaaaaaaaa
+- Services view
+- Services
+- Manually create network for services
+- Disable early updates
+- Sslip for localhost
+- ContainerStatusJob
+- Cannot delete env with available services
+- Sync command
+- Install script drops an error
+- Prevent sync version (it needs an option)
+- Instance fqdn setting
+- Sentry 4510197209
+- Sentry 4504136641
+- Sentry 4502634789
+
+## [4.0.0-beta.45] - 2023-09-24
+
+### 🚀 Features
+
+- Services
+- Image tag for services
+
+### 🐛 Bug Fixes
+
+- Applications with port mappins do a normal update (not rolling update)
+- Put back build pack chooser
+- Proxy configuration + starter
+- Show real storage name on services
+- New service template layout
+
+### 💼 Other
+
+- Fixed z-index for version link.
+- Add source button
+- Fixed z-index for magicbar
+- A bit better error
+- More visible feedback button
+- Update help modal
+- Help
+- Marketing emails
+
+## [4.0.0-beta.28] - 2023-09-08
+
+### 🚀 Features
+
+- Telegram topics separation
+- Developer view for env variables
+- Cache team settings
+- Generate public key from private keys
+- Able to invite more people at once
+- Trial
+- Dynamic trial period
+- Ssh-agent instead of filesystem based ssh keys
+- New container status checks
+- Generate ssh key
+- Sentry add email for better support
+- Healthcheck for apps
+- Add cloudflare tunnel support
+
+### 🐛 Bug Fixes
+
+- Db backup job
+- Sentry 4459819517
+- Sentry 4451028626
+- Ui
+- Retry notifications
+- Instance email settings
+- Ui
+- Test email on for admins or custom smtp
+- Coolify already exists should not throw error
+- Delete database related things when delete database
+- Remove -q from docker compose
+- Errors in views
+- Only send internal notifcations to enabled channels
+- Recovery code
+- Email sending error
+- Sentry 4469575117
+- Old docker version error
+- Errors
+- Proxy check, reduce jobs, etc
+- Queue after commit
+- Remove nixpkgarchive
+- Remove nixpkgarchive from ui
+- Webhooks should not run if server is not functional
+- Server is functional check
+- Confirm email before sending
+- Help should send cc on email
+- Sub type
+- Show help modal everywhere
+- Forgot password
+- Disable dockerfile based healtcheck for now
+- Add timeout for ssh commands
+- Prevent weird ui bug for validateServer
+- Lowercase email in forgot password
+- Lower case email on waitlist
+- Encrypt jobs
+- ProcessWithEnv()->run
+- Plus boarding step about Coolify
+- SaveConfigurationSync
+- Help uri
+- Sub for root
+- Redirect on server not found
+- Ip check
+- Uniqueips
+- Simply reply to help messages
+- Help
+- Rate limit
+- Collect billing address
+- Invitation
+- Smtp view
+- Ssh-agent revert
+- Restarting container state on ui
+- Generate new key
+- Missing upgrade js
+- Team error
+- 4.0.0-beta.37
+- Localhost
+- Proxy start (if not proxy defined, use Traefik)
+- Do not remove localhost in boarding
+- Allow non ip address (DNS)
+- InstallDocker id not found
+- Boarding
+- Errors
+- Proxy container status
+- Proxy configuration saving
+- Convert startProxy to action
+- Stop/start UI on apps and dbs
+- Improve localhost boarding process
+- Try to use old docker-compose
+- Boarding again
+- Send internal notifications of email errors
+- Add github app change on new app view
+- Delete environment variables on app/db delete
+- Save proxy configuration
+- Add proxy to network with periodic check
+- Proxy connections
+- Delete persistent storages on resource deletion
+- Prevent overwrite already existing env variables in services
+- Mappings
+- Sentry issue 4478125289
+- Make sure proxy path created
+- StartProxy
+- Server validation with cf tunnels
+- Only show traefik dashboard if its available
+- Services
+- Database schema
+- Report livewire errors
+- Links with path
+- Add traefik labels no matter if traefik is selected or not
+- Add expose port for containers
+- Also check docker socks permission on validation
+
+### 💼 Other
+
+- User should know that the public key
+- Services are not availble yet
+- Show registered users on waitlist page
+- Nixpacksarchive
+- Add Plausible analytics
+- Global env variables
+- Fix
+- Trial emails
+- Server check instead of app check
+- Show trial instead of sub
+- Server lost connection
+- Services
+- Services
+- Services
+- Ui for services
+- Services
+- Services
+- Services
+- Fixes
+- Fix typo
+
+## [4.0.0-beta.27] - 2023-09-08
+
+### 🐛 Bug Fixes
+
+- Bug
+
+## [4.0.0-beta.26] - 2023-09-08
+
+### 🚀 Features
+
+- Public database
+
+## [4.0.0-beta.25] - 2023-09-07
+
+### 🐛 Bug Fixes
+
+- SaveModel email settings
+
+## [4.0.0-beta.24] - 2023-09-06
+
+### 🚀 Features
+
+- Send request in cloud
+- Add discord notifications
+
+### 🐛 Bug Fixes
+
+- Form address
+- Show hosted email service, just disable for non pro subs
+- Add navbar for source + keys
+- Add docker network to build process
+- Overlapping apps
+- Do not show system wide git on cloud
+- Lowercase image names
+- Typo
+
+### 💼 Other
+
+- Backup existing database
+
+## [4.0.0-beta.23] - 2023-09-01
+
+### 🐛 Bug Fixes
+
+- Sentry bug
+- Button loading animation
+
+## [4.0.0-beta.22] - 2023-09-01
+
+### 🚀 Features
+
+- Add resend as transactional emails
+
+### 🐛 Bug Fixes
+
+- DockerCleanupjob
+- Validation
+- Webhook endpoint in cloud and no system wide gh app
+- Subscriptions
+- Password confirmation
+- Proxy start job
+- Dockerimage jobs are not overlapping
+
+## [4.0.0-beta.21] - 2023-08-27
+
+### 🚀 Features
+
+- Invite by email from waitlist
+- Rolling update
+
+### 🐛 Bug Fixes
+
+- Limits & server creation page
+- Fqdn on apps
+
+### 💼 Other
+
+- Boarding
+
+## [4.0.0-beta.20] - 2023-08-17
+
+### 🚀 Features
+
+- Send internal notification to discord
+- Monitor server connection
+
+### 🐛 Bug Fixes
+
+- Make coolify-db backups unique dir
+
+## [4.0.0-beta.19] - 2023-08-15
+
+### 🚀 Features
+
+- Pricing plans ans subs
+- Add s3 storages
+- Init postgresql database
+- Add backup notifications
+- Dockerfile build pack
+- Cloud
+- Force password reset + waitlist
+
+### 🐛 Bug Fixes
+
+- Remove buggregator from dev
+- Able to change localhost's private key
+- Readonly input box
+- Notifications
+- Licensing
+- Subscription link
+- Migrate db schema for smtp + discord
+- Text field
+- Null fqdn notifications
+- Remove old modal
+- Proxy stop/start ui
+- Proxy UI
+- Empty description
+- Input and textarea
+- Postgres_username name to not name, lol
+- DatabaseBackupJob.php
+- No storage
+- Backup now button
+- Ui + subscription
+- Self-hosted
+
+### 💼 Other
+
+- Scheduled backups
+
+## [4.0.0-beta.18] - 2023-07-14
+
+### 🚀 Features
+
+- Able to control multiplexing
+- Add runRemoteCommandSync
+- Github repo with deployment key
+- Add persistent volumes
+- Debuggable executeNow commands
+- Add private gh repos
+- Delete gh app
+- Installation/update github apps
+- Auto-deploy
+- Deploy key based deployments
+- Resource limits
+- Long running queue with 1 hour of timeout
+- Add arm build to dev
+- Disk cleanup threshold by server
+- Notify user of disk cleanup init
+
+### 🐛 Bug Fixes
+
+- Logo of CCCareers
+- Typo
+- Ssh
+- Nullable name on deploy_keys
+- Enviroments
+- Remove dd - oops
+- Add inprogress activity
+- Application view
+- Only set status in case the last command block is finished
+- Poll activity
+- Small typo
+- Show activity on load
+- Deployment should fail on error
+- Tests
+- Version
+- Status not needed
+- No project redirect
+- Gh actions
+- Set status
+- Seeders
+- Do not modify localhost
+- Deployment_uuid -> type_uuid
+- Read env from config, bc of cache
+- Private key change view
+- New destination
+- Do not update next channel all the time
+- Cancel deployment button
+- Public repo limit shown + branch should be preselected.
+- Better status on ui for apps
+- Arm coolify version
+- Formatting
+- Gh actions
+- Show github app secrets
+- Do not force next version updates
+- Debug log button
+- Deployment key based works
+- Deployment cancel/debug buttons
+- Upgrade button
+- Changing static build changes port
+- Overwrite default nginx configuration
+- Do not overlap docker image names
+- Oops
+- Found image name
+- Name length
+- Semicolons encoding by traefik
+- Base_dir wip & outputs
+- Cleanup docker images
+- Nginx try_files
+- Master is the default, not main
+- No ms in rate limit resets
+- Loading after button text
+- Default value
+- Localhost is usable
+- Update docker-compose prod
+- Cloud/checkoutid/lms
+- Type of license code
+- More verbose error
+- Version lol
+- Update prod compose
+- Version
+
+### 💼 Other
+
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Persisting data
+
+## [3.12.28] - 2023-03-16
+
+### 🐛 Bug Fixes
+
+- Revert from dockerhub if ghcr.io does not exists
+
+## [3.12.27] - 2023-03-07
+
+### 🐛 Bug Fixes
+
+- Show ip address as host in public dbs
+
+## [3.12.24] - 2023-03-04
+
+### 🐛 Bug Fixes
+
+- Nestjs buildpack
+
+## [3.12.22] - 2023-03-03
+
+### 🚀 Features
+
+- Add host path to any container
+
+### 🐛 Bug Fixes
+
+- Set PACK_VERSION to 0.27.0
+- PublishDirectory
+- Host volumes
+- Replace . & .. & $PWD with ~
+- Handle log format volumes
+
+## [3.12.19] - 2023-02-20
+
+### 🚀 Features
+
+- Github raw icon url
+- Remove svg support
+
+### 🐛 Bug Fixes
+
+- Typos in docs
+- Url
+- Network in compose files
+- Escape new line chars in wp custom configs
+- Applications cannot be deleted
+- Arm servics
+- Base directory not found
+- Cannot delete resource when you are not on root team
+- Empty port in docker compose
+
+## [3.12.18] - 2023-01-24
+
+### 🐛 Bug Fixes
+
+- CleanupStuckedContainers
+- CleanupStuckedContainers
+
+## [3.12.16] - 2023-01-20
+
+### 🐛 Bug Fixes
+
+- Stucked containers
+
+## [3.12.15] - 2023-01-20
+
+### 🐛 Bug Fixes
+
+- Cleanup function
+- Cleanup stucked containers
+- Deletion + cleanupStuckedContainers
+
+## [3.12.14] - 2023-01-19
+
+### 🐛 Bug Fixes
+
+- Www redirect
+
+## [3.12.13] - 2023-01-18
+
+### 🐛 Bug Fixes
+
+- Secrets
+
+## [3.12.12] - 2023-01-17
+
+### 🚀 Features
+
+- Init h2c (http2/grpc) support
+- Http + h2c paralel
+
+### 🐛 Bug Fixes
+
+- Build args docker compose
+- Grpc
+
+## [3.12.11] - 2023-01-16
+
+### 🐛 Bug Fixes
+
+- Compose file location
+- Docker log sequence
+- Delete apps with previews
+- Do not cleanup compose applications as unconfigured
+- Build env variables with docker compose
+- Public gh repo reload compose
+
+### 💼 Other
+
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+
+## [3.12.10] - 2023-01-11
+
+### 💼 Other
+
+- Add missing variables
+
+## [3.12.9] - 2023-01-11
+
+### 🚀 Features
+
+- Add Openblocks icon
+- Adding icon for whoogle
+- *(ui)* Add libretranslate service icon
+- Handle invite_only plausible analytics
+
+### 🐛 Bug Fixes
+
+- Custom gitlab git user
+- Add documentation link again
+- Remove prefetches
+- Doc link
+- Temporary disable dns check with dns servers
+- Local images for reverting
+- Secrets
+
+## [3.12.8] - 2022-12-27
+
+### 🐛 Bug Fixes
+
+- Parsing secrets
+- Read-only permission
+- Read-only iam
+- $ sign in secrets
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.12.5] - 2022-12-26
+
+### 🐛 Bug Fixes
+
+- Remove unused imports
+
+### 💼 Other
+
+- Conditional on environment
+
+## [3.12.2] - 2022-12-19
+
+### 🐛 Bug Fixes
+
+- Appwrite tmp volume
+- Do not replace secret
+- Root user for dbs on arm
+- Escape secrets
+- Escape env vars
+- Envs
+- Docker buildpack env
+- Secrets with newline
+- Secrets
+- Add default node_env variable
+- Add default node_env variable
+- Secrets
+- Secrets
+- Gh actions
+- Duplicate env variables
+- Cleanupstorage
+
+### 💼 Other
+
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.12.1] - 2022-12-13
+
+### 🐛 Bug Fixes
+
+- Build commands
+- Migration file
+- Adding missing appwrite volume
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.12.0] - 2022-12-09
+
+### 🚀 Features
+
+- Use registry for building
+- Docker registries working
+- Custom docker compose file location in repo
+- Save doNotTrackData to db
+- Add default sentry
+- Do not track in settings
+- System wide git out of beta
+- Custom previewseparator
+- Sentry frontend
+- Able to host static/php sites on arm
+- Save application data before deploying
+- SimpleDockerfile deployment
+- Able to push image to docker registry
+- Revert to remote image
+- *(api)* Name label
+
+### 🐛 Bug Fixes
+
+- 0 destinations redirect after creation
+- Seed
+- Sentry dsn update
+- Dnt
+- Ui
+- Only visible with publicrepo
+- Migrations
+- Prevent webhook errors to be logged
+- Login error
+- Remove beta from systemwide git
+- Git checkout
+- Remove sentry before migration
+- Webhook previewseparator
+- Apache on arm
+- Update PR/MRs with new previewSeparator
+- Static for arm
+- Failed builds should not push images
+- Turn off autodeploy for simpledockerfiles
+- Security hole
+- Rde
+- Delete resource on dashboard
+- Wrong port in case of docker compose
+- Public db icon on dashboard
+- Cleanup
+
+### 💼 Other
+
+- Pocketbase release
+
+## [3.11.10] - 2022-11-16
+
+### 🚀 Features
+
+- Only show expose if no proxy conf defined in template
+- Custom/private docker registries
+
+### 🐛 Bug Fixes
+
+- Local dev api/ws urls
+- Wrong template/type
+- Gitea icon is svg
+- Gh actions
+- Gh actions
+- Replace $$generate vars
+- Webhook traefik
+- Exposed ports
+- Wrong icons on dashboard
+- Escape % in secrets
+- Move debug log settings to build logs
+- Storage for compose bp + debug on
+- Hasura admin secret
+- Logs
+- Mounts
+- Load logs after build failed
+- Accept logged and not logged user in /base
+- Remote haproxy password/etc
+- Remove hardcoded sentry dsn
+- Nope in database strings
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+- Version++
+- Version++
+
+## [3.11.9] - 2022-11-15
+
+### 🐛 Bug Fixes
+
+- IsBot issue
+
+## [3.11.8] - 2022-11-14
+
+### 🐛 Bug Fixes
+
+- Default icon for new services
+
+## [3.11.1] - 2022-11-08
+
+### 🚀 Features
+
+- Rollback coolify
+
+### 🐛 Bug Fixes
+
+- Remove contribution docs
+- Umami template
+- Compose webhooks fixed
+- Variable replacements
+- Doc links
+- For rollback
+- N8n and weblate icon
+- Expose ports for services
+- Wp + mysql on arm
+- Show rollback button loading
+- No tags error
+- Update on mobile
+- Dashboard error
+- GetTemplates
+- Docker compose persistent volumes
+- Application persistent storage things
+- Volume names for undefined volume names in compose
+- Empty secrets on UI
+- Ports for services
+
+### 💼 Other
+
+- Secrets on apps
+- Fix
+- Fixes
+- Reload compose loading
+
+### 🚜 Refactor
+
+- Code
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Add jda icon for lavalink service
+- Version++
+
+### ◀️ Revert
+
+- Revert: revert
+
+## [3.11.0] - 2022-11-07
+
+### 🚀 Features
+
+- Initial support for specific git commit
+- Add default to latest commit and support for gitlab
+- Redirect catch-all rule
+
+### 🐛 Bug Fixes
+
+- Secret errors
+- Service logs
+- Heroku bp
+- Expose port is readonly on the wrong condition
+- Toast
+- Traefik proxy q 10s
+- App logs view
+- Tooltip
+- Toast, rde, webhooks
+- Pathprefix
+- Load public repos
+- Webhook simplified
+- Remote webhooks
+- Previews wbh
+- Webhooks
+- Websecure redirect
+- Wb for previews
+- Pr stopps main deployment
+- Preview wbh
+- Wh catchall for all
+- Remove old minio proxies
+- Template files
+- Compose icon
+- Templates
+- Confirm restart service
+- Template
+- Templates
+- Templates
+- Plausible analytics things
+- Appwrite webhook
+- Coolify instance proxy
+- Migrate template
+- Preview webhooks
+- Simplify webhooks
+- Remove ghost-mariadb from the list
+- More simplified webhooks
+- Umami + ghost issues
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.10.16] - 2022-10-12
+
+### 🐛 Bug Fixes
+
+- Single container logs and usage with compose
+
+### 💼 Other
+
+- New resource label
+
+## [3.10.15] - 2022-10-12
+
+### 🚀 Features
+
+- Monitoring by container
+
+### 🐛 Bug Fixes
+
+- Do not show nope as ip address for dbs
+- Add git sha to build args
+- Smart search for new services
+- Logs for not running containers
+- Update docker binaries
+- Gh release
+- Dev container
+- Gitlab auth and compose reload
+- Check compose domains in general
+- Port required if fqdn is set
+- Appwrite v1 missing containers
+- Dockerfile
+- Pull does not work remotely on huge compose file
+
+### ⚙️ Miscellaneous Tasks
+
+- Update staging release
+
+## [3.10.14] - 2022-10-05
+
+### 🚀 Features
+
+- Docker compose support
+- Docker compose
+- Docker compose
+
+### 🐛 Bug Fixes
+
+- Do not use npx
+- Pure docker based development
+
+### 💼 Other
+
+- Docker-compose support
+- Docker compose
+- Remove worker jobs
+- One less worker thread
+
+### 🧪 Testing
+
+- Remove prisma
+
+## [3.10.5] - 2022-09-26
+
+### 🚀 Features
+
+- Add migration button to appwrite
+- Custom certificate
+- Ssl cert on traefik config
+- Refresh resource status on dashboard
+- Ssl certificate sets custom ssl for applications
+- System-wide github apps
+- Cleanup unconfigured applications
+- Cleanup unconfigured services and databases
+
+### 🐛 Bug Fixes
+
+- Ui
+- Tooltip
+- Dropdown
+- Ssl certificate distribution
+- Db migration
+- Multiplex ssh connections
+- Able to search with id
+- Not found redirect
+- Settings db requests
+- Error during saving logs
+- Consider base directory in heroku bp
+- Basedirectory should be empty if null
+- Allow basedirectory for heroku
+- Stream logs for heroku bp
+- Debug log for bp
+- Scp without host verification & cert copy
+- Base directory & docker bp
+- Laravel php chooser
+- Multiplex ssh and ssl copy
+- Seed new preview secret types
+- Error notification
+- Empty preview value
+- Error notification
+- Seed
+- Service logs
+- Appwrite function network is not the default
+- Logs in docker bp
+- Able to delete apps in unconfigured state
+- Disable development low disk space
+- Only log things to console in dev mode
+- Do not get status of more than 10 resources defined by category
+- BaseDirectory
+- Dashboard statuses
+- Default buildImage and baseBuildImage
+- Initial deploy status
+- Show logs better
+- Do not start tcp proxy without main container
+- Cleanup stucked tcp proxies
+- Default 0 pending invitations
+- Handle forked repositories
+- Typo
+- Pr branches
+- Fork pr previews
+- Remove unnecessary things
+- Meilisearch data dir
+- Verify and configure remote docker engines
+- Add buildkit features
+- Nope if you are not logged in
+
+### 💼 Other
+
+- Responsive!
+- Fixes
+- Fix git icon
+- Dropdown as infobox
+- Small logs on mobile
+- Improvements
+- Fix destination view
+- Settings view
+- More UI improvements
+- Fixes
+- Fixes
+- Fix
+- Fixes
+- Beta features
+- Fix button
+- Service fixes
+- Fix basedirectory meaning
+- Resource button fix
+- Main resource search
+- Dev logs
+- Loading button
+- Fix gitlab importer view
+- Small fix
+- Beta flag
+- Hasura console notification
+- Fix
+- Fix
+- Fixes
+- Inprogress version of iam
+- Fix indicato
+- Iam & settings update
+- Send 200 for ping and installation wh
+- Settings icon
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
### ◀️ Revert
- Show usage everytime
-- Revert: revert
-- Wip
-- Variable parsing
-- Hc return code check
-- Instancesettings
-- Pull policy
-- Advanced dropdown
-- Databasebackup
-- Remove Cloudflare async tag attributes
-- Encrypting mount and fs_path
-- *(parser)* Enhance FQDN generation logic for services and applications
+
+## [3.10.2] - 2022-09-11
+
+### 🚀 Features
+
+- Add queue reset button
+- Previewapplications init
+- PreviewApplications finalized
+- Fluentbit
+- Show remote servers
+- *(layout)* Added drawer when user is in mobile
+- Re-apply ui improves
+- *(ui)* Improve header of pages
+- *(styles)* Make header css component
+- *(routes)* Improve ui for apps, databases and services logs
+
+### 🐛 Bug Fixes
+
+- Changing umami image URL to get latest version
+- Gitlab importer for public repos
+- Show error logs
+- Umami init sql
+- Plausible analytics actions
+- Login
+- Dev url
+- UpdateMany build logs
+- Fallback to db logs
+- Fluentbit configuration
+- Coolify update
+- Fluentbit and logs
+- Canceling build
+- Logging
+- Load more
+- Build logs
+- Versions of appwrite
+- Appwrite?!
+- Get building status
+- Await
+- Await #2
+- Update PR building status
+- Appwrite default version 1.0
+- Undead endpoint does not require JWT
+- *(routes)* Improve design of application page
+- *(routes)* Improve design of git sources page
+- *(routes)* Ui from destinations page
+- *(routes)* Ui from databases page
+- *(routes)* Ui from databases page
+- *(routes)* Ui from databases page
+- *(routes)* Ui from services page
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* Ui from settings page
+- *(routes)* Duplicates classes in services page
+- *(routes)* Searchbar ui
+- Github conflicts
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- Ui with headers
+- *(routes)* Header of settings page in databases
+- *(routes)* Ui from secrets table
+
+### 💼 Other
+
+- Fix plausible
+- Fix cleanup button
+- Fix buttons
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Minor changes
+- Minor changes
+- Minor changes
+- Whoops
+
+## [3.10.1] - 2022-09-10
+
+### 🐛 Bug Fixes
+
+- Show restarting apps
+- Show restarting application & logs
+- Remove unnecessary gitlab group name
+- Secrets for PR
+- Volumes for services
+- Build secrets for apps
+- Delete resource use window location
+
+### 💼 Other
+
+- Fix button
+- Fix follow button
+- Arm should be on next all the time
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.10.0] - 2022-09-08
+
+### 🚀 Features
+
+- New servers view
+
+### 🐛 Bug Fixes
+
+- Change to execa from utils
+- Save search input
+- Ispublic status on databases
+- Port checkers
+- Ui variables
+- Glitchtip env to pyhton boolean
+- Autoupdater
+
+### 💼 Other
+
+- Dashboard updates
+- Fix tooltip
+
+## [3.9.4] - 2022-09-07
+
+### 🐛 Bug Fixes
+
+- DnsServer formatting
+- Settings for service
+
+## [3.9.3] - 2022-09-07
+
+### 🐛 Bug Fixes
+
+- Pr previews
+
+## [3.9.2] - 2022-09-07
+
+### 🚀 Features
+
+- Add traefik acme json to coolify container
+- Database secrets
+
+### 🐛 Bug Fixes
+
+- Gitlab webhook
+- Use ip address instead of window location
+- Use ip instead of window location host
+- Service state update
+- Add initial DNS servers
+- Revert last change with domain check
+- Service volume generation
+- Minio default env variables
+- Add php 8.1/8.2
+- Edgedb ui
+- Edgedb stuff
+- Edgedb
+
+### 💼 Other
+
+- Fix login/register page
+- Update devcontainer
+- Add debug log
+- Fix initial loading icon bg
+- Fix loading start/stop db/services
+- Dashboard updates and a lot more
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+
+## [3.9.0] - 2022-09-06
+
+### 🐛 Bug Fixes
+
+- Debug api logging + gh actions
+- Workdir
+- Move restart button to settings
+
+## [3.9.1-rc.1] - 2022-09-06
+
+### 🚀 Features
+
+- *(routes)* Rework ui from login and register page
+
+### 🐛 Bug Fixes
+
+- Ssh pid agent name
+- Repository link trim
+- Fqdn or expose port required
+- Service deploymentEnabled
+- Expose port is not required
+- Remote verification
+- Dockerfile
+
+### 💼 Other
+
+- Database_branches
+- Login page
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+
+## [3.9.0-rc.1] - 2022-09-02
+
+### 🚀 Features
+
+- New service - weblate
+- Restart application
+- Show elapsed time on running builds
+- Github allow fual branches
+- Gitlab dual branch
+- Taiga
+
+### 🐛 Bug Fixes
+
+- Glitchtip things
+- Loading state on start
+- Ui
+- Submodule
+- Gitlab webhooks
+- UI + refactor
+- Exposedport on save
+- Appwrite letsencrypt
+- Traefik appwrite
+- Traefik
+- Finally works! :)
+- Rename components + remove PR/MR deployment from public repos
+- Settings missing id
+- Explainer component
+- Database name on logs view
+- Taiga
+
+### 💼 Other
+
+- Fixes
+- Change tooltips and info boxes
+- Added rc release
+
+### 🧪 Testing
+
+- Native binary target
+- Dockerfile
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.8.9] - 2022-08-30
+
+### 🐛 Bug Fixes
+
+- Oh god Prisma
+
+## [3.8.8] - 2022-08-30
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.8.6] - 2022-08-30
+
+### 🐛 Bug Fixes
+
+- Pr deployment
+- CompareVersions
+- Include
+- Include
+- Gitlab apps
+
+### 💼 Other
+
+- Fixes
+- Route to the correct path when creating destination from db config
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.8.5] - 2022-08-27
+
+### 🐛 Bug Fixes
+
+- Copy all files during install process
+- Typo
+- Process
+- White labeled icon on navbar
+- Whitelabeled icon
+- Next/nuxt deployment type
+- Again
+
+## [3.8.4] - 2022-08-27
+
+### 🐛 Bug Fixes
+
+- UI thinkgs
+- Delete team while it is active
+- Team switching
+- Queue cleanup
+- Decrypt secrets
+- Cleanup build cache as well
+- Pr deployments + remove public gits
+
+### 💼 Other
+
+- Dashbord fixes
+- Fixes
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.8.3] - 2022-08-26
+
+### 🐛 Bug Fixes
+
+- Secrets decryption
+
+## [3.8.2] - 2022-08-26
+
+### 🚀 Features
+
+- *(ui)* Rework home UI and with responsive design
+
+### 🐛 Bug Fixes
+
+- Never stop deplyo queue
+- Build queue system
+- High cpu usage
+- Worker
+- Better worker system
+
+### 💼 Other
+
+- Dashboard fine-tunes
+- Fine-tune
+- Fixes
+- Fix
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.8.1] - 2022-08-24
+
+### 🐛 Bug Fixes
+
+- Ui buttons
+- Clear queue on cancelling jobs
+- Cancelling jobs
+- Dashboard for admins
+
+## [3.8.0] - 2022-08-23
+
+### 🚀 Features
+
+- Searxng service
+
+### 🐛 Bug Fixes
+
+- Port checker
+- Cancel build after 5 seconds
+- ExposedPort checker
+- Batch secret =
+- Dashboard for non-root users
+- Stream build logs
+- Show build log start/end
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.7.0] - 2022-08-19
+
+### 🚀 Features
+
+- Add GlitchTip service
+
+### 🐛 Bug Fixes
+
+- Missing commas
+- ExposedPort is just optional
+
+### ⚙️ Miscellaneous Tasks
+
+- Add .pnpm-store in .gitignore
+- Version++
+
+## [3.6.0] - 2022-08-18
+
+### 🚀 Features
+
+- Import public repos (wip)
+- Public repo deployment
+- Force rebuild + env.PORT for port + public repo build
+
+### 🐛 Bug Fixes
+
+- Bots without exposed ports
+
+### 💼 Other
+
+- Fixes here and there
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.5.2] - 2022-08-17
+
+### 🐛 Bug Fixes
+
+- Restart containers on-failure instead of always
+- Show that Ghost values could be changed
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.5.1] - 2022-08-17
+
+### 🐛 Bug Fixes
+
+- Revert docker compose version to 2.6.1
+- Trim secrets
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.5.0] - 2022-08-17
+
+### 🚀 Features
+
+- Deploy bots (no domains)
+- Custom dns servers
+
+### 🐛 Bug Fixes
+
+- Dns button ui
+- Bot deployments
+- Bots
+- AutoUpdater & cleanupStorage jobs
+
+### 💼 Other
+
+- Typing
+
+## [3.4.0] - 2022-08-16
+
+### 🚀 Features
+
+- Appwrite service
+- Heroku deployments
+
+### 🐛 Bug Fixes
+
+- Replace docker compose with docker-compose on CSB
+- Dashboard ui
+- Create coolify-infra, if it does not exists
+- Gitpod conf and heroku buildpacks
+- Appwrite
+- Autoimport + readme
+- Services import
+- Heroku icon
+- Heroku icon
+
+## [3.3.4] - 2022-08-15
+
+### 🐛 Bug Fixes
+
+- Make it public button
+- Loading indicator
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.3.3] - 2022-08-14
+
+### 🐛 Bug Fixes
+
+- Decryption errors
+- Postgresql on ARM
+
+## [3.3.2] - 2022-08-12
+
+### 🐛 Bug Fixes
+
+- Debounce dashboard status requests
+
+### 💼 Other
+
+- Fider
+
+## [3.3.1] - 2022-08-12
+
+### 🐛 Bug Fixes
+
+- Empty buildpack icons
+
+## [3.2.3] - 2022-08-12
+
+### 🚀 Features
+
+- Databases on ARM
+- Mongodb arm support
+- New dashboard
+
+### 🐛 Bug Fixes
+
+- Cleanup stucked prisma-engines
+- Toast
+- Secrets
+- Cleanup prisma engine if there is more than 1
+- !isARM to isARM
+- Enterprise GH link
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.2.2] - 2022-08-11
+
+### 🐛 Bug Fixes
+
+- Coolify-network on verification
+
+## [3.2.1] - 2022-08-11
+
+### 🚀 Features
+
+- Init heroku buildpacks
+
+### 🐛 Bug Fixes
+
+- Follow/cancel buttons
+- Only remove coolify managed containers
+- White-labeled env
+- Schema
+
+### 💼 Other
+
+- Fix
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.2.0] - 2022-08-11
+
+### 🚀 Features
+
+- Persistent storage for all services
+- Cleanup clickhouse db
+
+### 🐛 Bug Fixes
+
+- Rde local ports
+- Empty remote destinations could be removed
+- Tips
+- Lowercase issues fider
+- Tooltip colors
+- Update clickhouse configuration
+- Cleanup command
+- Enterprise Github instance endpoint
+
+### 💼 Other
+
+- Local ssh port
+- Redesign a lot
+- Fixes
+- Loading indicator for plausible buttons
+
+## [3.1.4] - 2022-08-01
+
+### 🚀 Features
+
+- Moodle init
+- Remote docker engine init
+- Working on remote docker engine
+- Rde
+- Remote docker engine
+- Ipv4 and ipv6
+- Contributors
+- Add arch to database
+- Stop preview deployment
+
+### 🐛 Bug Fixes
+
+- Settings from api
+- Selectable destinations
+- Gitpod hardcodes
+- Typo
+- Typo
+- Expose port checker
+- States and exposed ports
+- CleanupStorage
+- Remote traefik webhook
+- Remote engine ip address
+- RemoteipAddress
+- Explanation for remote engine url
+- Tcp proxy
+- Lol
+- Webhook
+- Dns check for rde
+- Gitpod
+- Revert last commit
+- Dns check
+- Dns checker
+- Webhook
+- Df and more debug
+- Webhooks
+- Load previews async
+- Destination icon
+- Pr webhook
+- Cache image
+- No ssh key found
+- Prisma migration + update of docker and stuffs
+- Ui
+- Ui
+- Only 1 ssh-agent is needed
+- Reuse ssh connection
+- Ssh tunnel
+- Dns checking
+- Fider BASE_URL set correctly
+
+### 💼 Other
+
+- Error message https://github.com/coollabsio/coolify/issues/502
+- Changes
+- Settings
+- For removing app
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.1.3] - 2022-07-18
+
+### 🚀 Features
+
+- Init moodle and separate stuffs to shared package
+
+### 🐛 Bug Fixes
+
+- More types for API
+- More types
+- Do not rebuild in case image exists and sha not changed
+- Gitpod urls
+- Remove new service start process
+- Remove shared dir, deployment does not work
+- Gitlab custom url
+- Location url for services and apps
+
+## [3.1.2] - 2022-07-14
+
+### 🐛 Bug Fixes
+
+- Admin password reset should not timeout
+- Message for double branches
+- Turn off autodeploy if double branch is configured
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.1.1] - 2022-07-13
+
+### 🚀 Features
+
+- Gitpod integration
+
+### 🐛 Bug Fixes
+
+- Cleanup less often and can do it manually
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+
+## [3.1.0] - 2022-07-12
+
+### 🚀 Features
+
+- Ability to change deployment type for nextjs
+- Ability to change deployment type for nuxtjs
+- Gitpod ready code(almost)
+- Add Docker buildpack exposed port setting
+- Custom port for git instances
+
+### 🐛 Bug Fixes
+
+- GitLab pagination load data
+- Service domain checker
+- Wp missing ftp solution
+- Ftp WP issues
+- Ftp?!
+- Gitpod updates
+- Gitpod
+- Gitpod
+- Wordpress FTP permission issues
+- GitLab search fields
+- GitHub App button
+- GitLab loop on misconfigured source
+- Gitpod
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [3.0.3] - 2022-07-06
+
+### 🐛 Bug Fixes
+
+- Domain check
+- Domain check
+- TrustProxy for Fastify
+- Hostname issue
+
+## [3.0.2] - 2022-07-06
+
+### 🐛 Bug Fixes
+
+- New destination can be created
+- Include post
+- New destinations
+
+## [3.0.1] - 2022-07-06
+
+### 🐛 Bug Fixes
+
+- Seeding
+- Forgot that the version bump changed 😅
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.11] - 2022-06-20
+
+### 🐛 Bug Fixes
+
+- Be able to change database + service versions
+- Lock file
+
+## [2.9.10] - 2022-06-17
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.9] - 2022-06-10
+
+### 🐛 Bug Fixes
+
+- Host and reload for uvicorn
+- Remove package-lock
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.8] - 2022-06-10
+
+### 🐛 Bug Fixes
+
+- Persistent nocodb
+- Nocodb persistency
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.7] - 2022-06-09
+
+### 🐛 Bug Fixes
+
+- Plausible custom script
+- Plausible script and middlewares
+- Remove console log
+- Remove comments
+- Traefik middleware
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.6] - 2022-06-02
+
+### 🐛 Bug Fixes
+
+- Fider changed an env variable name
+- Pnpm command
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.5] - 2022-06-02
+
+### 🐛 Bug Fixes
+
+- Proxy stop missing argument
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.4] - 2022-06-01
+
+### 🐛 Bug Fixes
+
+- Demo version forms
+- Typo
+- Revert gh and gl cloning
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.3] - 2022-05-31
+
+### 🐛 Bug Fixes
+
+- Recurisve clone instead of submodule
+- Versions
+- Only reconfigure coolify proxy if its missconfigured
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.2] - 2022-05-31
+
+### 🐛 Bug Fixes
+
+- TrustProxy
+- Force restart proxy
+- Only restart coolify proxy in case of version prior to 2.9.2
+- Force restart proxy on seeding
+- Add GIT ENV variable for submodules
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.1] - 2022-05-31
+
+### 🐛 Bug Fixes
+
+- GitHub fixes
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.9.0] - 2022-05-31
+
+### 🚀 Features
+
+- PageLoader
+- Database + service usage
+
+### 🐛 Bug Fixes
+
+- Service checks
+- Remove console.log
+- Traefik
+- Remove debug things
+- WIP Traefik
+- Proxy for http
+- PR deployments view
+- Minio urls + domain checks
+- Remove gh token on git source changes
+- Do not fetch app state in case of missconfiguration
+- Demo instance save domain instantly
+- Instant save on demo instance
+- New source canceled view
+- Lint errors in database services
+- Otherfqdns
+- Host key verification
+- Ftp connection
+
+### 💼 Other
+
+- Appwrite
+- Testing WS
+- Traefik?!
+- Traefik
+- Traefik
+- Traefik migration
+- Traefik
+- Traefik
+- Traefik
+- Notifications and application usage
+- *(fix)* Traefik
+- Css
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.8.2] - 2022-05-16
+
+### 🐛 Bug Fixes
+
+- Gastby buildpack
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.8.1] - 2022-05-10
+
+### 🐛 Bug Fixes
+
+- WP custom db
+- UI
+
+## [2.6.1] - 2022-05-03
+
+### 🚀 Features
+
+- Basic server usage on dashboard
+- Show usage trends
+- Usage on dashboard
+- Custom script path for Plausible
+- WP could have custom db
+- Python image selection
+
+### 🐛 Bug Fixes
+
+- ExposedPorts
+- Logos for dbs
+- Do not run SSL renew in development
+- Check domain for coolify before saving
+- Remove debug info
+- Cancel jobs
+- Cancel old builds in database
+- Better DNS check to prevent errors
+- Check DNS in prod only
+- DNS check
+- Disable sentry for now
+- Cancel
+- Sentry
+- No image for Docker buildpack
+- Default packagemanager
+- Server usage only shown for root team
+- Expose ports for services
+- UI
+- Navbar UI
+- UI
+- UI
+- Remove RC python
+- UI
+- UI
+- UI
+- Default Python package
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+- Version++
+- Version++
+
+## [2.6.0] - 2022-05-02
+
+### 🚀 Features
+
+- Hasura as a service
+- Gzip compression
+- Laravel buildpack is working!
+- Laravel
+- Fider service
+- Database and services logs
+- DNS check settings for SSL generation
+- Cancel builds!
+
+### 🐛 Bug Fixes
+
+- Unami svg size
+- Team switching moved to IAM menu
+- Always use IP address for webhooks
+- Remove unnecessary test endpoint
+- UI
+- Migration
+- Fider envs
+- Checking low disk space
+- Build image
+- Update autoupdate env variable
+- Renew certificates
+- Webhook build images
+- Missing node versions
+
+### 💼 Other
+
+- Laravel
+
+## [2.4.11] - 2022-04-20
+
+### 🚀 Features
+
+- Deno DB migration
+- Show exited containers on UI & better UX
+- Query container state periodically
+- Install svelte-18n and init setup
+- Umami service
+- Coolify auto-updater
+- Autoupdater
+- Select base image for buildpacks
+
+### 🐛 Bug Fixes
+
+- Deno configurations
+- Text on deno buildpack
+- Correct branch shown in build logs
+- Vscode permission fix
+- I18n
+- Locales
+- Application logs is not reversed and queried better
+- Do not activate i18n for now
+- GitHub token cleanup on team switch
+- No logs found
+- Code cleanups
+- Reactivate posgtres password
+- Contribution guide
+- Simplify list services
+- Contribution
+- Contribution guide
+- Contribution guide
+- Packagemanager finder
+
+### 💼 Other
+
+- Umami service
+- Base image selector
+
+### 📚 Documentation
+
+- How to add new services
+- Update
+- Update
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+- Version++
+
+## [2.4.10] - 2022-04-17
+
+### 🚀 Features
+
+- Add persistent storage for services
+- Multiply dockerfile locations for docker buildpack
+- Testing fluentd logging driver
+- Fluentbit investigation
+- Initial deno support
+
+### 🐛 Bug Fixes
+
+- Switch from bitnami/redis to normal redis
+- Use redis-alpine
+- Wordpress extra config
+- Stop sFTP connection on wp stop
+- Change user's id in sftp wp instance
+- Use arm based certbot on arm
+- Buildlog line number is not string
+- Application logs paginated
+- Switch to stream on applications logs
+- Scroll to top for logs
+- Pull new images for services all the time it's started.
+- White-labeled custom logo
+- Application logs
+
+### 💼 Other
+
+- Show extraconfig if wp is running
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+
+## [2.4.9] - 2022-04-14
+
+### 🐛 Bug Fixes
+
+- Postgres root pw is pw field
+- Teams view
+- Improved tcp proxy monitoring for databases/ftp
+- Add HTTP proxy checks
+- Loading of new destinations
+- Better performance for cleanup images
+- Remove proxy container in case of dependent container is down
+- Restart local docker coolify proxy in case of something happens to it
+- Id of service container
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.8] - 2022-04-13
+
+### 🐛 Bug Fixes
+
+- Register should happen if coolify proxy cannot be started
+- GitLab typo
+- Remove system wide pw reset
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.7] - 2022-04-13
+
+### 🐛 Bug Fixes
+
+- Destinations to HAProxy
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.6] - 2022-04-13
+
+### 🐛 Bug Fixes
+
+- Cleanup images older than a day
+- Meilisearch service
+- Load all branches, not just the first 30
+- ProjectID for Github
+- DNS check before creating SSL cert
+- Try catch me
+- Restart policy for resources
+- No permission on first registration
+- Reverting postgres password for now
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.5] - 2022-04-12
+
+### 🐛 Bug Fixes
+
+- Types
+- Invitations
+- Timeout values
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.4] - 2022-04-12
+
+### 🐛 Bug Fixes
+
+- Haproxy build stuffs
+- Proxy
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.3] - 2022-04-12
+
+### 🐛 Bug Fixes
+
+- Remove unnecessary save button haha
+- Update dockerfile
+
+### ⚙️ Miscellaneous Tasks
+
+- Update packages
+- Version++
+- Update build scripts
+- Update build packages
+
+## [2.4.2] - 2022-04-09
+
+### 🐛 Bug Fixes
+
+- Missing install repositories GitHub
+- Return own and other sources better
+- Show config missing on sources
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.1] - 2022-04-09
+
+### 🐛 Bug Fixes
+
+- Enable https for Ghost
+- Postgres root passwor shown and set
+- Able to change postgres user password from ui
+- DB Connecting string generator
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.4.0] - 2022-04-08
+
+### 🚀 Features
+
+- Wordpress on-demand SFTP
+- Finalize on-demand sftp for wp
+- PHP Composer support
+- Working on-demand sftp to wp data
+- Admin team sees everything
+- Able to change service version/tag
+- Basic white labeled version
+- Able to modify database passwords
+
+### 🐛 Bug Fixes
+
+- Add openssl to image
+- Permission issues
+- On-demand sFTP for wp
+- Fix for fix haha
+- Do not pull latest image
+- Updated db versions
+- Only show proxy for admin team
+- Team view for root team
+- Do not trigger >1 webhooks on GitLab
+- Possible fix for spikes in CPU usage
+- Last commit
+- Www or not-www, that's the question
+- Fix for the fix that fixes the fix
+- Ton of updates for users/teams
+- Small typo
+- Unique storage paths
+- Self-hosted GitLab URL
+- No line during buildLog
+- Html/apiUrls cannot end with /
+- Typo
+- Missing buildpack
+
+### 💼 Other
+
+- Fix
+- Better layout for root team
+- Fix
+- Fixes
+- Fix
+- Fix
+- Fix
+- Fix
+- Fix
+- Fix
+- Fix
+- Insane amount
+- Fix
+- Fixes
+- Fixes
+- Fix
+- Fixes
+- Fixes
+
+### 📚 Documentation
+
+- Contribution guide
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.3.3] - 2022-04-05
+
+### 🐛 Bug Fixes
+
+- Add git lfs while deploying
+- Try to update build status several times
+- Update stucked builds
+- Update stucked builds on startup
+- Revert seed
+- Lame fixing
+- Remove asyncUntil
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.3.2] - 2022-04-04
+
+### 🐛 Bug Fixes
+
+- *(php)* If .htaccess file found use apache
+- Add default webhook domain for n8n
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.3.1] - 2022-04-04
+
+### 🐛 Bug Fixes
+
+- Secrets build/runtime coudl be changed after save
+- Default configuration
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.3.0] - 2022-04-04
+
+### 🚀 Features
+
+- Initial python support
+- Add loading on register button
+- *(dev)* Allow windows users to use pnpm dev
+- MeiliSearch service
+- Add abilitry to paste env files
+
+### 🐛 Bug Fixes
+
+- Ignore coolify proxy error for now
+- Python no wsgi
+- If user not found
+- Rename envs to secrets
+- Infinite loop on www domains
+- No need to paste clear text env for previews
+- Build log fix attempt #1
+- Small UI fix on logs
+- Lets await!
+- Async progress
+- Remove console.log
+- Build log
+- UI
+- Gitlab & Github urls
+
+### 💼 Other
+
+- Improvements
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+- Lock file + fix packages
+
+## [2.2.7] - 2022-04-01
+
+### 🐛 Bug Fixes
+
+- Haproxy errors
+- Build variables
+- Use NodeJS for sveltekit for now
+
+## [2.2.6] - 2022-03-31
+
+### 🐛 Bug Fixes
+
+- Add PROTO headers
+
+## [2.2.5] - 2022-03-31
+
+### 🐛 Bug Fixes
+
+- Registration enabled/disabled
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.2.4] - 2022-03-31
+
+### 🐛 Bug Fixes
+
+- Gitlab repo url
+- No need to dashify anymore
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.2.3] - 2022-03-31
+
+### 🐛 Bug Fixes
+
+- List ghost services
+- Reload window on settings saved
+- Persistent storage on webhooks
+- Add license
+- Space in repo names
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+- Version++
+- Fixed typo on New Git Source view
+
+## [2.2.0] - 2022-03-27
+
+### 🚀 Features
+
+- Add n8n.io service
+- Add update kuma service
+- Ghost service
+
+### 🐛 Bug Fixes
+
+- Ghost logo size
+- Ghost icon, remove console.log
+
+### 💼 Other
+
+- Colors on svelte-select
+
+### ⚙️ Miscellaneous Tasks
+
+- Version ++
+
+## [2.1.1] - 2022-03-25
+
+### 🐛 Bug Fixes
+
+- Cleanup only 2 hours+ old images
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.1.0] - 2022-03-23
+
+### 🚀 Features
+
+- Use compose instead of normal docker cmd
+- Be able to redeploy PRs
+
+### 🐛 Bug Fixes
+
+- Skip ssl cert in case of error
+- Volumes
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.31] - 2022-03-20
+
+### 🚀 Features
+
+- Add PHP modules
+
+### 🐛 Bug Fixes
+
+- Cleanup old builds
+- Only cleanup same app
+- Add nginx + htaccess files
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.30] - 2022-03-19
+
+### 🐛 Bug Fixes
+
+- No cookie found
+- Missing session data
+- No error if GitSource is missing
+- No webhook secret found?
+- Basedir for dockerfiles
+- Better queue system + more support on monorepos
+- Remove build logs in case of app removed
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.29] - 2022-03-11
+
+### 🚀 Features
+
+- Webhooks inititate all applications with the correct branch
+- Check ssl for new apps/services first
+- Autodeploy pause
+- Install pnpm into docker image if pnpm lock file is used
+
+### 🐛 Bug Fixes
+
+- Personal Gitlab repos
+- Autodeploy true by default for GH repos
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.28] - 2022-03-04
+
+### 🚀 Features
+
+- Service secrets
+
+### 🐛 Bug Fixes
+
+- Do not error if proxy is not running
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.27] - 2022-03-02
+
+### 🚀 Features
+
+- Send version with update request
+
+### 🐛 Bug Fixes
+
+- Check when a container is running
+- Reload haproxy if new cert is added
+- Cleanup coolify images
+- Application state in UI
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.26] - 2022-03-02
+
+### 🐛 Bug Fixes
+
+- Update process
+
+## [2.0.25] - 2022-03-02
+
+### 🚀 Features
+
+- Languagetool service
+
+### 🐛 Bug Fixes
+
+- Reload proxy on ssl cert
+- Volume name
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.24] - 2022-03-02
+
+### 🐛 Bug Fixes
+
+- Better proxy check
+- Ssl + sslrenew
+- Null proxyhash on restart
+- Reconfigure proxy on restart
+- Update process
+
+## [2.0.23] - 2022-02-28
+
+### 🐛 Bug Fixes
+
+- Be sure .env exists
+- Missing fqdn for services
+- Default npm command
+- Add coolify-image label for build images
+- Cleanup old images, > 3 days
+
+### 💼 Other
+
+- Colorful states
+- Application start
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.22] - 2022-02-27
+
+### 🐛 Bug Fixes
+
+- Coolify image pulls
+- Remove wrong/stuck proxy configurations
+- Always use a buildpack
+- Add icons for eleventy + astro
+- Fix proxy every 10 secs
+- Do not remove coolify proxy
+- Update version
+
+### 💼 Other
+
+- Remote docker engine
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.21] - 2022-02-24
+
+### 🚀 Features
+
+- Random subdomain for demo
+- Random domain for services
+- Astro buildpack
+- 11ty buildpack
+- Registration page
+
+### 🐛 Bug Fixes
+
+- Http for demo, oops
+- Docker scanner
+- Improvement on image pulls
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.20] - 2022-02-23
+
+### 🐛 Bug Fixes
+
+- Revert default network
+
+### 💼 Other
+
+- Dns check
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.19] - 2022-02-23
+
+### 🐛 Bug Fixes
+
+- Random network name for demo
+- Settings fqdn grr
+
+## [2.0.18] - 2022-02-22
+
+### 🚀 Features
+
+- Ports range
+
+### 🐛 Bug Fixes
+
+- Email is lowercased in login
+- Lowercase email everywhere
+- Use normal docker-compose in dev
+
+### 💼 Other
+
+- Make copy/password visible
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.17] - 2022-02-21
+
+### 🐛 Bug Fixes
+
+- Move tokens from session to cookie/store
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.14] - 2022-02-18
+
+### 🚀 Features
+
+- Basic password reset form
+- Scan for lock files and set right commands
+- Public port range (WIP)
+
+### 🐛 Bug Fixes
+
+- SSL app off
+- Local docker host
+- Typo
+- Lets encrypt
+- Remove SSL with stop
+- SSL off for services
+- Grr
+- Running state css
+- Minor fixes
+- Remove force SSL when doing let's encrypt request
+- GhToken in session now
+- Random port for certbot
+- Follow icon
+- Plausible volume fixed
+- Database connection strings
+- Gitlab webhooks fixed
+- If DNS not found, do not redirect
+- Github token
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version ++
+
+## [2.0.13] - 2022-02-17
+
+### 🐛 Bug Fixes
+
+- Login issues
+
+## [2.0.11] - 2022-02-15
+
+### 🚀 Features
+
+- Follow logs
+- Generate www & non-www SSL certs
+
+### 🐛 Bug Fixes
+
+- Window error in SSR
+- GitHub sync PR's
+- Load more button
+- Small fixes
+- Typo
+- Error with follow logs
+- IsDomainConfigured
+- TransactionIds
+- Coolify image cleanup
+- Cleanup every 10 mins
+- Cleanup images
+- Add no user redis to uri
+- Secure cookie disabled by default
+- Buggy svelte-kit-cookie-session
+
+### 💼 Other
+
+- Only allow cleanup in production
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+- Version++
+
+## [2.0.10] - 2022-02-15
+
+### 🐛 Bug Fixes
+
+- Typo
+- Error handling
+- Stopping service without proxy
+- Coolify proxy start
+
+### ⚙️ Miscellaneous Tasks
+
+- Version++
+
+## [2.0.8] - 2022-02-14
+
+### 🐛 Bug Fixes
+
+- Validate secrets
+- Truncate git clone errors
+- Branch used does not throw error
+
+## [2.0.7] - 2022-02-13
+
+### 🚀 Features
+
+- Www <-> non-www redirection for apps
+- Www <-> non-www redirection
+
+### 🐛 Bug Fixes
+
+- Package.json
+- Build secrets should be visible in runtime
+- New secret should have default values
+
+## [2.0.5] - 2022-02-11
+
+### 🚀 Features
+
+- VaultWarden service
+
+### 🐛 Bug Fixes
+
+- PreventDefault on a button, thats all
+- Haproxy check should not throw error
+- Delete all build files
+- Cleanup images
+- More error handling in proxy configuration + cleanups
+- Local static assets
+- Check sentry
+- Typo
+
+### ⚙️ Miscellaneous Tasks
+
+- Version
+- Version
+
+## [2.0.4] - 2022-02-11
+
+### 🚀 Features
+
+- Use tags in update
+- New update process (#115)
+
+### 🐛 Bug Fixes
+
+- Docker Engine bug related to live-restore and IPs
+- Version
+
+## [2.0.3] - 2022-02-10
+
+### 🐛 Bug Fixes
+
+- Capture non-error as error
+- Only delete id.rsa in case of it exists
+- Status is not available yet
+
+### ⚙️ Miscellaneous Tasks
+
+- Version bump
+
+## [2.0.2] - 2022-02-10
+
+### 🐛 Bug Fixes
+
+- Secrets join
+- ENV variables set differently
+
+## [1.0.0] - 2021-03-24
diff --git a/CLAUDE.md b/CLAUDE.md
index 6434ef877..b7c496e42 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,9 +2,9 @@ # CLAUDE.md
This file provides guidance to **Claude Code** (claude.ai/code) when working with code in this repository.
-> **Note for AI Assistants**: This file is specifically for Claude Code. If you're using Cursor IDE, refer to the `.cursor/rules/` directory for detailed rule files. Both systems share core principles but are optimized for their respective workflows.
+> **Note for AI Assistants**: This file is specifically for Claude Code. All detailed documentation is in the `.ai/` directory. Both Claude Code and Cursor IDE use the same source files in `.ai/` for consistency.
>
-> **Maintaining Instructions**: When updating AI instructions, see [.AI_INSTRUCTIONS_SYNC.md](.AI_INSTRUCTIONS_SYNC.md) for synchronization guidelines between CLAUDE.md and .cursor/rules/.
+> **Maintaining Instructions**: When updating AI instructions, see [.ai/meta/sync-guide.md](.ai/meta/sync-guide.md) and [.ai/meta/maintaining-docs.md](.ai/meta/maintaining-docs.md) for guidelines.
## Project Overview
@@ -27,7 +27,8 @@ ### Backend Development
### Code Quality
- `./vendor/bin/pint` - Run Laravel Pint for code formatting
- `./vendor/bin/phpstan` - Run PHPStan for static analysis
-- `./vendor/bin/pest` - Run Pest tests (unit tests only, without database)
+- `./vendor/bin/pest tests/Unit` - Run unit tests only (no database, can run outside Docker)
+- `./vendor/bin/pest` - Run ALL tests (includes Feature tests, may require database)
### Running Tests
**IMPORTANT**: Tests that require database connections MUST be run inside the Docker container:
@@ -39,12 +40,14 @@ ### Running Tests
## Architecture Overview
### Technology Stack
-- **Backend**: Laravel 12 (PHP 8.4)
-- **Frontend**: Livewire 3.5+ with Alpine.js and Tailwind CSS 4.1+
+- **Backend**: Laravel 12.4.1 (PHP 8.4.7)
+- **Frontend**: Livewire 3.5.20 with Alpine.js and Tailwind CSS 4.1.4
- **Database**: PostgreSQL 15 (primary), Redis 7 (cache/queues)
- **Real-time**: Soketi (WebSocket server)
- **Containerization**: Docker & Docker Compose
-- **Queue Management**: Laravel Horizon
+- **Queue Management**: Laravel Horizon 5.30.3
+
+> **Note**: For complete version information and all dependencies, see [.ai/core/technology-stack.md](.ai/core/technology-stack.md)
### Key Components
@@ -256,453 +259,61 @@ ## Important Reminders
## Additional Documentation
-This file contains high-level guidelines for Claude Code. For **more detailed, topic-specific documentation**, refer to the `.cursor/rules/` directory (also accessible by Cursor IDE and other AI assistants):
+This file contains high-level guidelines for Claude Code. For **more detailed, topic-specific documentation**, refer to the `.ai/` directory:
-> **Cross-Reference**: The `.cursor/rules/` directory contains comprehensive, detailed documentation organized by topic. Start with [.cursor/rules/README.mdc](.cursor/rules/README.mdc) for an overview, then explore specific topics below.
+> **Documentation Hub**: The `.ai/` directory contains comprehensive, detailed documentation organized by topic. Start with [.ai/README.md](.ai/README.md) for navigation, then explore specific topics below.
-### Architecture & Patterns
-- [Application Architecture](.cursor/rules/application-architecture.mdc) - Detailed application structure
-- [Deployment Architecture](.cursor/rules/deployment-architecture.mdc) - Deployment patterns and flows
-- [Database Patterns](.cursor/rules/database-patterns.mdc) - Database design and query patterns
-- [Frontend Patterns](.cursor/rules/frontend-patterns.mdc) - Livewire and Alpine.js patterns
-- [API & Routing](.cursor/rules/api-and-routing.mdc) - API design and routing conventions
+### Core Documentation
+- [Technology Stack](.ai/core/technology-stack.md) - All versions, packages, and dependencies (single source of truth)
+- [Project Overview](.ai/core/project-overview.md) - What Coolify is and how it works
+- [Application Architecture](.ai/core/application-architecture.md) - System design and component relationships
+- [Deployment Architecture](.ai/core/deployment-architecture.md) - How deployments work end-to-end
-### Development & Security
-- [Development Workflow](.cursor/rules/development-workflow.mdc) - Development best practices
-- [Security Patterns](.cursor/rules/security-patterns.mdc) - Security implementation details
-- [Form Components](.cursor/rules/form-components.mdc) - Enhanced form components with authorization
-- [Testing Patterns](.cursor/rules/testing-patterns.mdc) - Testing strategies and examples
+### Development Practices
+- [Development Workflow](.ai/development/development-workflow.md) - Development setup, commands, and workflows
+- [Testing Patterns](.ai/development/testing-patterns.md) - Testing strategies and examples (Docker requirements!)
+- [Laravel Boost](.ai/development/laravel-boost.md) - Laravel-specific guidelines and best practices
-### Project Information
-- [Project Overview](.cursor/rules/project-overview.mdc) - High-level project structure
-- [Technology Stack](.cursor/rules/technology-stack.mdc) - Detailed tech stack information
-- [Cursor Rules Guide](.cursor/rules/cursor_rules.mdc) - How to maintain cursor rules
+### Code Patterns
+- [Database Patterns](.ai/patterns/database-patterns.md) - Eloquent, migrations, relationships
+- [Frontend Patterns](.ai/patterns/frontend-patterns.md) - Livewire, Alpine.js, Tailwind CSS
+- [Security Patterns](.ai/patterns/security-patterns.md) - Authentication, authorization, security
+- [Form Components](.ai/patterns/form-components.md) - Enhanced form components with authorization
+- [API & Routing](.ai/patterns/api-and-routing.md) - API design and routing conventions
-===
+### Meta Documentation
+- [Maintaining Docs](.ai/meta/maintaining-docs.md) - How to update and improve AI documentation
+- [Sync Guide](.ai/meta/sync-guide.md) - Keeping documentation synchronized
-
-=== foundation rules ===
+## Laravel Boost Guidelines
-# Laravel Boost Guidelines
+> **Full Guidelines**: See [.ai/development/laravel-boost.md](.ai/development/laravel-boost.md) for complete Laravel Boost guidelines.
-The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
+### Essential Laravel Patterns
-## Foundational Context
-This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
+- Use PHP 8.4 constructor property promotion and typed properties
+- Follow PSR-12 (run `./vendor/bin/pint` before committing)
+- Use Eloquent ORM, avoid raw queries
+- Use Form Request classes for validation
+- Queue heavy operations with Laravel Horizon
+- Never use `env()` outside config files
+- Use named routes with `route()` function
+- Laravel 12 with Laravel 10 structure (no bootstrap/app.php)
-- php - 8.4.7
-- laravel/fortify (FORTIFY) - v1
-- laravel/framework (LARAVEL) - v12
-- laravel/horizon (HORIZON) - v5
-- laravel/prompts (PROMPTS) - v0
-- laravel/sanctum (SANCTUM) - v4
-- laravel/socialite (SOCIALITE) - v5
-- livewire/livewire (LIVEWIRE) - v3
-- laravel/dusk (DUSK) - v8
-- laravel/pint (PINT) - v1
-- laravel/telescope (TELESCOPE) - v5
-- pestphp/pest (PEST) - v3
-- phpunit/phpunit (PHPUNIT) - v11
-- rector/rector (RECTOR) - v2
-- laravel-echo (ECHO) - v2
-- tailwindcss (TAILWINDCSS) - v4
-- vue (VUE) - v3
+### Testing Requirements
+- **Unit tests**: No database, use mocking, run with `./vendor/bin/pest tests/Unit`
+- **Feature tests**: Can use database, run with `docker exec coolify php artisan test`
+- Every change must have tests
+- Use Pest for all tests
-## Conventions
-- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
-- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
-- Check for existing components to reuse before writing a new one.
+### Livewire & Frontend
-## Verification Scripts
-- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
-
-## Application Structure & Architecture
-- Stick to existing directory structure - don't create new base folders without approval.
-- Do not change the application's dependencies without approval.
-
-## Frontend Bundling
-- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
-
-## Replies
-- Be concise in your explanations - focus on what's important rather than explaining obvious details.
-
-## Documentation Files
-- You must only create documentation files if explicitly requested by the user.
-
-
-=== boost rules ===
-
-## Laravel Boost
-- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
-
-## Artisan
-- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
-
-## URLs
-- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
-
-## Tinker / Debugging
-- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
-- Use the `database-query` tool when you only need to read from the database.
-
-## Reading Browser Logs With the `browser-logs` Tool
-- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
-- Only recent browser logs will be useful - ignore old logs.
-
-## Searching Documentation (Critically Important)
-- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
-- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
-- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
-- Search the documentation before making code changes to ensure we are taking the correct approach.
-- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
-- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
-
-### Available Search Syntax
-- You can and should pass multiple queries at once. The most relevant results will be returned first.
-
-1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
-2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
-3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
-4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
-5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
-
-
-=== php rules ===
-
-## PHP
-
-- Always use curly braces for control structures, even if it has one line.
-
-### Constructors
-- Use PHP 8 constructor property promotion in `__construct()`.
- - public function __construct(public GitHub $github) { }
-- Do not allow empty `__construct()` methods with zero parameters.
-
-### Type Declarations
-- Always use explicit return type declarations for methods and functions.
-- Use appropriate PHP type hints for method parameters.
-
-
-protected function isAccessible(User $user, ?string $path = null): bool
-{
- ...
-}
-
-
-## Comments
-- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
-
-## PHPDoc Blocks
-- Add useful array shape type definitions for arrays when appropriate.
-
-## Enums
-- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
-
-
-=== laravel/core rules ===
-
-## Do Things the Laravel Way
-
-- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
-- If you're creating a generic PHP class, use `artisan make:class`.
-- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
-
-### Database
-- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
-- Use Eloquent models and relationships before suggesting raw database queries
-- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
-- Generate code that prevents N+1 query problems by using eager loading.
-- Use Laravel's query builder for very complex database operations.
-
-### Model Creation
-- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
-
-### APIs & Eloquent Resources
-- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
-
-### Controllers & Validation
-- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
-- Check sibling Form Requests to see if the application uses array or string based validation rules.
-
-### Queues
-- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
-
-### Authentication & Authorization
-- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
-
-### URL Generation
-- When generating links to other pages, prefer named routes and the `route()` function.
-
-### Configuration
-- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
-
-### Testing
-- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
-- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
-- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
-
-### Vite Error
-- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
-
-
-=== laravel/v12 rules ===
-
-## Laravel 12
-
-- Use the `search-docs` tool to get version specific documentation.
-- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure.
-- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not to need migrate to the new Laravel structure unless the user explicitly requests that.
-
-### Laravel 10 Structure
-- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`.
-- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
- - Middleware registration happens in `app/Http/Kernel.php`
- - Exception handling is in `app/Exceptions/Handler.php`
- - Console commands and schedule register in `app/Console/Kernel.php`
- - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
-
-### Database
-- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
-- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
-
-### Models
-- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
-
-
-=== livewire/core rules ===
-
-## Livewire Core
-- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
-- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components
-- State should live on the server, with the UI reflecting it.
-- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
-
-## Livewire Best Practices
-- Livewire components require a single root element.
-- Use `wire:loading` and `wire:dirty` for delightful loading states.
-- Add `wire:key` in loops:
-
- ```blade
- @foreach ($items as $item)
-
- {{ $item->name }}
-
- @endforeach
- ```
-
-- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects:
-
-
- public function mount(User $user) { $this->user = $user; }
- public function updatedSearch() { $this->resetPage(); }
-
-
-
-## Testing Livewire
-
-
- Livewire::test(Counter::class)
- ->assertSet('count', 0)
- ->call('increment')
- ->assertSet('count', 1)
- ->assertSee(1)
- ->assertStatus(200);
-
-
-
-
- $this->get('/posts/create')
- ->assertSeeLivewire(CreatePost::class);
-
-
-
-=== livewire/v3 rules ===
-
-## Livewire 3
-
-### Key Changes From Livewire 2
-- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
- - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
- - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
- - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
-
-### New Directives
-- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
-
-### Alpine
-- Alpine is now included with Livewire, don't manually include Alpine.js.
-- Plugins included with Alpine: persist, intersect, collapse, and focus.
-
-### Lifecycle Hooks
-- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
-
-
-document.addEventListener('livewire:init', function () {
- Livewire.hook('request', ({ fail }) => {
- if (fail && fail.status === 419) {
- alert('Your session expired');
- }
- });
-
- Livewire.hook('message.failed', (message, component) => {
- console.error(message);
- });
-});
-
-
-
-=== pint/core rules ===
-
-## Laravel Pint Code Formatter
-
-- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
-- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
-
-
-=== pest/core rules ===
-
-## Pest
-
-### Testing
-- If you need to verify a feature is working, write or update a Unit / Feature test.
-
-### Pest Tests
-- All tests must be written using Pest. Use `php artisan make:test --pest `.
-- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
-- Tests should test all of the happy paths, failure paths, and weird paths.
-- Tests live in the `tests/Feature` and `tests/Unit` directories.
-- **Unit tests** MUST use mocking and avoid database. They can run outside Docker.
-- **Feature tests** can use database but MUST run inside Docker container.
-- **Design for testability**: Structure code to be testable without database when possible. Use dependency injection and interfaces.
-- **Mock by default**: Prefer `Mockery::mock()` over `Model::factory()->create()` in unit tests.
-- Pest tests look and behave like this:
-
-it('is true', function () {
- expect(true)->toBeTrue();
-});
-
-
-### Running Tests
-**IMPORTANT**: Always run tests in the correct environment based on database dependencies:
-
-**Unit Tests (no database):**
-- Run outside Docker: `./vendor/bin/pest tests/Unit`
-- Run specific file: `./vendor/bin/pest tests/Unit/ProxyCustomCommandsTest.php`
-- These tests use mocking and don't require PostgreSQL
-
-**Feature Tests (with database):**
-- Run inside Docker: `docker exec coolify php artisan test`
-- Run specific file: `docker exec coolify php artisan test tests/Feature/ExampleTest.php`
-- Filter by name: `docker exec coolify php artisan test --filter=testName`
-- These tests require PostgreSQL and use factories/migrations
-
-**General Guidelines:**
-- Run the minimal number of tests using an appropriate filter before finalizing code edits
-- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite
-- If you get database connection errors, you're running a Feature test outside Docker - move it inside
-
-### Pest Assertions
-- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
-
-it('returns all', function () {
- $response = $this->postJson('/api/docs', []);
-
- $response->assertSuccessful();
-});
-
-
-### Mocking
-- Mocking can be very helpful when appropriate.
-- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
-- You can also create partial mocks using the same import or self method.
-
-### Datasets
-- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
-
-
-it('has emails', function (string $email) {
- expect($email)->not->toBeEmpty();
-})->with([
- 'james' => 'james@laravel.com',
- 'taylor' => 'taylor@laravel.com',
-]);
-
-
-
-=== tailwindcss/core rules ===
-
-## Tailwind Core
-
-- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
-- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
-- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
-- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
-
-### Spacing
-- When listing items, use gap utilities for spacing, don't use margins.
-
-
-
-
Superior
-
Michigan
-
Erie
-
-
-
-
-### Dark Mode
-- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
-
-
-=== tailwindcss/v4 rules ===
-
-## Tailwind 4
-
-- Always use Tailwind CSS v4 - do not use the deprecated utilities.
-- `corePlugins` is not supported in Tailwind v4.
-- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
-
-
-
-
-### Replaced Utilities
-- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
-- Opacity values are still numeric.
-
-| Deprecated | Replacement |
-|------------+--------------|
-| bg-opacity-* | bg-black/* |
-| text-opacity-* | text-black/* |
-| border-opacity-* | border-black/* |
-| divide-opacity-* | divide-black/* |
-| ring-opacity-* | ring-black/* |
-| placeholder-opacity-* | placeholder-black/* |
-| flex-shrink-* | shrink-* |
-| flex-grow-* | grow-* |
-| overflow-ellipsis | text-ellipsis |
-| decoration-slice | box-decoration-slice |
-| decoration-clone | box-decoration-clone |
-
-
-=== tests rules ===
-
-## Test Enforcement
-
-- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
-- Run the minimum number of tests needed to ensure code quality and speed.
-- **For Unit tests**: Use `./vendor/bin/pest tests/Unit/YourTest.php` (runs outside Docker)
-- **For Feature tests**: Use `docker exec coolify php artisan test --filter=YourTest` (runs inside Docker)
-- Choose the correct test type based on database dependency:
- - No database needed? → Unit test with mocking
- - Database needed? → Feature test in Docker
-
+- Livewire components require single root element
+- Use `wire:model.live` for real-time updates
+- Alpine.js included with Livewire
+- Tailwind CSS 4.1.4 (use new utilities, not deprecated ones)
+- Use `gap` utilities for spacing, not margins
Random other things you should remember:
diff --git a/README.md b/README.md
index f159cde89..a84b3bfa9 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,13 @@
-
+
-[](https://console.algora.io/org/coollabsio/bounties/new)
+# Coolify
+An open-source & self-hostable Heroku / Netlify / Vercel alternative.
-# About the Project
+ [](https://console.algora.io/org/coollabsio/bounties/new)
+
+
+## About the Project
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
@@ -15,7 +19,7 @@ # About the Project
For more information, take a look at our landing page at [coolify.io](https://coolify.io).
-# Installation
+## Installation
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -25,11 +29,11 @@ # Installation
> [!NOTE]
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
-# Support
+## Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
-# Cloud
+## Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io)
@@ -44,14 +48,14 @@ ## Why should I use the Cloud version?
- Better support
- Less maintenance for you
-# Donations
+## Donations
To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development.
[coolify.io/sponsorships](https://coolify.io/sponsorships)
Thank you so much!
-## Big Sponsors
+### Big Sponsors
* [23M](https://23m.com?ref=coolify.io) - Your experts for high-availability hosting solutions!
* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
@@ -69,7 +73,6 @@ ## Big Sponsors
* [Dade2](https://dade2.net/?ref=coolify.io) - IT Consulting, Cloud Solutions & System Integration
* [Formbricks](https://formbricks.com?ref=coolify.io) - The open source feedback platform
* [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions
-* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions
* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
@@ -89,7 +92,7 @@ ## Big Sponsors
* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
-## Small Sponsors
+### Small Sponsors
@@ -142,7 +145,7 @@ ## Small Sponsors
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
-# Recognitions
+## Recognitions
@@ -158,17 +161,17 @@ # Recognitions
-# Core Maintainers
+## Core Maintainers
| Andras Bacsai | 🏔️ Peak |
|------------|------------|
| | |
| | |
-# Repo Activity
+## Repo Activity

-# Star History
+## Star History
[](https://star-history.com/#coollabsio/coolify&Date)
diff --git a/app/Actions/Database/RestartDatabase.php b/app/Actions/Database/RestartDatabase.php
index 0400d924d..940bc69fb 100644
--- a/app/Actions/Database/RestartDatabase.php
+++ b/app/Actions/Database/RestartDatabase.php
@@ -22,7 +22,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- StopDatabase::run($database);
+ StopDatabase::run($database, dockerCleanup: false);
return StartDatabase::run($database);
}
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index 38f6d7bc8..7fdfe9aeb 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -105,7 +105,7 @@ public function handle(StandaloneClickhouse $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 300221d24..d1bb119af 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -192,7 +192,7 @@ public function handle(StandaloneDragonfly $database)
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 3a2ceebb3..128469e24 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -208,7 +208,7 @@ public function handle(StandaloneKeydb $database)
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 8a936c8ae..29dd7b8fe 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -209,7 +209,7 @@ public function handle(StandaloneMariadb $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 19699d684..5982b68be 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -260,7 +260,7 @@ public function handle(StandaloneMongodb $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
if ($this->database->enable_ssl) {
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 25546fa9d..c1df8d6db 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -210,7 +210,7 @@ public function handle(StandaloneMysql $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index ac011acbe..1ae0d56a0 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -223,7 +223,7 @@ public function handle(StandalonePostgresql $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
if ($this->database->enable_ssl) {
@@ -231,8 +231,6 @@ public function handle(StandalonePostgresql $database)
}
$this->commands[] = "echo 'Database started.'";
- ray($this->commands);
-
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 8a7ae42a4..4c99a0213 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -205,7 +205,7 @@ public function handle(StandaloneRedis $database)
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
- $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
+ $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index 63f5b1979..5c881e743 100644
--- a/app/Actions/Database/StopDatabase.php
+++ b/app/Actions/Database/StopDatabase.php
@@ -49,7 +49,7 @@ private function stopContainer($database, string $containerName, int $timeout =
{
$server = $database->destination->server;
instant_remote_process(command: [
- "docker stop --timeout=$timeout $containerName",
+ "docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
}
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index f5d5f82b6..61a3c4615 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -8,13 +8,17 @@
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
+use App\Services\ContainerStatusAggregator;
+use App\Traits\CalculatesExcludedStatus;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
{
use AsAction;
+ use CalculatesExcludedStatus;
public string $jobQueue = 'high';
@@ -28,6 +32,10 @@ class GetContainersStatus
protected ?Collection $applicationContainerStatuses;
+ protected ?Collection $applicationContainerRestartCounts;
+
+ protected ?Collection $serviceContainerStatuses;
+
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
{
$this->containers = $containers;
@@ -95,11 +103,15 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
$labels = data_get($container, 'Config.Labels');
}
$containerStatus = data_get($container, 'State.Status');
- $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
+ $containerHealth = data_get($container, 'State.Health.Status');
if ($containerStatus === 'restarting') {
- $containerStatus = "restarting ($containerHealth)";
+ $healthSuffix = $containerHealth ?? 'unknown';
+ $containerStatus = "restarting:$healthSuffix";
+ } elseif ($containerStatus === 'exited') {
+ // Keep as-is, no health suffix for exited containers
} else {
- $containerStatus = "$containerStatus ($containerHealth)";
+ $healthSuffix = $containerHealth ?? 'unknown';
+ $containerStatus = "$containerStatus:$healthSuffix";
}
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
@@ -136,6 +148,18 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
if ($containerName) {
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
}
+
+ // Track restart counts for applications
+ $restartCount = data_get($container, 'RestartCount', 0);
+ if (! isset($this->applicationContainerRestartCounts)) {
+ $this->applicationContainerRestartCounts = collect();
+ }
+ if (! $this->applicationContainerRestartCounts->has($applicationId)) {
+ $this->applicationContainerRestartCounts->put($applicationId, collect());
+ }
+ if ($containerName) {
+ $this->applicationContainerRestartCounts->get($applicationId)->put($containerName, $restartCount);
+ }
} else {
// Notify user that this container should not be there.
}
@@ -207,23 +231,34 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
- $service = $services->where('id', $serviceLabelId)->first();
- if (! $service) {
+ $parentService = $services->where('id', $serviceLabelId)->first();
+ if (! $parentService) {
continue;
}
+
+ // Store container status for aggregation
+ if (! isset($this->serviceContainerStatuses)) {
+ $this->serviceContainerStatuses = collect();
+ }
+
+ $key = $serviceLabelId.':'.$subType.':'.$subId;
+ if (! $this->serviceContainerStatuses->has($key)) {
+ $this->serviceContainerStatuses->put($key, collect());
+ }
+
+ $containerName = data_get($labels, 'com.docker.compose.service');
+ if ($containerName) {
+ $this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
+ }
+
+ // Mark service as found
if ($subType === 'application') {
- $service = $service->applications()->where('id', $subId)->first();
+ $service = $parentService->applications()->where('id', $subId)->first();
} else {
- $service = $service->databases()->where('id', $subId)->first();
+ $service = $parentService->databases()->where('id', $subId)->first();
}
if ($service) {
$foundServices[] = "$service->id-$service->name";
- $statusFromDb = $service->status;
- if ($statusFromDb !== $containerStatus) {
- $service->update(['status' => $containerStatus]);
- } else {
- $service->update(['last_online_at' => now()]);
- }
}
}
}
@@ -291,7 +326,24 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
continue;
}
- $application->update(['status' => 'exited']);
+ // If container was recently restarting (crash loop), keep it as degraded for a grace period
+ // This prevents false "exited" status during the brief moment between container removal and recreation
+ $recentlyRestarted = $application->restart_count > 0 &&
+ $application->last_restart_at &&
+ $application->last_restart_at->greaterThan(now()->subSeconds(30));
+
+ if ($recentlyRestarted) {
+ // Keep it as degraded if it was recently in a crash loop
+ $application->update(['status' => 'degraded:unhealthy']);
+ } else {
+ // Reset restart count when application exits completely
+ $application->update([
+ 'status' => 'exited',
+ 'restart_count' => 0,
+ 'last_restart_at' => null,
+ 'last_restart_type' => null,
+ ]);
+ }
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
@@ -340,88 +392,144 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
continue;
}
- $aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses);
- if ($aggregatedStatus) {
- $statusFromDb = $application->status;
- if ($statusFromDb !== $aggregatedStatus) {
- $application->update(['status' => $aggregatedStatus]);
- } else {
- $application->update(['last_online_at' => now()]);
- }
+ // Track restart counts first
+ $maxRestartCount = 0;
+ if (isset($this->applicationContainerRestartCounts) && $this->applicationContainerRestartCounts->has($applicationId)) {
+ $containerRestartCounts = $this->applicationContainerRestartCounts->get($applicationId);
+ $maxRestartCount = $containerRestartCounts->max() ?? 0;
}
+
+ // Wrap all database updates in a transaction to ensure consistency
+ DB::transaction(function () use ($application, $maxRestartCount, $containerStatuses) {
+ $previousRestartCount = $application->restart_count ?? 0;
+
+ if ($maxRestartCount > $previousRestartCount) {
+ // Restart count increased - this is a crash restart
+ $application->update([
+ 'restart_count' => $maxRestartCount,
+ 'last_restart_at' => now(),
+ 'last_restart_type' => 'crash',
+ ]);
+
+ // Send notification
+ $containerName = $application->name;
+ $projectUuid = data_get($application, 'environment.project.uuid');
+ $environmentName = data_get($application, 'environment.name');
+ $applicationUuid = data_get($application, 'uuid');
+
+ if ($projectUuid && $applicationUuid && $environmentName) {
+ $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
+ } else {
+ $url = null;
+ }
+ }
+
+ // Aggregate status after tracking restart counts
+ $aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses, $maxRestartCount);
+ if ($aggregatedStatus) {
+ $statusFromDb = $application->status;
+ if ($statusFromDb !== $aggregatedStatus) {
+ $application->update(['status' => $aggregatedStatus]);
+ } else {
+ $application->update(['last_online_at' => now()]);
+ }
+ }
+ });
}
}
+ // Aggregate multi-container service statuses
+ $this->aggregateServiceContainerStatuses($services);
+
ServiceChecked::dispatch($this->server->team->id);
}
- private function aggregateApplicationStatus($application, Collection $containerStatuses): ?string
+ private function aggregateApplicationStatus($application, Collection $containerStatuses, int $maxRestartCount = 0): ?string
{
// Parse docker compose to check for excluded containers
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
- $excludedContainers = collect();
-
- if ($dockerComposeRaw) {
- try {
- $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
- $services = data_get($dockerCompose, 'services', []);
-
- foreach ($services as $serviceName => $serviceConfig) {
- // Check if container should be excluded
- $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
- $restartPolicy = data_get($serviceConfig, 'restart', 'always');
-
- if ($excludeFromHc || $restartPolicy === 'no') {
- $excludedContainers->push($serviceName);
- }
- }
- } catch (\Exception $e) {
- // If we can't parse, treat all containers as included
- }
- }
+ $excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
// Filter out excluded containers
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
return ! $excludedContainers->contains($containerName);
});
- // If all containers are excluded, don't update status
+ // If all containers are excluded, calculate status from excluded containers
if ($relevantStatuses->isEmpty()) {
- return null;
+ return $this->calculateExcludedStatusFromStrings($containerStatuses);
}
- $hasRunning = false;
- $hasRestarting = false;
- $hasUnhealthy = false;
- $hasExited = false;
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
- foreach ($relevantStatuses as $status) {
- if (str($status)->contains('restarting')) {
- $hasRestarting = true;
- } elseif (str($status)->contains('running')) {
- $hasRunning = true;
- if (str($status)->contains('unhealthy')) {
- $hasUnhealthy = true;
+ return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount);
+ }
+
+ private function aggregateServiceContainerStatuses($services)
+ {
+ if (! isset($this->serviceContainerStatuses) || $this->serviceContainerStatuses->isEmpty()) {
+ return;
+ }
+
+ foreach ($this->serviceContainerStatuses as $key => $containerStatuses) {
+ // Parse key: serviceId:subType:subId
+ [$serviceId, $subType, $subId] = explode(':', $key);
+
+ $service = $services->where('id', $serviceId)->first();
+ if (! $service) {
+ continue;
+ }
+
+ // Get the service sub-resource (ServiceApplication or ServiceDatabase)
+ $subResource = null;
+ if ($subType === 'application') {
+ $subResource = $service->applications()->where('id', $subId)->first();
+ } elseif ($subType === 'database') {
+ $subResource = $service->databases()->where('id', $subId)->first();
+ }
+
+ if (! $subResource) {
+ continue;
+ }
+
+ // Parse docker compose from service to check for excluded containers
+ $dockerComposeRaw = data_get($service, 'docker_compose_raw');
+ $excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
+
+ // Filter out excluded containers
+ $relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
+ return ! $excludedContainers->contains($containerName);
+ });
+
+ // If all containers are excluded, calculate status from excluded containers
+ if ($relevantStatuses->isEmpty()) {
+ $aggregatedStatus = $this->calculateExcludedStatusFromStrings($containerStatuses);
+ if ($aggregatedStatus) {
+ $statusFromDb = $subResource->status;
+ if ($statusFromDb !== $aggregatedStatus) {
+ $subResource->update(['status' => $aggregatedStatus]);
+ } else {
+ $subResource->update(['last_online_at' => now()]);
+ }
+ }
+
+ continue;
+ }
+
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
+ $aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses);
+
+ // Update service sub-resource status with aggregated result
+ if ($aggregatedStatus) {
+ $statusFromDb = $subResource->status;
+ if ($statusFromDb !== $aggregatedStatus) {
+ $subResource->update(['status' => $aggregatedStatus]);
+ } else {
+ $subResource->update(['last_online_at' => now()]);
}
- } elseif (str($status)->contains('exited')) {
- $hasExited = true;
- $hasUnhealthy = true;
}
}
-
- if ($hasRestarting) {
- return 'degraded (unhealthy)';
- }
-
- if ($hasRunning && $hasExited) {
- return 'degraded (unhealthy)';
- }
-
- if ($hasRunning) {
- return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
- }
-
- // All containers are exited
- return 'exited (unhealthy)';
}
}
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 8671a5f27..bfc65d8d2 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -13,7 +13,7 @@ class StartProxy
{
use AsAction;
- public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
+ public function handle(Server $server, bool $async = true, bool $force = false, bool $restarting = false): string|Activity
{
$proxyType = $server->proxyType();
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
@@ -22,7 +22,10 @@ public function handle(Server $server, bool $async = true, bool $force = false):
$server->proxy->set('status', 'starting');
$server->save();
$server->refresh();
- ProxyStatusChangedUI::dispatch($server->team_id);
+
+ if (! $restarting) {
+ ProxyStatusChangedUI::dispatch($server->team_id);
+ }
$commands = collect([]);
$proxy_path = $server->proxyPath();
@@ -60,7 +63,16 @@ public function handle(Server $server, bool $async = true, bool $force = false):
'docker compose pull',
'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
" echo 'Stopping and removing existing coolify-proxy.'",
- ' docker rm -f coolify-proxy || true',
+ ' docker stop coolify-proxy 2>/dev/null || true',
+ ' docker rm -f coolify-proxy 2>/dev/null || true',
+ ' # Wait for container to be fully removed',
+ ' for i in {1..10}; do',
+ ' if ! docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
+ ' break',
+ ' fi',
+ ' echo "Waiting for coolify-proxy to be removed... ($i/10)"',
+ ' sleep 1',
+ ' done',
" echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi',
"echo 'Starting coolify-proxy.'",
diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php
index 29cc63b40..8f1b8af1c 100644
--- a/app/Actions/Proxy/StopProxy.php
+++ b/app/Actions/Proxy/StopProxy.php
@@ -12,17 +12,27 @@ class StopProxy
{
use AsAction;
- public function handle(Server $server, bool $forceStop = true, int $timeout = 30)
+ public function handle(Server $server, bool $forceStop = true, int $timeout = 30, bool $restarting = false)
{
try {
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$server->proxy->status = 'stopping';
$server->save();
- ProxyStatusChangedUI::dispatch($server->team_id);
+
+ if (! $restarting) {
+ ProxyStatusChangedUI::dispatch($server->team_id);
+ }
instant_remote_process(command: [
- "docker stop --time=$timeout $containerName",
- "docker rm -f $containerName",
+ "docker stop --time=$timeout $containerName 2>/dev/null || true",
+ "docker rm -f $containerName 2>/dev/null || true",
+ '# Wait for container to be fully removed',
+ 'for i in {1..10}; do',
+ " if ! docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
+ ' break',
+ ' fi',
+ ' sleep 1',
+ 'done',
], server: $server, throwError: false);
$server->proxy->force_stop = $forceStop;
@@ -32,7 +42,10 @@ public function handle(Server $server, bool $forceStop = true, int $timeout = 30
return handleError($e);
} finally {
ProxyDashboardCacheService::clearCache($server);
- ProxyStatusChanged::dispatch($server->id);
+
+ if (! $restarting) {
+ ProxyStatusChanged::dispatch($server->id);
+ }
}
}
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index 392562167..6bf094c32 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -20,7 +20,7 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
$realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime';
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
- $helperImageVersion = data_get($settings, 'helper_version');
+ $helperImageVersion = getHelperVersion();
$helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index 92dd7e8c3..36c540950 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -59,8 +59,6 @@ public function handle(Server $server)
$command = collect([]);
if (isDev() && $server->id === 0) {
$command = $command->merge([
- "echo 'Installing Prerequisites...'",
- 'sleep 1',
"echo 'Installing Docker Engine...'",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
'sleep 4',
@@ -70,35 +68,6 @@ public function handle(Server $server)
return remote_process($command, $server);
} else {
- if ($supported_os_type->contains('debian')) {
- $command = $command->merge([
- "echo 'Installing Prerequisites...'",
- 'apt-get update -y',
- 'command -v curl >/dev/null || apt install -y curl',
- 'command -v wget >/dev/null || apt install -y wget',
- 'command -v git >/dev/null || apt install -y git',
- 'command -v jq >/dev/null || apt install -y jq',
- ]);
- } elseif ($supported_os_type->contains('rhel')) {
- $command = $command->merge([
- "echo 'Installing Prerequisites...'",
- 'command -v curl >/dev/null || dnf install -y curl',
- 'command -v wget >/dev/null || dnf install -y wget',
- 'command -v git >/dev/null || dnf install -y git',
- 'command -v jq >/dev/null || dnf install -y jq',
- ]);
- } elseif ($supported_os_type->contains('sles')) {
- $command = $command->merge([
- "echo 'Installing Prerequisites...'",
- 'zypper update -y',
- 'command -v curl >/dev/null || zypper install -y curl',
- 'command -v wget >/dev/null || zypper install -y wget',
- 'command -v git >/dev/null || zypper install -y git',
- 'command -v jq >/dev/null || zypper install -y jq',
- ]);
- } else {
- throw new \Exception('Unsupported OS');
- }
$command = $command->merge([
"echo 'Installing Docker Engine...'",
]);
diff --git a/app/Actions/Server/InstallPrerequisites.php b/app/Actions/Server/InstallPrerequisites.php
new file mode 100644
index 000000000..1a7d3bbd9
--- /dev/null
+++ b/app/Actions/Server/InstallPrerequisites.php
@@ -0,0 +1,57 @@
+validateOS();
+ if (! $supported_os_type) {
+ throw new \Exception('Server OS type is not supported for automated installation. Please install prerequisites manually.');
+ }
+
+ $command = collect([]);
+
+ if ($supported_os_type->contains('debian')) {
+ $command = $command->merge([
+ "echo 'Installing Prerequisites...'",
+ 'apt-get update -y',
+ 'command -v curl >/dev/null || apt install -y curl',
+ 'command -v wget >/dev/null || apt install -y wget',
+ 'command -v git >/dev/null || apt install -y git',
+ 'command -v jq >/dev/null || apt install -y jq',
+ ]);
+ } elseif ($supported_os_type->contains('rhel')) {
+ $command = $command->merge([
+ "echo 'Installing Prerequisites...'",
+ 'command -v curl >/dev/null || dnf install -y curl',
+ 'command -v wget >/dev/null || dnf install -y wget',
+ 'command -v git >/dev/null || dnf install -y git',
+ 'command -v jq >/dev/null || dnf install -y jq',
+ ]);
+ } elseif ($supported_os_type->contains('sles')) {
+ $command = $command->merge([
+ "echo 'Installing Prerequisites...'",
+ 'zypper update -y',
+ 'command -v curl >/dev/null || zypper install -y curl',
+ 'command -v wget >/dev/null || zypper install -y wget',
+ 'command -v git >/dev/null || zypper install -y git',
+ 'command -v jq >/dev/null || zypper install -y jq',
+ ]);
+ } else {
+ throw new \Exception('Unsupported OS type for prerequisites installation');
+ }
+
+ $command->push("echo 'Prerequisites installed successfully.'");
+
+ return remote_process($command, $server);
+ }
+}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 2a06428e2..0bf763d78 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -2,7 +2,6 @@
namespace App\Actions\Server;
-use App\Jobs\PullHelperImageJob;
use App\Models\Server;
use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -50,7 +49,9 @@ public function handle($manual_update = false)
private function update()
{
- PullHelperImageJob::dispatch($this->server);
+ $helperImage = config('constants.coolify.helper_image');
+ $latest_version = getHelperVersion();
+ instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
instant_remote_process(["docker pull -q $image"], $this->server, false);
diff --git a/app/Actions/Server/ValidatePrerequisites.php b/app/Actions/Server/ValidatePrerequisites.php
new file mode 100644
index 000000000..23c1db1d0
--- /dev/null
+++ b/app/Actions/Server/ValidatePrerequisites.php
@@ -0,0 +1,40 @@
+, found: array}
+ */
+ public function handle(Server $server): array
+ {
+ $requiredCommands = ['git', 'curl', 'jq'];
+ $missing = [];
+ $found = [];
+
+ foreach ($requiredCommands as $cmd) {
+ $result = instant_remote_process(["command -v {$cmd}"], $server, false);
+ if (! $result) {
+ $missing[] = $cmd;
+ } else {
+ $found[] = $cmd;
+ }
+ }
+
+ return [
+ 'success' => empty($missing),
+ 'missing' => $missing,
+ 'found' => $found,
+ ];
+ }
+}
diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php
index 55b37a77c..0a20deae5 100644
--- a/app/Actions/Server/ValidateServer.php
+++ b/app/Actions/Server/ValidateServer.php
@@ -45,6 +45,16 @@ public function handle(Server $server)
throw new \Exception($this->error);
}
+ $validationResult = $server->validatePrerequisites();
+ if (! $validationResult['success']) {
+ $missingCommands = implode(', ', $validationResult['missing']);
+ $this->error = "Prerequisites ({$missingCommands}) are not installed. Please install them before continuing or use the validation with installation endpoint.";
+ $server->update([
+ 'validation_logs' => $this->error,
+ ]);
+ throw new \Exception($this->error);
+ }
+
$this->docker_installed = $server->validateDockerEngine();
$this->docker_compose_installed = $server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) {
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index dfef6a566..6b5e1d4ac 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -20,18 +20,23 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
}
$service->saveComposeConfigs();
$service->isConfigurationChanged(save: true);
- $commands[] = 'cd '.$service->workdir();
- $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
+ $workdir = $service->workdir();
+ // $commands[] = "cd {$workdir}";
+ $commands[] = "echo 'Saved configuration files to {$workdir}.'";
+ // Ensure .env exists in the correct directory before docker compose tries to load it
+ // This is defensive programming - saveComposeConfigs() already creates it,
+ // but we guarantee it here in case of any edge cases or manual deployments
+ $commands[] = "touch {$workdir}/.env";
if ($pullLatestImages) {
$commands[] = "echo 'Pulling images.'";
- $commands[] = 'docker compose pull';
+ $commands[] = "docker compose --project-directory {$workdir} pull";
}
if ($service->networks()->count() > 0) {
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
}
$commands[] = 'echo Starting service.';
- $commands[] = 'docker compose up -d --remove-orphans --force-recreate --build';
+ $commands[] = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} up -d --remove-orphans --force-recreate --build";
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []);
diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php
index e06136e3c..3649be986 100644
--- a/app/Actions/Shared/ComplexStatusCheck.php
+++ b/app/Actions/Shared/ComplexStatusCheck.php
@@ -3,11 +3,14 @@
namespace App\Actions\Shared;
use App\Models\Application;
+use App\Services\ContainerStatusAggregator;
+use App\Traits\CalculatesExcludedStatus;
use Lorisleiva\Actions\Concerns\AsAction;
class ComplexStatusCheck
{
use AsAction;
+ use CalculatesExcludedStatus;
public function handle(Application $application)
{
@@ -17,11 +20,11 @@ public function handle(Application $application)
$is_main_server = $application->destination->server->id === $server->id;
if (! $server->isFunctional()) {
if ($is_main_server) {
- $application->update(['status' => 'exited:unhealthy']);
+ $application->update(['status' => 'exited']);
continue;
} else {
- $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
+ $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited']);
continue;
}
@@ -46,11 +49,11 @@ public function handle(Application $application)
}
} else {
if ($is_main_server) {
- $application->update(['status' => 'exited:unhealthy']);
+ $application->update(['status' => 'exited']);
continue;
} else {
- $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
+ $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited']);
continue;
}
@@ -61,74 +64,25 @@ public function handle(Application $application)
private function aggregateContainerStatuses($application, $containers)
{
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
- $excludedContainers = collect();
+ $excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
- if ($dockerComposeRaw) {
- try {
- $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
- $services = data_get($dockerCompose, 'services', []);
-
- foreach ($services as $serviceName => $serviceConfig) {
- $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
- $restartPolicy = data_get($serviceConfig, 'restart', 'always');
-
- if ($excludeFromHc || $restartPolicy === 'no') {
- $excludedContainers->push($serviceName);
- }
- }
- } catch (\Exception $e) {
- // If we can't parse, treat all containers as included
- }
- }
-
- $hasRunning = false;
- $hasRestarting = false;
- $hasUnhealthy = false;
- $hasExited = false;
- $relevantContainerCount = 0;
-
- foreach ($containers as $container) {
+ // Filter non-excluded containers
+ $relevantContainers = collect($containers)->filter(function ($container) use ($excludedContainers) {
$labels = data_get($container, 'Config.Labels', []);
$serviceName = data_get($labels, 'com.docker.compose.service');
- if ($serviceName && $excludedContainers->contains($serviceName)) {
- continue;
- }
+ return ! ($serviceName && $excludedContainers->contains($serviceName));
+ });
- $relevantContainerCount++;
- $containerStatus = data_get($container, 'State.Status');
- $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
-
- if ($containerStatus === 'restarting') {
- $hasRestarting = true;
- $hasUnhealthy = true;
- } elseif ($containerStatus === 'running') {
- $hasRunning = true;
- if ($containerHealth === 'unhealthy') {
- $hasUnhealthy = true;
- }
- } elseif ($containerStatus === 'exited') {
- $hasExited = true;
- $hasUnhealthy = true;
- }
+ // If all containers are excluded, calculate status from excluded containers
+ // but mark it with :excluded to indicate monitoring is disabled
+ if ($relevantContainers->isEmpty()) {
+ return $this->calculateExcludedStatus($containers, $excludedContainers);
}
- if ($relevantContainerCount === 0) {
- return 'running:healthy';
- }
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
- if ($hasRestarting) {
- return 'degraded:unhealthy';
- }
-
- if ($hasRunning && $hasExited) {
- return 'degraded:unhealthy';
- }
-
- if ($hasRunning) {
- return $hasUnhealthy ? 'running:unhealthy' : 'running:healthy';
- }
-
- return 'exited:unhealthy';
+ return $aggregator->aggregateFromContainers($relevantContainers);
}
}
diff --git a/app/Console/Commands/CheckTraefikVersionCommand.php b/app/Console/Commands/CheckTraefikVersionCommand.php
new file mode 100644
index 000000000..48cc78093
--- /dev/null
+++ b/app/Console/Commands/CheckTraefikVersionCommand.php
@@ -0,0 +1,30 @@
+info('Checking Traefik versions on all servers...');
+
+ try {
+ CheckTraefikVersionJob::dispatch();
+ $this->info('Traefik version check job dispatched successfully.');
+ $this->info('Notifications will be sent to teams with outdated Traefik versions.');
+
+ return Command::SUCCESS;
+ } catch (\Exception $e) {
+ $this->error('Failed to dispatch Traefik version check job: '.$e->getMessage());
+
+ return Command::FAILURE;
+ }
+ }
+}
diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php
index f6a2de75b..abf8010c0 100644
--- a/app/Console/Commands/CleanupRedis.php
+++ b/app/Console/Commands/CleanupRedis.php
@@ -7,7 +7,7 @@
class CleanupRedis extends Command
{
- protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks}';
+ protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks} {--restart : Aggressive cleanup mode for system restart (marks all processing jobs as failed)}';
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, cache locks, and related data)';
@@ -63,6 +63,14 @@ public function handle()
$deletedCount += $locksCleaned;
}
+ // Clean up stuck jobs (restart mode = aggressive, runtime mode = conservative)
+ $isRestart = $this->option('restart');
+ if ($isRestart || $this->option('clear-locks')) {
+ $this->info($isRestart ? 'Cleaning up stuck jobs (RESTART MODE - aggressive)...' : 'Checking for stuck jobs (runtime mode - conservative)...');
+ $jobsCleaned = $this->cleanupStuckJobs($redis, $prefix, $dryRun, $isRestart);
+ $deletedCount += $jobsCleaned;
+ }
+
if ($dryRun) {
$this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys");
} else {
@@ -332,4 +340,130 @@ private function cleanupCacheLocks(bool $dryRun): int
return $cleanedCount;
}
+
+ /**
+ * Clean up stuck jobs based on mode (restart vs runtime).
+ *
+ * @param mixed $redis Redis connection
+ * @param string $prefix Horizon prefix
+ * @param bool $dryRun Dry run mode
+ * @param bool $isRestart Restart mode (aggressive) vs runtime mode (conservative)
+ * @return int Number of jobs cleaned
+ */
+ private function cleanupStuckJobs($redis, string $prefix, bool $dryRun, bool $isRestart): int
+ {
+ $cleanedCount = 0;
+ $now = time();
+
+ // Get all keys with the horizon prefix
+ $cursor = 0;
+ $keys = [];
+ do {
+ $result = $redis->scan($cursor, ['match' => '*', 'count' => 100]);
+
+ // Guard against scan() returning false
+ if ($result === false) {
+ $this->error('Redis scan failed, stopping key retrieval');
+ break;
+ }
+
+ $cursor = $result[0];
+ $keys = array_merge($keys, $result[1]);
+ } while ($cursor !== 0);
+
+ foreach ($keys as $key) {
+ $keyWithoutPrefix = str_replace($prefix, '', $key);
+ $type = $redis->command('type', [$keyWithoutPrefix]);
+
+ // Only process hash-type keys (individual jobs)
+ if ($type !== 5) {
+ continue;
+ }
+
+ $data = $redis->command('hgetall', [$keyWithoutPrefix]);
+ $status = data_get($data, 'status');
+ $payload = data_get($data, 'payload');
+
+ // Only process jobs in "processing" or "reserved" state
+ if (! in_array($status, ['processing', 'reserved'])) {
+ continue;
+ }
+
+ // Parse job payload to get job class and started time
+ $payloadData = json_decode($payload, true);
+
+ // Check for JSON decode errors
+ if ($payloadData === null || json_last_error() !== JSON_ERROR_NONE) {
+ $errorMsg = json_last_error_msg();
+ $truncatedPayload = is_string($payload) ? substr($payload, 0, 200) : 'non-string payload';
+ $this->error("Failed to decode job payload for {$keyWithoutPrefix}: {$errorMsg}. Payload: {$truncatedPayload}");
+
+ continue;
+ }
+
+ $jobClass = data_get($payloadData, 'displayName', 'Unknown');
+
+ // Prefer reserved_at (when job started processing), fallback to created_at
+ $reservedAt = (int) data_get($data, 'reserved_at', 0);
+ $createdAt = (int) data_get($data, 'created_at', 0);
+ $startTime = $reservedAt ?: $createdAt;
+
+ // If we can't determine when the job started, skip it
+ if (! $startTime) {
+ continue;
+ }
+
+ // Calculate how long the job has been processing
+ $processingTime = $now - $startTime;
+
+ $shouldFail = false;
+ $reason = '';
+
+ if ($isRestart) {
+ // RESTART MODE: Mark ALL processing/reserved jobs as failed
+ // Safe because all workers are dead on restart
+ $shouldFail = true;
+ $reason = 'System restart - all workers terminated';
+ } else {
+ // RUNTIME MODE: Only mark truly stuck jobs as failed
+ // Be conservative to avoid killing legitimate long-running jobs
+
+ // Skip ApplicationDeploymentJob entirely (has dynamic_timeout, can run 2+ hours)
+ if (str_contains($jobClass, 'ApplicationDeploymentJob')) {
+ continue;
+ }
+
+ // Skip DatabaseBackupJob (large backups can take hours)
+ if (str_contains($jobClass, 'DatabaseBackupJob')) {
+ continue;
+ }
+
+ // For other jobs, only fail if processing > 12 hours
+ if ($processingTime > 43200) { // 12 hours
+ $shouldFail = true;
+ $reason = 'Processing for more than 12 hours';
+ }
+ }
+
+ if ($shouldFail) {
+ if ($dryRun) {
+ $this->warn(" Would mark as FAILED: {$jobClass} (processing for ".round($processingTime / 60, 1)." min) - {$reason}");
+ } else {
+ // Mark job as failed
+ $redis->command('hset', [$keyWithoutPrefix, 'status', 'failed']);
+ $redis->command('hset', [$keyWithoutPrefix, 'failed_at', $now]);
+ $redis->command('hset', [$keyWithoutPrefix, 'exception', "Job cleaned up by cleanup:redis - {$reason}"]);
+
+ $this->info(" ✓ Marked as FAILED: {$jobClass} (processing for ".round($processingTime / 60, 1).' min) - '.$reason);
+ }
+ $cleanedCount++;
+ }
+ }
+
+ if ($cleanedCount === 0) {
+ $this->info($isRestart ? ' No jobs to clean up' : ' No stuck jobs found (all jobs running normally)');
+ }
+
+ return $cleanedCount;
+ }
}
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index 0b13462ef..165a3ae21 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -222,9 +222,14 @@ private function cleanup_stucked_resources()
try {
$scheduled_backups = ScheduledDatabaseBackup::all();
foreach ($scheduled_backups as $scheduled_backup) {
- if (! $scheduled_backup->server()) {
- echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
- $scheduled_backup->delete();
+ try {
+ $server = $scheduled_backup->server();
+ if (! $server) {
+ echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
+ $scheduled_backup->delete();
+ }
+ } catch (\Throwable $e) {
+ echo "Error checking server for scheduledbackup {$scheduled_backup->id}: {$e->getMessage()}\n";
}
}
} catch (\Throwable $e) {
@@ -416,7 +421,7 @@ private function cleanup_stucked_resources()
foreach ($serviceApplications as $service) {
if (! data_get($service, 'service')) {
echo 'ServiceApplication without service: '.$service->name.'\n';
- DeleteResourceJob::dispatch($service);
+ $service->forceDelete();
continue;
}
@@ -429,7 +434,7 @@ private function cleanup_stucked_resources()
foreach ($serviceDatabases as $service) {
if (! data_get($service, 'service')) {
echo 'ServiceDatabase without service: '.$service->name.'\n';
- DeleteResourceJob::dispatch($service);
+ $service->forceDelete();
continue;
}
diff --git a/app/Console/Commands/Cloud/RestoreDatabase.php b/app/Console/Commands/Cloud/RestoreDatabase.php
new file mode 100644
index 000000000..7c6c0d4c6
--- /dev/null
+++ b/app/Console/Commands/Cloud/RestoreDatabase.php
@@ -0,0 +1,219 @@
+debug = $this->option('debug');
+
+ if (! $this->isDevelopment()) {
+ $this->error('This command can only be run in development mode.');
+
+ return 1;
+ }
+
+ $filePath = $this->argument('file');
+
+ if (! file_exists($filePath)) {
+ $this->error("File not found: {$filePath}");
+
+ return 1;
+ }
+
+ if (! is_readable($filePath)) {
+ $this->error("File is not readable: {$filePath}");
+
+ return 1;
+ }
+
+ try {
+ $this->info('Starting database restoration...');
+
+ $database = config('database.connections.pgsql.database');
+ $host = config('database.connections.pgsql.host');
+ $port = config('database.connections.pgsql.port');
+ $username = config('database.connections.pgsql.username');
+ $password = config('database.connections.pgsql.password');
+
+ if (! $database || ! $username) {
+ $this->error('Database configuration is incomplete.');
+
+ return 1;
+ }
+
+ $this->info("Restoring to database: {$database}");
+
+ // Drop all tables
+ if (! $this->dropAllTables($database, $host, $port, $username, $password)) {
+ return 1;
+ }
+
+ // Restore the database dump
+ if (! $this->restoreDatabaseDump($filePath, $database, $host, $port, $username, $password)) {
+ return 1;
+ }
+
+ $this->info('Database restoration completed successfully!');
+
+ return 0;
+ } catch (\Exception $e) {
+ $this->error("An error occurred: {$e->getMessage()}");
+
+ return 1;
+ }
+ }
+
+ private function dropAllTables(string $database, string $host, string $port, string $username, string $password): bool
+ {
+ $this->info('Dropping all tables...');
+
+ // SQL to drop all tables
+ $dropTablesSQL = <<<'SQL'
+ DO $$ DECLARE
+ r RECORD;
+ BEGIN
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
+ EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
+ END LOOP;
+ END $$;
+ SQL;
+
+ // Build the psql command to drop all tables
+ $command = sprintf(
+ 'PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -c %s',
+ escapeshellarg($password),
+ escapeshellarg($host),
+ escapeshellarg($port),
+ escapeshellarg($username),
+ escapeshellarg($database),
+ escapeshellarg($dropTablesSQL)
+ );
+
+ if ($this->debug) {
+ $this->line('Executing drop command: ');
+ $this->line($command);
+ }
+
+ $output = shell_exec($command.' 2>&1');
+
+ if ($this->debug) {
+ $this->line("Output: {$output}");
+ }
+
+ $this->info('All tables dropped successfully.');
+
+ return true;
+ }
+
+ private function restoreDatabaseDump(string $filePath, string $database, string $host, string $port, string $username, string $password): bool
+ {
+ $this->info('Restoring database from dump file...');
+
+ // Handle gzipped files by decompressing first
+ $actualFile = $filePath;
+ if (str_ends_with($filePath, '.gz')) {
+ $actualFile = rtrim($filePath, '.gz');
+ $this->info('Decompressing gzipped dump file...');
+
+ $decompressCommand = sprintf(
+ 'gunzip -c %s > %s',
+ escapeshellarg($filePath),
+ escapeshellarg($actualFile)
+ );
+
+ if ($this->debug) {
+ $this->line('Executing decompress command: ');
+ $this->line($decompressCommand);
+ }
+
+ $decompressOutput = shell_exec($decompressCommand.' 2>&1');
+ if ($this->debug && $decompressOutput) {
+ $this->line("Decompress output: {$decompressOutput}");
+ }
+ }
+
+ // Use pg_restore for custom format dumps
+ $command = sprintf(
+ 'PGPASSWORD=%s pg_restore -h %s -p %s -U %s -d %s -v %s',
+ escapeshellarg($password),
+ escapeshellarg($host),
+ escapeshellarg($port),
+ escapeshellarg($username),
+ escapeshellarg($database),
+ escapeshellarg($actualFile)
+ );
+
+ if ($this->debug) {
+ $this->line('Executing restore command: ');
+ $this->line($command);
+ }
+
+ // Execute the restore command
+ $process = proc_open(
+ $command,
+ [
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ],
+ $pipes
+ );
+
+ if (! is_resource($process)) {
+ $this->error('Failed to start restoration process.');
+
+ return false;
+ }
+
+ $output = stream_get_contents($pipes[1]);
+ $error = stream_get_contents($pipes[2]);
+ $exitCode = proc_close($process);
+
+ // Clean up decompressed file if we created one
+ if ($actualFile !== $filePath && file_exists($actualFile)) {
+ unlink($actualFile);
+ }
+
+ if ($this->debug) {
+ if ($output) {
+ $this->line('Output: ');
+ $this->line($output);
+ }
+ if ($error) {
+ $this->line('Error output: ');
+ $this->line($error);
+ }
+ $this->line("Exit code: {$exitCode}");
+ }
+
+ if ($exitCode !== 0) {
+ $this->error("Restoration failed with exit code: {$exitCode}");
+ if ($error) {
+ $this->error('Error details:');
+ $this->error($error);
+ }
+
+ return false;
+ }
+
+ if ($output && ! $this->debug) {
+ $this->line($output);
+ }
+
+ return true;
+ }
+
+ private function isDevelopment(): bool
+ {
+ return app()->environment(['local', 'development', 'dev']);
+ }
+}
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 8f26d78ff..acc6dc2f9 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -4,6 +4,9 @@
use App\Jobs\CheckHelperImageJob;
use App\Models\InstanceSettings;
+use App\Models\ScheduledDatabaseBackupExecution;
+use App\Models\ScheduledTaskExecution;
+use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
@@ -45,6 +48,44 @@ public function init()
} else {
echo "Instance already initialized.\n";
}
+
+ // Clean up stuck jobs and stale locks on development startup
+ try {
+ echo "Cleaning up Redis (stuck jobs and stale locks)...\n";
+ Artisan::call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]);
+ echo "Redis cleanup completed.\n";
+ } catch (\Throwable $e) {
+ echo "Error in cleanup:redis: {$e->getMessage()}\n";
+ }
+
+ try {
+ $updatedTaskCount = ScheduledTaskExecution::where('status', 'running')->update([
+ 'status' => 'failed',
+ 'message' => 'Marked as failed during Coolify startup - job was interrupted',
+ 'finished_at' => Carbon::now(),
+ ]);
+
+ if ($updatedTaskCount > 0) {
+ echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n";
+ }
+ } catch (\Throwable $e) {
+ echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n";
+ }
+
+ try {
+ $updatedBackupCount = ScheduledDatabaseBackupExecution::where('status', 'running')->update([
+ 'status' => 'failed',
+ 'message' => 'Marked as failed during Coolify startup - job was interrupted',
+ 'finished_at' => Carbon::now(),
+ ]);
+
+ if ($updatedBackupCount > 0) {
+ echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n";
+ }
+ } catch (\Throwable $e) {
+ echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n";
+ }
+
CheckHelperImageJob::dispatch();
}
}
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index a022d54dc..43ba06804 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -167,7 +167,7 @@ public function handle()
]);
}
$output = 'Because of an error, the backup of the database '.$db->name.' failed.';
- $this->mail = (new BackupFailed($backup, $db, $output))->toMail();
+ $this->mail = (new BackupFailed($backup, $db, $output, $backup->database_name ?? 'unknown'))->toMail();
$this->sendEmail();
break;
case 'backup-success':
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 4bc818f0a..66cb77838 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -10,9 +10,12 @@
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
+use App\Models\ScheduledDatabaseBackupExecution;
+use App\Models\ScheduledTaskExecution;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\User;
+use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
@@ -73,7 +76,7 @@ public function handle()
$this->cleanupUnusedNetworkFromCoolifyProxy();
try {
- $this->call('cleanup:redis', ['--clear-locks' => true]);
+ $this->call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]);
} catch (\Throwable $e) {
echo "Error in cleanup:redis command: {$e->getMessage()}\n";
}
@@ -86,6 +89,7 @@ public function handle()
$this->call('cleanup:stucked-resources');
} catch (\Throwable $e) {
echo "Error in cleanup:stucked-resources command: {$e->getMessage()}\n";
+ echo "Continuing with initialization - cleanup errors will not prevent Coolify from starting\n";
}
try {
$updatedCount = ApplicationDeploymentQueue::whereIn('status', [
@@ -102,6 +106,34 @@ public function handle()
echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n";
}
+ try {
+ $updatedTaskCount = ScheduledTaskExecution::where('status', 'running')->update([
+ 'status' => 'failed',
+ 'message' => 'Marked as failed during Coolify startup - job was interrupted',
+ 'finished_at' => Carbon::now(),
+ ]);
+
+ if ($updatedTaskCount > 0) {
+ echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n";
+ }
+ } catch (\Throwable $e) {
+ echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n";
+ }
+
+ try {
+ $updatedBackupCount = ScheduledDatabaseBackupExecution::where('status', 'running')->update([
+ 'status' => 'failed',
+ 'message' => 'Marked as failed during Coolify startup - job was interrupted',
+ 'finished_at' => Carbon::now(),
+ ]);
+
+ if ($updatedBackupCount > 0) {
+ echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n";
+ }
+ } catch (\Throwable $e) {
+ echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n";
+ }
+
try {
$localhost = $this->servers->where('id', 0)->first();
if ($localhost) {
diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php
index b0cd24715..e634feadb 100644
--- a/app/Console/Commands/SyncBunny.php
+++ b/app/Console/Commands/SyncBunny.php
@@ -26,9 +26,9 @@ class SyncBunny extends Command
protected $description = 'Sync files to BunnyCDN';
/**
- * Fetch GitHub releases and sync to CDN
+ * Fetch GitHub releases and sync to GitHub repository
*/
- private function syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn)
+ private function syncReleasesToGitHubRepo(): bool
{
$this->info('Fetching releases from GitHub...');
try {
@@ -37,33 +37,122 @@ private function syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny
'per_page' => 30, // Fetch more releases for better changelog
]);
- if ($response->successful()) {
- $releases = $response->json();
-
- // Save releases to a temporary file
- $releases_file = "$parent_dir/releases.json";
- file_put_contents($releases_file, json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
-
- // Upload to CDN
- Http::pool(fn (Pool $pool) => [
- $pool->storage(fileName: $releases_file)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/releases.json"),
- $pool->purge("$bunny_cdn/coolify/releases.json"),
- ]);
-
- // Clean up temporary file
- unlink($releases_file);
-
- $this->info('releases.json uploaded & purged...');
- $this->info('Total releases synced: '.count($releases));
-
- return true;
- } else {
+ if (! $response->successful()) {
$this->error('Failed to fetch releases from GitHub: '.$response->status());
return false;
}
+
+ $releases = $response->json();
+ $timestamp = time();
+ $tmpDir = sys_get_temp_dir().'/coolify-cdn-'.$timestamp;
+ $branchName = 'update-releases-'.$timestamp;
+
+ // Clone the repository
+ $this->info('Cloning coolify-cdn repository...');
+ exec('gh repo clone coollabsio/coolify-cdn '.escapeshellarg($tmpDir).' 2>&1', $output, $returnCode);
+ if ($returnCode !== 0) {
+ $this->error('Failed to clone repository: '.implode("\n", $output));
+
+ return false;
+ }
+
+ // Create feature branch
+ $this->info('Creating feature branch...');
+ exec('cd '.escapeshellarg($tmpDir).' && git checkout -b '.escapeshellarg($branchName).' 2>&1', $output, $returnCode);
+ if ($returnCode !== 0) {
+ $this->error('Failed to create branch: '.implode("\n", $output));
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return false;
+ }
+
+ // Write releases.json
+ $this->info('Writing releases.json...');
+ $releasesPath = "$tmpDir/json/releases.json";
+ $jsonContent = json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ $bytesWritten = file_put_contents($releasesPath, $jsonContent);
+
+ if ($bytesWritten === false) {
+ $this->error("Failed to write releases.json to: $releasesPath");
+ $this->error('Possible reasons: directory does not exist, permission denied, or disk full.');
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return false;
+ }
+
+ // Stage and commit
+ $this->info('Committing changes...');
+ exec('cd '.escapeshellarg($tmpDir).' && git add json/releases.json 2>&1', $output, $returnCode);
+ if ($returnCode !== 0) {
+ $this->error('Failed to stage changes: '.implode("\n", $output));
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return false;
+ }
+
+ $this->info('Checking for changes...');
+ $statusOutput = [];
+ exec('cd '.escapeshellarg($tmpDir).' && git status --porcelain json/releases.json 2>&1', $statusOutput, $returnCode);
+ if ($returnCode !== 0) {
+ $this->error('Failed to check repository status: '.implode("\n", $statusOutput));
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return false;
+ }
+
+ if (empty(array_filter($statusOutput))) {
+ $this->info('Releases are already up to date. No changes to commit.');
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return true;
+ }
+
+ $commitMessage = 'Update releases.json with latest releases - '.date('Y-m-d H:i:s');
+ $output = [];
+ exec('cd '.escapeshellarg($tmpDir).' && git commit -m '.escapeshellarg($commitMessage).' 2>&1', $output, $returnCode);
+ if ($returnCode !== 0) {
+ $this->error('Failed to commit changes: '.implode("\n", $output));
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return false;
+ }
+
+ // Push to remote
+ $this->info('Pushing branch to remote...');
+ exec('cd '.escapeshellarg($tmpDir).' && git push origin '.escapeshellarg($branchName).' 2>&1', $output, $returnCode);
+ if ($returnCode !== 0) {
+ $this->error('Failed to push branch: '.implode("\n", $output));
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ return false;
+ }
+
+ // Create pull request
+ $this->info('Creating pull request...');
+ $prTitle = 'Update releases.json - '.date('Y-m-d H:i:s');
+ $prBody = 'Automated update of releases.json with latest '.count($releases).' releases from GitHub API';
+ $prCommand = 'gh pr create --repo coollabsio/coolify-cdn --title '.escapeshellarg($prTitle).' --body '.escapeshellarg($prBody).' --base main --head '.escapeshellarg($branchName).' 2>&1';
+ exec($prCommand, $output, $returnCode);
+
+ // Clean up
+ exec('rm -rf '.escapeshellarg($tmpDir));
+
+ if ($returnCode !== 0) {
+ $this->error('Failed to create PR: '.implode("\n", $output));
+
+ return false;
+ }
+
+ $this->info('Pull request created successfully!');
+ if (! empty($output)) {
+ $this->info('PR Output: '.implode("\n", $output));
+ }
+ $this->info('Total releases synced: '.count($releases));
+
+ return true;
} catch (\Throwable $e) {
- $this->error('Error fetching releases: '.$e->getMessage());
+ $this->error('Error syncing releases: '.$e->getMessage());
return false;
}
@@ -174,11 +263,7 @@ public function handle()
return;
}
- // First sync GitHub releases
- $this->info('Syncing GitHub releases first...');
- $this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
-
- // Then sync versions.json
+ // Sync versions.json to BunnyCDN
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
@@ -187,14 +272,14 @@ public function handle()
return;
} elseif ($only_github_releases) {
- $this->info('About to sync GitHub releases to BunnyCDN.');
+ $this->info('About to sync GitHub releases to GitHub repository.');
$confirmed = confirm('Are you sure you want to sync GitHub releases?');
if (! $confirmed) {
return;
}
- // Use the reusable function
- $this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
+ // Sync releases to GitHub repository
+ $this->syncReleasesToGitHubRepo();
return;
}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index c2ea27274..832bed5ae 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -5,6 +5,7 @@
use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob;
+use App\Jobs\CheckTraefikVersionJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\PullChangelog;
use App\Jobs\PullTemplatesFromCDN;
@@ -83,6 +84,8 @@ protected function schedule(Schedule $schedule): void
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
+ $this->scheduleInstance->job(new CheckTraefikVersionJob)->weekly()->sundays()->at('00:00')->timezone($this->instanceTimezone)->onOneServer();
+
$this->scheduleInstance->command('cleanup:database --yes')->daily();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
diff --git a/app/Data/ServerMetadata.php b/app/Data/ServerMetadata.php
index d95944b15..cdd9c8c08 100644
--- a/app/Data/ServerMetadata.php
+++ b/app/Data/ServerMetadata.php
@@ -10,6 +10,8 @@ class ServerMetadata extends Data
{
public function __construct(
public ?ProxyTypes $type,
- public ?ProxyStatus $status
+ public ?ProxyStatus $status,
+ public ?string $last_saved_settings = null,
+ public ?string $last_applied_settings = null
) {}
}
diff --git a/app/Events/RestoreJobFinished.php b/app/Events/RestoreJobFinished.php
index d3adb7798..8690e01f6 100644
--- a/app/Events/RestoreJobFinished.php
+++ b/app/Events/RestoreJobFinished.php
@@ -17,17 +17,23 @@ public function __construct($data)
$tmpPath = data_get($data, 'tmpPath');
$container = data_get($data, 'container');
$serverId = data_get($data, 'serverId');
- if (filled($scriptPath) && filled($tmpPath) && filled($container) && filled($serverId)) {
- if (str($tmpPath)->startsWith('/tmp/')
- && str($scriptPath)->startsWith('/tmp/')
- && ! str($tmpPath)->contains('..')
- && ! str($scriptPath)->contains('..')
- && strlen($tmpPath) > 5 // longer than just "/tmp/"
- && strlen($scriptPath) > 5
- ) {
- $commands[] = "docker exec {$container} sh -c 'rm {$scriptPath}'";
- $commands[] = "docker exec {$container} sh -c 'rm {$tmpPath}'";
- instant_remote_process($commands, Server::find($serverId), throwError: true);
+
+ if (filled($container) && filled($serverId)) {
+ $commands = [];
+
+ if (isSafeTmpPath($scriptPath)) {
+ $commands[] = 'docker exec '.escapeshellarg($container)." sh -c 'rm ".escapeshellarg($scriptPath)." 2>/dev/null || true'";
+ }
+
+ if (isSafeTmpPath($tmpPath)) {
+ $commands[] = 'docker exec '.escapeshellarg($container)." sh -c 'rm ".escapeshellarg($tmpPath)." 2>/dev/null || true'";
+ }
+
+ if (! empty($commands)) {
+ $server = Server::find($serverId);
+ if ($server) {
+ instant_remote_process($commands, $server, throwError: false);
+ }
}
}
}
diff --git a/app/Events/S3RestoreJobFinished.php b/app/Events/S3RestoreJobFinished.php
new file mode 100644
index 000000000..e1f844558
--- /dev/null
+++ b/app/Events/S3RestoreJobFinished.php
@@ -0,0 +1,56 @@
+/dev/null || true';
+ }
+
+ // Clean up server temp file if still exists (should already be cleaned)
+ if (isSafeTmpPath($serverTmpPath)) {
+ $commands[] = 'rm -f '.escapeshellarg($serverTmpPath).' 2>/dev/null || true';
+ }
+
+ // Clean up any remaining files in database container (may already be cleaned)
+ if (filled($container)) {
+ if (isSafeTmpPath($containerTmpPath)) {
+ $commands[] = 'docker exec '.escapeshellarg($container).' rm -f '.escapeshellarg($containerTmpPath).' 2>/dev/null || true';
+ }
+ if (isSafeTmpPath($scriptPath)) {
+ $commands[] = 'docker exec '.escapeshellarg($container).' rm -f '.escapeshellarg($scriptPath).' 2>/dev/null || true';
+ }
+ }
+
+ if (! empty($commands)) {
+ $server = Server::find($serverId);
+ if ($server) {
+ instant_remote_process($commands, $server, throwError: false);
+ }
+ }
+ }
+ }
+}
diff --git a/app/Exceptions/DeploymentException.php b/app/Exceptions/DeploymentException.php
new file mode 100644
index 000000000..01e0a8235
--- /dev/null
+++ b/app/Exceptions/DeploymentException.php
@@ -0,0 +1,32 @@
+getMessage(), $exception->getCode(), $exception);
+ }
+}
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 3d731223d..71de48bcd 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -30,6 +30,7 @@ class Handler extends ExceptionHandler
protected $dontReport = [
ProcessException::class,
NonReportableException::class,
+ DeploymentException::class,
];
/**
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 5ba9c08e7..a1fcaa7f5 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -246,6 +246,40 @@ public function manual(Request $request)
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
+ // Cancel any active deployments for this PR immediately
+ $activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id)
+ ->where('pull_request_id', $pull_request_id)
+ ->whereIn('status', [
+ \App\Enums\ApplicationDeploymentStatus::QUEUED->value,
+ \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ])
+ ->first();
+
+ if ($activeDeployment) {
+ try {
+ // Mark deployment as cancelled
+ $activeDeployment->update([
+ 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
+ ]);
+
+ // Add cancellation log entry
+ $activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
+
+ // Check if helper container exists and kill it
+ $deployment_uuid = $activeDeployment->deployment_uuid;
+ $server = $application->destination->server;
+ $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'";
+ $containerExists = instant_remote_process([$checkCommand], $server);
+
+ if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
+ instant_remote_process(["docker rm -f {$deployment_uuid}"], $server);
+ $activeDeployment->addLogEntry('Deployment container stopped.');
+ }
+ } catch (\Throwable $e) {
+ // Silently handle errors during deployment cancellation
+ }
+ }
+
DeleteResourceJob::dispatch($found);
$return_payloads->push([
'application' => $application->name,
@@ -481,6 +515,42 @@ public function normal(Request $request)
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
+ // Cancel any active deployments for this PR immediately
+ $activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id)
+ ->where('pull_request_id', $pull_request_id)
+ ->whereIn('status', [
+ \App\Enums\ApplicationDeploymentStatus::QUEUED->value,
+ \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ])
+ ->first();
+
+ if ($activeDeployment) {
+ try {
+ // Mark deployment as cancelled
+ $activeDeployment->update([
+ 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
+ ]);
+
+ // Add cancellation log entry
+ $activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
+
+ // Check if helper container exists and kill it
+ $deployment_uuid = $activeDeployment->deployment_uuid;
+ $server = $application->destination->server;
+ $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'";
+ $containerExists = instant_remote_process([$checkCommand], $server);
+
+ if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
+ instant_remote_process(["docker rm -f {$deployment_uuid}"], $server);
+ $activeDeployment->addLogEntry('Deployment container stopped.');
+ }
+
+ } catch (\Throwable $e) {
+ // Silently handle errors during deployment cancellation
+ }
+ }
+
+ // Clean up any deployed containers
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
if ($containers->isNotEmpty()) {
$containers->each(function ($container) use ($application) {
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 9bbf048b9..8c1769181 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -7,6 +7,7 @@
use App\Enums\ProcessStatus;
use App\Events\ApplicationConfigurationChanged;
use App\Events\ServiceStatusChanged;
+use App\Exceptions\DeploymentException;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
@@ -31,7 +32,6 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
-use RuntimeException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@@ -41,6 +41,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, EnvironmentVariableAnalyzer, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
+ public const BUILD_TIME_ENV_PATH = '/artifacts/build-time.env';
+
+ private const BUILD_SCRIPT_PATH = '/artifacts/build.sh';
+
+ private const NIXPACKS_PLAN_PATH = '/artifacts/thegameplan.json';
+
public $tries = 1;
public $timeout = 3600;
@@ -341,20 +347,42 @@ public function handle(): void
$this->fail($e);
throw $e;
} finally {
- $this->application_deployment_queue->update([
- 'finished_at' => Carbon::now()->toImmutable(),
- ]);
-
- if ($this->use_build_server) {
- $this->server = $this->build_server;
- } else {
- $this->write_deployment_configurations();
+ // Wrap cleanup operations in try-catch to prevent exceptions from interfering
+ // with Laravel's job failure handling and status updates
+ try {
+ $this->application_deployment_queue->update([
+ 'finished_at' => Carbon::now()->toImmutable(),
+ ]);
+ } catch (Exception $e) {
+ // Log but don't fail - finished_at is not critical
+ \Log::warning('Failed to update finished_at for deployment '.$this->deployment_uuid.': '.$e->getMessage());
}
- $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
- $this->graceful_shutdown_container($this->deployment_uuid);
+ try {
+ if ($this->use_build_server) {
+ $this->server = $this->build_server;
+ } else {
+ $this->write_deployment_configurations();
+ }
+ } catch (Exception $e) {
+ // Log but don't fail - configuration writing errors shouldn't prevent status updates
+ $this->application_deployment_queue->addLogEntry('Warning: Failed to write deployment configurations: '.$e->getMessage(), 'stderr');
+ }
- ServiceStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
+ try {
+ $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
+ $this->graceful_shutdown_container($this->deployment_uuid);
+ } catch (Exception $e) {
+ // Log but don't fail - container cleanup errors are expected when container is already gone
+ \Log::warning('Failed to shutdown container '.$this->deployment_uuid.': '.$e->getMessage());
+ }
+
+ try {
+ ServiceStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
+ } catch (Exception $e) {
+ // Log but don't fail - event dispatch errors shouldn't prevent status updates
+ \Log::warning('Failed to dispatch ServiceStatusChanged for deployment '.$this->deployment_uuid.': '.$e->getMessage());
+ }
}
}
@@ -630,11 +658,27 @@ private function deploy_docker_compose_buildpack()
$this->save_buildtime_environment_variables();
if ($this->docker_compose_custom_build_command) {
+ // Auto-inject -f (compose file) and --env-file flags using helper function
+ $build_command = injectDockerComposeFlags(
+ $this->docker_compose_custom_build_command,
+ "{$this->workdir}{$this->docker_compose_location}",
+ self::BUILD_TIME_ENV_PATH
+ );
+
// Prepend DOCKER_BUILDKIT=1 if BuildKit is supported
- $build_command = $this->docker_compose_custom_build_command;
if ($this->dockerBuildkitSupported) {
$build_command = "DOCKER_BUILDKIT=1 {$build_command}";
}
+
+ // Append build arguments if not using build secrets (matching default behavior)
+ if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
+ $build_args_string = $this->build_args->implode(' ');
+ // Escape single quotes for bash -c context used by executeInDocker
+ $build_args_string = str_replace("'", "'\\''", $build_args_string);
+ $build_command .= " {$build_args_string}";
+ $this->application_deployment_queue->addLogEntry('Adding build arguments to custom Docker Compose build command.');
+ }
+
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true],
);
@@ -645,7 +689,7 @@ private function deploy_docker_compose_buildpack()
$command = "DOCKER_BUILDKIT=1 {$command}";
}
// Use build-time .env file from /artifacts (outside Docker context to prevent it from being in the image)
- $command .= ' --env-file /artifacts/build-time.env';
+ $command .= ' --env-file '.self::BUILD_TIME_ENV_PATH;
if ($this->force_rebuild) {
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache";
} else {
@@ -693,9 +737,16 @@ private function deploy_docker_compose_buildpack()
$server_workdir = $this->application->workdir();
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
+ // Auto-inject -f (compose file) and --env-file flags using helper function
+ $start_command = injectDockerComposeFlags(
+ $this->docker_compose_custom_start_command,
+ "{$server_workdir}{$this->docker_compose_location}",
+ "{$server_workdir}/.env"
+ );
+
$this->write_deployment_configurations();
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$start_command}"), 'hidden' => true],
);
} else {
$this->write_deployment_configurations();
@@ -711,9 +762,18 @@ private function deploy_docker_compose_buildpack()
}
} else {
if ($this->docker_compose_custom_start_command) {
+ // Auto-inject -f (compose file) and --env-file flags using helper function
+ // Use $this->workdir for non-preserve-repository mode
+ $workdir_path = $this->preserveRepository ? $server_workdir : $this->workdir;
+ $start_command = injectDockerComposeFlags(
+ $this->docker_compose_custom_start_command,
+ "{$workdir_path}{$this->docker_compose_location}",
+ "{$workdir_path}/.env"
+ );
+
$this->write_deployment_configurations();
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$start_command}"), 'hidden' => true],
);
} else {
$command = "{$this->coolify_variables} docker compose";
@@ -954,7 +1014,7 @@ private function push_to_docker_registry()
} catch (Exception $e) {
$this->application_deployment_queue->addLogEntry('Failed to push image to docker registry. Please check debug logs for more information.');
if ($forceFail) {
- throw new RuntimeException($e->getMessage(), 69420);
+ throw new DeploymentException(get_class($e).': '.$e->getMessage(), $e->getCode(), $e);
}
}
}
@@ -1146,6 +1206,18 @@ private function generate_runtime_environment_variables()
foreach ($runtime_environment_variables as $env) {
$envs->push($env->key.'='.$env->real_value);
}
+
+ // Check for PORT environment variable mismatch with ports_exposes
+ if ($this->build_pack !== 'dockercompose') {
+ $detectedPort = $this->application->detectPortFromEnvironment(false);
+ if ($detectedPort && ! empty($ports) && ! in_array($detectedPort, $ports)) {
+ $this->application_deployment_queue->addLogEntry(
+ "Warning: PORT environment variable ({$detectedPort}) does not match configured ports_exposes: ".implode(',', $ports).'. It could case "bad gateway" or "no server" errors. Check the "General" page to fix it.',
+ 'stderr'
+ );
+ }
+ }
+
// Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
@@ -1291,7 +1363,7 @@ private function save_runtime_environment_variables()
$envs_base64 = base64_encode($environment_variables->implode("\n"));
// Write .env file to workdir (for container runtime)
- $this->application_deployment_queue->addLogEntry('Creating .env file with runtime variables for build phase.', hidden: true);
+ $this->application_deployment_queue->addLogEntry('Creating .env file with runtime variables for container.', hidden: true);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/.env > /dev/null"),
@@ -1330,7 +1402,7 @@ private function generate_buildtime_environment_variables()
}
$envs = collect([]);
- $coolify_envs = $this->generate_coolify_env_variables();
+ $coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
// Add COOLIFY variables
$coolify_envs->each(function ($item, $key) use ($envs) {
@@ -1500,10 +1572,10 @@ private function save_buildtime_environment_variables()
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee /artifacts/build-time.env > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee ".self::BUILD_TIME_ENV_PATH.' > /dev/null'),
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build-time.env'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_TIME_ENV_PATH),
'hidden' => true,
],
);
@@ -1514,7 +1586,7 @@ private function save_buildtime_environment_variables()
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, 'touch /artifacts/build-time.env'),
+ executeInDocker($this->deployment_uuid, 'touch '.self::BUILD_TIME_ENV_PATH),
]
);
}
@@ -1576,123 +1648,131 @@ private function laravel_finetunes()
private function rolling_update()
{
- $this->checkForCancellation();
- if ($this->server->isSwarm()) {
- $this->application_deployment_queue->addLogEntry('Rolling update started.');
- $this->execute_remote_command(
- [
- executeInDocker($this->deployment_uuid, "docker stack deploy --detach=true --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
- ],
- );
- $this->application_deployment_queue->addLogEntry('Rolling update completed.');
- } else {
- if ($this->use_build_server) {
- $this->write_deployment_configurations();
- $this->server = $this->original_server;
- }
- if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
- $this->application_deployment_queue->addLogEntry('----------------------------------------');
- if (count($this->application->ports_mappings_array) > 0) {
- $this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
- }
- if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
- $this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.');
- }
- if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
- $this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.');
- }
- if ($this->pull_request_id !== 0) {
- $this->application->settings->is_consistent_container_name_enabled = true;
- $this->application_deployment_queue->addLogEntry('Pull request deployment, rolling update is not supported.');
- }
- if (str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
- $this->application_deployment_queue->addLogEntry('Custom IP address is set, rolling update is not supported.');
- }
- $this->stop_running_container(force: true);
- $this->start_by_compose_file();
- } else {
- $this->application_deployment_queue->addLogEntry('----------------------------------------');
+ try {
+ $this->checkForCancellation();
+ if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry('Rolling update started.');
- $this->start_by_compose_file();
- $this->health_check();
- $this->stop_running_container();
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "docker stack deploy --detach=true --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
+ ],
+ );
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
+ } else {
+ if ($this->use_build_server) {
+ $this->write_deployment_configurations();
+ $this->server = $this->original_server;
+ }
+ if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
+ $this->application_deployment_queue->addLogEntry('----------------------------------------');
+ if (count($this->application->ports_mappings_array) > 0) {
+ $this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
+ }
+ if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
+ $this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.');
+ }
+ if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
+ $this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.');
+ }
+ if ($this->pull_request_id !== 0) {
+ $this->application->settings->is_consistent_container_name_enabled = true;
+ $this->application_deployment_queue->addLogEntry('Pull request deployment, rolling update is not supported.');
+ }
+ if (str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
+ $this->application_deployment_queue->addLogEntry('Custom IP address is set, rolling update is not supported.');
+ }
+ $this->stop_running_container(force: true);
+ $this->start_by_compose_file();
+ } else {
+ $this->application_deployment_queue->addLogEntry('----------------------------------------');
+ $this->application_deployment_queue->addLogEntry('Rolling update started.');
+ $this->start_by_compose_file();
+ $this->health_check();
+ $this->stop_running_container();
+ $this->application_deployment_queue->addLogEntry('Rolling update completed.');
+ }
}
+ } catch (Exception $e) {
+ throw new DeploymentException('Rolling update failed ('.get_class($e).'): '.$e->getMessage(), $e->getCode(), $e);
}
}
private function health_check()
{
- if ($this->server->isSwarm()) {
- // Implement healthcheck for swarm
- } else {
- if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) {
- $this->newVersionIsHealthy = true;
+ try {
+ if ($this->server->isSwarm()) {
+ // Implement healthcheck for swarm
+ } else {
+ if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) {
+ $this->newVersionIsHealthy = true;
- return;
- }
- if ($this->application->custom_healthcheck_found) {
- $this->application_deployment_queue->addLogEntry('Custom healthcheck found in Dockerfile.');
- }
- if ($this->container_name) {
- $counter = 1;
- $this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
- if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
- $this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
+ return;
}
- $this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
- $sleeptime = 0;
- while ($sleeptime < $this->application->health_check_start_period) {
- Sleep::for(1)->seconds();
- $sleeptime++;
+ if ($this->application->custom_healthcheck_found) {
+ $this->application_deployment_queue->addLogEntry('Custom healthcheck found in Dockerfile.');
}
- while ($counter <= $this->application->health_check_retries) {
- $this->execute_remote_command(
- [
- "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
- 'hidden' => true,
- 'save' => 'health_check',
- 'append' => false,
- ],
- [
- "docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}",
- 'hidden' => true,
- 'save' => 'health_check_logs',
- 'append' => false,
- ],
- );
- $this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
- $health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)');
- if (empty($health_check_logs)) {
- $health_check_logs = '(no logs)';
+ if ($this->container_name) {
+ $counter = 1;
+ $this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
+ if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
+ $this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
}
- $health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)');
- if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') {
- $this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
- }
-
- if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
- $this->newVersionIsHealthy = true;
- $this->application->update(['status' => 'running']);
- $this->application_deployment_queue->addLogEntry('New container is healthy.');
- break;
- }
- if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
- $this->newVersionIsHealthy = false;
- $this->query_logs();
- break;
- }
- $counter++;
+ $this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
$sleeptime = 0;
- while ($sleeptime < $this->application->health_check_interval) {
+ while ($sleeptime < $this->application->health_check_start_period) {
Sleep::for(1)->seconds();
$sleeptime++;
}
- }
- if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
- $this->query_logs();
+ while ($counter <= $this->application->health_check_retries) {
+ $this->execute_remote_command(
+ [
+ "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
+ 'hidden' => true,
+ 'save' => 'health_check',
+ 'append' => false,
+ ],
+ [
+ "docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}",
+ 'hidden' => true,
+ 'save' => 'health_check_logs',
+ 'append' => false,
+ ],
+ );
+ $this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
+ $health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)');
+ if (empty($health_check_logs)) {
+ $health_check_logs = '(no logs)';
+ }
+ $health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)');
+ if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') {
+ $this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
+ }
+
+ if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
+ $this->newVersionIsHealthy = true;
+ $this->application->update(['status' => 'running']);
+ $this->application_deployment_queue->addLogEntry('New container is healthy.');
+ break;
+ }
+ if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
+ $this->newVersionIsHealthy = false;
+ $this->query_logs();
+ break;
+ }
+ $counter++;
+ $sleeptime = 0;
+ while ($sleeptime < $this->application->health_check_interval) {
+ Sleep::for(1)->seconds();
+ $sleeptime++;
+ }
+ }
+ if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
+ $this->query_logs();
+ }
}
}
+ } catch (Exception $e) {
+ throw new DeploymentException('Health check failed ('.get_class($e).'): '.$e->getMessage(), $e->getCode(), $e);
}
}
@@ -1780,9 +1860,8 @@ private function create_workdir()
private function prepare_builder_image(bool $firstTry = true)
{
$this->checkForCancellation();
- $settings = instanceSettings();
$helperImage = config('constants.coolify.helper_image');
- $helperImage = "{$helperImage}:{$settings->helper_version}";
+ $helperImage = "{$helperImage}:".getHelperVersion();
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -1790,7 +1869,7 @@ private function prepare_builder_image(bool $firstTry = true)
$env_flags = $this->generate_docker_env_flags_for_secrets();
if ($this->use_build_server) {
if ($this->dockerConfigFileExists === 'NOK') {
- throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.');
+ throw new DeploymentException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.');
}
$runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
@@ -1900,7 +1979,6 @@ private function set_coolify_variables()
$this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
}
$this->coolify_variables .= "COOLIFY_RESOURCE_UUID={$this->application->uuid} ";
- $this->coolify_variables .= "COOLIFY_CONTAINER_NAME={$this->container_name} ";
}
private function check_git_if_build_needed()
@@ -2056,7 +2134,7 @@ private function generate_nixpacks_confs()
if ($this->saved_outputs->get('nixpacks_type')) {
$this->nixpacks_type = $this->saved_outputs->get('nixpacks_type');
if (str($this->nixpacks_type)->isEmpty()) {
- throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
+ throw new DeploymentException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
}
}
@@ -2151,7 +2229,7 @@ private function generate_nixpacks_env_variables()
}
// Add COOLIFY_* environment variables to Nixpacks build context
- $coolify_envs = $this->generate_coolify_env_variables();
+ $coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
$coolify_envs->each(function ($value, $key) {
$this->env_nixpacks_args->push("--env {$key}={$value}");
});
@@ -2159,7 +2237,7 @@ private function generate_nixpacks_env_variables()
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
}
- private function generate_coolify_env_variables(): Collection
+ private function generate_coolify_env_variables(bool $forBuildTime = false): Collection
{
$coolify_envs = collect([]);
$local_branch = $this->branch;
@@ -2194,8 +2272,11 @@ private function generate_coolify_env_variables(): Collection
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid);
}
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
- $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
+ // Only add COOLIFY_CONTAINER_NAME for runtime (not build-time) - it changes every deployment and breaks Docker cache
+ if (! $forBuildTime) {
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
+ }
}
}
@@ -2232,8 +2313,11 @@ private function generate_coolify_env_variables(): Collection
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid);
}
- if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
- $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
+ // Only add COOLIFY_CONTAINER_NAME for runtime (not build-time) - it changes every deployment and breaks Docker cache
+ if (! $forBuildTime) {
+ if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
+ }
}
}
@@ -2249,7 +2333,7 @@ private function generate_env_variables()
$this->env_args = collect([]);
$this->env_args->put('SOURCE_COMMIT', $this->commit);
- $coolify_envs = $this->generate_coolify_env_variables();
+ $coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
$coolify_envs->each(function ($value, $key) {
$this->env_args->put($key, $value);
});
@@ -2633,15 +2717,15 @@ private function build_static_image()
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
],
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
@@ -2649,7 +2733,7 @@ private function build_static_image()
}
/**
- * Wrap a docker build command with environment export from /artifacts/build-time.env
+ * Wrap a docker build command with environment export from build-time .env file
* This enables shell interpolation of variables (e.g., APP_URL=$COOLIFY_URL)
*
* @param string $build_command The docker build command to wrap
@@ -2657,7 +2741,7 @@ private function build_static_image()
*/
private function wrap_build_command_with_env_export(string $build_command): string
{
- return "cd {$this->workdir} && set -a && source /artifacts/build-time.env && set +a && {$build_command}";
+ return "cd {$this->workdir} && set -a && source ".self::BUILD_TIME_ENV_PATH." && set +a && {$build_command}";
}
private function build_image()
@@ -2669,7 +2753,7 @@ private function build_image()
} else {
// Traditional build args approach - generate COOLIFY_ variables locally
// Generate COOLIFY_ variables locally for build args
- $coolify_envs = $this->generate_coolify_env_variables();
+ $coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
$coolify_envs->each(function ($value, $key) {
$this->build_args->push("--build-arg '{$key}'");
});
@@ -2696,10 +2780,10 @@ private function build_image()
}
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
- $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
+ $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee ".self::NIXPACKS_PLAN_PATH.' > /dev/null'), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
+ executeInDocker($this->deployment_uuid, 'nixpacks build -c '.self::NIXPACKS_PLAN_PATH." --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
], [
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
@@ -2719,7 +2803,7 @@ private function build_image()
}
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
+ executeInDocker($this->deployment_uuid, 'nixpacks build -c '.self::NIXPACKS_PLAN_PATH." --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
], [
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
@@ -2743,19 +2827,19 @@ private function build_image()
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
- $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
+ $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
@@ -2787,15 +2871,15 @@ private function build_image()
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
@@ -2826,15 +2910,15 @@ private function build_image()
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
],
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
@@ -2861,25 +2945,25 @@ private function build_image()
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
} else {
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
- $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
+ $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee ".self::NIXPACKS_PLAN_PATH.' > /dev/null'), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
+ executeInDocker($this->deployment_uuid, 'nixpacks build -c '.self::NIXPACKS_PLAN_PATH." --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
], [
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
@@ -2900,7 +2984,7 @@ private function build_image()
}
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
+ executeInDocker($this->deployment_uuid, 'nixpacks build -c '.self::NIXPACKS_PLAN_PATH." --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
], [
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
@@ -2923,19 +3007,19 @@ private function build_image()
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
- $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
+ $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
@@ -2968,15 +3052,15 @@ private function build_image()
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee ".self::BUILD_SCRIPT_PATH.' > /dev/null'),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'cat '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ executeInDocker($this->deployment_uuid, 'bash '.self::BUILD_SCRIPT_PATH),
'hidden' => true,
]
);
@@ -3001,53 +3085,66 @@ private function graceful_shutdown_container(string $containerName)
private function stop_running_container(bool $force = false)
{
- $this->application_deployment_queue->addLogEntry('Removing old containers.');
- if ($this->newVersionIsHealthy || $force) {
- if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
- $this->graceful_shutdown_container($this->container_name);
- } else {
- $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
- if ($this->pull_request_id === 0) {
- $containers = $containers->filter(function ($container) {
- return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== addPreviewDeploymentSuffix($this->container_name, $this->pull_request_id);
+ try {
+ $this->application_deployment_queue->addLogEntry('Removing old containers.');
+ if ($this->newVersionIsHealthy || $force) {
+ if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
+ $this->graceful_shutdown_container($this->container_name);
+ } else {
+ $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
+ if ($this->pull_request_id === 0) {
+ $containers = $containers->filter(function ($container) {
+ return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== addPreviewDeploymentSuffix($this->container_name, $this->pull_request_id);
+ });
+ }
+ $containers->each(function ($container) {
+ $this->graceful_shutdown_container(data_get($container, 'Names'));
});
}
- $containers->each(function ($container) {
- $this->graceful_shutdown_container(data_get($container, 'Names'));
- });
+ } else {
+ if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
+ $this->application_deployment_queue->addLogEntry('----------------------------------------');
+ $this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI.");
+ $this->application_deployment_queue->addLogEntry('----------------------------------------');
+ }
+ $this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
+ $this->failDeployment();
+ $this->graceful_shutdown_container($this->container_name);
}
- } else {
- if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
- $this->application_deployment_queue->addLogEntry('----------------------------------------');
- $this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI.");
- $this->application_deployment_queue->addLogEntry('----------------------------------------');
- }
- $this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
- $this->failDeployment();
- $this->graceful_shutdown_container($this->container_name);
+ } catch (Exception $e) {
+ throw new DeploymentException("Failed to stop running container: {$e->getMessage()}", $e->getCode(), $e);
}
}
private function start_by_compose_file()
{
- if ($this->application->build_pack === 'dockerimage') {
- $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
+ try {
+ // Ensure .env file exists before docker compose tries to load it (defensive programming)
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
- [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
+ ["touch {$this->configuration_dir}/.env", 'hidden' => true],
);
- } else {
- if ($this->use_build_server) {
+
+ if ($this->application->build_pack === 'dockerimage') {
+ $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
- ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
);
} else {
- $this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
- );
+ if ($this->use_build_server) {
+ $this->execute_remote_command(
+ ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
+ );
+ } else {
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
+ );
+ }
}
+ $this->application_deployment_queue->addLogEntry('New container started.');
+ } catch (Exception $e) {
+ throw new DeploymentException("Failed to start container: {$e->getMessage()}", $e->getCode(), $e);
}
- $this->application_deployment_queue->addLogEntry('New container started.');
}
private function analyzeBuildTimeVariables($variables)
@@ -3202,7 +3299,9 @@ private function generate_build_secrets(Collection $variables)
private function generate_secrets_hash($variables)
{
if (! $this->secrets_hash_key) {
- $this->secrets_hash_key = bin2hex(random_bytes(32));
+ // Use APP_KEY as deterministic hash key to preserve Docker build cache
+ // Random keys would change every deployment, breaking cache even when secrets haven't changed
+ $this->secrets_hash_key = config('app.key');
}
if ($variables instanceof Collection) {
@@ -3227,6 +3326,20 @@ private function generate_secrets_hash($variables)
return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key);
}
+ protected function findFromInstructionLines($dockerfile): array
+ {
+ $fromLines = [];
+ foreach ($dockerfile as $index => $line) {
+ $trimmedLine = trim($line);
+ // Check if line starts with FROM (case-insensitive)
+ if (preg_match('/^FROM\s+/i', $trimmedLine)) {
+ $fromLines[] = $index;
+ }
+ }
+
+ return $fromLines;
+ }
+
private function add_build_env_variables_to_dockerfile()
{
if ($this->dockerBuildkitSupported) {
@@ -3239,6 +3352,18 @@ private function add_build_env_variables_to_dockerfile()
'ignore_errors' => true,
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
+
+ // Find all FROM instruction positions
+ $fromLines = $this->findFromInstructionLines($dockerfile);
+
+ // If no FROM instructions found, skip ARG insertion
+ if (empty($fromLines)) {
+ return;
+ }
+
+ // Collect all ARG statements to insert
+ $argsToInsert = collect();
+
if ($this->pull_request_id === 0) {
// Only add environment variables that are available during build
$envs = $this->application->environment_variables()
@@ -3247,9 +3372,9 @@ private function add_build_env_variables_to_dockerfile()
->get();
foreach ($envs as $env) {
if (data_get($env, 'is_multiline') === true) {
- $dockerfile->splice(1, 0, ["ARG {$env->key}"]);
+ $argsToInsert->push("ARG {$env->key}");
} else {
- $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]);
+ $argsToInsert->push("ARG {$env->key}={$env->real_value}");
}
}
// Add Coolify variables as ARGs
@@ -3259,9 +3384,7 @@ private function add_build_env_variables_to_dockerfile()
->map(function ($var) {
return "ARG {$var}";
});
- foreach ($coolify_vars as $arg) {
- $dockerfile->splice(1, 0, [$arg]);
- }
+ $argsToInsert = $argsToInsert->merge($coolify_vars);
}
} else {
// Only add preview environment variables that are available during build
@@ -3271,9 +3394,9 @@ private function add_build_env_variables_to_dockerfile()
->get();
foreach ($envs as $env) {
if (data_get($env, 'is_multiline') === true) {
- $dockerfile->splice(1, 0, ["ARG {$env->key}"]);
+ $argsToInsert->push("ARG {$env->key}");
} else {
- $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]);
+ $argsToInsert->push("ARG {$env->key}={$env->real_value}");
}
}
// Add Coolify variables as ARGs
@@ -3283,15 +3406,23 @@ private function add_build_env_variables_to_dockerfile()
->map(function ($var) {
return "ARG {$var}";
});
- foreach ($coolify_vars as $arg) {
- $dockerfile->splice(1, 0, [$arg]);
- }
+ $argsToInsert = $argsToInsert->merge($coolify_vars);
}
}
- if ($envs->isNotEmpty()) {
- $secrets_hash = $this->generate_secrets_hash($envs);
- $dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]);
+ // Insert ARGs after each FROM instruction (in reverse order to maintain correct line numbers)
+ if ($argsToInsert->isNotEmpty()) {
+ foreach (array_reverse($fromLines) as $fromLineIndex) {
+ // Insert all ARGs after this FROM instruction
+ foreach ($argsToInsert->reverse() as $arg) {
+ $dockerfile->splice($fromLineIndex + 1, 0, [$arg]);
+ }
+ }
+ $envs_mapped = $envs->mapWithKeys(function ($env) {
+ return [$env->key => $env->real_value];
+ });
+ $secrets_hash = $this->generate_secrets_hash($envs_mapped);
+ $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
@@ -3606,7 +3737,7 @@ private function run_pre_deployment_command()
return;
}
}
- throw new RuntimeException('Pre-deployment command: Could not find a valid container. Is the container name correct?');
+ throw new DeploymentException('Pre-deployment command: Could not find a valid container. Is the container name correct?');
}
private function run_post_deployment_command()
@@ -3642,7 +3773,7 @@ private function run_post_deployment_command()
return;
}
}
- throw new RuntimeException('Post-deployment command: Could not find a valid container. Is the container name correct?');
+ throw new DeploymentException('Post-deployment command: Could not find a valid container. Is the container name correct?');
}
/**
@@ -3653,7 +3784,7 @@ private function checkForCancellation(): void
$this->application_deployment_queue->refresh();
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
$this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.');
- throw new \RuntimeException('Deployment cancelled by user', 69420);
+ throw new DeploymentException('Deployment cancelled by user', 69420);
}
}
@@ -3686,7 +3817,7 @@ private function isInTerminalState(): bool
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
$this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.');
- throw new \RuntimeException('Deployment cancelled by user', 69420);
+ throw new DeploymentException('Deployment cancelled by user', 69420);
}
return false;
@@ -3759,7 +3890,7 @@ private function completeDeployment(): void
* Fail the deployment.
* Sends failure notification and queues next deployment.
*/
- private function failDeployment(): void
+ protected function failDeployment(): void
{
$this->transitionToStatus(ApplicationDeploymentStatus::FAILED);
}
@@ -3767,11 +3898,39 @@ private function failDeployment(): void
public function failed(Throwable $exception): void
{
$this->failDeployment();
- $this->application_deployment_queue->addLogEntry('Oops something is not okay, are you okay? 😢', 'stderr');
- if (str($exception->getMessage())->isNotEmpty()) {
- $this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
+
+ // Log comprehensive error information
+ $errorMessage = $exception->getMessage() ?: 'Unknown error occurred';
+ $errorCode = $exception->getCode();
+ $errorClass = get_class($exception);
+
+ $this->application_deployment_queue->addLogEntry('========================================', 'stderr');
+ $this->application_deployment_queue->addLogEntry("Deployment failed: {$errorMessage}", 'stderr');
+ $this->application_deployment_queue->addLogEntry("Error type: {$errorClass}", 'stderr', hidden: true);
+ $this->application_deployment_queue->addLogEntry("Error code: {$errorCode}", 'stderr', hidden: true);
+
+ // Log the exception file and line for debugging
+ $this->application_deployment_queue->addLogEntry("Location: {$exception->getFile()}:{$exception->getLine()}", 'stderr', hidden: true);
+
+ // Log previous exceptions if they exist (for chained exceptions)
+ $previous = $exception->getPrevious();
+ if ($previous) {
+ $this->application_deployment_queue->addLogEntry('Caused by:', 'stderr', hidden: true);
+ $previousMessage = $previous->getMessage() ?: 'No message';
+ $previousClass = get_class($previous);
+ $this->application_deployment_queue->addLogEntry(" {$previousClass}: {$previousMessage}", 'stderr', hidden: true);
+ $this->application_deployment_queue->addLogEntry(" at {$previous->getFile()}:{$previous->getLine()}", 'stderr', hidden: true);
}
+ // Log first few lines of stack trace for debugging
+ $trace = $exception->getTraceAsString();
+ $traceLines = explode("\n", $trace);
+ $this->application_deployment_queue->addLogEntry('Stack trace (first 5 lines):', 'stderr', hidden: true);
+ foreach (array_slice($traceLines, 0, 5) as $traceLine) {
+ $this->application_deployment_queue->addLogEntry(" {$traceLine}", 'stderr', hidden: true);
+ }
+ $this->application_deployment_queue->addLogEntry('========================================', 'stderr');
+
if ($this->application->build_pack !== 'dockercompose') {
$code = $exception->getCode();
if ($code !== 69420) {
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index 1d3a345e1..4f2bfa68c 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -33,6 +33,9 @@ public function handle(): void
// New version available
$settings->update(['new_version_available' => true]);
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
+
+ // Invalidate cache to ensure fresh data is loaded
+ invalidate_versions_cache();
} else {
$settings->update(['new_version_available' => false]);
}
diff --git a/app/Jobs/CheckTraefikVersionForServerJob.php b/app/Jobs/CheckTraefikVersionForServerJob.php
new file mode 100644
index 000000000..88484bcce
--- /dev/null
+++ b/app/Jobs/CheckTraefikVersionForServerJob.php
@@ -0,0 +1,173 @@
+server);
+
+ // Update detected version in database
+ $this->server->update(['detected_traefik_version' => $currentVersion]);
+
+ if (! $currentVersion) {
+ return;
+ }
+
+ // Check if image tag is 'latest' by inspecting the image (makes SSH call)
+ $imageTag = instant_remote_process([
+ "docker inspect coolify-proxy --format '{{.Config.Image}}' 2>/dev/null",
+ ], $this->server, false);
+
+ // Handle empty/null response from SSH command
+ if (empty(trim($imageTag))) {
+ return;
+ }
+
+ if (str_contains(strtolower(trim($imageTag)), ':latest')) {
+ return;
+ }
+
+ // Parse current version to extract major.minor.patch
+ $current = ltrim($currentVersion, 'v');
+ if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) {
+ return;
+ }
+
+ $currentBranch = $matches[1]; // e.g., "3.6"
+
+ // Find the latest version for this branch
+ $latestForBranch = $this->traefikVersions["v{$currentBranch}"] ?? null;
+
+ if (! $latestForBranch) {
+ // User is on a branch we don't track - check if newer branches exist
+ $newerBranchInfo = $this->getNewerBranchInfo($currentBranch);
+
+ if ($newerBranchInfo) {
+ $this->storeOutdatedInfo($current, $newerBranchInfo['latest'], 'minor_upgrade', $newerBranchInfo['target']);
+ } else {
+ // No newer branch found, clear outdated info
+ $this->server->update(['traefik_outdated_info' => null]);
+ }
+
+ return;
+ }
+
+ // Compare patch version within the same branch
+ $latest = ltrim($latestForBranch, 'v');
+
+ // Always check for newer branches first
+ $newerBranchInfo = $this->getNewerBranchInfo($currentBranch);
+
+ if (version_compare($current, $latest, '<')) {
+ // Patch update available
+ $this->storeOutdatedInfo($current, $latest, 'patch_update', null, $newerBranchInfo);
+ } elseif ($newerBranchInfo) {
+ // Only newer branch available (no patch update)
+ $this->storeOutdatedInfo($current, $newerBranchInfo['latest'], 'minor_upgrade', $newerBranchInfo['target']);
+ } else {
+ // Fully up to date
+ $this->server->update(['traefik_outdated_info' => null]);
+ }
+ }
+
+ /**
+ * Get information about newer branches if available.
+ */
+ private function getNewerBranchInfo(string $currentBranch): ?array
+ {
+ $newestBranch = null;
+ $newestVersion = null;
+
+ foreach ($this->traefikVersions as $branch => $version) {
+ $branchNum = ltrim($branch, 'v');
+ if (version_compare($branchNum, $currentBranch, '>')) {
+ if (! $newestVersion || version_compare($version, $newestVersion, '>')) {
+ $newestBranch = $branchNum;
+ $newestVersion = $version;
+ }
+ }
+ }
+
+ if ($newestVersion) {
+ return [
+ 'target' => "v{$newestBranch}",
+ 'latest' => ltrim($newestVersion, 'v'),
+ ];
+ }
+
+ return null;
+ }
+
+ /**
+ * Store outdated information in database and send immediate notification.
+ */
+ private function storeOutdatedInfo(string $current, string $latest, string $type, ?string $upgradeTarget = null, ?array $newerBranchInfo = null): void
+ {
+ $outdatedInfo = [
+ 'current' => $current,
+ 'latest' => $latest,
+ 'type' => $type,
+ 'checked_at' => now()->toIso8601String(),
+ ];
+
+ // For minor upgrades, add the upgrade_target field (e.g., "v3.6")
+ if ($type === 'minor_upgrade' && $upgradeTarget) {
+ $outdatedInfo['upgrade_target'] = $upgradeTarget;
+ }
+
+ // If there's a newer branch available (even for patch updates), include that info
+ if ($newerBranchInfo) {
+ $outdatedInfo['newer_branch_target'] = $newerBranchInfo['target'];
+ $outdatedInfo['newer_branch_latest'] = $newerBranchInfo['latest'];
+ }
+
+ $this->server->update(['traefik_outdated_info' => $outdatedInfo]);
+
+ // Send immediate notification to the team
+ $this->sendNotification($outdatedInfo);
+ }
+
+ /**
+ * Send notification to team about outdated Traefik.
+ */
+ private function sendNotification(array $outdatedInfo): void
+ {
+ // Attach the outdated info as a dynamic property for the notification
+ $this->server->outdatedInfo = $outdatedInfo;
+
+ // Get the team and send notification
+ $team = $this->server->team()->first();
+
+ if ($team) {
+ $team->notify(new TraefikVersionOutdated(collect([$this->server])));
+ }
+ }
+}
diff --git a/app/Jobs/CheckTraefikVersionJob.php b/app/Jobs/CheckTraefikVersionJob.php
new file mode 100644
index 000000000..a513f280e
--- /dev/null
+++ b/app/Jobs/CheckTraefikVersionJob.php
@@ -0,0 +1,45 @@
+whereProxyType(ProxyTypes::TRAEFIK->value)
+ ->whereRelation('settings', 'is_reachable', true)
+ ->whereRelation('settings', 'is_usable', true)
+ ->get();
+
+ if ($servers->isEmpty()) {
+ return;
+ }
+
+ // Dispatch individual server check jobs in parallel
+ // Each job will send immediate notifications when outdated Traefik is detected
+ foreach ($servers as $server) {
+ CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
+ }
+ }
+}
diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php
index c82a27ce9..f6f5e8b5b 100644
--- a/app/Jobs/CleanupHelperContainersJob.php
+++ b/app/Jobs/CleanupHelperContainersJob.php
@@ -2,6 +2,8 @@
namespace App\Jobs;
+use App\Enums\ApplicationDeploymentStatus;
+use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -20,10 +22,51 @@ public function __construct(public Server $server) {}
public function handle(): void
{
try {
+ // Get all active deployments on this server
+ $activeDeployments = ApplicationDeploymentQueue::where('server_id', $this->server->id)
+ ->whereIn('status', [
+ ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ApplicationDeploymentStatus::QUEUED->value,
+ ])
+ ->pluck('deployment_uuid')
+ ->toArray();
+
+ \Log::info('CleanupHelperContainersJob - Active deployments', [
+ 'server' => $this->server->name,
+ 'active_deployment_uuids' => $activeDeployments,
+ ]);
+
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false);
- $containerIds = collect(json_decode($containers))->pluck('ID');
- if ($containerIds->count() > 0) {
- foreach ($containerIds as $containerId) {
+ $helperContainers = collect(json_decode($containers));
+
+ if ($helperContainers->count() > 0) {
+ foreach ($helperContainers as $container) {
+ $containerId = data_get($container, 'ID');
+ $containerName = data_get($container, 'Names');
+
+ // Check if this container belongs to an active deployment
+ $isActiveDeployment = false;
+ foreach ($activeDeployments as $deploymentUuid) {
+ if (str_contains($containerName, $deploymentUuid)) {
+ $isActiveDeployment = true;
+ break;
+ }
+ }
+
+ if ($isActiveDeployment) {
+ \Log::info('CleanupHelperContainersJob - Skipping active deployment container', [
+ 'container' => $containerName,
+ 'id' => $containerId,
+ ]);
+
+ continue;
+ }
+
+ \Log::info('CleanupHelperContainersJob - Removing orphaned helper container', [
+ 'container' => $containerName,
+ 'id' => $containerId,
+ ]);
+
instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false);
}
}
diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php
index 49a5ba8dd..ce535e036 100755
--- a/app/Jobs/CoolifyTask.php
+++ b/app/Jobs/CoolifyTask.php
@@ -3,18 +3,35 @@
namespace App\Jobs;
use App\Actions\CoolifyTask\RunRemoteProcess;
+use App\Enums\ProcessStatus;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
use Spatie\Activitylog\Models\Activity;
class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ /**
+ * The number of times the job may be attempted.
+ */
+ public $tries = 3;
+
+ /**
+ * The maximum number of unhandled exceptions to allow before failing.
+ */
+ public $maxExceptions = 1;
+
+ /**
+ * The number of seconds the job can run before timing out.
+ */
+ public $timeout = 600;
+
/**
* Create a new job instance.
*/
@@ -42,4 +59,53 @@ public function handle(): void
$remote_process();
}
+
+ /**
+ * Calculate the number of seconds to wait before retrying the job.
+ */
+ public function backoff(): array
+ {
+ return [30, 90, 180]; // 30s, 90s, 180s between retries
+ }
+
+ /**
+ * Handle a job failure.
+ */
+ public function failed(?\Throwable $exception): void
+ {
+ Log::channel('scheduled-errors')->error('CoolifyTask permanently failed', [
+ 'job' => 'CoolifyTask',
+ 'activity_id' => $this->activity->id,
+ 'server_uuid' => $this->activity->getExtraProperty('server_uuid'),
+ 'command_preview' => substr($this->activity->getExtraProperty('command') ?? '', 0, 200),
+ 'error' => $exception?->getMessage(),
+ 'total_attempts' => $this->attempts(),
+ 'trace' => $exception?->getTraceAsString(),
+ ]);
+
+ // Update activity status to reflect permanent failure
+ $this->activity->properties = $this->activity->properties->merge([
+ 'status' => ProcessStatus::ERROR->value,
+ 'error' => $exception?->getMessage() ?? 'Job permanently failed',
+ 'failed_at' => now()->toIso8601String(),
+ ]);
+ $this->activity->save();
+
+ // Dispatch cleanup event on failure (same as on success)
+ if ($this->call_event_on_finish) {
+ try {
+ $eventClass = "App\\Events\\$this->call_event_on_finish";
+ if (! is_null($this->call_event_data)) {
+ event(new $eventClass($this->call_event_data));
+ } else {
+ event(new $eventClass($this->activity->causer_id));
+ }
+ Log::info('Cleanup event dispatched after job failure', [
+ 'event' => $this->call_event_on_finish,
+ ]);
+ } catch (\Throwable $e) {
+ Log::error('Error dispatching cleanup event on failure: '.$e->getMessage());
+ }
+ }
+ }
}
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 11da6fac1..45ac6eb7d 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -23,6 +23,7 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
use Visus\Cuid2\Cuid2;
@@ -31,6 +32,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ public $maxExceptions = 1;
+
public ?Team $team = null;
public Server $server;
@@ -74,7 +77,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public ScheduledDatabaseBackup $backup)
{
$this->onQueue('high');
- $this->timeout = $backup->timeout;
+ $this->timeout = $backup->timeout ?? 3600;
}
public function handle(): void
@@ -636,7 +639,13 @@ private function upload_to_s3(): void
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
- $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$endpoint} {$key} \"{$secret}\"";
+
+ // Escape S3 credentials to prevent command injection
+ $escapedEndpoint = escapeshellarg($endpoint);
+ $escapedKey = escapeshellarg($key);
+ $escapedSecret = escapeshellarg($secret);
+
+ $commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
@@ -653,24 +662,42 @@ private function upload_to_s3(): void
private function getFullImageName(): string
{
- $settings = instanceSettings();
$helperImage = config('constants.coolify.helper_image');
- $latestVersion = $settings->helper_version;
+ $latestVersion = getHelperVersion();
return "{$helperImage}:{$latestVersion}";
}
public function failed(?Throwable $exception): void
{
+ Log::channel('scheduled-errors')->error('DatabaseBackup permanently failed', [
+ 'job' => 'DatabaseBackupJob',
+ 'backup_id' => $this->backup->uuid,
+ 'database' => $this->database?->name ?? 'unknown',
+ 'database_type' => get_class($this->database ?? new \stdClass),
+ 'server' => $this->server?->name ?? 'unknown',
+ 'total_attempts' => $this->attempts(),
+ 'error' => $exception?->getMessage(),
+ 'trace' => $exception?->getTraceAsString(),
+ ]);
+
$log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first();
if ($log) {
$log->update([
'status' => 'failed',
- 'message' => 'Job failed: '.($exception?->getMessage() ?? 'Unknown error'),
+ 'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'),
'size' => 0,
'filename' => null,
+ 'finished_at' => Carbon::now(),
]);
}
+
+ // Notify team about permanent failure
+ if ($this->team) {
+ $databaseName = $log?->database_name ?? 'unknown';
+ $output = $this->backup_output ?? $exception?->getMessage() ?? 'Unknown error';
+ $this->team->notify(new BackupFailed($this->backup, $this->database, $output, $databaseName));
+ }
}
}
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index b9fbebcc9..c4358570e 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -124,16 +124,54 @@ private function deleteApplicationPreview()
$this->resource->delete();
}
+ // Cancel any active deployments for this PR (same logic as API cancel_deployment)
+ $activeDeployments = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id)
+ ->where('pull_request_id', $pull_request_id)
+ ->whereIn('status', [
+ \App\Enums\ApplicationDeploymentStatus::QUEUED->value,
+ \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ])
+ ->get();
+
+ foreach ($activeDeployments as $activeDeployment) {
+ try {
+ // Mark deployment as cancelled
+ $activeDeployment->update([
+ 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
+ ]);
+
+ // Add cancellation log entry
+ $activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
+
+ // Check if helper container exists and kill it
+ $deployment_uuid = $activeDeployment->deployment_uuid;
+ $escapedDeploymentUuid = escapeshellarg($deployment_uuid);
+ $checkCommand = "docker ps -a --filter name={$escapedDeploymentUuid} --format '{{.Names}}'";
+ $containerExists = instant_remote_process([$checkCommand], $server);
+
+ if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
+ instant_remote_process(["docker rm -f {$escapedDeploymentUuid}"], $server);
+ $activeDeployment->addLogEntry('Deployment container stopped.');
+ } else {
+ $activeDeployment->addLogEntry('Helper container not yet started. Deployment will be cancelled when job checks status.');
+ }
+
+ } catch (\Throwable $e) {
+ // Silently handle errors during deployment cancellation
+ }
+ }
+
try {
if ($server->isSwarm()) {
- instant_remote_process(["docker stack rm {$application->uuid}-{$pull_request_id}"], $server);
+ $escapedStackName = escapeshellarg("{$application->uuid}-{$pull_request_id}");
+ instant_remote_process(["docker stack rm {$escapedStackName}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($server, $application->id, $pull_request_id)->toArray();
$this->stopPreviewContainers($containers, $server);
}
} catch (\Throwable $e) {
// Log the error but don't fail the job
- ray('Error stopping preview containers: '.$e->getMessage());
+ \Log::warning('Error stopping preview containers for application '.$application->uuid.', PR #'.$pull_request_id.': '.$e->getMessage());
}
// Finally, force delete to trigger resource cleanup
@@ -156,7 +194,6 @@ private function stopPreviewContainers(array $containers, $server, int $timeout
"docker stop --time=$timeout $containerList",
"docker rm -f $containerList",
];
-
instant_remote_process(
command: $commands,
server: $server,
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
deleted file mode 100644
index b92886d38..000000000
--- a/app/Jobs/PullHelperImageJob.php
+++ /dev/null
@@ -1,30 +0,0 @@
-onQueue('high');
- }
-
- public function handle(): void
- {
- $helperImage = config('constants.coolify.helper_image');
- $latest_version = instanceSettings()->helper_version;
- instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
- }
-}
diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php
index 7726c2c73..9d44e08f9 100644
--- a/app/Jobs/PushServerUpdateJob.php
+++ b/app/Jobs/PushServerUpdateJob.php
@@ -13,6 +13,8 @@
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
+use App\Services\ContainerStatusAggregator;
+use App\Traits\CalculatesExcludedStatus;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -25,6 +27,7 @@
class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
{
+ use CalculatesExcludedStatus;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
@@ -67,6 +70,8 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
public Collection $applicationContainerStatuses;
+ public Collection $serviceContainerStatuses;
+
public bool $foundProxy = false;
public bool $foundLogDrainContainer = false;
@@ -90,6 +95,7 @@ public function __construct(public Server $server, public $data)
$this->foundApplicationPreviewsIds = collect();
$this->foundServiceDatabaseIds = collect();
$this->applicationContainerStatuses = collect();
+ $this->serviceContainerStatuses = collect();
$this->allApplicationIds = collect();
$this->allDatabaseUuids = collect();
$this->allTcpProxyUuids = collect();
@@ -99,6 +105,20 @@ public function __construct(public Server $server, public $data)
public function handle()
{
+ // Defensive initialization for Collection properties to handle queue deserialization edge cases
+ $this->serviceContainerStatuses ??= collect();
+ $this->applicationContainerStatuses ??= collect();
+ $this->foundApplicationIds ??= collect();
+ $this->foundDatabaseUuids ??= collect();
+ $this->foundServiceApplicationIds ??= collect();
+ $this->foundApplicationPreviewsIds ??= collect();
+ $this->foundServiceDatabaseIds ??= collect();
+ $this->allApplicationIds ??= collect();
+ $this->allDatabaseUuids ??= collect();
+ $this->allTcpProxyUuids ??= collect();
+ $this->allServiceApplicationIds ??= collect();
+ $this->allServiceDatabaseIds ??= collect();
+
// TODO: Swarm is not supported yet
if (! $this->data) {
throw new \Exception('No data provided');
@@ -108,7 +128,6 @@ public function handle()
$this->server->sentinelHeartbeat();
$this->containers = collect(data_get($data, 'containers'));
-
$filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage');
ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot);
@@ -141,65 +160,88 @@ public function handle()
foreach ($this->containers as $container) {
$containerStatus = data_get($container, 'state', 'exited');
- $containerHealth = data_get($container, 'health_status', 'unhealthy');
- $containerStatus = "$containerStatus ($containerHealth)";
+ $rawHealthStatus = data_get($container, 'health_status');
+ $containerHealth = $rawHealthStatus ?? 'unknown';
+ // Only append health status if container is not exited
+ if ($containerStatus !== 'exited') {
+ $containerStatus = "$containerStatus:$containerHealth";
+ }
$labels = collect(data_get($container, 'labels'));
$coolify_managed = $labels->has('coolify.managed');
- if ($coolify_managed) {
- $name = data_get($container, 'name');
- if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
- $this->foundLogDrainContainer = true;
- }
- if ($labels->has('coolify.applicationId')) {
- $applicationId = $labels->get('coolify.applicationId');
- $pullRequestId = $labels->get('coolify.pullRequestId', '0');
- try {
- if ($pullRequestId === '0') {
- if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
- $this->foundApplicationIds->push($applicationId);
- }
- // Store container status for aggregation
- if (! $this->applicationContainerStatuses->has($applicationId)) {
- $this->applicationContainerStatuses->put($applicationId, collect());
- }
- $containerName = $labels->get('com.docker.compose.service');
- if ($containerName) {
- $this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
- }
- } else {
- $previewKey = $applicationId.':'.$pullRequestId;
- if ($this->allApplicationPreviewsIds->contains($previewKey) && $this->isRunning($containerStatus)) {
- $this->foundApplicationPreviewsIds->push($previewKey);
- }
- $this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
+
+ if (! $coolify_managed) {
+ continue;
+ }
+
+ $name = data_get($container, 'name');
+ if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
+ $this->foundLogDrainContainer = true;
+ }
+ if ($labels->has('coolify.applicationId')) {
+ $applicationId = $labels->get('coolify.applicationId');
+ $pullRequestId = $labels->get('coolify.pullRequestId', '0');
+ try {
+ if ($pullRequestId === '0') {
+ if ($this->allApplicationIds->contains($applicationId)) {
+ $this->foundApplicationIds->push($applicationId);
+ }
+ // Store container status for aggregation
+ if (! $this->applicationContainerStatuses->has($applicationId)) {
+ $this->applicationContainerStatuses->put($applicationId, collect());
+ }
+ $containerName = $labels->get('com.docker.compose.service');
+ if ($containerName) {
+ $this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
}
- } catch (\Exception $e) {
- }
- } elseif ($labels->has('coolify.serviceId')) {
- $serviceId = $labels->get('coolify.serviceId');
- $subType = $labels->get('coolify.service.subType');
- $subId = $labels->get('coolify.service.subId');
- if ($subType === 'application' && $this->isRunning($containerStatus)) {
- $this->foundServiceApplicationIds->push($subId);
- $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
- } elseif ($subType === 'database' && $this->isRunning($containerStatus)) {
- $this->foundServiceDatabaseIds->push($subId);
- $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
- }
- } else {
- $uuid = $labels->get('com.docker.compose.service');
- $type = $labels->get('coolify.type');
- if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
- $this->foundProxy = true;
- } elseif ($type === 'service' && $this->isRunning($containerStatus)) {
} else {
- if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
- $this->foundDatabaseUuids->push($uuid);
- if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
- $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
- } else {
- $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
- }
+ $previewKey = $applicationId.':'.$pullRequestId;
+ if ($this->allApplicationPreviewsIds->contains($previewKey)) {
+ $this->foundApplicationPreviewsIds->push($previewKey);
+ }
+ $this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
+ }
+ } catch (\Exception $e) {
+ }
+ } elseif ($labels->has('coolify.serviceId')) {
+ $serviceId = $labels->get('coolify.serviceId');
+ $subType = $labels->get('coolify.service.subType');
+ $subId = $labels->get('coolify.service.subId');
+ if ($subType === 'application') {
+ $this->foundServiceApplicationIds->push($subId);
+ // Store container status for aggregation
+ $key = $serviceId.':'.$subType.':'.$subId;
+ if (! $this->serviceContainerStatuses->has($key)) {
+ $this->serviceContainerStatuses->put($key, collect());
+ }
+ $containerName = $labels->get('com.docker.compose.service');
+ if ($containerName) {
+ $this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
+ }
+ } elseif ($subType === 'database') {
+ $this->foundServiceDatabaseIds->push($subId);
+ // Store container status for aggregation
+ $key = $serviceId.':'.$subType.':'.$subId;
+ if (! $this->serviceContainerStatuses->has($key)) {
+ $this->serviceContainerStatuses->put($key, collect());
+ }
+ $containerName = $labels->get('com.docker.compose.service');
+ if ($containerName) {
+ $this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
+ }
+ }
+ } else {
+ $uuid = $labels->get('com.docker.compose.service');
+ $type = $labels->get('coolify.type');
+ if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
+ $this->foundProxy = true;
+ } elseif ($type === 'service' && $this->isRunning($containerStatus)) {
+ } else {
+ if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
+ $this->foundDatabaseUuids->push($uuid);
+ if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
+ $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
+ } else {
+ $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
}
}
}
@@ -218,6 +260,9 @@ public function handle()
// Aggregate multi-container application statuses
$this->aggregateMultiContainerStatuses();
+ // Aggregate multi-container service statuses
+ $this->aggregateServiceContainerStatuses();
+
$this->checkLogDrainContainer();
}
@@ -235,57 +280,28 @@ private function aggregateMultiContainerStatuses()
// Parse docker compose to check for excluded containers
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
- $excludedContainers = collect();
-
- if ($dockerComposeRaw) {
- try {
- $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
- $services = data_get($dockerCompose, 'services', []);
-
- foreach ($services as $serviceName => $serviceConfig) {
- // Check if container should be excluded
- $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
- $restartPolicy = data_get($serviceConfig, 'restart', 'always');
-
- if ($excludeFromHc || $restartPolicy === 'no') {
- $excludedContainers->push($serviceName);
- }
- }
- } catch (\Exception $e) {
- // If we can't parse, treat all containers as included
- }
- }
+ $excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
// Filter out excluded containers
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
return ! $excludedContainers->contains($containerName);
});
- // If all containers are excluded, don't update status
+ // If all containers are excluded, calculate status from excluded containers
if ($relevantStatuses->isEmpty()) {
+ $aggregatedStatus = $this->calculateExcludedStatusFromStrings($containerStatuses);
+
+ if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
+ $application->status = $aggregatedStatus;
+ $application->save();
+ }
+
continue;
}
- // Aggregate status: if any container is running, app is running
- $hasRunning = false;
- $hasUnhealthy = false;
-
- foreach ($relevantStatuses as $status) {
- if (str($status)->contains('running')) {
- $hasRunning = true;
- if (str($status)->contains('unhealthy')) {
- $hasUnhealthy = true;
- }
- }
- }
-
- $aggregatedStatus = null;
- if ($hasRunning) {
- $aggregatedStatus = $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
- } else {
- // All containers are exited
- $aggregatedStatus = 'exited (unhealthy)';
- }
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
+ $aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
// Update application status with aggregated result
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
@@ -295,6 +311,66 @@ private function aggregateMultiContainerStatuses()
}
}
+ private function aggregateServiceContainerStatuses()
+ {
+ if ($this->serviceContainerStatuses->isEmpty()) {
+ return;
+ }
+
+ foreach ($this->serviceContainerStatuses as $key => $containerStatuses) {
+ // Parse key: serviceId:subType:subId
+ [$serviceId, $subType, $subId] = explode(':', $key);
+
+ $service = $this->services->where('id', $serviceId)->first();
+ if (! $service) {
+ continue;
+ }
+
+ // Get the service sub-resource (ServiceApplication or ServiceDatabase)
+ $subResource = null;
+ if ($subType === 'application') {
+ $subResource = $service->applications()->where('id', $subId)->first();
+ } elseif ($subType === 'database') {
+ $subResource = $service->databases()->where('id', $subId)->first();
+ }
+
+ if (! $subResource) {
+ continue;
+ }
+
+ // Parse docker compose from service to check for excluded containers
+ $dockerComposeRaw = data_get($service, 'docker_compose_raw');
+ $excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
+
+ // Filter out excluded containers
+ $relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
+ return ! $excludedContainers->contains($containerName);
+ });
+
+ // If all containers are excluded, calculate status from excluded containers
+ if ($relevantStatuses->isEmpty()) {
+ $aggregatedStatus = $this->calculateExcludedStatusFromStrings($containerStatuses);
+ if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
+ $subResource->status = $aggregatedStatus;
+ $subResource->save();
+ }
+
+ continue;
+ }
+
+ // Use ContainerStatusAggregator service for state machine logic
+ // NOTE: Sentinel does NOT provide restart count data, so maxRestartCount is always 0
+ $aggregator = new ContainerStatusAggregator;
+ $aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
+
+ // Update service sub-resource status with aggregated result
+ if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
+ $subResource->status = $aggregatedStatus;
+ $subResource->save();
+ }
+ }
+ }
+
private function updateApplicationStatus(string $applicationId, string $containerStatus)
{
$application = $this->applications->where('id', $applicationId)->first();
diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php
index dba4f4ac8..e3e809c8d 100644
--- a/app/Jobs/RestartProxyJob.php
+++ b/app/Jobs/RestartProxyJob.php
@@ -31,12 +31,12 @@ public function __construct(public Server $server) {}
public function handle()
{
try {
- StopProxy::run($this->server);
+ StopProxy::run($this->server, restarting: true);
$this->server->proxy->force_stop = false;
$this->server->save();
- StartProxy::run($this->server, force: true);
+ StartProxy::run($this->server, force: true, restarting: true);
} catch (\Throwable $e) {
return handleError($e);
diff --git a/app/Jobs/ScheduledJobManager.php b/app/Jobs/ScheduledJobManager.php
index 9937444b8..75ff883c2 100644
--- a/app/Jobs/ScheduledJobManager.php
+++ b/app/Jobs/ScheduledJobManager.php
@@ -52,7 +52,7 @@ public function middleware(): array
{
return [
(new WithoutOverlapping('scheduled-job-manager'))
- ->expireAfter(60) // Lock expires after 1 minute to prevent stale locks
+ ->expireAfter(90) // Lock expires after 90s to handle high-load environments with many tasks
->dontRelease(), // Don't re-queue on lock conflict
];
}
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 609595356..e55db5440 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -18,14 +18,30 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ /**
+ * The number of times the job may be attempted.
+ */
+ public $tries = 3;
+
+ /**
+ * The maximum number of unhandled exceptions to allow before failing.
+ */
+ public $maxExceptions = 1;
+
+ /**
+ * The number of seconds the job can run before timing out.
+ */
+ public $timeout = 300;
+
public Team $team;
- public Server $server;
+ public ?Server $server = null;
public ScheduledTask $task;
@@ -33,6 +49,11 @@ class ScheduledTaskJob implements ShouldQueue
public ?ScheduledTaskExecution $task_log = null;
+ /**
+ * Store execution ID to survive job serialization for timeout handling.
+ */
+ protected ?int $executionId = null;
+
public string $task_status = 'failed';
public ?string $task_output = null;
@@ -55,6 +76,9 @@ public function __construct($task)
}
$this->team = Team::findOrFail($task->team_id);
$this->server_timezone = $this->getServerTimezone();
+
+ // Set timeout from task configuration
+ $this->timeout = $this->task->timeout ?? 300;
}
private function getServerTimezone(): string
@@ -70,11 +94,18 @@ private function getServerTimezone(): string
public function handle(): void
{
+ $startTime = Carbon::now();
+
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
+ 'started_at' => $startTime,
+ 'retry_count' => $this->attempts() - 1,
]);
+ // Store execution ID for timeout handling
+ $this->executionId = $this->task_log->id;
+
$this->server = $this->resource->destination->server;
if ($this->resource->type() === 'application') {
@@ -129,15 +160,101 @@ public function handle(): void
'message' => $this->task_output ?? $e->getMessage(),
]);
}
- $this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
+
+ // Log the error to the scheduled-errors channel
+ Log::channel('scheduled-errors')->error('ScheduledTask execution failed', [
+ 'job' => 'ScheduledTaskJob',
+ 'task_id' => $this->task->uuid,
+ 'task_name' => $this->task->name,
+ 'server' => $this->server?->name ?? 'unknown',
+ 'attempt' => $this->attempts(),
+ 'error' => $e->getMessage(),
+ ]);
+
+ // Only notify and throw on final failure
+
+ // Re-throw to trigger Laravel's retry mechanism with backoff
throw $e;
} finally {
ScheduledTaskDone::dispatch($this->team->id);
if ($this->task_log) {
+ $finishedAt = Carbon::now();
+ $duration = round($startTime->floatDiffInSeconds($finishedAt), 2);
+
$this->task_log->update([
- 'finished_at' => Carbon::now()->toImmutable(),
+ 'finished_at' => $finishedAt->toImmutable(),
+ 'duration' => $duration,
]);
}
}
}
+
+ /**
+ * Calculate the number of seconds to wait before retrying the job.
+ */
+ public function backoff(): array
+ {
+ return [30, 60, 120]; // 30s, 60s, 120s between retries
+ }
+
+ /**
+ * Handle a job failure.
+ */
+ public function failed(?\Throwable $exception): void
+ {
+ Log::channel('scheduled-errors')->error('ScheduledTask permanently failed', [
+ 'job' => 'ScheduledTaskJob',
+ 'task_id' => $this->task->uuid,
+ 'task_name' => $this->task->name,
+ 'server' => $this->server?->name ?? 'unknown',
+ 'total_attempts' => $this->attempts(),
+ 'error' => $exception?->getMessage(),
+ 'trace' => $exception?->getTraceAsString(),
+ ]);
+
+ // Reload execution log from database
+ // When a job times out, failed() is called in a fresh process with the original
+ // queue payload, so $executionId will be null. We need to query for the latest execution.
+ $execution = null;
+
+ // Try to find execution using stored ID first (works for non-timeout failures)
+ if ($this->executionId) {
+ $execution = ScheduledTaskExecution::find($this->executionId);
+ }
+
+ // If no stored ID or not found, query for the most recent execution log for this task
+ if (! $execution) {
+ $execution = ScheduledTaskExecution::query()
+ ->where('scheduled_task_id', $this->task->id)
+ ->orderBy('created_at', 'desc')
+ ->first();
+ }
+
+ // Last resort: check task_log property
+ if (! $execution && $this->task_log) {
+ $execution = $this->task_log;
+ }
+
+ if ($execution) {
+ $errorMessage = 'Job permanently failed after '.$this->attempts().' attempts';
+ if ($exception) {
+ $errorMessage .= ': '.$exception->getMessage();
+ }
+
+ $execution->update([
+ 'status' => 'failed',
+ 'message' => $errorMessage,
+ 'error_details' => $exception?->getTraceAsString(),
+ 'finished_at' => Carbon::now()->toImmutable(),
+ ]);
+ } else {
+ Log::channel('scheduled-errors')->warning('Could not find execution log to update', [
+ 'execution_id' => $this->executionId,
+ 'task_id' => $this->task->uuid,
+ ]);
+ }
+
+ // Notify team about permanent failure
+ $this->team?->notify(new TaskFailed($this->task, $exception?->getMessage() ?? 'Unknown error'));
+ }
}
diff --git a/app/Jobs/ServerManagerJob.php b/app/Jobs/ServerManagerJob.php
index 043845c00..45ab1dde8 100644
--- a/app/Jobs/ServerManagerJob.php
+++ b/app/Jobs/ServerManagerJob.php
@@ -87,7 +87,7 @@ private function dispatchConnectionChecks(Collection $servers): void
Log::channel('scheduled-errors')->error('Failed to dispatch ServerConnectionCheck', [
'server_id' => $server->id,
'server_name' => $server->name,
- 'error' => $e->getMessage(),
+ 'error' => get_class($e).': '.$e->getMessage(),
]);
}
});
@@ -103,7 +103,7 @@ private function processScheduledTasks(Collection $servers): void
Log::channel('scheduled-errors')->error('Error processing server tasks', [
'server_id' => $server->id,
'server_name' => $server->name,
- 'error' => $e->getMessage(),
+ 'error' => get_class($e).': '.$e->getMessage(),
]);
}
}
diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php
index 388791f10..ff5c2e4f5 100644
--- a/app/Jobs/ValidateAndInstallServerJob.php
+++ b/app/Jobs/ValidateAndInstallServerJob.php
@@ -72,6 +72,42 @@ public function handle(): void
return;
}
+ // Check and install prerequisites
+ $validationResult = $this->server->validatePrerequisites();
+ if (! $validationResult['success']) {
+ if ($this->numberOfTries >= $this->maxTries) {
+ $missingCommands = implode(', ', $validationResult['missing']);
+ $errorMessage = "Prerequisites ({$missingCommands}) could not be installed after {$this->maxTries} attempts. Please install them manually before continuing.";
+ $this->server->update([
+ 'validation_logs' => $errorMessage,
+ 'is_validating' => false,
+ ]);
+ Log::error('ValidateAndInstallServer: Prerequisites installation failed after max tries', [
+ 'server_id' => $this->server->id,
+ 'attempts' => $this->numberOfTries,
+ 'missing_commands' => $validationResult['missing'],
+ 'found_commands' => $validationResult['found'],
+ ]);
+
+ return;
+ }
+
+ Log::info('ValidateAndInstallServer: Installing prerequisites', [
+ 'server_id' => $this->server->id,
+ 'attempt' => $this->numberOfTries + 1,
+ 'missing_commands' => $validationResult['missing'],
+ 'found_commands' => $validationResult['found'],
+ ]);
+
+ // Install prerequisites
+ $this->server->installPrerequisites();
+
+ // Retry validation after installation
+ self::dispatch($this->server, $this->numberOfTries + 1)->delay(now()->addSeconds(30));
+
+ return;
+ }
+
// Check if Docker is installed
$dockerInstalled = $this->server->validateDockerEngine();
$dockerComposeInstalled = $this->server->validateDockerCompose();
diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php
index 54034ef7a..d01b55afb 100644
--- a/app/Livewire/ActivityMonitor.php
+++ b/app/Livewire/ActivityMonitor.php
@@ -28,12 +28,20 @@ class ActivityMonitor extends Component
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
- public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null)
+ public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null, $header = null)
{
+ // Reset event dispatched flag for new activity
+ self::$eventDispatched = false;
+
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->eventData = $eventData;
+ // Update header if provided
+ if ($header !== null) {
+ $this->header = $header;
+ }
+
$this->hydrateActivity();
$this->isPollingActive = true;
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 7912c4b85..ab1a1aae9 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -14,7 +14,10 @@
class Index extends Component
{
- protected $listeners = ['refreshBoardingIndex' => 'validateServer'];
+ protected $listeners = [
+ 'refreshBoardingIndex' => 'validateServer',
+ 'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
+ ];
#[\Livewire\Attributes\Url(as: 'step', history: true)]
public string $currentState = 'welcome';
@@ -76,6 +79,10 @@ class Index extends Component
public ?string $minDockerVersion = null;
+ public int $prerequisiteInstallAttempts = 0;
+
+ public int $maxPrerequisiteInstallAttempts = 3;
+
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
@@ -320,6 +327,62 @@ public function validateServer()
return handleError(error: $e, livewire: $this);
}
+ try {
+ // Check prerequisites
+ $validationResult = $this->createdServer->validatePrerequisites();
+ if (! $validationResult['success']) {
+ // Check if we've exceeded max attempts
+ if ($this->prerequisiteInstallAttempts >= $this->maxPrerequisiteInstallAttempts) {
+ $missingCommands = implode(', ', $validationResult['missing']);
+ throw new \Exception("Prerequisites ({$missingCommands}) could not be installed after {$this->maxPrerequisiteInstallAttempts} attempts. Please install them manually.");
+ }
+
+ // Start async installation and wait for completion via ActivityMonitor
+ $activity = $this->createdServer->installPrerequisites();
+ $this->prerequisiteInstallAttempts++;
+ $this->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled');
+
+ // Return early - handlePrerequisitesInstalled() will be called when installation completes
+ return;
+ }
+
+ // Prerequisites are already installed, continue with validation
+ $this->continueValidation();
+ } catch (\Throwable $e) {
+ return handleError(error: $e, livewire: $this);
+ }
+ }
+
+ public function handlePrerequisitesInstalled()
+ {
+ try {
+ // Revalidate prerequisites after installation completes
+ $validationResult = $this->createdServer->validatePrerequisites();
+ if (! $validationResult['success']) {
+ // Installation completed but prerequisites still missing - retry
+ $missingCommands = implode(', ', $validationResult['missing']);
+
+ if ($this->prerequisiteInstallAttempts >= $this->maxPrerequisiteInstallAttempts) {
+ throw new \Exception("Prerequisites ({$missingCommands}) could not be installed after {$this->maxPrerequisiteInstallAttempts} attempts. Please install them manually.");
+ }
+
+ // Try again
+ $activity = $this->createdServer->installPrerequisites();
+ $this->prerequisiteInstallAttempts++;
+ $this->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled');
+
+ return;
+ }
+
+ // Prerequisites validated successfully - continue with Docker validation
+ $this->continueValidation();
+ } catch (\Throwable $e) {
+ return handleError(error: $e, livewire: $this);
+ }
+ }
+
+ private function continueValidation()
+ {
try {
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
@@ -347,6 +410,8 @@ public function selectProxy(?string $proxyType = null)
}
$this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited';
+ $this->createdServer->proxy->last_saved_settings = null;
+ $this->createdServer->proxy->last_applied_settings = null;
$this->createdServer->save();
$this->getProjects();
}
diff --git a/app/Livewire/Concerns/SynchronizesModelData.php b/app/Livewire/Concerns/SynchronizesModelData.php
deleted file mode 100644
index f8218c715..000000000
--- a/app/Livewire/Concerns/SynchronizesModelData.php
+++ /dev/null
@@ -1,35 +0,0 @@
- Array mapping property names to model keys (e.g., ['content' => 'fileStorage.content'])
- */
- abstract protected function getModelBindings(): array;
-
- /**
- * Synchronize component properties TO the model.
- * Copies values from component properties to the model.
- */
- protected function syncToModel(): void
- {
- foreach ($this->getModelBindings() as $property => $modelKey) {
- data_set($this, $modelKey, $this->{$property});
- }
- }
-
- /**
- * Synchronize component properties FROM the model.
- * Copies values from the model to component properties.
- */
- protected function syncFromModel(): void
- {
- foreach ($this->getModelBindings() as $property => $modelKey) {
- $this->{$property} = data_get($this, $modelKey);
- }
- }
-}
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index 28d1cb866..b914fbd94 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -62,6 +62,9 @@ class Discord extends Component
#[Validate(['boolean'])]
public bool $serverPatchDiscordNotifications = false;
+ #[Validate(['boolean'])]
+ public bool $traefikOutdatedDiscordNotifications = true;
+
#[Validate(['boolean'])]
public bool $discordPingEnabled = true;
@@ -98,6 +101,7 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
$this->settings->server_patch_discord_notifications = $this->serverPatchDiscordNotifications;
+ $this->settings->traefik_outdated_discord_notifications = $this->traefikOutdatedDiscordNotifications;
$this->settings->discord_ping_enabled = $this->discordPingEnabled;
@@ -120,6 +124,7 @@ public function syncData(bool $toModel = false)
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
$this->serverPatchDiscordNotifications = $this->settings->server_patch_discord_notifications;
+ $this->traefikOutdatedDiscordNotifications = $this->settings->traefik_outdated_discord_notifications;
$this->discordPingEnabled = $this->settings->discord_ping_enabled;
}
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index d62a08417..847f10765 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -104,6 +104,9 @@ class Email extends Component
#[Validate(['boolean'])]
public bool $serverPatchEmailNotifications = false;
+ #[Validate(['boolean'])]
+ public bool $traefikOutdatedEmailNotifications = true;
+
#[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null;
@@ -155,6 +158,7 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
$this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
$this->settings->server_patch_email_notifications = $this->serverPatchEmailNotifications;
+ $this->settings->traefik_outdated_email_notifications = $this->traefikOutdatedEmailNotifications;
$this->settings->save();
} else {
@@ -187,6 +191,7 @@ public function syncData(bool $toModel = false)
$this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
$this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
$this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications;
+ $this->traefikOutdatedEmailNotifications = $this->settings->traefik_outdated_email_notifications;
}
}
diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php
index 9c7ff64ad..d79eea87b 100644
--- a/app/Livewire/Notifications/Pushover.php
+++ b/app/Livewire/Notifications/Pushover.php
@@ -70,6 +70,9 @@ class Pushover extends Component
#[Validate(['boolean'])]
public bool $serverPatchPushoverNotifications = false;
+ #[Validate(['boolean'])]
+ public bool $traefikOutdatedPushoverNotifications = true;
+
public function mount()
{
try {
@@ -104,6 +107,7 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications;
$this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications;
$this->settings->server_patch_pushover_notifications = $this->serverPatchPushoverNotifications;
+ $this->settings->traefik_outdated_pushover_notifications = $this->traefikOutdatedPushoverNotifications;
$this->settings->save();
refreshSession();
@@ -125,6 +129,7 @@ public function syncData(bool $toModel = false)
$this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications;
$this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications;
$this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications;
+ $this->traefikOutdatedPushoverNotifications = $this->settings->traefik_outdated_pushover_notifications;
}
}
diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php
index d21399c42..fa8c97ae9 100644
--- a/app/Livewire/Notifications/Slack.php
+++ b/app/Livewire/Notifications/Slack.php
@@ -67,6 +67,9 @@ class Slack extends Component
#[Validate(['boolean'])]
public bool $serverPatchSlackNotifications = false;
+ #[Validate(['boolean'])]
+ public bool $traefikOutdatedSlackNotifications = true;
+
public function mount()
{
try {
@@ -100,6 +103,7 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
$this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
$this->settings->server_patch_slack_notifications = $this->serverPatchSlackNotifications;
+ $this->settings->traefik_outdated_slack_notifications = $this->traefikOutdatedSlackNotifications;
$this->settings->save();
refreshSession();
@@ -120,6 +124,7 @@ public function syncData(bool $toModel = false)
$this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
$this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
$this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications;
+ $this->traefikOutdatedSlackNotifications = $this->settings->traefik_outdated_slack_notifications;
}
}
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index ca9df47c1..fc3966cf6 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -70,6 +70,9 @@ class Telegram extends Component
#[Validate(['boolean'])]
public bool $serverPatchTelegramNotifications = false;
+ #[Validate(['boolean'])]
+ public bool $traefikOutdatedTelegramNotifications = true;
+
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDeploymentSuccessThreadId = null;
@@ -109,6 +112,9 @@ class Telegram extends Component
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerPatchThreadId = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsTraefikOutdatedThreadId = null;
+
public function mount()
{
try {
@@ -143,6 +149,7 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
$this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
$this->settings->server_patch_telegram_notifications = $this->serverPatchTelegramNotifications;
+ $this->settings->traefik_outdated_telegram_notifications = $this->traefikOutdatedTelegramNotifications;
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
@@ -157,6 +164,7 @@ public function syncData(bool $toModel = false)
$this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId;
$this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId;
$this->settings->telegram_notifications_server_patch_thread_id = $this->telegramNotificationsServerPatchThreadId;
+ $this->settings->telegram_notifications_traefik_outdated_thread_id = $this->telegramNotificationsTraefikOutdatedThreadId;
$this->settings->save();
} else {
@@ -177,6 +185,7 @@ public function syncData(bool $toModel = false)
$this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
$this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
+ $this->traefikOutdatedTelegramNotifications = $this->settings->traefik_outdated_telegram_notifications;
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
@@ -191,6 +200,7 @@ public function syncData(bool $toModel = false)
$this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id;
$this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id;
$this->telegramNotificationsServerPatchThreadId = $this->settings->telegram_notifications_server_patch_thread_id;
+ $this->telegramNotificationsTraefikOutdatedThreadId = $this->settings->telegram_notifications_traefik_outdated_thread_id;
}
}
diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php
index cf4e71105..8af70c6eb 100644
--- a/app/Livewire/Notifications/Webhook.php
+++ b/app/Livewire/Notifications/Webhook.php
@@ -62,6 +62,9 @@ class Webhook extends Component
#[Validate(['boolean'])]
public bool $serverPatchWebhookNotifications = false;
+ #[Validate(['boolean'])]
+ public bool $traefikOutdatedWebhookNotifications = true;
+
public function mount()
{
try {
@@ -95,6 +98,7 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_webhook_notifications = $this->serverReachableWebhookNotifications;
$this->settings->server_unreachable_webhook_notifications = $this->serverUnreachableWebhookNotifications;
$this->settings->server_patch_webhook_notifications = $this->serverPatchWebhookNotifications;
+ $this->settings->traefik_outdated_webhook_notifications = $this->traefikOutdatedWebhookNotifications;
$this->settings->save();
refreshSession();
@@ -115,6 +119,7 @@ public function syncData(bool $toModel = false)
$this->serverReachableWebhookNotifications = $this->settings->server_reachable_webhook_notifications;
$this->serverUnreachableWebhookNotifications = $this->settings->server_unreachable_webhook_notifications;
$this->serverPatchWebhookNotifications = $this->settings->server_patch_webhook_notifications;
+ $this->traefikOutdatedWebhookNotifications = $this->settings->traefik_outdated_webhook_notifications;
}
}
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 8e8add430..71ca9720e 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -3,11 +3,11 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
-use App\Livewire\Concerns\SynchronizesModelData;
use App\Models\Application;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -15,7 +15,6 @@
class General extends Component
{
use AuthorizesRequests;
- use SynchronizesModelData;
public string $applicationId;
@@ -23,94 +22,136 @@ class General extends Component
public Collection $services;
+ #[Validate('required|regex:/^[a-zA-Z0-9\s\-_.\/:()]+$/')]
public string $name;
+ #[Validate(['string', 'nullable'])]
public ?string $description = null;
+ #[Validate(['nullable'])]
public ?string $fqdn = null;
- public string $git_repository;
+ #[Validate(['required'])]
+ public string $gitRepository;
- public string $git_branch;
+ #[Validate(['required'])]
+ public string $gitBranch;
- public ?string $git_commit_sha = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $gitCommitSha = null;
- public ?string $install_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $installCommand = null;
- public ?string $build_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $buildCommand = null;
- public ?string $start_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $startCommand = null;
- public string $build_pack;
+ #[Validate(['required'])]
+ public string $buildPack;
- public string $static_image;
+ #[Validate(['required'])]
+ public string $staticImage;
- public string $base_directory;
+ #[Validate(['required'])]
+ public string $baseDirectory;
- public ?string $publish_directory = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $publishDirectory = null;
- public ?string $ports_exposes = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $portsExposes = null;
- public ?string $ports_mappings = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $portsMappings = null;
- public ?string $custom_network_aliases = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $customNetworkAliases = null;
+ #[Validate(['string', 'nullable'])]
public ?string $dockerfile = null;
- public ?string $dockerfile_location = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerfileLocation = null;
- public ?string $dockerfile_target_build = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerfileTargetBuild = null;
- public ?string $docker_registry_image_name = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerRegistryImageName = null;
- public ?string $docker_registry_image_tag = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerRegistryImageTag = null;
- public ?string $docker_compose_location = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerComposeLocation = null;
- public ?string $docker_compose = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerCompose = null;
- public ?string $docker_compose_raw = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerComposeRaw = null;
- public ?string $docker_compose_custom_start_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerComposeCustomStartCommand = null;
- public ?string $docker_compose_custom_build_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $dockerComposeCustomBuildCommand = null;
- public ?string $custom_labels = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $customDockerRunOptions = null;
- public ?string $custom_docker_run_options = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $preDeploymentCommand = null;
- public ?string $pre_deployment_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $preDeploymentCommandContainer = null;
- public ?string $pre_deployment_command_container = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $postDeploymentCommand = null;
- public ?string $post_deployment_command = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $postDeploymentCommandContainer = null;
- public ?string $post_deployment_command_container = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $customNginxConfiguration = null;
- public ?string $custom_nginx_configuration = null;
+ #[Validate(['boolean', 'required'])]
+ public bool $isStatic = false;
- public bool $is_static = false;
+ #[Validate(['boolean', 'required'])]
+ public bool $isSpa = false;
- public bool $is_spa = false;
+ #[Validate(['boolean', 'required'])]
+ public bool $isBuildServerEnabled = false;
- public bool $is_build_server_enabled = false;
+ #[Validate(['boolean', 'required'])]
+ public bool $isPreserveRepositoryEnabled = false;
- public bool $is_preserve_repository_enabled = false;
+ #[Validate(['boolean', 'required'])]
+ public bool $isContainerLabelEscapeEnabled = true;
- public bool $is_container_label_escape_enabled = true;
+ #[Validate(['boolean', 'required'])]
+ public bool $isContainerLabelReadonlyEnabled = false;
- public bool $is_container_label_readonly_enabled = false;
+ #[Validate(['boolean', 'required'])]
+ public bool $isHttpBasicAuthEnabled = false;
- public bool $is_http_basic_auth_enabled = false;
+ #[Validate(['string', 'nullable'])]
+ public ?string $httpBasicAuthUsername = null;
- public ?string $http_basic_auth_username = null;
+ #[Validate(['string', 'nullable'])]
+ public ?string $httpBasicAuthPassword = null;
- public ?string $http_basic_auth_password = null;
-
- public ?string $watch_paths = null;
+ #[Validate(['nullable'])]
+ public ?string $watchPaths = null;
+ #[Validate(['string', 'required'])]
public string $redirect;
+ #[Validate(['nullable'])]
public $customLabels;
public bool $labelsChanged = false;
@@ -141,46 +182,46 @@ protected function rules(): array
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'fqdn' => 'nullable',
- 'git_repository' => 'required',
- 'git_branch' => 'required',
- 'git_commit_sha' => 'nullable',
- 'install_command' => 'nullable',
- 'build_command' => 'nullable',
- 'start_command' => 'nullable',
- 'build_pack' => 'required',
- 'static_image' => 'required',
- 'base_directory' => 'required',
- 'publish_directory' => 'nullable',
- 'ports_exposes' => 'required',
- 'ports_mappings' => 'nullable',
- 'custom_network_aliases' => 'nullable',
+ 'gitRepository' => 'required',
+ 'gitBranch' => 'required',
+ 'gitCommitSha' => 'nullable',
+ 'installCommand' => 'nullable',
+ 'buildCommand' => 'nullable',
+ 'startCommand' => 'nullable',
+ 'buildPack' => 'required',
+ 'staticImage' => 'required',
+ 'baseDirectory' => 'required',
+ 'publishDirectory' => 'nullable',
+ 'portsExposes' => 'required',
+ 'portsMappings' => 'nullable',
+ 'customNetworkAliases' => 'nullable',
'dockerfile' => 'nullable',
- 'docker_registry_image_name' => 'nullable',
- 'docker_registry_image_tag' => 'nullable',
- 'dockerfile_location' => 'nullable',
- 'docker_compose_location' => 'nullable',
- 'docker_compose' => 'nullable',
- 'docker_compose_raw' => 'nullable',
- 'dockerfile_target_build' => 'nullable',
- 'docker_compose_custom_start_command' => 'nullable',
- 'docker_compose_custom_build_command' => 'nullable',
- 'custom_labels' => 'nullable',
- 'custom_docker_run_options' => 'nullable',
- 'pre_deployment_command' => 'nullable',
- 'pre_deployment_command_container' => 'nullable',
- 'post_deployment_command' => 'nullable',
- 'post_deployment_command_container' => 'nullable',
- 'custom_nginx_configuration' => 'nullable',
- 'is_static' => 'boolean|required',
- 'is_spa' => 'boolean|required',
- 'is_build_server_enabled' => 'boolean|required',
- 'is_container_label_escape_enabled' => 'boolean|required',
- 'is_container_label_readonly_enabled' => 'boolean|required',
- 'is_preserve_repository_enabled' => 'boolean|required',
- 'is_http_basic_auth_enabled' => 'boolean|required',
- 'http_basic_auth_username' => 'string|nullable',
- 'http_basic_auth_password' => 'string|nullable',
- 'watch_paths' => 'nullable',
+ 'dockerRegistryImageName' => 'nullable',
+ 'dockerRegistryImageTag' => 'nullable',
+ 'dockerfileLocation' => 'nullable',
+ 'dockerComposeLocation' => 'nullable',
+ 'dockerCompose' => 'nullable',
+ 'dockerComposeRaw' => 'nullable',
+ 'dockerfileTargetBuild' => 'nullable',
+ 'dockerComposeCustomStartCommand' => 'nullable',
+ 'dockerComposeCustomBuildCommand' => 'nullable',
+ 'customLabels' => 'nullable',
+ 'customDockerRunOptions' => 'nullable',
+ 'preDeploymentCommand' => 'nullable',
+ 'preDeploymentCommandContainer' => 'nullable',
+ 'postDeploymentCommand' => 'nullable',
+ 'postDeploymentCommandContainer' => 'nullable',
+ 'customNginxConfiguration' => 'nullable',
+ 'isStatic' => 'boolean|required',
+ 'isSpa' => 'boolean|required',
+ 'isBuildServerEnabled' => 'boolean|required',
+ 'isContainerLabelEscapeEnabled' => 'boolean|required',
+ 'isContainerLabelReadonlyEnabled' => 'boolean|required',
+ 'isPreserveRepositoryEnabled' => 'boolean|required',
+ 'isHttpBasicAuthEnabled' => 'boolean|required',
+ 'httpBasicAuthUsername' => 'string|nullable',
+ 'httpBasicAuthPassword' => 'string|nullable',
+ 'watchPaths' => 'nullable',
'redirect' => 'string|required',
];
}
@@ -193,26 +234,26 @@ protected function messages(): array
'name.required' => 'The Name field is required.',
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
- 'git_repository.required' => 'The Git Repository field is required.',
- 'git_branch.required' => 'The Git Branch field is required.',
- 'build_pack.required' => 'The Build Pack field is required.',
- 'static_image.required' => 'The Static Image field is required.',
- 'base_directory.required' => 'The Base Directory field is required.',
- 'ports_exposes.required' => 'The Exposed Ports field is required.',
- 'is_static.required' => 'The Static setting is required.',
- 'is_static.boolean' => 'The Static setting must be true or false.',
- 'is_spa.required' => 'The SPA setting is required.',
- 'is_spa.boolean' => 'The SPA setting must be true or false.',
- 'is_build_server_enabled.required' => 'The Build Server setting is required.',
- 'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
- 'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
- 'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
- 'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
- 'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
- 'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
- 'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
- 'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
- 'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
+ 'gitRepository.required' => 'The Git Repository field is required.',
+ 'gitBranch.required' => 'The Git Branch field is required.',
+ 'buildPack.required' => 'The Build Pack field is required.',
+ 'staticImage.required' => 'The Static Image field is required.',
+ 'baseDirectory.required' => 'The Base Directory field is required.',
+ 'portsExposes.required' => 'The Exposed Ports field is required.',
+ 'isStatic.required' => 'The Static setting is required.',
+ 'isStatic.boolean' => 'The Static setting must be true or false.',
+ 'isSpa.required' => 'The SPA setting is required.',
+ 'isSpa.boolean' => 'The SPA setting must be true or false.',
+ 'isBuildServerEnabled.required' => 'The Build Server setting is required.',
+ 'isBuildServerEnabled.boolean' => 'The Build Server setting must be true or false.',
+ 'isContainerLabelEscapeEnabled.required' => 'The Container Label Escape setting is required.',
+ 'isContainerLabelEscapeEnabled.boolean' => 'The Container Label Escape setting must be true or false.',
+ 'isContainerLabelReadonlyEnabled.required' => 'The Container Label Readonly setting is required.',
+ 'isContainerLabelReadonlyEnabled.boolean' => 'The Container Label Readonly setting must be true or false.',
+ 'isPreserveRepositoryEnabled.required' => 'The Preserve Repository setting is required.',
+ 'isPreserveRepositoryEnabled.boolean' => 'The Preserve Repository setting must be true or false.',
+ 'isHttpBasicAuthEnabled.required' => 'The HTTP Basic Auth setting is required.',
+ 'isHttpBasicAuthEnabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
'redirect.required' => 'The Redirect setting is required.',
'redirect.string' => 'The Redirect setting must be a string.',
]
@@ -220,43 +261,43 @@ protected function messages(): array
}
protected $validationAttributes = [
- 'application.name' => 'name',
- 'application.description' => 'description',
- 'application.fqdn' => 'FQDN',
- 'application.git_repository' => 'Git repository',
- 'application.git_branch' => 'Git branch',
- 'application.git_commit_sha' => 'Git commit SHA',
- 'application.install_command' => 'Install command',
- 'application.build_command' => 'Build command',
- 'application.start_command' => 'Start command',
- 'application.build_pack' => 'Build pack',
- 'application.static_image' => 'Static image',
- 'application.base_directory' => 'Base directory',
- 'application.publish_directory' => 'Publish directory',
- 'application.ports_exposes' => 'Ports exposes',
- 'application.ports_mappings' => 'Ports mappings',
- 'application.dockerfile' => 'Dockerfile',
- 'application.docker_registry_image_name' => 'Docker registry image name',
- 'application.docker_registry_image_tag' => 'Docker registry image tag',
- 'application.dockerfile_location' => 'Dockerfile location',
- 'application.docker_compose_location' => 'Docker compose location',
- 'application.docker_compose' => 'Docker compose',
- 'application.docker_compose_raw' => 'Docker compose raw',
- 'application.custom_labels' => 'Custom labels',
- 'application.dockerfile_target_build' => 'Dockerfile target build',
- 'application.custom_docker_run_options' => 'Custom docker run commands',
- 'application.custom_network_aliases' => 'Custom docker network aliases',
- 'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
- 'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
- 'application.custom_nginx_configuration' => 'Custom Nginx configuration',
- 'application.settings.is_static' => 'Is static',
- 'application.settings.is_spa' => 'Is SPA',
- 'application.settings.is_build_server_enabled' => 'Is build server enabled',
- 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
- 'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
- 'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled',
- 'application.watch_paths' => 'Watch paths',
- 'application.redirect' => 'Redirect',
+ 'name' => 'name',
+ 'description' => 'description',
+ 'fqdn' => 'FQDN',
+ 'gitRepository' => 'Git repository',
+ 'gitBranch' => 'Git branch',
+ 'gitCommitSha' => 'Git commit SHA',
+ 'installCommand' => 'Install command',
+ 'buildCommand' => 'Build command',
+ 'startCommand' => 'Start command',
+ 'buildPack' => 'Build pack',
+ 'staticImage' => 'Static image',
+ 'baseDirectory' => 'Base directory',
+ 'publishDirectory' => 'Publish directory',
+ 'portsExposes' => 'Ports exposes',
+ 'portsMappings' => 'Ports mappings',
+ 'dockerfile' => 'Dockerfile',
+ 'dockerRegistryImageName' => 'Docker registry image name',
+ 'dockerRegistryImageTag' => 'Docker registry image tag',
+ 'dockerfileLocation' => 'Dockerfile location',
+ 'dockerComposeLocation' => 'Docker compose location',
+ 'dockerCompose' => 'Docker compose',
+ 'dockerComposeRaw' => 'Docker compose raw',
+ 'customLabels' => 'Custom labels',
+ 'dockerfileTargetBuild' => 'Dockerfile target build',
+ 'customDockerRunOptions' => 'Custom docker run commands',
+ 'customNetworkAliases' => 'Custom docker network aliases',
+ 'dockerComposeCustomStartCommand' => 'Docker compose custom start command',
+ 'dockerComposeCustomBuildCommand' => 'Docker compose custom build command',
+ 'customNginxConfiguration' => 'Custom Nginx configuration',
+ 'isStatic' => 'Is static',
+ 'isSpa' => 'Is SPA',
+ 'isBuildServerEnabled' => 'Is build server enabled',
+ 'isContainerLabelEscapeEnabled' => 'Is container label escape enabled',
+ 'isContainerLabelReadonlyEnabled' => 'Is container label readonly',
+ 'isPreserveRepositoryEnabled' => 'Is preserve repository enabled',
+ 'watchPaths' => 'Watch paths',
+ 'redirect' => 'Redirect',
];
public function mount()
@@ -266,14 +307,14 @@ public function mount()
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
// Still sync data even if parse fails, so form fields are populated
- $this->syncFromModel();
+ $this->syncData();
return;
}
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
// Still sync data even on error, so form fields are populated
- $this->syncFromModel();
+ $this->syncData();
}
if ($this->application->build_pack === 'dockercompose') {
// Only update if user has permission
@@ -325,57 +366,114 @@ public function mount()
// Sync data from model to properties at the END, after all business logic
// This ensures any modifications to $this->application during mount() are reflected in properties
- $this->syncFromModel();
+ $this->syncData();
}
- protected function getModelBindings(): array
+ public function syncData(bool $toModel = false): void
{
- return [
- 'name' => 'application.name',
- 'description' => 'application.description',
- 'fqdn' => 'application.fqdn',
- 'git_repository' => 'application.git_repository',
- 'git_branch' => 'application.git_branch',
- 'git_commit_sha' => 'application.git_commit_sha',
- 'install_command' => 'application.install_command',
- 'build_command' => 'application.build_command',
- 'start_command' => 'application.start_command',
- 'build_pack' => 'application.build_pack',
- 'static_image' => 'application.static_image',
- 'base_directory' => 'application.base_directory',
- 'publish_directory' => 'application.publish_directory',
- 'ports_exposes' => 'application.ports_exposes',
- 'ports_mappings' => 'application.ports_mappings',
- 'custom_network_aliases' => 'application.custom_network_aliases',
- 'dockerfile' => 'application.dockerfile',
- 'dockerfile_location' => 'application.dockerfile_location',
- 'dockerfile_target_build' => 'application.dockerfile_target_build',
- 'docker_registry_image_name' => 'application.docker_registry_image_name',
- 'docker_registry_image_tag' => 'application.docker_registry_image_tag',
- 'docker_compose_location' => 'application.docker_compose_location',
- 'docker_compose' => 'application.docker_compose',
- 'docker_compose_raw' => 'application.docker_compose_raw',
- 'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command',
- 'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command',
- 'custom_labels' => 'application.custom_labels',
- 'custom_docker_run_options' => 'application.custom_docker_run_options',
- 'pre_deployment_command' => 'application.pre_deployment_command',
- 'pre_deployment_command_container' => 'application.pre_deployment_command_container',
- 'post_deployment_command' => 'application.post_deployment_command',
- 'post_deployment_command_container' => 'application.post_deployment_command_container',
- 'custom_nginx_configuration' => 'application.custom_nginx_configuration',
- 'is_static' => 'application.settings.is_static',
- 'is_spa' => 'application.settings.is_spa',
- 'is_build_server_enabled' => 'application.settings.is_build_server_enabled',
- 'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled',
- 'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled',
- 'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled',
- 'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled',
- 'http_basic_auth_username' => 'application.http_basic_auth_username',
- 'http_basic_auth_password' => 'application.http_basic_auth_password',
- 'watch_paths' => 'application.watch_paths',
- 'redirect' => 'application.redirect',
- ];
+ if ($toModel) {
+ $this->validate();
+
+ // Application properties
+ $this->application->name = $this->name;
+ $this->application->description = $this->description;
+ $this->application->fqdn = $this->fqdn;
+ $this->application->git_repository = $this->gitRepository;
+ $this->application->git_branch = $this->gitBranch;
+ $this->application->git_commit_sha = $this->gitCommitSha;
+ $this->application->install_command = $this->installCommand;
+ $this->application->build_command = $this->buildCommand;
+ $this->application->start_command = $this->startCommand;
+ $this->application->build_pack = $this->buildPack;
+ $this->application->static_image = $this->staticImage;
+ $this->application->base_directory = $this->baseDirectory;
+ $this->application->publish_directory = $this->publishDirectory;
+ $this->application->ports_exposes = $this->portsExposes;
+ $this->application->ports_mappings = $this->portsMappings;
+ $this->application->custom_network_aliases = $this->customNetworkAliases;
+ $this->application->dockerfile = $this->dockerfile;
+ $this->application->dockerfile_location = $this->dockerfileLocation;
+ $this->application->dockerfile_target_build = $this->dockerfileTargetBuild;
+ $this->application->docker_registry_image_name = $this->dockerRegistryImageName;
+ $this->application->docker_registry_image_tag = $this->dockerRegistryImageTag;
+ $this->application->docker_compose_location = $this->dockerComposeLocation;
+ $this->application->docker_compose = $this->dockerCompose;
+ $this->application->docker_compose_raw = $this->dockerComposeRaw;
+ $this->application->docker_compose_custom_start_command = $this->dockerComposeCustomStartCommand;
+ $this->application->docker_compose_custom_build_command = $this->dockerComposeCustomBuildCommand;
+ $this->application->custom_labels = is_null($this->customLabels)
+ ? null
+ : base64_encode($this->customLabels);
+ $this->application->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->application->pre_deployment_command = $this->preDeploymentCommand;
+ $this->application->pre_deployment_command_container = $this->preDeploymentCommandContainer;
+ $this->application->post_deployment_command = $this->postDeploymentCommand;
+ $this->application->post_deployment_command_container = $this->postDeploymentCommandContainer;
+ $this->application->custom_nginx_configuration = $this->customNginxConfiguration;
+ $this->application->is_http_basic_auth_enabled = $this->isHttpBasicAuthEnabled;
+ $this->application->http_basic_auth_username = $this->httpBasicAuthUsername;
+ $this->application->http_basic_auth_password = $this->httpBasicAuthPassword;
+ $this->application->watch_paths = $this->watchPaths;
+ $this->application->redirect = $this->redirect;
+
+ // Application settings properties
+ $this->application->settings->is_static = $this->isStatic;
+ $this->application->settings->is_spa = $this->isSpa;
+ $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled;
+ $this->application->settings->is_preserve_repository_enabled = $this->isPreserveRepositoryEnabled;
+ $this->application->settings->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled;
+ $this->application->settings->is_container_label_readonly_enabled = $this->isContainerLabelReadonlyEnabled;
+
+ $this->application->settings->save();
+ } else {
+ // From model to properties
+ $this->name = $this->application->name;
+ $this->description = $this->application->description;
+ $this->fqdn = $this->application->fqdn;
+ $this->gitRepository = $this->application->git_repository;
+ $this->gitBranch = $this->application->git_branch;
+ $this->gitCommitSha = $this->application->git_commit_sha;
+ $this->installCommand = $this->application->install_command;
+ $this->buildCommand = $this->application->build_command;
+ $this->startCommand = $this->application->start_command;
+ $this->buildPack = $this->application->build_pack;
+ $this->staticImage = $this->application->static_image;
+ $this->baseDirectory = $this->application->base_directory;
+ $this->publishDirectory = $this->application->publish_directory;
+ $this->portsExposes = $this->application->ports_exposes;
+ $this->portsMappings = $this->application->ports_mappings;
+ $this->customNetworkAliases = $this->application->custom_network_aliases;
+ $this->dockerfile = $this->application->dockerfile;
+ $this->dockerfileLocation = $this->application->dockerfile_location;
+ $this->dockerfileTargetBuild = $this->application->dockerfile_target_build;
+ $this->dockerRegistryImageName = $this->application->docker_registry_image_name;
+ $this->dockerRegistryImageTag = $this->application->docker_registry_image_tag;
+ $this->dockerComposeLocation = $this->application->docker_compose_location;
+ $this->dockerCompose = $this->application->docker_compose;
+ $this->dockerComposeRaw = $this->application->docker_compose_raw;
+ $this->dockerComposeCustomStartCommand = $this->application->docker_compose_custom_start_command;
+ $this->dockerComposeCustomBuildCommand = $this->application->docker_compose_custom_build_command;
+ $this->customLabels = $this->application->parseContainerLabels();
+ $this->customDockerRunOptions = $this->application->custom_docker_run_options;
+ $this->preDeploymentCommand = $this->application->pre_deployment_command;
+ $this->preDeploymentCommandContainer = $this->application->pre_deployment_command_container;
+ $this->postDeploymentCommand = $this->application->post_deployment_command;
+ $this->postDeploymentCommandContainer = $this->application->post_deployment_command_container;
+ $this->customNginxConfiguration = $this->application->custom_nginx_configuration;
+ $this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled;
+ $this->httpBasicAuthUsername = $this->application->http_basic_auth_username;
+ $this->httpBasicAuthPassword = $this->application->http_basic_auth_password;
+ $this->watchPaths = $this->application->watch_paths;
+ $this->redirect = $this->application->redirect;
+
+ // Application settings properties
+ $this->isStatic = $this->application->settings->is_static;
+ $this->isSpa = $this->application->settings->is_spa;
+ $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled;
+ $this->isPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
+ $this->isContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
+ $this->isContainerLabelReadonlyEnabled = $this->application->settings->is_container_label_readonly_enabled;
+ }
}
public function instantSave()
@@ -386,33 +484,36 @@ public function instantSave()
$oldPortsExposes = $this->application->ports_exposes;
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
$oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
+ $oldIsSpa = $this->application->settings->is_spa;
+ $oldIsHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled;
- $this->syncToModel();
+ $this->syncData(toModel: true);
- if ($this->application->settings->isDirty('is_spa')) {
- $this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
+ if ($oldIsSpa !== $this->isSpa) {
+ $this->generateNginxConfiguration($this->isSpa ? 'spa' : 'static');
}
- if ($this->application->isDirty('is_http_basic_auth_enabled')) {
+ if ($oldIsHttpBasicAuthEnabled !== $this->isHttpBasicAuthEnabled) {
$this->application->save();
}
- $this->application->settings->save();
+
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
- $this->syncFromModel();
+
+ $this->syncData();
// If port_exposes changed, reset default labels
- if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
+ if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
$this->resetDefaultLabels(false);
}
- if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) {
- if ($this->is_preserve_repository_enabled === false) {
+ if ($oldIsPreserveRepositoryEnabled !== $this->isPreserveRepositoryEnabled) {
+ if ($this->isPreserveRepositoryEnabled === false) {
$this->application->fileStorages->each(function ($storage) {
- $storage->is_based_on_git = $this->is_preserve_repository_enabled;
+ $storage->is_based_on_git = $this->isPreserveRepositoryEnabled;
$storage->save();
});
}
}
- if ($this->is_container_label_readonly_enabled) {
+ if ($this->isContainerLabelReadonlyEnabled) {
$this->resetDefaultLabels(false);
}
} catch (\Throwable $e) {
@@ -441,7 +542,7 @@ public function loadComposeFile($isInit = false, $showToast = true)
// Sync the docker_compose_raw from the model to the component property
// This ensures the Monaco editor displays the loaded compose file
- $this->syncFromModel();
+ $this->syncData();
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
// Convert service names with dots and dashes to use underscores for HTML form binding
@@ -507,7 +608,7 @@ public function generateDomain(string $serviceName)
public function updatedBaseDirectory()
{
- if ($this->build_pack === 'dockercompose') {
+ if ($this->buildPack === 'dockercompose') {
$this->loadComposeFile();
}
}
@@ -527,24 +628,22 @@ public function updatedBuildPack()
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
// User doesn't have permission, revert the change and return
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
return;
}
// Sync property to model before checking/modifying
- $this->syncToModel();
+ $this->syncData(toModel: true);
- if ($this->build_pack !== 'nixpacks') {
- $this->is_static = false;
+ if ($this->buildPack !== 'nixpacks') {
+ $this->isStatic = false;
$this->application->settings->is_static = false;
$this->application->settings->save();
} else {
- $this->ports_exposes = 3000;
- $this->application->ports_exposes = 3000;
$this->resetDefaultLabels(false);
}
- if ($this->build_pack === 'dockercompose') {
+ if ($this->buildPack === 'dockercompose') {
// Only update if user has permission
try {
$this->authorize('update', $this->application);
@@ -554,22 +653,10 @@ public function updatedBuildPack()
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
// User doesn't have update permission, just continue without saving
}
- } else {
- // Clear Docker Compose specific data when switching away from dockercompose
- if ($this->application->getOriginal('build_pack') === 'dockercompose') {
- $this->application->docker_compose_domains = null;
- $this->application->docker_compose_raw = null;
-
- // Remove SERVICE_FQDN_* and SERVICE_URL_* environment variables
- $this->application->environment_variables()->where('key', 'LIKE', 'SERVICE_FQDN_%')->delete();
- $this->application->environment_variables()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
- $this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_FQDN_%')->delete();
- $this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
- }
}
- if ($this->build_pack === 'static') {
- $this->ports_exposes = 80;
- $this->application->ports_exposes = 80;
+ if ($this->buildPack === 'static') {
+ $this->portsExposes = '80';
+ $this->application->ports_exposes = '80';
$this->resetDefaultLabels(false);
$this->generateNginxConfiguration();
}
@@ -586,10 +673,10 @@ public function getWildcardDomain()
if ($server) {
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
$this->fqdn = $fqdn;
- $this->syncToModel();
+ $this->syncData(toModel: true);
$this->application->save();
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
$this->resetDefaultLabels();
$this->dispatch('success', 'Wildcard domain generated.');
}
@@ -603,11 +690,11 @@ public function generateNginxConfiguration($type = 'static')
try {
$this->authorize('update', $this->application);
- $this->custom_nginx_configuration = defaultNginxConfiguration($type);
- $this->syncToModel();
+ $this->customNginxConfiguration = defaultNginxConfiguration($type);
+ $this->syncData(toModel: true);
$this->application->save();
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
$this->dispatch('success', 'Nginx configuration generated.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -617,16 +704,15 @@ public function generateNginxConfiguration($type = 'static')
public function resetDefaultLabels($manualReset = false)
{
try {
- if (! $this->is_container_label_readonly_enabled && ! $manualReset) {
+ if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) {
return;
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
- $this->custom_labels = base64_encode($this->customLabels);
- $this->syncToModel();
+ $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
$this->application->refresh();
- $this->syncFromModel();
- if ($this->build_pack === 'dockercompose') {
+ $this->syncData();
+ if ($this->buildPack === 'dockercompose') {
$this->loadComposeFile(showToast: false);
}
$this->dispatch('configurationChanged');
@@ -722,7 +808,7 @@ public function submit($showToaster = true)
$this->dispatch('warning', __('warning.sslipdomain'));
}
- $this->syncToModel();
+ $this->syncData(toModel: true);
if ($this->application->isDirty('redirect')) {
$this->setRedirect();
@@ -742,42 +828,42 @@ public function submit($showToaster = true)
$this->application->save();
}
- if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) {
+ if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
$compose_return = $this->loadComposeFile(showToast: false);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
}
}
- if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
+ if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
$this->resetDefaultLabels();
}
- if ($this->build_pack === 'dockerimage') {
+ if ($this->buildPack === 'dockerimage') {
$this->validate([
- 'docker_registry_image_name' => 'required',
+ 'dockerRegistryImageName' => 'required',
]);
}
- if ($this->custom_docker_run_options) {
- $this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString();
- $this->application->custom_docker_run_options = $this->custom_docker_run_options;
+ if ($this->customDockerRunOptions) {
+ $this->customDockerRunOptions = str($this->customDockerRunOptions)->trim()->toString();
+ $this->application->custom_docker_run_options = $this->customDockerRunOptions;
}
if ($this->dockerfile) {
$port = get_port_from_dockerfile($this->dockerfile);
- if ($port && ! $this->ports_exposes) {
- $this->ports_exposes = $port;
+ if ($port && ! $this->portsExposes) {
+ $this->portsExposes = $port;
$this->application->ports_exposes = $port;
}
}
- if ($this->base_directory && $this->base_directory !== '/') {
- $this->base_directory = rtrim($this->base_directory, '/');
- $this->application->base_directory = $this->base_directory;
+ if ($this->baseDirectory && $this->baseDirectory !== '/') {
+ $this->baseDirectory = rtrim($this->baseDirectory, '/');
+ $this->application->base_directory = $this->baseDirectory;
}
- if ($this->publish_directory && $this->publish_directory !== '/') {
- $this->publish_directory = rtrim($this->publish_directory, '/');
- $this->application->publish_directory = $this->publish_directory;
+ if ($this->publishDirectory && $this->publishDirectory !== '/') {
+ $this->publishDirectory = rtrim($this->publishDirectory, '/');
+ $this->application->publish_directory = $this->publishDirectory;
}
- if ($this->build_pack === 'dockercompose') {
+ if ($this->buildPack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
if ($this->application->isDirty('docker_compose_domains')) {
foreach ($this->parsedServiceDomains as $service) {
@@ -809,11 +895,11 @@ public function submit($showToaster = true)
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) {
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
return handleError($e, $this);
} finally {
@@ -900,4 +986,60 @@ private function updateServiceEnvironmentVariables()
}
}
}
+
+ public function getDetectedPortInfoProperty(): ?array
+ {
+ $detectedPort = $this->application->detectPortFromEnvironment();
+
+ if (! $detectedPort) {
+ return null;
+ }
+
+ $portsExposesArray = $this->application->ports_exposes_array;
+ $isMatch = in_array($detectedPort, $portsExposesArray);
+ $isEmpty = empty($portsExposesArray);
+
+ return [
+ 'port' => $detectedPort,
+ 'matches' => $isMatch,
+ 'isEmpty' => $isEmpty,
+ ];
+ }
+
+ public function getDockerComposeBuildCommandPreviewProperty(): string
+ {
+ if (! $this->dockerComposeCustomBuildCommand) {
+ return '';
+ }
+
+ // Normalize baseDirectory to prevent double slashes (e.g., when baseDirectory is '/')
+ $normalizedBase = $this->baseDirectory === '/' ? '' : rtrim($this->baseDirectory, '/');
+
+ // Use relative path for clarity in preview (e.g., ./backend/docker-compose.yaml)
+ // Actual deployment uses absolute path: /artifacts/{deployment_uuid}{base_directory}{docker_compose_location}
+ // Build-time env path references ApplicationDeploymentJob::BUILD_TIME_ENV_PATH as source of truth
+ return injectDockerComposeFlags(
+ $this->dockerComposeCustomBuildCommand,
+ ".{$normalizedBase}{$this->dockerComposeLocation}",
+ \App\Jobs\ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
+ );
+ }
+
+ public function getDockerComposeStartCommandPreviewProperty(): string
+ {
+ if (! $this->dockerComposeCustomStartCommand) {
+ return '';
+ }
+
+ // Normalize baseDirectory to prevent double slashes (e.g., when baseDirectory is '/')
+ $normalizedBase = $this->baseDirectory === '/' ? '' : rtrim($this->baseDirectory, '/');
+
+ // Use relative path for clarity in preview (e.g., ./backend/docker-compose.yaml)
+ // Placeholder {workdir}/.env shows it's the workdir .env file (runtime env, not build-time)
+ return injectDockerComposeFlags(
+ $this->dockerComposeCustomStartCommand,
+ ".{$normalizedBase}{$this->dockerComposeLocation}",
+ '{workdir}/.env'
+ );
+ }
}
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 5231438e5..fc63c7f4b 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -101,11 +101,18 @@ public function deploy(bool $force_rebuild = false)
force_rebuild: $force_rebuild,
);
if ($result['status'] === 'skipped') {
- $this->dispatch('success', 'Deployment skipped', $result['message']);
+ $this->dispatch('error', 'Deployment skipped', $result['message']);
return;
}
+ // Reset restart count on successful deployment
+ $this->application->update([
+ 'restart_count' => 0,
+ 'last_restart_at' => null,
+ 'last_restart_type' => null,
+ ]);
+
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
@@ -137,6 +144,7 @@ public function restart()
return;
}
+
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
@@ -149,6 +157,13 @@ public function restart()
return;
}
+ // Reset restart count on manual restart
+ $this->application->update([
+ 'restart_count' => 0,
+ 'last_restart_at' => now(),
+ 'last_restart_type' => 'manual',
+ ]);
+
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 7deaa82a9..da543a049 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -79,7 +79,7 @@ class BackupEdit extends Component
#[Validate(['required', 'boolean'])]
public bool $dumpAll = false;
- #[Validate(['required', 'int', 'min:1', 'max:36000'])]
+ #[Validate(['required', 'int', 'min:60', 'max:36000'])]
public int $timeout = 3600;
public function mount()
diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php
index 7d6ac3131..fd191e587 100644
--- a/app/Livewire/Project/Database/Import.php
+++ b/app/Livewire/Project/Database/Import.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Database;
+use App\Models\S3Storage;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
@@ -12,6 +13,92 @@ class Import extends Component
{
use AuthorizesRequests;
+ /**
+ * Validate that a string is safe for use as an S3 bucket name.
+ * Allows alphanumerics, dots, dashes, and underscores.
+ */
+ private function validateBucketName(string $bucket): bool
+ {
+ return preg_match('/^[a-zA-Z0-9.\-_]+$/', $bucket) === 1;
+ }
+
+ /**
+ * Validate that a string is safe for use as an S3 path.
+ * Allows alphanumerics, dots, dashes, underscores, slashes, and common file characters.
+ */
+ private function validateS3Path(string $path): bool
+ {
+ // Must not be empty
+ if (empty($path)) {
+ return false;
+ }
+
+ // Must not contain dangerous shell metacharacters or command injection patterns
+ $dangerousPatterns = [
+ '..', // Directory traversal
+ '$(', // Command substitution
+ '`', // Backtick command substitution
+ '|', // Pipe
+ ';', // Command separator
+ '&', // Background/AND
+ '>', // Redirect
+ '<', // Redirect
+ "\n", // Newline
+ "\r", // Carriage return
+ "\0", // Null byte
+ "'", // Single quote
+ '"', // Double quote
+ '\\', // Backslash
+ ];
+
+ foreach ($dangerousPatterns as $pattern) {
+ if (str_contains($path, $pattern)) {
+ return false;
+ }
+ }
+
+ // Allow alphanumerics, dots, dashes, underscores, slashes, spaces, plus, equals, at
+ return preg_match('/^[a-zA-Z0-9.\-_\/\s+@=]+$/', $path) === 1;
+ }
+
+ /**
+ * Validate that a string is safe for use as a file path on the server.
+ */
+ private function validateServerPath(string $path): bool
+ {
+ // Must be an absolute path
+ if (! str_starts_with($path, '/')) {
+ return false;
+ }
+
+ // Must not contain dangerous shell metacharacters or command injection patterns
+ $dangerousPatterns = [
+ '..', // Directory traversal
+ '$(', // Command substitution
+ '`', // Backtick command substitution
+ '|', // Pipe
+ ';', // Command separator
+ '&', // Background/AND
+ '>', // Redirect
+ '<', // Redirect
+ "\n", // Newline
+ "\r", // Carriage return
+ "\0", // Null byte
+ "'", // Single quote
+ '"', // Double quote
+ '\\', // Backslash
+ ];
+
+ foreach ($dangerousPatterns as $pattern) {
+ if (str_contains($path, $pattern)) {
+ return false;
+ }
+ }
+
+ // Allow alphanumerics, dots, dashes, underscores, slashes, and spaces
+ return preg_match('/^[a-zA-Z0-9.\-_\/\s]+$/', $path) === 1;
+ }
+
public bool $unsupported = false;
public $resource;
@@ -54,6 +141,15 @@ class Import extends Component
public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=';
+ // S3 Restore properties
+ public $availableS3Storages = [];
+
+ public ?int $s3StorageId = null;
+
+ public string $s3Path = '';
+
+ public ?int $s3FileSize = null;
+
public function getListeners()
{
$userId = Auth::id();
@@ -65,11 +161,9 @@ public function getListeners()
public function mount()
{
- if (isDev()) {
- $this->customLocation = '/data/coolify/pg-dump-all-1736245863.gz';
- }
$this->parameters = get_route_parameters();
$this->getContainers();
+ $this->loadAvailableS3Storages();
}
public function updatedDumpAll($value)
@@ -152,8 +246,16 @@ public function getContainers()
public function checkFile()
{
if (filled($this->customLocation)) {
+ // Validate the custom location to prevent command injection
+ if (! $this->validateServerPath($this->customLocation)) {
+ $this->dispatch('error', 'Invalid file path. Path must be absolute and contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).');
+
+ return;
+ }
+
try {
- $result = instant_remote_process(["ls -l {$this->customLocation}"], $this->server, throwError: false);
+ $escapedPath = escapeshellarg($this->customLocation);
+ $result = instant_remote_process(["ls -l {$escapedPath}"], $this->server, throwError: false);
if (blank($result)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
@@ -179,59 +281,35 @@ public function runImport()
try {
$this->importRunning = true;
$this->importCommands = [];
- if (filled($this->customLocation)) {
- $backupFileName = '/tmp/restore_'.$this->resource->uuid;
- $this->importCommands[] = "docker cp {$this->customLocation} {$this->container}:{$backupFileName}";
- $tmpPath = $backupFileName;
- } else {
- $backupFileName = "upload/{$this->resource->uuid}/restore";
- $path = Storage::path($backupFileName);
- if (! Storage::exists($backupFileName)) {
- $this->dispatch('error', 'The file does not exist or has been deleted.');
+ $backupFileName = "upload/{$this->resource->uuid}/restore";
- return;
- }
+ // Check if an uploaded file exists first (takes priority over custom location)
+ if (Storage::exists($backupFileName)) {
+ $path = Storage::path($backupFileName);
$tmpPath = '/tmp/'.basename($backupFileName).'_'.$this->resource->uuid;
instant_scp($path, $tmpPath, $this->server);
Storage::delete($backupFileName);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
+ } elseif (filled($this->customLocation)) {
+ // Validate the custom location to prevent command injection
+ if (! $this->validateServerPath($this->customLocation)) {
+ $this->dispatch('error', 'Invalid file path. Path must be absolute and contain only safe characters.');
+
+ return;
+ }
+ $tmpPath = '/tmp/restore_'.$this->resource->uuid;
+ $escapedCustomLocation = escapeshellarg($this->customLocation);
+ $this->importCommands[] = "docker cp {$escapedCustomLocation} {$this->container}:{$tmpPath}";
+ } else {
+ $this->dispatch('error', 'The file does not exist or has been deleted.');
+
+ return;
}
// Copy the restore command to a script file
$scriptPath = "/tmp/restore_{$this->resource->uuid}.sh";
- switch ($this->resource->getMorphClass()) {
- case \App\Models\StandaloneMariadb::class:
- $restoreCommand = $this->mariadbRestoreCommand;
- if ($this->dumpAll) {
- $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mariadb -u root -p\$MARIADB_ROOT_PASSWORD";
- } else {
- $restoreCommand .= " < {$tmpPath}";
- }
- break;
- case \App\Models\StandaloneMysql::class:
- $restoreCommand = $this->mysqlRestoreCommand;
- if ($this->dumpAll) {
- $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mysql -u root -p\$MYSQL_ROOT_PASSWORD";
- } else {
- $restoreCommand .= " < {$tmpPath}";
- }
- break;
- case \App\Models\StandalonePostgresql::class:
- $restoreCommand = $this->postgresqlRestoreCommand;
- if ($this->dumpAll) {
- $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \$POSTGRES_USER postgres";
- } else {
- $restoreCommand .= " {$tmpPath}";
- }
- break;
- case \App\Models\StandaloneMongodb::class:
- $restoreCommand = $this->mongodbRestoreCommand;
- if ($this->dumpAll === false) {
- $restoreCommand .= "{$tmpPath}";
- }
- break;
- }
+ $restoreCommand = $this->buildRestoreCommand($tmpPath);
$restoreCommandBase64 = base64_encode($restoreCommand);
$this->importCommands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}";
@@ -248,7 +326,10 @@ public function runImport()
'container' => $this->container,
'serverId' => $this->server->id,
]);
+
+ // Dispatch activity to the monitor and open slide-over
$this->dispatch('activityMonitor', $activity->id);
+ $this->dispatch('databaserestore');
}
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -257,4 +338,264 @@ public function runImport()
$this->importCommands = [];
}
}
+
+ public function loadAvailableS3Storages()
+ {
+ try {
+ $this->availableS3Storages = S3Storage::ownedByCurrentTeam(['id', 'name', 'description'])
+ ->where('is_usable', true)
+ ->get();
+ } catch (\Throwable $e) {
+ $this->availableS3Storages = collect();
+ }
+ }
+
+ public function updatedS3Path($value)
+ {
+ // Reset validation state when path changes
+ $this->s3FileSize = null;
+
+ // Ensure path starts with a slash
+ if ($value !== null && $value !== '') {
+ $this->s3Path = str($value)->trim()->start('/')->value();
+ }
+ }
+
+ public function updatedS3StorageId()
+ {
+ // Reset validation state when storage changes
+ $this->s3FileSize = null;
+ }
+
+ public function checkS3File()
+ {
+ if (! $this->s3StorageId) {
+ $this->dispatch('error', 'Please select an S3 storage.');
+
+ return;
+ }
+
+ if (blank($this->s3Path)) {
+ $this->dispatch('error', 'Please provide an S3 path.');
+
+ return;
+ }
+
+ // Clean the path (remove leading slash if present)
+ $cleanPath = ltrim($this->s3Path, '/');
+
+ // Validate the S3 path early to prevent command injection in subsequent operations
+ if (! $this->validateS3Path($cleanPath)) {
+ $this->dispatch('error', 'Invalid S3 path. Path must contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).');
+
+ return;
+ }
+
+ try {
+ $s3Storage = S3Storage::ownedByCurrentTeam()->findOrFail($this->s3StorageId);
+
+ // Validate bucket name early
+ if (! $this->validateBucketName($s3Storage->bucket)) {
+ $this->dispatch('error', 'Invalid S3 bucket name. Bucket name must contain only alphanumerics, dots, dashes, and underscores.');
+
+ return;
+ }
+
+ // Test connection
+ $s3Storage->testConnection();
+
+ // Build S3 disk configuration
+ $disk = Storage::build([
+ 'driver' => 's3',
+ 'region' => $s3Storage->region,
+ 'key' => $s3Storage->key,
+ 'secret' => $s3Storage->secret,
+ 'bucket' => $s3Storage->bucket,
+ 'endpoint' => $s3Storage->endpoint,
+ 'use_path_style_endpoint' => true,
+ ]);
+
+ // Check if file exists
+ if (! $disk->exists($cleanPath)) {
+ $this->dispatch('error', 'File not found in S3. Please check the path.');
+
+ return;
+ }
+
+ // Get file size
+ $this->s3FileSize = $disk->size($cleanPath);
+
+ $this->dispatch('success', 'File found in S3. Size: '.formatBytes($this->s3FileSize));
+ } catch (\Throwable $e) {
+ $this->s3FileSize = null;
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function restoreFromS3()
+ {
+ $this->authorize('update', $this->resource);
+
+ if (! $this->s3StorageId || blank($this->s3Path)) {
+ $this->dispatch('error', 'Please select S3 storage and provide a path first.');
+
+ return;
+ }
+
+ if (is_null($this->s3FileSize)) {
+ $this->dispatch('error', 'Please check the file first by clicking "Check File".');
+
+ return;
+ }
+
+ try {
+ $this->importRunning = true;
+
+ $s3Storage = S3Storage::ownedByCurrentTeam()->findOrFail($this->s3StorageId);
+
+ $key = $s3Storage->key;
+ $secret = $s3Storage->secret;
+ $bucket = $s3Storage->bucket;
+ $endpoint = $s3Storage->endpoint;
+
+ // Validate bucket name to prevent command injection
+ if (! $this->validateBucketName($bucket)) {
+ $this->dispatch('error', 'Invalid S3 bucket name. Bucket name must contain only alphanumerics, dots, dashes, and underscores.');
+
+ return;
+ }
+
+ // Clean the S3 path
+ $cleanPath = ltrim($this->s3Path, '/');
+
+ // Validate the S3 path to prevent command injection
+ if (! $this->validateS3Path($cleanPath)) {
+ $this->dispatch('error', 'Invalid S3 path. Path must contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).');
+
+ return;
+ }
+
+ // Get helper image
+ $helperImage = config('constants.coolify.helper_image');
+ $latestVersion = getHelperVersion();
+ $fullImageName = "{$helperImage}:{$latestVersion}";
+
+ // Get the database destination network
+ $destinationNetwork = $this->resource->destination->network ?? 'coolify';
+
+ // Generate unique names for this operation
+ $containerName = "s3-restore-{$this->resource->uuid}";
+ $helperTmpPath = '/tmp/'.basename($cleanPath);
+ $serverTmpPath = "/tmp/s3-restore-{$this->resource->uuid}-".basename($cleanPath);
+ $containerTmpPath = "/tmp/restore_{$this->resource->uuid}-".basename($cleanPath);
+ $scriptPath = "/tmp/restore_{$this->resource->uuid}.sh";
+
+ // Prepare all commands in sequence
+ $commands = [];
+
+ // 1. Clean up any existing helper container and temp files from previous runs
+ $commands[] = "docker rm -f {$containerName} 2>/dev/null || true";
+ $commands[] = "rm -f {$serverTmpPath} 2>/dev/null || true";
+ $commands[] = "docker exec {$this->container} rm -f {$containerTmpPath} {$scriptPath} 2>/dev/null || true";
+
+ // 2. Start helper container on the database network
+ $commands[] = "docker run -d --network {$destinationNetwork} --name {$containerName} {$fullImageName} sleep 3600";
+
+ // 3. Configure S3 access in helper container
+ $escapedEndpoint = escapeshellarg($endpoint);
+ $escapedKey = escapeshellarg($key);
+ $escapedSecret = escapeshellarg($secret);
+ $commands[] = "docker exec {$containerName} mc alias set s3temp {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
+
+ // 4. Check file exists in S3 (bucket and path already validated above)
+ $escapedBucket = escapeshellarg($bucket);
+ $escapedCleanPath = escapeshellarg($cleanPath);
+ $escapedS3Source = escapeshellarg("s3temp/{$bucket}/{$cleanPath}");
+ $commands[] = "docker exec {$containerName} mc stat {$escapedS3Source}";
+
+ // 5. Download from S3 to helper container (progress shown by default)
+ $escapedHelperTmpPath = escapeshellarg($helperTmpPath);
+ $commands[] = "docker exec {$containerName} mc cp {$escapedS3Source} {$escapedHelperTmpPath}";
+
+ // 6. Copy from helper to server, then immediately to database container
+ $commands[] = "docker cp {$containerName}:{$helperTmpPath} {$serverTmpPath}";
+ $commands[] = "docker cp {$serverTmpPath} {$this->container}:{$containerTmpPath}";
+
+ // 7. Cleanup helper container and server temp file immediately (no longer needed)
+ $commands[] = "docker rm -f {$containerName} 2>/dev/null || true";
+ $commands[] = "rm -f {$serverTmpPath} 2>/dev/null || true";
+
+ // 8. Build and execute restore command inside database container
+ $restoreCommand = $this->buildRestoreCommand($containerTmpPath);
+
+ $restoreCommandBase64 = base64_encode($restoreCommand);
+ $commands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}";
+ $commands[] = "chmod +x {$scriptPath}";
+ $commands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}";
+
+ // 9. Execute restore and cleanup temp files immediately after completion
+ $commands[] = "docker exec {$this->container} sh -c '{$scriptPath} && rm -f {$containerTmpPath} {$scriptPath}'";
+ $commands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'";
+
+ // Execute all commands with cleanup event (as safety net for edge cases)
+ $activity = remote_process($commands, $this->server, ignore_errors: true, callEventOnFinish: 'S3RestoreJobFinished', callEventData: [
+ 'containerName' => $containerName,
+ 'serverTmpPath' => $serverTmpPath,
+ 'scriptPath' => $scriptPath,
+ 'containerTmpPath' => $containerTmpPath,
+ 'container' => $this->container,
+ 'serverId' => $this->server->id,
+ ]);
+
+ // Dispatch activity to the monitor and open slide-over
+ $this->dispatch('activityMonitor', $activity->id);
+ $this->dispatch('databaserestore');
+ $this->dispatch('info', 'Restoring database from S3. Progress will be shown in the activity monitor...');
+ } catch (\Throwable $e) {
+ $this->importRunning = false;
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function buildRestoreCommand(string $tmpPath): string
+ {
+ switch ($this->resource->getMorphClass()) {
+ case \App\Models\StandaloneMariadb::class:
+ $restoreCommand = $this->mariadbRestoreCommand;
+ if ($this->dumpAll) {
+ $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mariadb -u root -p\$MARIADB_ROOT_PASSWORD";
+ } else {
+ $restoreCommand .= " < {$tmpPath}";
+ }
+ break;
+ case \App\Models\StandaloneMysql::class:
+ $restoreCommand = $this->mysqlRestoreCommand;
+ if ($this->dumpAll) {
+ $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mysql -u root -p\$MYSQL_ROOT_PASSWORD";
+ } else {
+ $restoreCommand .= " < {$tmpPath}";
+ }
+ break;
+ case \App\Models\StandalonePostgresql::class:
+ $restoreCommand = $this->postgresqlRestoreCommand;
+ if ($this->dumpAll) {
+ $restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \$POSTGRES_USER postgres";
+ } else {
+ $restoreCommand .= " {$tmpPath}";
+ }
+ break;
+ case \App\Models\StandaloneMongodb::class:
+ $restoreCommand = $this->mongodbRestoreCommand;
+ if ($this->dumpAll === false) {
+ $restoreCommand .= "{$tmpPath}";
+ }
+ break;
+ default:
+ $restoreCommand = '';
+ }
+
+ return $restoreCommand;
+ }
}
diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php
index f759dd71e..7158b6e40 100644
--- a/app/Livewire/Project/Service/EditDomain.php
+++ b/app/Livewire/Project/Service/EditDomain.php
@@ -2,14 +2,16 @@
namespace App\Livewire\Project\Service;
-use App\Livewire\Concerns\SynchronizesModelData;
use App\Models\ServiceApplication;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
class EditDomain extends Component
{
- use SynchronizesModelData;
+ use AuthorizesRequests;
+
public $applicationId;
public ServiceApplication $application;
@@ -20,6 +22,13 @@ class EditDomain extends Component
public $forceSaveDomains = false;
+ public $showPortWarningModal = false;
+
+ public $forceRemovePort = false;
+
+ public $requiredPort = null;
+
+ #[Validate(['nullable'])]
public ?string $fqdn = null;
protected $rules = [
@@ -28,16 +37,25 @@ class EditDomain extends Component
public function mount()
{
- $this->application = ServiceApplication::query()->findOrFail($this->applicationId);
+ $this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId);
$this->authorize('view', $this->application);
- $this->syncFromModel();
+ $this->requiredPort = $this->application->getRequiredPort();
+ $this->syncData();
}
- protected function getModelBindings(): array
+ public function syncData(bool $toModel = false): void
{
- return [
- 'fqdn' => 'application.fqdn',
- ];
+ if ($toModel) {
+ $this->validate();
+
+ // Sync to model
+ $this->application->fqdn = $this->fqdn;
+
+ $this->application->save();
+ } else {
+ // Sync from model
+ $this->fqdn = $this->application->fqdn;
+ }
}
public function confirmDomainUsage()
@@ -47,6 +65,19 @@ public function confirmDomainUsage()
$this->submit();
}
+ public function confirmRemovePort()
+ {
+ $this->forceRemovePort = true;
+ $this->showPortWarningModal = false;
+ $this->submit();
+ }
+
+ public function cancelRemovePort()
+ {
+ $this->showPortWarningModal = false;
+ $this->syncData(); // Reset to original FQDN
+ }
+
public function submit()
{
try {
@@ -64,8 +95,8 @@ public function submit()
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
- // Sync to model for domain conflict check
- $this->syncToModel();
+ // Sync to model for domain conflict check (without validation)
+ $this->application->fqdn = $this->fqdn;
// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains) {
$result = checkDomainUsage(resource: $this->application);
@@ -80,10 +111,44 @@ public function submit()
$this->forceSaveDomains = false;
}
+ // Check for required port
+ if (! $this->forceRemovePort) {
+ $requiredPort = $this->application->getRequiredPort();
+
+ if ($requiredPort !== null) {
+ // Check if all FQDNs have a port
+ $fqdns = str($this->fqdn)->trim()->explode(',');
+ $missingPort = false;
+
+ foreach ($fqdns as $fqdn) {
+ $fqdn = trim($fqdn);
+ if (empty($fqdn)) {
+ continue;
+ }
+
+ $port = ServiceApplication::extractPortFromUrl($fqdn);
+ if ($port === null) {
+ $missingPort = true;
+ break;
+ }
+ }
+
+ if ($missingPort) {
+ $this->requiredPort = $requiredPort;
+ $this->showPortWarningModal = true;
+
+ return;
+ }
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceRemovePort = false;
+ }
+
$this->validate();
$this->application->save();
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED. Only use multiple domains if you know what you are doing.');
@@ -96,7 +161,7 @@ public function submit()
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
- $this->syncFromModel();
+ $this->syncData();
}
return handleError($e, $this);
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 40539b13e..2ce4374a0 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -2,7 +2,6 @@
namespace App\Livewire\Project\Service;
-use App\Livewire\Concerns\SynchronizesModelData;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\LocalFileVolume;
@@ -19,11 +18,12 @@
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class FileStorage extends Component
{
- use AuthorizesRequests, SynchronizesModelData;
+ use AuthorizesRequests;
public LocalFileVolume $fileStorage;
@@ -37,8 +37,10 @@ class FileStorage extends Component
public bool $isReadOnly = false;
+ #[Validate(['nullable'])]
public ?string $content = null;
+ #[Validate(['required', 'boolean'])]
public bool $isBasedOnGit = false;
protected $rules = [
@@ -61,15 +63,24 @@ public function mount()
}
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
- $this->syncFromModel();
+ $this->syncData();
}
- protected function getModelBindings(): array
+ public function syncData(bool $toModel = false): void
{
- return [
- 'content' => 'fileStorage.content',
- 'isBasedOnGit' => 'fileStorage.is_based_on_git',
- ];
+ if ($toModel) {
+ $this->validate();
+
+ // Sync to model
+ $this->fileStorage->content = $this->content;
+ $this->fileStorage->is_based_on_git = $this->isBasedOnGit;
+
+ $this->fileStorage->save();
+ } else {
+ // Sync from model
+ $this->content = $this->fileStorage->content;
+ $this->isBasedOnGit = $this->fileStorage->is_based_on_git;
+ }
}
public function convertToDirectory()
@@ -96,7 +107,7 @@ public function loadStorageOnServer()
$this->authorize('update', $this->resource);
$this->fileStorage->loadStorageOnServer();
- $this->syncFromModel();
+ $this->syncData();
$this->dispatch('success', 'File storage loaded from server.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -165,14 +176,16 @@ public function submit()
if ($this->fileStorage->is_directory) {
$this->content = null;
}
- $this->syncToModel();
+ // Sync component properties to model
+ $this->fileStorage->content = $this->content;
+ $this->fileStorage->is_based_on_git = $this->isBasedOnGit;
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
$this->dispatch('success', 'File updated.');
} catch (\Throwable $e) {
$this->fileStorage->setRawAttributes($original);
$this->fileStorage->save();
- $this->syncFromModel();
+ $this->syncData();
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php
index 20358218f..259b9dbec 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -2,20 +2,19 @@
namespace App\Livewire\Project\Service;
-use App\Livewire\Concerns\SynchronizesModelData;
use App\Models\InstanceSettings;
use App\Models\ServiceApplication;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
class ServiceApplicationView extends Component
{
use AuthorizesRequests;
- use SynchronizesModelData;
public ServiceApplication $application;
@@ -31,20 +30,34 @@ class ServiceApplicationView extends Component
public $forceSaveDomains = false;
+ public $showPortWarningModal = false;
+
+ public $forceRemovePort = false;
+
+ public $requiredPort = null;
+
+ #[Validate(['nullable'])]
public ?string $humanName = null;
+ #[Validate(['nullable'])]
public ?string $description = null;
+ #[Validate(['nullable'])]
public ?string $fqdn = null;
+ #[Validate(['string', 'nullable'])]
public ?string $image = null;
+ #[Validate(['required', 'boolean'])]
public bool $excludeFromStatus = false;
+ #[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
+ #[Validate(['nullable', 'boolean'])]
public bool $isGzipEnabled = false;
+ #[Validate(['nullable', 'boolean'])]
public bool $isStripprefixEnabled = false;
protected $rules = [
@@ -79,7 +92,15 @@ public function instantSaveAdvanced()
return;
}
- $this->syncToModel();
+ // Sync component properties to model
+ $this->application->human_name = $this->humanName;
+ $this->application->description = $this->description;
+ $this->application->fqdn = $this->fqdn;
+ $this->application->image = $this->image;
+ $this->application->exclude_from_status = $this->excludeFromStatus;
+ $this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->application->is_gzip_enabled = $this->isGzipEnabled;
+ $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
$this->application->save();
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (\Throwable $e) {
@@ -114,24 +135,53 @@ public function mount()
try {
$this->parameters = get_route_parameters();
$this->authorize('view', $this->application);
- $this->syncFromModel();
+ $this->requiredPort = $this->application->getRequiredPort();
+ $this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- protected function getModelBindings(): array
+ public function confirmRemovePort()
{
- return [
- 'humanName' => 'application.human_name',
- 'description' => 'application.description',
- 'fqdn' => 'application.fqdn',
- 'image' => 'application.image',
- 'excludeFromStatus' => 'application.exclude_from_status',
- 'isLogDrainEnabled' => 'application.is_log_drain_enabled',
- 'isGzipEnabled' => 'application.is_gzip_enabled',
- 'isStripprefixEnabled' => 'application.is_stripprefix_enabled',
- ];
+ $this->forceRemovePort = true;
+ $this->showPortWarningModal = false;
+ $this->submit();
+ }
+
+ public function cancelRemovePort()
+ {
+ $this->showPortWarningModal = false;
+ $this->syncData(); // Reset to original FQDN
+ }
+
+ public function syncData(bool $toModel = false): void
+ {
+ if ($toModel) {
+ $this->validate();
+
+ // Sync to model
+ $this->application->human_name = $this->humanName;
+ $this->application->description = $this->description;
+ $this->application->fqdn = $this->fqdn;
+ $this->application->image = $this->image;
+ $this->application->exclude_from_status = $this->excludeFromStatus;
+ $this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->application->is_gzip_enabled = $this->isGzipEnabled;
+ $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
+
+ $this->application->save();
+ } else {
+ // Sync from model
+ $this->humanName = $this->application->human_name;
+ $this->description = $this->application->description;
+ $this->fqdn = $this->application->fqdn;
+ $this->image = $this->application->image;
+ $this->excludeFromStatus = data_get($this->application, 'exclude_from_status', false);
+ $this->isLogDrainEnabled = data_get($this->application, 'is_log_drain_enabled', false);
+ $this->isGzipEnabled = data_get($this->application, 'is_gzip_enabled', true);
+ $this->isStripprefixEnabled = data_get($this->application, 'is_stripprefix_enabled', true);
+ }
}
public function convertToDatabase()
@@ -193,8 +243,15 @@ public function submit()
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
- // Sync to model for domain conflict check
- $this->syncToModel();
+ // Sync to model for domain conflict check (without validation)
+ $this->application->human_name = $this->humanName;
+ $this->application->description = $this->description;
+ $this->application->fqdn = $this->fqdn;
+ $this->application->image = $this->image;
+ $this->application->exclude_from_status = $this->excludeFromStatus;
+ $this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->application->is_gzip_enabled = $this->isGzipEnabled;
+ $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains) {
$result = checkDomainUsage(resource: $this->application);
@@ -209,10 +266,44 @@ public function submit()
$this->forceSaveDomains = false;
}
+ // Check for required port
+ if (! $this->forceRemovePort) {
+ $requiredPort = $this->application->getRequiredPort();
+
+ if ($requiredPort !== null) {
+ // Check if all FQDNs have a port
+ $fqdns = str($this->fqdn)->trim()->explode(',');
+ $missingPort = false;
+
+ foreach ($fqdns as $fqdn) {
+ $fqdn = trim($fqdn);
+ if (empty($fqdn)) {
+ continue;
+ }
+
+ $port = ServiceApplication::extractPortFromUrl($fqdn);
+ if ($port === null) {
+ $missingPort = true;
+ break;
+ }
+ }
+
+ if ($missingPort) {
+ $this->requiredPort = $requiredPort;
+ $this->showPortWarningModal = true;
+
+ return;
+ }
+ }
+ } else {
+ // Reset the force flag after using it
+ $this->forceRemovePort = false;
+ }
+
$this->validate();
$this->application->save();
$this->application->refresh();
- $this->syncFromModel();
+ $this->syncData();
updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED. Only use multiple domains if you know what you are doing.');
@@ -224,7 +315,7 @@ public function submit()
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
- $this->syncFromModel();
+ $this->syncData();
}
return handleError($e, $this);
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 85cd21a7f..72ae6915a 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -5,6 +5,7 @@
use App\Models\Service;
use App\Support\ValidationPatterns;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
use Livewire\Component;
class StackForm extends Component
@@ -22,7 +23,7 @@ class StackForm extends Component
public string $dockerComposeRaw;
- public string $dockerCompose;
+ public ?string $dockerCompose = null;
public ?bool $connectToDockerNetwork = null;
@@ -30,7 +31,7 @@ protected function rules(): array
{
$baseRules = [
'dockerComposeRaw' => 'required',
- 'dockerCompose' => 'required',
+ 'dockerCompose' => 'nullable',
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'connectToDockerNetwork' => 'nullable',
@@ -140,18 +141,27 @@ public function submit($notify = true)
$this->validate();
$this->syncData(true);
- // Validate for command injection BEFORE saving to database
+ // Validate for command injection BEFORE any database operations
validateDockerComposeForInjection($this->service->docker_compose_raw);
- $this->service->save();
- $this->service->saveExtraFields($this->fields);
- $this->service->parse();
+ // Use transaction to ensure atomicity - if parse fails, save is rolled back
+ DB::transaction(function () {
+ $this->service->save();
+ $this->service->saveExtraFields($this->fields);
+ $this->service->parse();
+ });
+ // Refresh and write files after a successful commit
$this->service->refresh();
$this->service->saveComposeConfigs();
+
$this->dispatch('refreshEnvs');
$this->dispatch('refreshServices');
$notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
+ // On error, refresh from database to restore clean state
+ $this->service->refresh();
+ $this->syncData(false);
+
return handleError($e, $this);
} finally {
if (is_null($this->service->config_hash)) {
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
index 5f5e12e0a..fa65e8bd2 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
@@ -2,8 +2,11 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
+use App\Models\Environment;
+use App\Models\Project;
use App\Traits\EnvironmentVariableAnalyzer;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Livewire\Attributes\Computed;
use Livewire\Component;
class Add extends Component
@@ -56,6 +59,72 @@ public function mount()
$this->problematicVariables = self::getProblematicVariablesForFrontend();
}
+ #[Computed]
+ public function availableSharedVariables(): array
+ {
+ $team = currentTeam();
+ $result = [
+ 'team' => [],
+ 'project' => [],
+ 'environment' => [],
+ ];
+
+ // Early return if no team
+ if (! $team) {
+ return $result;
+ }
+
+ // Check if user can view team variables
+ try {
+ $this->authorize('view', $team);
+ $result['team'] = $team->environment_variables()
+ ->pluck('key')
+ ->toArray();
+ } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ // User not authorized to view team variables
+ }
+
+ // Get project variables if we have a project_uuid in route
+ $projectUuid = data_get($this->parameters, 'project_uuid');
+ if ($projectUuid) {
+ $project = Project::where('team_id', $team->id)
+ ->where('uuid', $projectUuid)
+ ->first();
+
+ if ($project) {
+ try {
+ $this->authorize('view', $project);
+ $result['project'] = $project->environment_variables()
+ ->pluck('key')
+ ->toArray();
+
+ // Get environment variables if we have an environment_uuid in route
+ $environmentUuid = data_get($this->parameters, 'environment_uuid');
+ if ($environmentUuid) {
+ $environment = $project->environments()
+ ->where('uuid', $environmentUuid)
+ ->first();
+
+ if ($environment) {
+ try {
+ $this->authorize('view', $environment);
+ $result['environment'] = $environment->environment_variables()
+ ->pluck('key')
+ ->toArray();
+ } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ // User not authorized to view environment variables
+ }
+ }
+ }
+ } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ // User not authorized to view project variables
+ }
+ }
+ }
+
+ return $result;
+ }
+
public function submit()
{
$this->validate();
diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php
index c8029761d..05f786690 100644
--- a/app/Livewire/Project/Shared/HealthChecks.php
+++ b/app/Livewire/Project/Shared/HealthChecks.php
@@ -2,42 +2,54 @@
namespace App\Livewire\Project\Shared;
-use App\Livewire\Concerns\SynchronizesModelData;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class HealthChecks extends Component
{
use AuthorizesRequests;
- use SynchronizesModelData;
public $resource;
// Explicit properties
+ #[Validate(['boolean'])]
public bool $healthCheckEnabled = false;
+ #[Validate(['string'])]
public string $healthCheckMethod;
+ #[Validate(['string'])]
public string $healthCheckScheme;
+ #[Validate(['string'])]
public string $healthCheckHost;
+ #[Validate(['nullable', 'string'])]
public ?string $healthCheckPort = null;
+ #[Validate(['string'])]
public string $healthCheckPath;
+ #[Validate(['integer'])]
public int $healthCheckReturnCode;
+ #[Validate(['nullable', 'string'])]
public ?string $healthCheckResponseText = null;
+ #[Validate(['integer', 'min:1'])]
public int $healthCheckInterval;
+ #[Validate(['integer', 'min:1'])]
public int $healthCheckTimeout;
+ #[Validate(['integer', 'min:1'])]
public int $healthCheckRetries;
+ #[Validate(['integer'])]
public int $healthCheckStartPeriod;
+ #[Validate(['boolean'])]
public bool $customHealthcheckFound = false;
protected $rules = [
@@ -56,36 +68,69 @@ class HealthChecks extends Component
'customHealthcheckFound' => 'boolean',
];
- protected function getModelBindings(): array
- {
- return [
- 'healthCheckEnabled' => 'resource.health_check_enabled',
- 'healthCheckMethod' => 'resource.health_check_method',
- 'healthCheckScheme' => 'resource.health_check_scheme',
- 'healthCheckHost' => 'resource.health_check_host',
- 'healthCheckPort' => 'resource.health_check_port',
- 'healthCheckPath' => 'resource.health_check_path',
- 'healthCheckReturnCode' => 'resource.health_check_return_code',
- 'healthCheckResponseText' => 'resource.health_check_response_text',
- 'healthCheckInterval' => 'resource.health_check_interval',
- 'healthCheckTimeout' => 'resource.health_check_timeout',
- 'healthCheckRetries' => 'resource.health_check_retries',
- 'healthCheckStartPeriod' => 'resource.health_check_start_period',
- 'customHealthcheckFound' => 'resource.custom_healthcheck_found',
- ];
- }
-
public function mount()
{
$this->authorize('view', $this->resource);
- $this->syncFromModel();
+ $this->syncData();
+ }
+
+ public function syncData(bool $toModel = false): void
+ {
+ if ($toModel) {
+ $this->validate();
+
+ // Sync to model
+ $this->resource->health_check_enabled = $this->healthCheckEnabled;
+ $this->resource->health_check_method = $this->healthCheckMethod;
+ $this->resource->health_check_scheme = $this->healthCheckScheme;
+ $this->resource->health_check_host = $this->healthCheckHost;
+ $this->resource->health_check_port = $this->healthCheckPort;
+ $this->resource->health_check_path = $this->healthCheckPath;
+ $this->resource->health_check_return_code = $this->healthCheckReturnCode;
+ $this->resource->health_check_response_text = $this->healthCheckResponseText;
+ $this->resource->health_check_interval = $this->healthCheckInterval;
+ $this->resource->health_check_timeout = $this->healthCheckTimeout;
+ $this->resource->health_check_retries = $this->healthCheckRetries;
+ $this->resource->health_check_start_period = $this->healthCheckStartPeriod;
+ $this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
+
+ $this->resource->save();
+ } else {
+ // Sync from model
+ $this->healthCheckEnabled = $this->resource->health_check_enabled;
+ $this->healthCheckMethod = $this->resource->health_check_method;
+ $this->healthCheckScheme = $this->resource->health_check_scheme;
+ $this->healthCheckHost = $this->resource->health_check_host;
+ $this->healthCheckPort = $this->resource->health_check_port;
+ $this->healthCheckPath = $this->resource->health_check_path;
+ $this->healthCheckReturnCode = $this->resource->health_check_return_code;
+ $this->healthCheckResponseText = $this->resource->health_check_response_text;
+ $this->healthCheckInterval = $this->resource->health_check_interval;
+ $this->healthCheckTimeout = $this->resource->health_check_timeout;
+ $this->healthCheckRetries = $this->resource->health_check_retries;
+ $this->healthCheckStartPeriod = $this->resource->health_check_start_period;
+ $this->customHealthcheckFound = $this->resource->custom_healthcheck_found;
+ }
}
public function instantSave()
{
$this->authorize('update', $this->resource);
- $this->syncToModel();
+ // Sync component properties to model
+ $this->resource->health_check_enabled = $this->healthCheckEnabled;
+ $this->resource->health_check_method = $this->healthCheckMethod;
+ $this->resource->health_check_scheme = $this->healthCheckScheme;
+ $this->resource->health_check_host = $this->healthCheckHost;
+ $this->resource->health_check_port = $this->healthCheckPort;
+ $this->resource->health_check_path = $this->healthCheckPath;
+ $this->resource->health_check_return_code = $this->healthCheckReturnCode;
+ $this->resource->health_check_response_text = $this->healthCheckResponseText;
+ $this->resource->health_check_interval = $this->healthCheckInterval;
+ $this->resource->health_check_timeout = $this->healthCheckTimeout;
+ $this->resource->health_check_retries = $this->healthCheckRetries;
+ $this->resource->health_check_start_period = $this->healthCheckStartPeriod;
+ $this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
$this->resource->save();
$this->dispatch('success', 'Health check updated.');
}
@@ -96,7 +141,20 @@ public function submit()
$this->authorize('update', $this->resource);
$this->validate();
- $this->syncToModel();
+ // Sync component properties to model
+ $this->resource->health_check_enabled = $this->healthCheckEnabled;
+ $this->resource->health_check_method = $this->healthCheckMethod;
+ $this->resource->health_check_scheme = $this->healthCheckScheme;
+ $this->resource->health_check_host = $this->healthCheckHost;
+ $this->resource->health_check_port = $this->healthCheckPort;
+ $this->resource->health_check_path = $this->healthCheckPath;
+ $this->resource->health_check_return_code = $this->healthCheckReturnCode;
+ $this->resource->health_check_response_text = $this->healthCheckResponseText;
+ $this->resource->health_check_interval = $this->healthCheckInterval;
+ $this->resource->health_check_timeout = $this->healthCheckTimeout;
+ $this->resource->health_check_retries = $this->healthCheckRetries;
+ $this->resource->health_check_start_period = $this->healthCheckStartPeriod;
+ $this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
$this->resource->save();
$this->dispatch('success', 'Health check updated.');
} catch (\Throwable $e) {
@@ -111,7 +169,20 @@ public function toggleHealthcheck()
$wasEnabled = $this->healthCheckEnabled;
$this->healthCheckEnabled = ! $this->healthCheckEnabled;
- $this->syncToModel();
+ // Sync component properties to model
+ $this->resource->health_check_enabled = $this->healthCheckEnabled;
+ $this->resource->health_check_method = $this->healthCheckMethod;
+ $this->resource->health_check_scheme = $this->healthCheckScheme;
+ $this->resource->health_check_host = $this->healthCheckHost;
+ $this->resource->health_check_port = $this->healthCheckPort;
+ $this->resource->health_check_path = $this->healthCheckPath;
+ $this->resource->health_check_return_code = $this->healthCheckReturnCode;
+ $this->resource->health_check_response_text = $this->healthCheckResponseText;
+ $this->resource->health_check_interval = $this->healthCheckInterval;
+ $this->resource->health_check_timeout = $this->healthCheckTimeout;
+ $this->resource->health_check_retries = $this->healthCheckRetries;
+ $this->resource->health_check_start_period = $this->healthCheckStartPeriod;
+ $this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
$this->resource->save();
if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) {
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php
index e4b666532..d7210c15d 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Add.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php
@@ -34,11 +34,14 @@ class Add extends Component
public ?string $container = '';
+ public int $timeout = 300;
+
protected $rules = [
'name' => 'required|string',
'command' => 'required|string',
'frequency' => 'required|string',
'container' => 'nullable|string',
+ 'timeout' => 'required|integer|min:60|max:3600',
];
protected $validationAttributes = [
@@ -46,6 +49,7 @@ class Add extends Component
'command' => 'command',
'frequency' => 'frequency',
'container' => 'container',
+ 'timeout' => 'timeout',
];
public function mount()
@@ -103,6 +107,7 @@ public function saveScheduledTask()
$task->command = $this->command;
$task->frequency = $this->frequency;
$task->container = $this->container;
+ $task->timeout = $this->timeout;
$task->team_id = currentTeam()->id;
switch ($this->type) {
@@ -130,5 +135,6 @@ public function clear()
$this->command = '';
$this->frequency = '';
$this->container = '';
+ $this->timeout = 300;
}
}
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index c8d07ae36..088de0a76 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -40,6 +40,9 @@ class Show extends Component
#[Validate(['string', 'nullable'])]
public ?string $container = null;
+ #[Validate(['integer', 'required', 'min:60', 'max:3600'])]
+ public $timeout = 300;
+
#[Locked]
public ?string $application_uuid;
@@ -99,6 +102,7 @@ public function syncData(bool $toModel = false)
$this->task->command = str($this->command)->trim()->value();
$this->task->frequency = str($this->frequency)->trim()->value();
$this->task->container = str($this->container)->trim()->value();
+ $this->task->timeout = (int) $this->timeout;
$this->task->save();
} else {
$this->isEnabled = $this->task->enabled;
@@ -106,6 +110,7 @@ public function syncData(bool $toModel = false)
$this->command = $this->task->command;
$this->frequency = $this->task->frequency;
$this->container = $this->task->container;
+ $this->timeout = $this->task->timeout ?? 300;
}
}
diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php
index 6baa54672..4e3481912 100644
--- a/app/Livewire/Server/Navbar.php
+++ b/app/Livewire/Server/Navbar.php
@@ -5,7 +5,8 @@
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
-use App\Jobs\RestartProxyJob;
+use App\Enums\ProxyTypes;
+use App\Jobs\CheckTraefikVersionForServerJob;
use App\Models\Server;
use App\Services\ProxyDashboardCacheService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -61,7 +62,18 @@ public function restart()
{
try {
$this->authorize('manageProxy', $this->server);
- RestartProxyJob::dispatch($this->server);
+ StopProxy::run($this->server, restarting: true);
+
+ $this->server->proxy->force_stop = false;
+ $this->server->save();
+
+ $activity = StartProxy::run($this->server, force: true, restarting: true);
+ $this->dispatch('activityMonitor', $activity->id);
+
+ // Check Traefik version after restart to provide immediate feedback
+ if ($this->server->proxyType() === ProxyTypes::TRAEFIK->value) {
+ CheckTraefikVersionForServerJob::dispatch($this->server, get_traefik_versions());
+ }
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -118,19 +130,25 @@ public function checkProxyStatus()
public function showNotification()
{
+ $previousStatus = $this->proxyStatus;
$this->server->refresh();
$this->proxyStatus = $this->server->proxy->status ?? 'unknown';
switch ($this->proxyStatus) {
case 'running':
$this->loadProxyConfiguration();
- $this->dispatch('success', 'Proxy is running.');
- break;
- case 'restarting':
- $this->dispatch('info', 'Initiating proxy restart.');
+ // Only show "Proxy is running" notification when transitioning from a stopped/error state
+ // Don't show during normal start/restart flows (starting, restarting, stopping)
+ if (in_array($previousStatus, ['exited', 'stopped', 'unknown', null])) {
+ $this->dispatch('success', 'Proxy is running.');
+ }
break;
case 'exited':
- $this->dispatch('info', 'Proxy has exited.');
+ // Only show "Proxy has exited" notification when transitioning from running state
+ // Don't show during normal stop/restart flows (stopping, restarting)
+ if (in_array($previousStatus, ['running'])) {
+ $this->dispatch('info', 'Proxy has exited.');
+ }
break;
case 'stopping':
$this->dispatch('info', 'Proxy is stopping.');
@@ -154,6 +172,22 @@ public function refreshServer()
$this->server->load('settings');
}
+ /**
+ * Check if Traefik has any outdated version info (patch or minor upgrade).
+ * This shows a warning indicator in the navbar.
+ */
+ public function getHasTraefikOutdatedProperty(): bool
+ {
+ if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value) {
+ return false;
+ }
+
+ // Check if server has outdated info stored
+ $outdatedInfo = $this->server->traefik_outdated_info;
+
+ return ! empty($outdatedInfo) && isset($outdatedInfo['type']);
+ }
+
public function render()
{
return view('livewire.server.navbar');
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index bc7e9bde4..c92f73f17 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -4,6 +4,7 @@
use App\Actions\Proxy\GetProxyConfiguration;
use App\Actions\Proxy\SaveProxyConfiguration;
+use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
@@ -24,6 +25,12 @@ class Proxy extends Component
public bool $generateExactLabels = false;
+ /**
+ * Cache the versions.json file data in memory for this component instance.
+ * This avoids multiple file reads during a single request/render cycle.
+ */
+ protected ?array $cachedVersionsFile = null;
+
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -55,6 +62,34 @@ private function syncData(bool $toModel = false): void
}
}
+ /**
+ * Get Traefik versions from cached data with in-memory optimization.
+ * Returns array like: ['v3.5' => '3.5.6', 'v3.6' => '3.6.2']
+ *
+ * This method adds an in-memory cache layer on top of the global
+ * get_traefik_versions() helper to avoid multiple calls during
+ * a single component lifecycle/render.
+ */
+ protected function getTraefikVersions(): ?array
+ {
+ // In-memory cache for this component instance (per-request)
+ if ($this->cachedVersionsFile !== null) {
+ return data_get($this->cachedVersionsFile, 'traefik');
+ }
+
+ // Load from global cached helper (Redis + filesystem)
+ $versionsData = get_versions_data();
+ $this->cachedVersionsFile = $versionsData;
+
+ if (! $versionsData) {
+ return null;
+ }
+
+ $traefikVersions = data_get($versionsData, 'traefik');
+
+ return is_array($traefikVersions) ? $traefikVersions : null;
+ }
+
public function getConfigurationFilePathProperty()
{
return $this->server->proxyPath().'docker-compose.yml';
@@ -144,4 +179,131 @@ public function loadProxyConfiguration()
return handleError($e, $this);
}
}
+
+ /**
+ * Get the latest Traefik version for this server's current branch.
+ *
+ * This compares the server's detected version against available versions
+ * in versions.json to determine the latest patch for the current branch,
+ * or the newest available version if no current version is detected.
+ */
+ public function getLatestTraefikVersionProperty(): ?string
+ {
+ try {
+ $traefikVersions = $this->getTraefikVersions();
+
+ if (! $traefikVersions) {
+ return null;
+ }
+
+ // Get this server's current version
+ $currentVersion = $this->server->detected_traefik_version;
+
+ // If we have a current version, try to find matching branch
+ if ($currentVersion && $currentVersion !== 'latest') {
+ $current = ltrim($currentVersion, 'v');
+ if (preg_match('/^(\d+\.\d+)/', $current, $matches)) {
+ $branch = "v{$matches[1]}";
+ if (isset($traefikVersions[$branch])) {
+ $version = $traefikVersions[$branch];
+
+ return str_starts_with($version, 'v') ? $version : "v{$version}";
+ }
+ }
+ }
+
+ // Return the newest available version
+ $newestVersion = collect($traefikVersions)
+ ->map(fn ($v) => ltrim($v, 'v'))
+ ->sortBy(fn ($v) => $v, SORT_NATURAL)
+ ->last();
+
+ return $newestVersion ? "v{$newestVersion}" : null;
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
+
+ public function getIsTraefikOutdatedProperty(): bool
+ {
+ if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value) {
+ return false;
+ }
+
+ $currentVersion = $this->server->detected_traefik_version;
+ if (! $currentVersion || $currentVersion === 'latest') {
+ return false;
+ }
+
+ $latestVersion = $this->latestTraefikVersion;
+ if (! $latestVersion) {
+ return false;
+ }
+
+ // Compare versions (strip 'v' prefix)
+ $current = ltrim($currentVersion, 'v');
+ $latest = ltrim($latestVersion, 'v');
+
+ return version_compare($current, $latest, '<');
+ }
+
+ /**
+ * Check if a newer Traefik branch (minor version) is available for this server.
+ * Returns the branch identifier (e.g., "v3.6") if a newer branch exists.
+ */
+ public function getNewerTraefikBranchAvailableProperty(): ?string
+ {
+ try {
+ if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value) {
+ return null;
+ }
+
+ // Get this server's current version
+ $currentVersion = $this->server->detected_traefik_version;
+ if (! $currentVersion || $currentVersion === 'latest') {
+ return null;
+ }
+
+ // Check if we have outdated info stored for this server (faster than computing)
+ $outdatedInfo = $this->server->traefik_outdated_info;
+ if ($outdatedInfo && isset($outdatedInfo['type']) && $outdatedInfo['type'] === 'minor_upgrade') {
+ // Use the upgrade_target field if available (e.g., "v3.6")
+ if (isset($outdatedInfo['upgrade_target'])) {
+ return str_starts_with($outdatedInfo['upgrade_target'], 'v')
+ ? $outdatedInfo['upgrade_target']
+ : "v{$outdatedInfo['upgrade_target']}";
+ }
+ }
+
+ // Fallback: compute from cached versions data
+ $traefikVersions = $this->getTraefikVersions();
+
+ if (! $traefikVersions) {
+ return null;
+ }
+
+ // Extract current branch (e.g., "3.5" from "3.5.6")
+ $current = ltrim($currentVersion, 'v');
+ if (! preg_match('/^(\d+\.\d+)/', $current, $matches)) {
+ return null;
+ }
+
+ $currentBranch = $matches[1];
+
+ // Find the newest branch that's greater than current
+ $newestBranch = null;
+ foreach ($traefikVersions as $branch => $version) {
+ $branchNum = ltrim($branch, 'v');
+ if (version_compare($branchNum, $currentBranch, '>')) {
+ if (! $newestBranch || version_compare($branchNum, $newestBranch, '>')) {
+ $newestBranch = $branchNum;
+ }
+ }
+ }
+
+ return $newestBranch ? "v{$newestBranch}" : null;
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
}
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index bbd7f3dd9..c2dcd877b 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -25,6 +25,8 @@ class ValidateAndInstall extends Component
public $supported_os_type = null;
+ public $prerequisites_installed = null;
+
public $docker_installed = null;
public $docker_compose_installed = null;
@@ -33,12 +35,15 @@ class ValidateAndInstall extends Component
public $error = null;
+ public string $installationStep = 'Prerequisites';
+
public bool $ask = false;
protected $listeners = [
'init',
'validateConnection',
'validateOS',
+ 'validatePrerequisites',
'validateDockerEngine',
'validateDockerVersion',
'refresh' => '$refresh',
@@ -48,6 +53,7 @@ public function init(int $data = 0)
{
$this->uptime = null;
$this->supported_os_type = null;
+ $this->prerequisites_installed = null;
$this->docker_installed = null;
$this->docker_version = null;
$this->docker_compose_installed = null;
@@ -69,6 +75,7 @@ public function retry()
$this->authorize('update', $this->server);
$this->uptime = null;
$this->supported_os_type = null;
+ $this->prerequisites_installed = null;
$this->docker_installed = null;
$this->docker_compose_installed = null;
$this->docker_version = null;
@@ -103,6 +110,43 @@ public function validateOS()
return;
}
+ $this->dispatch('validatePrerequisites');
+ }
+
+ public function validatePrerequisites()
+ {
+ $validationResult = $this->server->validatePrerequisites();
+ $this->prerequisites_installed = $validationResult['success'];
+ if (! $validationResult['success']) {
+ if ($this->install) {
+ if ($this->number_of_tries == $this->max_tries) {
+ $missingCommands = implode(', ', $validationResult['missing']);
+ $this->error = "Prerequisites ({$missingCommands}) could not be installed. Please install them manually before continuing.";
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
+
+ return;
+ } else {
+ if ($this->number_of_tries <= $this->max_tries) {
+ $this->installationStep = 'Prerequisites';
+ $activity = $this->server->installPrerequisites();
+ $this->number_of_tries++;
+ $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs");
+ }
+
+ return;
+ }
+ } else {
+ $missingCommands = implode(', ', $validationResult['missing']);
+ $this->error = "Prerequisites ({$missingCommands}) are not installed. Please install them before continuing.";
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
+
+ return;
+ }
+ }
$this->dispatch('validateDockerEngine');
}
@@ -121,9 +165,10 @@ public function validateDockerEngine()
return;
} else {
if ($this->number_of_tries <= $this->max_tries) {
+ $this->installationStep = 'Docker';
$activity = $this->server->installDocker();
$this->number_of_tries++;
- $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries);
+ $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs");
}
return;
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index 13d690352..7a96eabb2 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -35,12 +35,17 @@ class Index extends Component
#[Validate('required|string|timezone')]
public string $instance_timezone;
+ #[Validate('nullable|string|max:50')]
+ public ?string $dev_helper_version = null;
+
public array $domainConflicts = [];
public bool $showDomainConflictModal = false;
public bool $forceSaveDomains = false;
+ public $buildActivityId = null;
+
public function render()
{
return view('livewire.settings.index');
@@ -60,6 +65,7 @@ public function mount()
$this->public_ipv4 = $this->settings->public_ipv4;
$this->public_ipv6 = $this->settings->public_ipv6;
$this->instance_timezone = $this->settings->instance_timezone;
+ $this->dev_helper_version = $this->settings->dev_helper_version;
}
#[Computed]
@@ -81,6 +87,7 @@ public function instantSave($isSave = true)
$this->settings->public_ipv4 = $this->public_ipv4;
$this->settings->public_ipv6 = $this->public_ipv6;
$this->settings->instance_timezone = $this->instance_timezone;
+ $this->settings->dev_helper_version = $this->dev_helper_version;
if ($isSave) {
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
@@ -146,4 +153,37 @@ public function submit()
return handleError($e, $this);
}
}
+
+ public function buildHelperImage()
+ {
+ try {
+ if (! isDev()) {
+ $this->dispatch('error', 'Building helper image is only available in development mode.');
+
+ return;
+ }
+
+ $version = $this->dev_helper_version ?: config('constants.coolify.helper_version');
+ if (empty($version)) {
+ $this->dispatch('error', 'Please specify a version to build.');
+
+ return;
+ }
+
+ $buildCommand = "docker build -t ghcr.io/coollabsio/coolify-helper:{$version} -f docker/coolify-helper/Dockerfile .";
+
+ $activity = remote_process(
+ command: [$buildCommand],
+ server: $this->server,
+ type: 'build-helper-image'
+ );
+
+ $this->buildActivityId = $activity->id;
+ $this->dispatch('activityMonitor', $activity->id);
+
+ $this->dispatch('success', "Building coolify-helper:{$version}...");
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
+ }
}
diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php
index d97550693..d101d7b58 100644
--- a/app/Livewire/Storage/Form.php
+++ b/app/Livewire/Storage/Form.php
@@ -120,9 +120,16 @@ public function testConnection()
$this->storage->testConnection(shouldSave: true);
+ // Update component property to reflect the new validation status
+ $this->isUsable = $this->storage->is_usable;
+
return $this->dispatch('success', 'Connection is working.', 'Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) {
- $this->dispatch('error', 'Failed to create storage.', $e->getMessage());
+ // Refresh model and sync to get the latest state
+ $this->storage->refresh();
+ $this->isUsable = $this->storage->is_usable;
+
+ $this->dispatch('error', 'Failed to test connection.', $e->getMessage());
}
}
diff --git a/app/Livewire/Storage/Show.php b/app/Livewire/Storage/Show.php
index bdea9a3b0..fdf3d0d28 100644
--- a/app/Livewire/Storage/Show.php
+++ b/app/Livewire/Storage/Show.php
@@ -3,10 +3,13 @@
namespace App\Livewire\Storage;
use App\Models\S3Storage;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Show extends Component
{
+ use AuthorizesRequests;
+
public $storage = null;
public function mount()
@@ -15,6 +18,7 @@ public function mount()
if (! $this->storage) {
abort(404);
}
+ $this->authorize('view', $this->storage);
}
public function render()
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 615e35f68..821c69bca 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -121,6 +121,8 @@ class Application extends BaseModel
protected $casts = [
'http_basic_auth_password' => 'encrypted',
+ 'restart_count' => 'integer',
+ 'last_restart_at' => 'datetime',
];
protected static function booted()
@@ -174,6 +176,39 @@ protected static function booted()
if (count($payload) > 0) {
$application->forceFill($payload);
}
+
+ // Buildpack switching cleanup logic
+ if ($application->isDirty('build_pack')) {
+ $originalBuildPack = $application->getOriginal('build_pack');
+
+ // Clear Docker Compose specific data when switching away from dockercompose
+ if ($originalBuildPack === 'dockercompose') {
+ $application->docker_compose_domains = null;
+ $application->docker_compose_raw = null;
+
+ // Remove SERVICE_FQDN_* and SERVICE_URL_* environment variables
+ $application->environment_variables()
+ ->where(function ($q) {
+ $q->where('key', 'LIKE', 'SERVICE_FQDN_%')
+ ->orWhere('key', 'LIKE', 'SERVICE_URL_%');
+ })
+ ->delete();
+ $application->environment_variables_preview()
+ ->where(function ($q) {
+ $q->where('key', 'LIKE', 'SERVICE_FQDN_%')
+ ->orWhere('key', 'LIKE', 'SERVICE_URL_%');
+ })
+ ->delete();
+ }
+
+ // Clear Dockerfile specific data when switching away from dockerfile
+ if ($originalBuildPack === 'dockerfile') {
+ $application->dockerfile = null;
+ $application->dockerfile_location = null;
+ $application->dockerfile_target_build = null;
+ $application->custom_healthcheck_found = false;
+ }
+ }
});
static::created(function ($application) {
ApplicationSetting::create([
@@ -634,21 +669,23 @@ protected function serverStatus(): Attribute
{
return Attribute::make(
get: function () {
- if (! $this->relationLoaded('additional_servers') || $this->additional_servers->count() === 0) {
- return $this->destination?->server?->isFunctional() ?? false;
+ // Check main server infrastructure health
+ $main_server_functional = $this->destination?->server?->isFunctional() ?? false;
+
+ if (! $main_server_functional) {
+ return false;
}
- $additional_servers_status = $this->additional_servers->pluck('pivot.status');
- $main_server_status = $this->destination?->server?->isFunctional() ?? false;
-
- foreach ($additional_servers_status as $status) {
- $server_status = str($status)->before(':')->value();
- if ($server_status !== 'running') {
- return false;
+ // Check additional servers infrastructure health (not container status!)
+ if ($this->relationLoaded('additional_servers') && $this->additional_servers->count() > 0) {
+ foreach ($this->additional_servers as $server) {
+ if (! $server->isFunctional()) {
+ return false; // Real server infrastructure problem
+ }
}
}
- return $main_server_status;
+ return true;
}
);
}
@@ -772,6 +809,24 @@ public function main_port()
return $this->settings->is_static ? [80] : $this->ports_exposes_array;
}
+ public function detectPortFromEnvironment(?bool $isPreview = false): ?int
+ {
+ $envVars = $isPreview
+ ? $this->environment_variables_preview
+ : $this->environment_variables;
+
+ $portVar = $envVars->firstWhere('key', 'PORT');
+
+ if ($portVar && $portVar->real_value) {
+ $portValue = trim($portVar->real_value);
+ if (is_numeric($portValue)) {
+ return (int) $portValue;
+ }
+ }
+
+ return null;
+ }
+
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php
index 4b03c69e1..26cb937b3 100644
--- a/app/Models/ApplicationSetting.php
+++ b/app/Models/ApplicationSetting.php
@@ -7,8 +7,14 @@
class ApplicationSetting extends Model
{
- protected $cast = [
+ protected $casts = [
'is_static' => 'boolean',
+ 'is_spa' => 'boolean',
+ 'is_build_server_enabled' => 'boolean',
+ 'is_preserve_repository_enabled' => 'boolean',
+ 'is_container_label_escape_enabled' => 'boolean',
+ 'is_container_label_readonly_enabled' => 'boolean',
+ 'use_build_secrets' => 'boolean',
'is_auto_deploy_enabled' => 'boolean',
'is_force_https_enabled' => 'boolean',
'is_debug_enabled' => 'boolean',
diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php
index 34adfc997..23e1f0f12 100644
--- a/app/Models/DiscordNotificationSettings.php
+++ b/app/Models/DiscordNotificationSettings.php
@@ -29,6 +29,7 @@ class DiscordNotificationSettings extends Model
'server_reachable_discord_notifications',
'server_unreachable_discord_notifications',
'server_patch_discord_notifications',
+ 'traefik_outdated_discord_notifications',
'discord_ping_enabled',
];
@@ -48,6 +49,7 @@ class DiscordNotificationSettings extends Model
'server_reachable_discord_notifications' => 'boolean',
'server_unreachable_discord_notifications' => 'boolean',
'server_patch_discord_notifications' => 'boolean',
+ 'traefik_outdated_discord_notifications' => 'boolean',
'discord_ping_enabled' => 'boolean',
];
diff --git a/app/Models/EmailNotificationSettings.php b/app/Models/EmailNotificationSettings.php
index 39617b4cf..ee31a49b6 100644
--- a/app/Models/EmailNotificationSettings.php
+++ b/app/Models/EmailNotificationSettings.php
@@ -36,6 +36,7 @@ class EmailNotificationSettings extends Model
'scheduled_task_failure_email_notifications',
'server_disk_usage_email_notifications',
'server_patch_email_notifications',
+ 'traefik_outdated_email_notifications',
];
protected $casts = [
@@ -63,6 +64,7 @@ class EmailNotificationSettings extends Model
'scheduled_task_failure_email_notifications' => 'boolean',
'server_disk_usage_email_notifications' => 'boolean',
'server_patch_email_notifications' => 'boolean',
+ 'traefik_outdated_email_notifications' => 'boolean',
];
public function team()
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index cd1c05de4..62b576012 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -2,7 +2,6 @@
namespace App\Models;
-use App\Jobs\PullHelperImageJob;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Spatie\Url\Url;
@@ -35,14 +34,6 @@ class InstanceSettings extends Model
protected static function booted(): void
{
static::updated(function ($settings) {
- if ($settings->wasChanged('helper_version')) {
- Server::chunkById(100, function ($servers) {
- foreach ($servers as $server) {
- PullHelperImageJob::dispatch($server);
- }
- });
- }
-
// Clear trusted hosts cache when FQDN changes
if ($settings->wasChanged('fqdn')) {
\Cache::forget('instance_settings_fqdn_host');
diff --git a/app/Models/PushoverNotificationSettings.php b/app/Models/PushoverNotificationSettings.php
index a75fd71d7..189d05dd4 100644
--- a/app/Models/PushoverNotificationSettings.php
+++ b/app/Models/PushoverNotificationSettings.php
@@ -30,6 +30,7 @@ class PushoverNotificationSettings extends Model
'server_reachable_pushover_notifications',
'server_unreachable_pushover_notifications',
'server_patch_pushover_notifications',
+ 'traefik_outdated_pushover_notifications',
];
protected $casts = [
@@ -49,6 +50,7 @@ class PushoverNotificationSettings extends Model
'server_reachable_pushover_notifications' => 'boolean',
'server_unreachable_pushover_notifications' => 'boolean',
'server_patch_pushover_notifications' => 'boolean',
+ 'traefik_outdated_pushover_notifications' => 'boolean',
];
public function team()
diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php
index de27bbca6..47652eb35 100644
--- a/app/Models/S3Storage.php
+++ b/app/Models/S3Storage.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\HasSafeStringAttribute;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Storage;
@@ -41,6 +42,19 @@ public function awsUrl()
return "{$this->endpoint}/{$this->bucket}";
}
+ protected function path(): Attribute
+ {
+ return Attribute::make(
+ set: function (?string $value) {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ return str($value)->trim()->start('/')->value();
+ }
+ );
+ }
+
public function testConnection(bool $shouldSave = false)
{
try {
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 06903ffb6..bada0b7a5 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -12,6 +12,14 @@ class ScheduledTask extends BaseModel
protected $guarded = [];
+ protected function casts(): array
+ {
+ return [
+ 'enabled' => 'boolean',
+ 'timeout' => 'integer',
+ ];
+ }
+
public function service()
{
return $this->belongsTo(Service::class);
diff --git a/app/Models/ScheduledTaskExecution.php b/app/Models/ScheduledTaskExecution.php
index de13fefb0..02fd6917a 100644
--- a/app/Models/ScheduledTaskExecution.php
+++ b/app/Models/ScheduledTaskExecution.php
@@ -8,6 +8,16 @@ class ScheduledTaskExecution extends BaseModel
{
protected $guarded = [];
+ protected function casts(): array
+ {
+ return [
+ 'started_at' => 'datetime',
+ 'finished_at' => 'datetime',
+ 'retry_count' => 'integer',
+ 'duration' => 'decimal:2',
+ ];
+ }
+
public function scheduledTask(): BelongsTo
{
return $this->belongsTo(ScheduledTask::class);
diff --git a/app/Models/Server.php b/app/Models/Server.php
index e39526949..8b153c8ac 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -4,7 +4,9 @@
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallDocker;
+use App\Actions\Server\InstallPrerequisites;
use App\Actions\Server\StartSentinel;
+use App\Actions\Server\ValidatePrerequisites;
use App\Enums\ProxyTypes;
use App\Events\ServerReachabilityChanged;
use App\Helpers\SslHelper;
@@ -31,6 +33,51 @@
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
+/**
+ * @property array{
+ * current: string,
+ * latest: string,
+ * type: 'patch_update'|'minor_upgrade',
+ * checked_at: string,
+ * newer_branch_target?: string,
+ * newer_branch_latest?: string,
+ * upgrade_target?: string
+ * }|null $traefik_outdated_info Traefik version tracking information.
+ *
+ * This JSON column stores information about outdated Traefik proxy versions on this server.
+ * The structure varies depending on the type of update available:
+ *
+ * **For patch updates** (e.g., 3.5.0 → 3.5.2):
+ * ```php
+ * [
+ * 'current' => '3.5.0', // Current version (without 'v' prefix)
+ * 'latest' => '3.5.2', // Latest patch version available
+ * 'type' => 'patch_update', // Update type identifier
+ * 'checked_at' => '2025-11-14T10:00:00Z', // ISO8601 timestamp
+ * 'newer_branch_target' => 'v3.6', // (Optional) Available major/minor version
+ * 'newer_branch_latest' => '3.6.2' // (Optional) Latest version in that branch
+ * ]
+ * ```
+ *
+ * **For minor/major upgrades** (e.g., 3.5.6 → 3.6.2):
+ * ```php
+ * [
+ * 'current' => '3.5.6', // Current version
+ * 'latest' => '3.6.2', // Latest version in target branch
+ * 'type' => 'minor_upgrade', // Update type identifier
+ * 'upgrade_target' => 'v3.6', // Target branch (with 'v' prefix)
+ * 'checked_at' => '2025-11-14T10:00:00Z' // ISO8601 timestamp
+ * ]
+ * ```
+ *
+ * **Null value**: Set to null when:
+ * - Server is fully up-to-date with the latest version
+ * - Traefik image uses the 'latest' tag (no fixed version tracking)
+ * - No Traefik version detected on the server
+ *
+ * @see \App\Jobs\CheckTraefikVersionForServerJob Where this data is populated
+ * @see \App\Livewire\Server\Proxy Where this data is read and displayed
+ */
#[OA\Schema(
description: 'Server model',
type: 'object',
@@ -142,6 +189,7 @@ protected static function booted()
protected $casts = [
'proxy' => SchemalessAttributes::class,
+ 'traefik_outdated_info' => 'array',
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
@@ -167,6 +215,8 @@ protected static function booted()
'hetzner_server_id',
'hetzner_server_status',
'is_validating',
+ 'detected_traefik_version',
+ 'traefik_outdated_info',
];
protected $guarded = [];
@@ -522,6 +572,11 @@ public function scopeWithProxy(): Builder
return $this->proxy->modelScope();
}
+ public function scopeWhereProxyType(Builder $query, string $proxyType): Builder
+ {
+ return $query->where('proxy->type', $proxyType);
+ }
+
public function isLocalhost()
{
return $this->ip === 'host.docker.internal' || $this->id === 0;
@@ -1131,6 +1186,21 @@ public function installDocker()
return InstallDocker::run($this);
}
+ /**
+ * Validate that required commands are available on the server.
+ *
+ * @return array{success: bool, missing: array, found: array}
+ */
+ public function validatePrerequisites(): array
+ {
+ return ValidatePrerequisites::run($this);
+ }
+
+ public function installPrerequisites()
+ {
+ return InstallPrerequisites::run($this);
+ }
+
public function validateDockerEngine($throwError = false)
{
$dockerBinary = instant_remote_process(['command -v docker'], $this, false, no_sudo: true);
diff --git a/app/Models/Service.php b/app/Models/Service.php
index c4b8623e0..2f8a64464 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\ProcessStatus;
+use App\Services\ContainerStatusAggregator;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -173,6 +174,21 @@ public function deleteConnectedNetworks()
instant_remote_process(["docker network rm {$this->uuid}"], $server, false);
}
+ /**
+ * Calculate the service's aggregate status from its applications and databases.
+ *
+ * This method aggregates status from Eloquent model relationships (not Docker containers).
+ * It differs from the CalculatesExcludedStatus trait which works with Docker container objects
+ * during container inspection. This accessor runs on-demand for UI display and works with
+ * already-stored status strings from the database.
+ *
+ * Status format: "{status}:{health}" or "{status}:{health}:excluded"
+ * - Status values: running, exited, degraded, starting, paused, restarting
+ * - Health values: healthy, unhealthy, unknown
+ * - :excluded suffix: Indicates all containers are excluded from health monitoring
+ *
+ * @return string The aggregate status in format "status:health" or "status:health:excluded"
+ */
public function getStatusAttribute()
{
if ($this->isStarting()) {
@@ -182,71 +198,102 @@ public function getStatusAttribute()
$applications = $this->applications;
$databases = $this->databases;
- $complexStatus = null;
- $complexHealth = null;
+ [$complexStatus, $complexHealth, $hasNonExcluded] = $this->aggregateResourceStatuses(
+ $applications,
+ $databases,
+ excludedOnly: false
+ );
- foreach ($applications as $application) {
- if ($application->exclude_from_status) {
- continue;
+ // If all services are excluded from status checks, calculate status from excluded containers
+ // but mark it with :excluded to indicate monitoring is disabled
+ if (! $hasNonExcluded && ($complexStatus === null && $complexHealth === null)) {
+ [$excludedStatus, $excludedHealth] = $this->aggregateResourceStatuses(
+ $applications,
+ $databases,
+ excludedOnly: true
+ );
+
+ // Return status with :excluded suffix to indicate monitoring is disabled
+ if ($excludedStatus && $excludedHealth) {
+ return "{$excludedStatus}:{$excludedHealth}:excluded";
}
- $status = str($application->status)->before('(')->trim();
- $health = str($application->status)->between('(', ')')->trim();
- if ($complexStatus === 'degraded') {
- continue;
- }
- if ($status->startsWith('running')) {
- if ($complexStatus === 'exited') {
- $complexStatus = 'degraded';
- } else {
- $complexStatus = 'running';
- }
- } elseif ($status->startsWith('restarting')) {
- $complexStatus = 'degraded';
- } elseif ($status->startsWith('exited')) {
- $complexStatus = 'exited';
- }
- if ($health->value() === 'healthy') {
- if ($complexHealth === 'unhealthy') {
- continue;
- }
- $complexHealth = 'healthy';
- } else {
- $complexHealth = 'unhealthy';
+
+ // If no status was calculated at all (no containers exist), return unknown
+ if ($excludedStatus === null && $excludedHealth === null) {
+ return 'unknown:unknown:excluded';
}
+
+ return 'exited';
}
- foreach ($databases as $database) {
- if ($database->exclude_from_status) {
- continue;
- }
- $status = str($database->status)->before('(')->trim();
- $health = str($database->status)->between('(', ')')->trim();
- if ($complexStatus === 'degraded') {
- continue;
- }
- if ($status->startsWith('running')) {
- if ($complexStatus === 'exited') {
- $complexStatus = 'degraded';
- } else {
- $complexStatus = 'running';
- }
- } elseif ($status->startsWith('restarting')) {
- $complexStatus = 'degraded';
- } elseif ($status->startsWith('exited')) {
- $complexStatus = 'exited';
- }
- if ($health->value() === 'healthy') {
- if ($complexHealth === 'unhealthy') {
- continue;
- }
- $complexHealth = 'healthy';
- } else {
- $complexHealth = 'unhealthy';
- }
+
+ // If health is null/empty, return just the status without trailing colon
+ if ($complexHealth === null || $complexHealth === '') {
+ return $complexStatus;
}
return "{$complexStatus}:{$complexHealth}";
}
+ /**
+ * Aggregate status and health from collections of applications and databases.
+ *
+ * This helper method consolidates status aggregation logic using ContainerStatusAggregator.
+ * It processes container status strings stored in the database (not live Docker data).
+ *
+ * @param \Illuminate\Database\Eloquent\Collection $applications Collection of Application models
+ * @param \Illuminate\Database\Eloquent\Collection $databases Collection of Database models
+ * @param bool $excludedOnly If true, only process excluded containers; if false, only process non-excluded
+ * @return array{0: string|null, 1: string|null, 2?: bool} [status, health, hasNonExcluded (only when excludedOnly=false)]
+ */
+ private function aggregateResourceStatuses($applications, $databases, bool $excludedOnly = false): array
+ {
+ $hasNonExcluded = false;
+ $statusStrings = collect();
+
+ // Process both applications and databases using the same logic
+ $resources = $applications->concat($databases);
+
+ foreach ($resources as $resource) {
+ $isExcluded = $resource->exclude_from_status || str($resource->status)->contains(':excluded');
+
+ // Filter based on excludedOnly flag
+ if ($excludedOnly && ! $isExcluded) {
+ continue;
+ }
+ if (! $excludedOnly && $isExcluded) {
+ continue;
+ }
+
+ if (! $excludedOnly) {
+ $hasNonExcluded = true;
+ }
+
+ // Strip :excluded suffix before aggregation (it's in the 3rd part of "status:health:excluded")
+ $status = str($resource->status)->before(':excluded')->toString();
+ $statusStrings->push($status);
+ }
+
+ // If no status strings collected, return nulls
+ if ($statusStrings->isEmpty()) {
+ return $excludedOnly ? [null, null] : [null, null, $hasNonExcluded];
+ }
+
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
+ $aggregatedStatus = $aggregator->aggregateFromStrings($statusStrings);
+
+ // Parse the aggregated "status:health" string
+ $parts = explode(':', $aggregatedStatus);
+ $status = $parts[0] ?? null;
+ $health = $parts[1] ?? null;
+
+ if ($excludedOnly) {
+ return [$status, $health];
+ }
+
+ return [$status, $health, $hasNonExcluded];
+ }
+
public function extraFields()
{
$fields = collect([]);
@@ -1184,6 +1231,31 @@ public function documentation()
return data_get($service, 'documentation', config('constants.urls.docs'));
}
+ /**
+ * Get the required port for this service from the template definition.
+ */
+ public function getRequiredPort(): ?int
+ {
+ try {
+ $services = get_service_templates();
+ $serviceName = str($this->name)->beforeLast('-')->value();
+ $service = data_get($services, $serviceName, []);
+ $port = data_get($service, 'port');
+
+ return $port ? (int) $port : null;
+ } catch (\Throwable) {
+ return null;
+ }
+ }
+
+ /**
+ * Check if this service requires a port to function correctly.
+ */
+ public function requiresPort(): bool
+ {
+ return $this->getRequiredPort() !== null;
+ }
+
public function applications()
{
return $this->hasMany(ServiceApplication::class);
@@ -1262,6 +1334,11 @@ public function workdir()
public function saveComposeConfigs()
{
+ // Guard against null or empty docker_compose
+ if (! $this->docker_compose) {
+ return;
+ }
+
$workdir = $this->workdir();
instant_remote_process([
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index 5cafc9042..aef74b402 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -109,6 +109,11 @@ public function fileStorages()
return $this->morphMany(LocalFileVolume::class, 'resource');
}
+ public function environment_variables()
+ {
+ return $this->morphMany(EnvironmentVariable::class, 'resourceable');
+ }
+
public function fqdns(): Attribute
{
return Attribute::make(
@@ -118,6 +123,53 @@ public function fqdns(): Attribute
);
}
+ /**
+ * Extract port number from a given FQDN URL.
+ * Returns null if no port is specified.
+ */
+ public static function extractPortFromUrl(string $url): ?int
+ {
+ try {
+ // Ensure URL has a scheme for proper parsing
+ if (! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://')) {
+ $url = 'http://'.$url;
+ }
+
+ $parsed = parse_url($url);
+ $port = $parsed['port'] ?? null;
+
+ return $port ? (int) $port : null;
+ } catch (\Throwable) {
+ return null;
+ }
+ }
+
+ /**
+ * Check if all FQDNs have a port specified.
+ */
+ public function allFqdnsHavePort(): bool
+ {
+ if (is_null($this->fqdn) || $this->fqdn === '') {
+ return false;
+ }
+
+ $fqdns = explode(',', $this->fqdn);
+
+ foreach ($fqdns as $fqdn) {
+ $fqdn = trim($fqdn);
+ if (empty($fqdn)) {
+ continue;
+ }
+
+ $port = self::extractPortFromUrl($fqdn);
+ if ($port === null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public function getFilesFromServer(bool $isInit = false)
{
getFilesystemVolumesFromServer($this, $isInit);
@@ -127,4 +179,78 @@ public function isBackupSolutionAvailable()
{
return false;
}
+
+ /**
+ * Get the required port for this service application.
+ * Extracts port from SERVICE_URL_* or SERVICE_FQDN_* environment variables
+ * stored at the Service level, filtering by normalized container name.
+ * Falls back to service-level port if no port-specific variable is found.
+ */
+ public function getRequiredPort(): ?int
+ {
+ try {
+ // Parse the Docker Compose to find SERVICE_URL/SERVICE_FQDN variables DIRECTLY DECLARED
+ // for this specific service container (not just referenced from other containers)
+ $dockerComposeRaw = data_get($this->service, 'docker_compose_raw');
+ if (! $dockerComposeRaw) {
+ // Fall back to service-level port if no compose file
+ return $this->service->getRequiredPort();
+ }
+
+ $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
+ $serviceConfig = data_get($dockerCompose, "services.{$this->name}");
+ if (! $serviceConfig) {
+ return $this->service->getRequiredPort();
+ }
+
+ $environment = data_get($serviceConfig, 'environment', []);
+
+ // Extract SERVICE_URL and SERVICE_FQDN variables DIRECTLY DECLARED in this service's environment
+ // (not variables that are merely referenced with ${VAR} syntax)
+ $portFound = null;
+ foreach ($environment as $key => $value) {
+ if (is_int($key) && is_string($value)) {
+ // List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value"
+ // Extract variable name (before '=' if present)
+ $envVarName = str($value)->before('=')->trim();
+
+ // Only process direct declarations
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ // Parse to check if it has a port suffix
+ $parsed = parseServiceEnvironmentVariable($envVarName->value());
+ if ($parsed['has_port'] && $parsed['port']) {
+ // Found a port-specific variable for this service
+ $portFound = (int) $parsed['port'];
+ break;
+ }
+ }
+ } elseif (is_string($key)) {
+ // Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost"
+ $envVarName = str($key);
+
+ // Only process direct declarations
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ // Parse to check if it has a port suffix
+ $parsed = parseServiceEnvironmentVariable($envVarName->value());
+ if ($parsed['has_port'] && $parsed['port']) {
+ // Found a port-specific variable for this service
+ $portFound = (int) $parsed['port'];
+ break;
+ }
+ }
+ }
+ }
+
+ // If a port was found in the template, return it
+ if ($portFound !== null) {
+ return $portFound;
+ }
+
+ // No port-specific variables found for this service, return null
+ // (DO NOT fall back to service-level port, as that applies to all services)
+ return null;
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
}
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index d595721d8..3a249059c 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -84,6 +84,10 @@ public function databaseType()
$image = str($this->image)->before(':');
if ($image->contains('supabase/postgres')) {
$finalImage = 'supabase/postgres';
+ } elseif ($image->contains('timescale')) {
+ $finalImage = 'postgresql';
+ } elseif ($image->contains('pgvector')) {
+ $finalImage = 'postgresql';
} elseif ($image->contains('postgres') || $image->contains('postgis')) {
$finalImage = 'postgresql';
} else {
diff --git a/app/Models/SlackNotificationSettings.php b/app/Models/SlackNotificationSettings.php
index 2b52bfd5b..128b25221 100644
--- a/app/Models/SlackNotificationSettings.php
+++ b/app/Models/SlackNotificationSettings.php
@@ -29,6 +29,7 @@ class SlackNotificationSettings extends Model
'server_reachable_slack_notifications',
'server_unreachable_slack_notifications',
'server_patch_slack_notifications',
+ 'traefik_outdated_slack_notifications',
];
protected $casts = [
@@ -47,6 +48,7 @@ class SlackNotificationSettings extends Model
'server_reachable_slack_notifications' => 'boolean',
'server_unreachable_slack_notifications' => 'boolean',
'server_patch_slack_notifications' => 'boolean',
+ 'traefik_outdated_slack_notifications' => 'boolean',
];
public function team()
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 6c30389ee..5cb186942 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -49,7 +49,9 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
protected static function booted()
{
static::created(function ($team) {
- $team->emailNotificationSettings()->create();
+ $team->emailNotificationSettings()->create([
+ 'use_instance_email_settings' => isDev(),
+ ]);
$team->discordNotificationSettings()->create();
$team->slackNotificationSettings()->create();
$team->telegramNotificationSettings()->create();
diff --git a/app/Models/TelegramNotificationSettings.php b/app/Models/TelegramNotificationSettings.php
index 94315ee30..73889910e 100644
--- a/app/Models/TelegramNotificationSettings.php
+++ b/app/Models/TelegramNotificationSettings.php
@@ -30,6 +30,7 @@ class TelegramNotificationSettings extends Model
'server_reachable_telegram_notifications',
'server_unreachable_telegram_notifications',
'server_patch_telegram_notifications',
+ 'traefik_outdated_telegram_notifications',
'telegram_notifications_deployment_success_thread_id',
'telegram_notifications_deployment_failure_thread_id',
@@ -43,6 +44,7 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_server_reachable_thread_id',
'telegram_notifications_server_unreachable_thread_id',
'telegram_notifications_server_patch_thread_id',
+ 'telegram_notifications_traefik_outdated_thread_id',
];
protected $casts = [
@@ -62,6 +64,7 @@ class TelegramNotificationSettings extends Model
'server_reachable_telegram_notifications' => 'boolean',
'server_unreachable_telegram_notifications' => 'boolean',
'server_patch_telegram_notifications' => 'boolean',
+ 'traefik_outdated_telegram_notifications' => 'boolean',
'telegram_notifications_deployment_success_thread_id' => 'encrypted',
'telegram_notifications_deployment_failure_thread_id' => 'encrypted',
@@ -75,6 +78,7 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_server_reachable_thread_id' => 'encrypted',
'telegram_notifications_server_unreachable_thread_id' => 'encrypted',
'telegram_notifications_server_patch_thread_id' => 'encrypted',
+ 'telegram_notifications_traefik_outdated_thread_id' => 'encrypted',
];
public function team()
diff --git a/app/Models/WebhookNotificationSettings.php b/app/Models/WebhookNotificationSettings.php
index 4ca89e0d3..731006181 100644
--- a/app/Models/WebhookNotificationSettings.php
+++ b/app/Models/WebhookNotificationSettings.php
@@ -24,11 +24,13 @@ class WebhookNotificationSettings extends Model
'backup_failure_webhook_notifications',
'scheduled_task_success_webhook_notifications',
'scheduled_task_failure_webhook_notifications',
- 'docker_cleanup_webhook_notifications',
+ 'docker_cleanup_success_webhook_notifications',
+ 'docker_cleanup_failure_webhook_notifications',
'server_disk_usage_webhook_notifications',
'server_reachable_webhook_notifications',
'server_unreachable_webhook_notifications',
'server_patch_webhook_notifications',
+ 'traefik_outdated_webhook_notifications',
];
protected function casts(): array
@@ -44,11 +46,13 @@ protected function casts(): array
'backup_failure_webhook_notifications' => 'boolean',
'scheduled_task_success_webhook_notifications' => 'boolean',
'scheduled_task_failure_webhook_notifications' => 'boolean',
- 'docker_cleanup_webhook_notifications' => 'boolean',
+ 'docker_cleanup_success_webhook_notifications' => 'boolean',
+ 'docker_cleanup_failure_webhook_notifications' => 'boolean',
'server_disk_usage_webhook_notifications' => 'boolean',
'server_reachable_webhook_notifications' => 'boolean',
'server_unreachable_webhook_notifications' => 'boolean',
'server_patch_webhook_notifications' => 'boolean',
+ 'traefik_outdated_webhook_notifications' => 'boolean',
];
}
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 245bd85f0..234bc37ad 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -101,6 +101,38 @@ public function send(SendsEmail $notifiable, Notification $notification): void
$mailer->send($email);
}
+ } catch (\Resend\Exceptions\ErrorException $e) {
+ // Map HTTP status codes to user-friendly messages
+ $userMessage = match ($e->getErrorCode()) {
+ 403 => 'Invalid Resend API key. Please verify your API key in the Resend dashboard and update it in settings.',
+ 401 => 'Your Resend API key has restricted permissions. Please use an API key with Full Access permissions.',
+ 429 => 'Resend rate limit exceeded. Please try again in a few minutes.',
+ 400 => 'Email validation failed: '.$e->getErrorMessage(),
+ default => 'Failed to send email via Resend: '.$e->getErrorMessage(),
+ };
+
+ // Log detailed error for admin debugging (redact sensitive data)
+ $emailSettings = $notifiable->emailNotificationSettings ?? instanceSettings();
+ data_set($emailSettings, 'smtp_password', '********');
+ data_set($emailSettings, 'resend_api_key', '********');
+
+ send_internal_notification(sprintf(
+ "Resend Error\nStatus Code: %s\nMessage: %s\nNotification: %s\nEmail Settings:\n%s",
+ $e->getErrorCode(),
+ $e->getErrorMessage(),
+ get_class($notification),
+ json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
+ ));
+
+ // Don't report expected errors (invalid keys, validation) to Sentry
+ if (in_array($e->getErrorCode(), [403, 401, 400])) {
+ throw NonReportableException::fromException(new \Exception($userMessage, $e->getCode(), $e));
+ }
+
+ throw new \Exception($userMessage, $e->getCode(), $e);
+ } catch (\Resend\Exceptions\TransporterException $e) {
+ send_internal_notification("Resend Transport Error: {$e->getMessage()}");
+ throw new \Exception('Unable to connect to Resend API. Please check your internet connection and try again.');
} catch (\Throwable $e) {
// Check if this is a Resend domain verification error on cloud instances
if (isCloud() && str_contains($e->getMessage(), 'domain is not verified')) {
diff --git a/app/Notifications/Server/TraefikVersionOutdated.php b/app/Notifications/Server/TraefikVersionOutdated.php
new file mode 100644
index 000000000..09ef4257d
--- /dev/null
+++ b/app/Notifications/Server/TraefikVersionOutdated.php
@@ -0,0 +1,262 @@
+onQueue('high');
+ }
+
+ public function via(object $notifiable): array
+ {
+ return $notifiable->getEnabledChannels('traefik_outdated');
+ }
+
+ private function formatVersion(string $version): string
+ {
+ // Add 'v' prefix if not present for consistent display
+ return str_starts_with($version, 'v') ? $version : "v{$version}";
+ }
+
+ private function getUpgradeTarget(array $info): string
+ {
+ // For minor upgrades, use the upgrade_target field (e.g., "v3.6")
+ if (($info['type'] ?? 'patch_update') === 'minor_upgrade' && isset($info['upgrade_target'])) {
+ return $this->formatVersion($info['upgrade_target']);
+ }
+
+ // For patch updates, show the full version
+ return $this->formatVersion($info['latest'] ?? 'unknown');
+ }
+
+ public function toMail($notifiable = null): MailMessage
+ {
+ $mail = new MailMessage;
+ $count = $this->servers->count();
+
+ $mail->subject("Coolify: Traefik proxy outdated on {$count} server(s)");
+ $mail->view('emails.traefik-version-outdated', [
+ 'servers' => $this->servers,
+ 'count' => $count,
+ ]);
+
+ return $mail;
+ }
+
+ public function toDiscord(): DiscordMessage
+ {
+ $count = $this->servers->count();
+ $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
+ isset($s->outdatedInfo['newer_branch_target'])
+ );
+
+ $description = "**{$count} server(s)** running outdated Traefik proxy. Update recommended for security and features.\n\n";
+ $description .= "**Affected servers:**\n";
+
+ foreach ($this->servers as $server) {
+ $info = $server->outdatedInfo ?? [];
+ $current = $this->formatVersion($info['current'] ?? 'unknown');
+ $latest = $this->formatVersion($info['latest'] ?? 'unknown');
+ $upgradeTarget = $this->getUpgradeTarget($info);
+ $isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
+ $hasNewerBranch = isset($info['newer_branch_target']);
+
+ if ($isPatch && $hasNewerBranch) {
+ $newerBranchTarget = $info['newer_branch_target'];
+ $newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
+ $description .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
+ $description .= " ↳ Also available: {$newerBranchTarget} (latest patch: {$newerBranchLatest}) - new minor version\n";
+ } elseif ($isPatch) {
+ $description .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
+ } else {
+ $description .= "• {$server->name}: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
+ }
+ }
+
+ $description .= "\n⚠️ It is recommended to test before switching the production version.";
+
+ if ($hasUpgrades) {
+ $description .= "\n\n📖 **For minor version upgrades**: Read the Traefik changelog before upgrading to understand breaking changes and new features.";
+ }
+
+ return new DiscordMessage(
+ title: ':warning: Coolify: Traefik proxy outdated',
+ description: $description,
+ color: DiscordMessage::warningColor(),
+ );
+ }
+
+ public function toTelegram(): array
+ {
+ $count = $this->servers->count();
+ $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
+ isset($s->outdatedInfo['newer_branch_target'])
+ );
+
+ $message = "⚠️ Coolify: Traefik proxy outdated on {$count} server(s)!\n\n";
+ $message .= "Update recommended for security and features.\n";
+ $message .= "📊 Affected servers:\n";
+
+ foreach ($this->servers as $server) {
+ $info = $server->outdatedInfo ?? [];
+ $current = $this->formatVersion($info['current'] ?? 'unknown');
+ $latest = $this->formatVersion($info['latest'] ?? 'unknown');
+ $upgradeTarget = $this->getUpgradeTarget($info);
+ $isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
+ $hasNewerBranch = isset($info['newer_branch_target']);
+
+ if ($isPatch && $hasNewerBranch) {
+ $newerBranchTarget = $info['newer_branch_target'];
+ $newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
+ $message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
+ $message .= " ↳ Also available: {$newerBranchTarget} (latest patch: {$newerBranchLatest}) - new minor version\n";
+ } elseif ($isPatch) {
+ $message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
+ } else {
+ $message .= "• {$server->name}: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
+ }
+ }
+
+ $message .= "\n⚠️ It is recommended to test before switching the production version.";
+
+ if ($hasUpgrades) {
+ $message .= "\n\n📖 For minor version upgrades: Read the Traefik changelog before upgrading to understand breaking changes and new features.";
+ }
+
+ return [
+ 'message' => $message,
+ 'buttons' => [],
+ ];
+ }
+
+ public function toPushover(): PushoverMessage
+ {
+ $count = $this->servers->count();
+ $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
+ isset($s->outdatedInfo['newer_branch_target'])
+ );
+
+ $message = "Traefik proxy outdated on {$count} server(s)!\n";
+ $message .= "Affected servers:\n";
+
+ foreach ($this->servers as $server) {
+ $info = $server->outdatedInfo ?? [];
+ $current = $this->formatVersion($info['current'] ?? 'unknown');
+ $latest = $this->formatVersion($info['latest'] ?? 'unknown');
+ $upgradeTarget = $this->getUpgradeTarget($info);
+ $isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
+ $hasNewerBranch = isset($info['newer_branch_target']);
+
+ if ($isPatch && $hasNewerBranch) {
+ $newerBranchTarget = $info['newer_branch_target'];
+ $newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
+ $message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
+ $message .= " Also: {$newerBranchTarget} (latest: {$newerBranchLatest}) - new minor version\n";
+ } elseif ($isPatch) {
+ $message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
+ } else {
+ $message .= "• {$server->name}: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
+ }
+ }
+
+ $message .= "\nIt is recommended to test before switching the production version.";
+
+ if ($hasUpgrades) {
+ $message .= "\n\nFor minor version upgrades: Read the Traefik changelog before upgrading.";
+ }
+
+ return new PushoverMessage(
+ title: 'Traefik proxy outdated',
+ level: 'warning',
+ message: $message,
+ );
+ }
+
+ public function toSlack(): SlackMessage
+ {
+ $count = $this->servers->count();
+ $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
+ isset($s->outdatedInfo['newer_branch_target'])
+ );
+
+ $description = "Traefik proxy outdated on {$count} server(s)!\n";
+ $description .= "*Affected servers:*\n";
+
+ foreach ($this->servers as $server) {
+ $info = $server->outdatedInfo ?? [];
+ $current = $this->formatVersion($info['current'] ?? 'unknown');
+ $latest = $this->formatVersion($info['latest'] ?? 'unknown');
+ $upgradeTarget = $this->getUpgradeTarget($info);
+ $isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
+ $hasNewerBranch = isset($info['newer_branch_target']);
+
+ if ($isPatch && $hasNewerBranch) {
+ $newerBranchTarget = $info['newer_branch_target'];
+ $newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
+ $description .= "• `{$server->name}`: {$current} → {$upgradeTarget} (patch update available)\n";
+ $description .= " ↳ Also available: {$newerBranchTarget} (latest patch: {$newerBranchLatest}) - new minor version\n";
+ } elseif ($isPatch) {
+ $description .= "• `{$server->name}`: {$current} → {$upgradeTarget} (patch update available)\n";
+ } else {
+ $description .= "• `{$server->name}`: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
+ }
+ }
+
+ $description .= "\n:warning: It is recommended to test before switching the production version.";
+
+ if ($hasUpgrades) {
+ $description .= "\n\n:book: For minor version upgrades: Read the Traefik changelog before upgrading to understand breaking changes and new features.";
+ }
+
+ return new SlackMessage(
+ title: 'Coolify: Traefik proxy outdated',
+ description: $description,
+ color: SlackMessage::warningColor()
+ );
+ }
+
+ public function toWebhook(): array
+ {
+ $servers = $this->servers->map(function ($server) {
+ $info = $server->outdatedInfo ?? [];
+
+ $webhookData = [
+ 'name' => $server->name,
+ 'uuid' => $server->uuid,
+ 'current_version' => $info['current'] ?? 'unknown',
+ 'latest_version' => $info['latest'] ?? 'unknown',
+ 'update_type' => $info['type'] ?? 'patch_update',
+ ];
+
+ // For minor upgrades, include the upgrade target (e.g., "v3.6")
+ if (($info['type'] ?? 'patch_update') === 'minor_upgrade' && isset($info['upgrade_target'])) {
+ $webhookData['upgrade_target'] = $info['upgrade_target'];
+ }
+
+ // Include newer branch info if available
+ if (isset($info['newer_branch_target'])) {
+ $webhookData['newer_branch_target'] = $info['newer_branch_target'];
+ $webhookData['newer_branch_latest'] = $info['newer_branch_latest'];
+ }
+
+ return $webhookData;
+ })->toArray();
+
+ return [
+ 'success' => false,
+ 'message' => 'Traefik proxy outdated',
+ 'event' => 'traefik_version_outdated',
+ 'affected_servers_count' => $this->servers->count(),
+ 'servers' => $servers,
+ ];
+ }
+}
diff --git a/app/Policies/InstanceSettingsPolicy.php b/app/Policies/InstanceSettingsPolicy.php
new file mode 100644
index 000000000..a04f07a28
--- /dev/null
+++ b/app/Policies/InstanceSettingsPolicy.php
@@ -0,0 +1,25 @@
+ \App\Policies\ApiTokenPolicy::class,
+ // Instance settings policy
+ \App\Models\InstanceSettings::class => \App\Policies\InstanceSettingsPolicy::class,
+
// Team policy
\App\Models\Team::class => \App\Policies\TeamPolicy::class,
diff --git a/app/Services/ContainerStatusAggregator.php b/app/Services/ContainerStatusAggregator.php
new file mode 100644
index 000000000..4a17ecdd6
--- /dev/null
+++ b/app/Services/ContainerStatusAggregator.php
@@ -0,0 +1,251 @@
+ $maxRestartCount,
+ ]);
+ $maxRestartCount = 0;
+ }
+
+ if ($maxRestartCount > 1000) {
+ Log::warning('High maxRestartCount detected', [
+ 'maxRestartCount' => $maxRestartCount,
+ 'containers' => $containerStatuses->count(),
+ ]);
+ }
+
+ if ($containerStatuses->isEmpty()) {
+ return 'exited';
+ }
+
+ // Initialize state flags
+ $hasRunning = false;
+ $hasRestarting = false;
+ $hasUnhealthy = false;
+ $hasUnknown = false;
+ $hasExited = false;
+ $hasStarting = false;
+ $hasPaused = false;
+ $hasDead = false;
+
+ // Parse each status string and set flags
+ foreach ($containerStatuses as $status) {
+ if (str($status)->contains('restarting')) {
+ $hasRestarting = true;
+ } elseif (str($status)->contains('running')) {
+ $hasRunning = true;
+ if (str($status)->contains('unhealthy')) {
+ $hasUnhealthy = true;
+ }
+ if (str($status)->contains('unknown')) {
+ $hasUnknown = true;
+ }
+ } elseif (str($status)->contains('exited')) {
+ $hasExited = true;
+ } elseif (str($status)->contains('created') || str($status)->contains('starting')) {
+ $hasStarting = true;
+ } elseif (str($status)->contains('paused')) {
+ $hasPaused = true;
+ } elseif (str($status)->contains('dead') || str($status)->contains('removing')) {
+ $hasDead = true;
+ }
+ }
+
+ // Priority-based status resolution
+ return $this->resolveStatus(
+ $hasRunning,
+ $hasRestarting,
+ $hasUnhealthy,
+ $hasUnknown,
+ $hasExited,
+ $hasStarting,
+ $hasPaused,
+ $hasDead,
+ $maxRestartCount
+ );
+ }
+
+ /**
+ * Aggregate container statuses from Docker container objects.
+ *
+ * @param Collection $containers Collection of Docker container objects with State property
+ * @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
+ * @return string Aggregated status in colon format (e.g., "running:healthy")
+ */
+ public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0): string
+ {
+ // Validate maxRestartCount parameter
+ if ($maxRestartCount < 0) {
+ Log::warning('Negative maxRestartCount corrected to 0', [
+ 'original_value' => $maxRestartCount,
+ ]);
+ $maxRestartCount = 0;
+ }
+
+ if ($maxRestartCount > 1000) {
+ Log::warning('High maxRestartCount detected', [
+ 'maxRestartCount' => $maxRestartCount,
+ 'containers' => $containers->count(),
+ ]);
+ }
+
+ if ($containers->isEmpty()) {
+ return 'exited';
+ }
+
+ // Initialize state flags
+ $hasRunning = false;
+ $hasRestarting = false;
+ $hasUnhealthy = false;
+ $hasUnknown = false;
+ $hasExited = false;
+ $hasStarting = false;
+ $hasPaused = false;
+ $hasDead = false;
+
+ // Parse each container object and set flags
+ foreach ($containers as $container) {
+ $state = data_get($container, 'State.Status', 'exited');
+ $health = data_get($container, 'State.Health.Status');
+
+ if ($state === 'restarting') {
+ $hasRestarting = true;
+ } elseif ($state === 'running') {
+ $hasRunning = true;
+ if ($health === 'unhealthy') {
+ $hasUnhealthy = true;
+ } elseif (is_null($health) || $health === 'starting') {
+ $hasUnknown = true;
+ }
+ } elseif ($state === 'exited') {
+ $hasExited = true;
+ } elseif ($state === 'created' || $state === 'starting') {
+ $hasStarting = true;
+ } elseif ($state === 'paused') {
+ $hasPaused = true;
+ } elseif ($state === 'dead' || $state === 'removing') {
+ $hasDead = true;
+ }
+ }
+
+ // Priority-based status resolution
+ return $this->resolveStatus(
+ $hasRunning,
+ $hasRestarting,
+ $hasUnhealthy,
+ $hasUnknown,
+ $hasExited,
+ $hasStarting,
+ $hasPaused,
+ $hasDead,
+ $maxRestartCount
+ );
+ }
+
+ /**
+ * Resolve the aggregated status based on state flags (priority-based state machine).
+ *
+ * @param bool $hasRunning Has at least one running container
+ * @param bool $hasRestarting Has at least one restarting container
+ * @param bool $hasUnhealthy Has at least one unhealthy container
+ * @param bool $hasUnknown Has at least one container with unknown health
+ * @param bool $hasExited Has at least one exited container
+ * @param bool $hasStarting Has at least one starting/created container
+ * @param bool $hasPaused Has at least one paused container
+ * @param bool $hasDead Has at least one dead/removing container
+ * @param int $maxRestartCount Maximum restart count (for crash loop detection)
+ * @return string Status in colon format (e.g., "running:healthy")
+ */
+ private function resolveStatus(
+ bool $hasRunning,
+ bool $hasRestarting,
+ bool $hasUnhealthy,
+ bool $hasUnknown,
+ bool $hasExited,
+ bool $hasStarting,
+ bool $hasPaused,
+ bool $hasDead,
+ int $maxRestartCount
+ ): string {
+ // Priority 1: Restarting containers (degraded state)
+ if ($hasRestarting) {
+ return 'degraded:unhealthy';
+ }
+
+ // Priority 2: Crash loop detection (exited with restart count > 0)
+ if ($hasExited && $maxRestartCount > 0) {
+ return 'degraded:unhealthy';
+ }
+
+ // Priority 3: Mixed state (some running, some exited = degraded)
+ if ($hasRunning && $hasExited) {
+ return 'degraded:unhealthy';
+ }
+
+ // Priority 4: Running containers (check health status)
+ if ($hasRunning) {
+ if ($hasUnhealthy) {
+ return 'running:unhealthy';
+ } elseif ($hasUnknown) {
+ return 'running:unknown';
+ } else {
+ return 'running:healthy';
+ }
+ }
+
+ // Priority 5: Dead or removing containers
+ if ($hasDead) {
+ return 'degraded:unhealthy';
+ }
+
+ // Priority 6: Paused containers
+ if ($hasPaused) {
+ return 'paused:unknown';
+ }
+
+ // Priority 7: Starting/created containers
+ if ($hasStarting) {
+ return 'starting:unknown';
+ }
+
+ // Priority 8: All containers exited (no restart count = truly stopped)
+ return 'exited';
+ }
+}
diff --git a/app/Traits/CalculatesExcludedStatus.php b/app/Traits/CalculatesExcludedStatus.php
new file mode 100644
index 000000000..5219878c0
--- /dev/null
+++ b/app/Traits/CalculatesExcludedStatus.php
@@ -0,0 +1,166 @@
+filter(function ($container) use ($excludedContainers) {
+ $labels = data_get($container, 'Config.Labels', []);
+ $serviceName = data_get($labels, 'com.docker.compose.service');
+
+ return $serviceName && $excludedContainers->contains($serviceName);
+ });
+
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
+ $status = $aggregator->aggregateFromContainers($excludedOnly);
+
+ // Append :excluded suffix
+ return $this->appendExcludedSuffix($status);
+ }
+
+ /**
+ * Calculate status for containers when all containers are excluded (simplified version).
+ *
+ * This version works with status strings (e.g., "running:healthy") instead of full
+ * container objects, suitable for Sentinel updates that don't have full container data.
+ *
+ * @param Collection $containerStatuses Collection of status strings keyed by container name
+ * @return string Status string with :excluded suffix
+ */
+ protected function calculateExcludedStatusFromStrings(Collection $containerStatuses): string
+ {
+ // Use ContainerStatusAggregator service for state machine logic
+ $aggregator = new ContainerStatusAggregator;
+ $status = $aggregator->aggregateFromStrings($containerStatuses);
+
+ // Append :excluded suffix
+ $finalStatus = $this->appendExcludedSuffix($status);
+
+ return $finalStatus;
+ }
+
+ /**
+ * Append :excluded suffix to a status string.
+ *
+ * Converts status formats like:
+ * - "running:healthy" → "running:healthy:excluded"
+ * - "degraded:unhealthy" → "degraded:excluded" (simplified)
+ * - "paused:unknown" → "paused:excluded" (simplified)
+ *
+ * @param string $status The base status string
+ * @return string Status with :excluded suffix
+ */
+ private function appendExcludedSuffix(string $status): string
+ {
+ // For degraded states, simplify to just "degraded:excluded"
+ if (str($status)->startsWith('degraded')) {
+ return 'degraded:excluded';
+ }
+
+ // For paused/starting/exited states, simplify to just "state:excluded"
+ if (str($status)->startsWith('paused')) {
+ return 'paused:excluded';
+ }
+
+ if (str($status)->startsWith('starting')) {
+ return 'starting:excluded';
+ }
+
+ if (str($status)->startsWith('exited')) {
+ return 'exited';
+ }
+
+ // For running states, keep the health status: "running:healthy:excluded"
+ return "$status:excluded";
+ }
+
+ /**
+ * Get excluded containers from docker-compose YAML.
+ *
+ * Containers are excluded if:
+ * - They have exclude_from_hc: true label
+ * - They have restart: no policy
+ *
+ * @param string|null $dockerComposeRaw The raw docker-compose YAML content
+ * @return Collection Collection of excluded container names
+ */
+ protected function getExcludedContainersFromDockerCompose(?string $dockerComposeRaw): Collection
+ {
+ $excludedContainers = collect();
+
+ if (! $dockerComposeRaw) {
+ return $excludedContainers;
+ }
+
+ try {
+ $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
+
+ // Validate structure
+ if (! is_array($dockerCompose)) {
+ Log::warning('Docker Compose YAML did not parse to array', [
+ 'yaml_length' => strlen($dockerComposeRaw),
+ 'parsed_type' => gettype($dockerCompose),
+ ]);
+
+ return $excludedContainers;
+ }
+
+ $services = data_get($dockerCompose, 'services', []);
+
+ if (! is_array($services)) {
+ Log::warning('Docker Compose services is not an array', [
+ 'services_type' => gettype($services),
+ ]);
+
+ return $excludedContainers;
+ }
+
+ foreach ($services as $serviceName => $serviceConfig) {
+ $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
+ $restartPolicy = data_get($serviceConfig, 'restart', 'always');
+
+ if ($excludeFromHc || $restartPolicy === 'no') {
+ $excludedContainers->push($serviceName);
+ }
+ }
+ } catch (ParseException $e) {
+ // Specific YAML parsing errors
+ Log::warning('Failed to parse Docker Compose YAML for health check exclusions', [
+ 'error' => $e->getMessage(),
+ 'line' => $e->getParsedLine(),
+ 'snippet' => $e->getSnippet(),
+ ]);
+
+ return $excludedContainers;
+ } catch (\Exception $e) {
+ // Unexpected errors
+ Log::error('Unexpected error parsing Docker Compose YAML', [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+
+ return $excludedContainers;
+ }
+
+ return $excludedContainers;
+ }
+}
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 4aa5aae8b..58ae5f249 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -219,9 +219,22 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe
$process_result = $process->wait();
if ($process_result->exitCode() !== 0) {
if (! $ignore_errors) {
+ // Check if deployment was cancelled while command was running
+ if (isset($this->application_deployment_queue)) {
+ $this->application_deployment_queue->refresh();
+ if ($this->application_deployment_queue->status === \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
+ throw new \RuntimeException('Deployment cancelled by user', 69420);
+ }
+ }
+
// Don't immediately set to FAILED - let the retry logic handle it
// This prevents premature status changes during retryable SSH errors
- throw new \RuntimeException($process_result->errorOutput());
+ $error = $process_result->errorOutput();
+ if (empty($error)) {
+ $error = $process_result->output() ?: 'Command failed with no error output';
+ }
+ $redactedCommand = $this->redact_sensitive_info($command);
+ throw new \RuntimeException("Command execution failed (exit code {$process_result->exitCode()}): {$redactedCommand}\nError: {$error}");
}
}
}
diff --git a/app/View/Components/Forms/EnvVarInput.php b/app/View/Components/Forms/EnvVarInput.php
new file mode 100644
index 000000000..7cf8ee8fa
--- /dev/null
+++ b/app/View/Components/Forms/EnvVarInput.php
@@ -0,0 +1,89 @@
+canGate && $this->canResource && $this->autoDisable) {
+ $hasPermission = Gate::allows($this->canGate, $this->canResource);
+
+ if (! $hasPermission) {
+ $this->disabled = true;
+ }
+ }
+ }
+
+ public function render(): View|Closure|string
+ {
+ // Store original ID for wire:model binding (property name)
+ $this->modelBinding = $this->id;
+
+ if (is_null($this->id)) {
+ $this->id = new Cuid2;
+ // Don't create wire:model binding for auto-generated IDs
+ $this->modelBinding = 'null';
+ }
+ // Generate unique HTML ID by adding random suffix
+ // This prevents duplicate IDs when multiple forms are on the same page
+ if ($this->modelBinding && $this->modelBinding !== 'null') {
+ // Use original ID with random suffix for uniqueness
+ $uniqueSuffix = new Cuid2;
+ $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
+ } else {
+ $this->htmlId = (string) $this->id;
+ }
+
+ if (is_null($this->name)) {
+ $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
+ }
+
+ $this->scopeUrls = [
+ 'team' => route('shared-variables.team.index'),
+ 'project' => route('shared-variables.project.index'),
+ 'environment' => $this->projectUuid && $this->environmentUuid
+ ? route('shared-variables.environment.show', [
+ 'project_uuid' => $this->projectUuid,
+ 'environment_uuid' => $this->environmentUuid,
+ ])
+ : route('shared-variables.environment.index'),
+ 'default' => route('shared-variables.index'),
+ ];
+
+ return view('components.forms.env-var-input');
+ }
+}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index 382e2d015..f588b6c00 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -47,6 +47,8 @@
'neo4j',
'influxdb',
'clickhouse/clickhouse-server',
+ 'timescaledb/timescaledb',
+ 'pgvector/pgvector',
];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index d6c9b5bdf..feee4536e 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -17,24 +17,44 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
if (! $server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
+
$containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) {
$labels = data_get($container, 'Labels');
- if (! str($labels)->contains('coolify.pullRequestId=')) {
- data_set($container, 'Labels', $labels.",coolify.pullRequestId={$pullRequestId}");
+ $containerName = data_get($container, 'Names');
+ $hasPrLabel = str($labels)->contains('coolify.pullRequestId=');
+ $prLabelValue = null;
+ if ($hasPrLabel) {
+ preg_match('/coolify\.pullRequestId=(\d+)/', $labels, $matches);
+ $prLabelValue = $matches[1] ?? null;
+ }
+
+ // Treat pullRequestId=0 or missing label as base deployment (convention: 0 = no PR)
+ $isBaseDeploy = ! $hasPrLabel || (int) $prLabelValue === 0;
+
+ // If we're looking for a specific PR and this is a base deployment, exclude it
+ if ($pullRequestId !== null && $pullRequestId !== 0 && $isBaseDeploy) {
+ return null;
+ }
+
+ // If this is a base deployment, include it when not filtering for PRs
+ if ($isBaseDeploy) {
return $container;
}
+
if ($includePullrequests) {
return $container;
}
- if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) {
+ if ($pullRequestId !== null && $pullRequestId !== 0 && str($labels)->contains("coolify.pullRequestId={$pullRequestId}")) {
return $container;
}
return null;
});
- return $containers->filter();
+ $filtered = $containers->filter();
+
+ return $filtered;
}
return $containers;
@@ -942,6 +962,7 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
'--shm-size' => 'shm_size',
'--gpus' => 'gpus',
'--hostname' => 'hostname',
+ '--entrypoint' => 'entrypoint',
]);
foreach ($matches as $match) {
$option = $match[1];
@@ -962,6 +983,37 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
$options[$option] = array_unique($options[$option]);
}
}
+ if ($option === '--entrypoint') {
+ // Match --entrypoint=value or --entrypoint value
+ // Handle quoted strings with escaped quotes: --entrypoint "python -c \"print('hi')\""
+ // Pattern matches: double-quoted (with escapes), single-quoted (with escapes), or unquoted values
+ if (preg_match(
+ '/--entrypoint(?:=|\s+)(?"(?:\\\\.|[^"])*"|\'(?:\\\\.|[^\'])*\'|[^\s]+)/',
+ $custom_docker_run_options,
+ $entrypoint_matches
+ )) {
+ $rawValue = $entrypoint_matches['raw'];
+ // Handle double-quoted strings: strip quotes and unescape special characters
+ if (str_starts_with($rawValue, '"') && str_ends_with($rawValue, '"')) {
+ $inner = substr($rawValue, 1, -1);
+ // Unescape backslash sequences: \" \$ \` \\
+ $value = preg_replace('/\\\\(["$`\\\\])/', '$1', $inner);
+ } elseif (str_starts_with($rawValue, "'") && str_ends_with($rawValue, "'")) {
+ // Handle single-quoted strings: just strip quotes (no unescaping per shell rules)
+ $value = substr($rawValue, 1, -1);
+ } else {
+ // Handle unquoted values
+ $value = $rawValue;
+ }
+ }
+
+ if (isset($value) && trim($value) !== '') {
+ $options[$option][] = $value;
+ $options[$option] = array_values(array_unique($options[$option]));
+ }
+
+ continue;
+ }
if (isset($match[2]) && $match[2] !== '') {
$value = $match[2];
$options[$option][] = $value;
@@ -1002,6 +1054,12 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
if (! is_null($value) && is_array($value) && count($value) > 0 && ! empty(trim($value[0]))) {
$compose_options->put($mapping[$option], $value[0]);
}
+ } elseif ($option === '--entrypoint') {
+ if (! is_null($value) && is_array($value) && count($value) > 0 && ! empty(trim($value[0]))) {
+ // Docker compose accepts entrypoint as either a string or an array
+ // Keep it as a string for simplicity - docker compose will handle it
+ $compose_options->put($mapping[$option], $value[0]);
+ }
} elseif ($option === '--gpus') {
$payload = [
'driver' => 'nvidia',
@@ -1063,6 +1121,44 @@ function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker
return $docker_compose;
}
+/**
+ * Remove Coolify's custom Docker Compose fields from parsed YAML array
+ *
+ * Coolify extends Docker Compose with custom fields that are processed during
+ * parsing and deployment but must be removed before sending to Docker.
+ *
+ * Custom fields:
+ * - exclude_from_hc (service-level): Exclude service from health check monitoring
+ * - content (volume-level): Auto-create file with specified content during init
+ * - isDirectory / is_directory (volume-level): Mark bind mount as directory
+ *
+ * @param array $yamlCompose Parsed Docker Compose array
+ * @return array Cleaned Docker Compose array with custom fields removed
+ */
+function stripCoolifyCustomFields(array $yamlCompose): array
+{
+ foreach ($yamlCompose['services'] ?? [] as $serviceName => $service) {
+ // Remove service-level custom fields
+ unset($yamlCompose['services'][$serviceName]['exclude_from_hc']);
+
+ // Remove volume-level custom fields (only for long syntax - arrays)
+ if (isset($service['volumes'])) {
+ foreach ($service['volumes'] as $volumeName => $volume) {
+ // Skip if volume is string (short syntax like 'db-data:/var/lib/postgresql/data')
+ if (! is_array($volume)) {
+ continue;
+ }
+
+ unset($yamlCompose['services'][$serviceName]['volumes'][$volumeName]['content']);
+ unset($yamlCompose['services'][$serviceName]['volumes'][$volumeName]['isDirectory']);
+ unset($yamlCompose['services'][$serviceName]['volumes'][$volumeName]['is_directory']);
+ }
+ }
+ }
+
+ return $yamlCompose;
+}
+
function validateComposeFile(string $compose, int $server_id): string|Throwable
{
$uuid = Str::random(18);
@@ -1072,13 +1168,10 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
throw new \Exception('Server not found');
}
$yaml_compose = Yaml::parse($compose);
- foreach ($yaml_compose['services'] as $service_name => $service) {
- foreach ($service['volumes'] as $volume_name => $volume) {
- if (data_get($volume, 'type') === 'bind' && data_get($volume, 'content')) {
- unset($yaml_compose['services'][$service_name]['volumes'][$volume_name]['content']);
- }
- }
- }
+
+ // Remove Coolify's custom fields before Docker validation
+ $yaml_compose = stripCoolifyCustomFields($yaml_compose);
+
$base64_compose = base64_encode(Yaml::dump($yaml_compose));
instant_remote_process([
"echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null",
@@ -1249,3 +1342,36 @@ function generateDockerEnvFlags($variables): string
})
->implode(' ');
}
+
+/**
+ * Auto-inject -f and --env-file flags into a docker compose command if not already present
+ *
+ * @param string $command The docker compose command to modify
+ * @param string $composeFilePath The path to the compose file
+ * @param string $envFilePath The path to the .env file
+ * @return string The modified command with injected flags
+ *
+ * @example
+ * Input: "docker compose build"
+ * Output: "docker compose -f ./docker-compose.yml --env-file .env build"
+ */
+function injectDockerComposeFlags(string $command, string $composeFilePath, string $envFilePath): string
+{
+ $dockerComposeReplacement = 'docker compose';
+
+ // Add -f flag if not present (checks for both -f and --file with various formats)
+ // Detects: -f path, -f=path, -fpath (concatenated with path chars: . / ~), --file path, --file=path
+ // Note: Uses [.~/]|$ instead of \S to prevent false positives with flags like -foo, -from, -feature
+ if (! preg_match('/(?:^|\s)(?:-f(?:[=\s]|[.\/~]|$)|--file(?:=|\s))/', $command)) {
+ $dockerComposeReplacement .= " -f {$composeFilePath}";
+ }
+
+ // Add --env-file flag if not present (checks for --env-file with various formats)
+ // Detects: --env-file path, --env-file=path with any whitespace
+ if (! preg_match('/(?:^|\s)--env-file(?:=|\s)/', $command)) {
+ $dockerComposeReplacement .= " --env-file {$envFilePath}";
+ }
+
+ // Replace only first occurrence to avoid modifying comments/strings/chained commands
+ return preg_replace('/docker\s+compose/', $dockerComposeReplacement, $command, 1);
+}
diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php
index 01ae50f6b..dfcc3e190 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -59,11 +59,13 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (isset($volume['source'])) {
$source = $volume['source'];
if (is_string($source)) {
- // Allow simple env vars and env vars with defaults (validated in parseDockerVolumeString)
+ // Allow env vars and env vars with defaults (validated in parseDockerVolumeString)
+ // Also allow env vars followed by safe path concatenation (e.g., ${VAR}/path)
$isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $source);
$isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $source);
+ $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $source);
- if (! $isSimpleEnvVar && ! $isEnvVarWithDefault) {
+ if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($source, 'volume source');
} catch (\Exception $e) {
@@ -310,15 +312,17 @@ function parseDockerVolumeString(string $volumeString): array
// Validate source path for command injection attempts
// We validate the final source value after environment variable processing
if ($source !== null) {
- // Allow simple environment variables like ${VAR_NAME} or ${VAR}
- // but validate everything else for shell metacharacters
+ // Allow environment variables like ${VAR_NAME} or ${VAR}
+ // Also allow env vars followed by safe path concatenation (e.g., ${VAR}/path)
$sourceStr = is_string($source) ? $source : $source;
// Skip validation for simple environment variable references
- // Pattern: ${WORD_CHARS} with no special characters inside
+ // Pattern 1: ${WORD_CHARS} with no special characters inside
+ // Pattern 2: ${WORD_CHARS}/path/to/file (env var with path concatenation)
$isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceStr);
+ $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceStr);
- if (! $isSimpleEnvVar) {
+ if (! $isSimpleEnvVar && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceStr, 'volume source');
} catch (\Exception $e) {
@@ -453,13 +457,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// for example SERVICE_FQDN_APP_3000 (without a value)
if ($key->startsWith('SERVICE_FQDN_')) {
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
- if (substr_count(str($key)->value(), '_') === 3) {
- $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
- $port = $key->afterLast('_')->value();
- } else {
- $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
- $port = null;
- }
+ $parsed = parseServiceEnvironmentVariable($key->value());
+ $fqdnFor = $parsed['service_name'];
+ $port = $parsed['port'];
$fqdn = $resource->fqdn;
if (blank($resource->fqdn)) {
$fqdn = generateFqdn(server: $server, random: "$uuid", parserVersion: $resource->compose_parsing_version);
@@ -482,7 +482,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$resource->save();
}
- if (substr_count(str($key)->value(), '_') === 2) {
+ if (! $parsed['has_port']) {
$resource->environment_variables()->updateOrCreate([
'key' => $key->value(),
'resourceable_type' => get_class($resource),
@@ -492,7 +492,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
'is_preview' => false,
]);
}
- if (substr_count(str($key)->value(), '_') === 3) {
+ if ($parsed['has_port']) {
$newKey = str($key)->beforeLast('_');
$resource->environment_variables()->updateOrCreate([
@@ -514,75 +514,96 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$key = str($key);
$value = replaceVariables($value);
$command = parseCommandFromMagicEnvVariable($key);
- if ($command->value() === 'FQDN') {
- $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
- $originalFqdnFor = str($fqdnFor)->replace('_', '-');
- if (str($fqdnFor)->contains('-')) {
- $fqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_');
+ if ($command->value() === 'FQDN' || $command->value() === 'URL') {
+ // ALWAYS create BOTH SERVICE_URL and SERVICE_FQDN pairs regardless of which one is in template
+ $parsed = parseServiceEnvironmentVariable($key->value());
+ $serviceName = $parsed['service_name'];
+ $port = $parsed['port'];
+
+ // Extract case-preserved service name from template
+ $strKey = str($key->value());
+ if ($parsed['has_port']) {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceNamePreserved = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
+ } else {
+ $serviceNamePreserved = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
+ }
+ } else {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceNamePreserved = $strKey->after('SERVICE_URL_')->value();
+ } else {
+ $serviceNamePreserved = $strKey->after('SERVICE_FQDN_')->value();
+ }
}
- // Generated FQDN & URL
- $fqdn = generateFqdn(server: $server, random: "$originalFqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
- $url = generateUrl(server: $server, random: "$originalFqdnFor-$uuid");
+
+ $originalServiceName = str($serviceName)->replace('_', '-')->value();
+ // Always normalize service names to match docker_compose_domains lookup
+ $serviceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
+
+ // Generate BOTH FQDN & URL
+ $fqdn = generateFqdn(server: $server, random: "$originalServiceName-$uuid", parserVersion: $resource->compose_parsing_version);
+ $url = generateUrl(server: $server, random: "$originalServiceName-$uuid");
+
+ // IMPORTANT: SERVICE_FQDN env vars should NOT contain scheme (host only)
+ // But $fqdn variable itself may contain scheme (used for database domain field)
+ // Strip scheme for environment variable values
+ $fqdnValueForEnv = str($fqdn)->after('://')->value();
+
+ // Append port if specified
+ $urlWithPort = $url;
+ $fqdnValueForEnvWithPort = $fqdnValueForEnv;
+ if ($port && is_numeric($port)) {
+ $urlWithPort = "$url:$port";
+ $fqdnValueForEnvWithPort = "$fqdnValueForEnv:$port";
+ }
+
+ // ALWAYS create base SERVICE_FQDN variable (host only, no scheme)
$resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_FQDN_{$serviceNamePreserved}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
- 'value' => $fqdn,
+ 'value' => $fqdnValueForEnv,
'is_preview' => false,
]);
- if ($resource->build_pack === 'dockercompose') {
- // Check if a service with this name actually exists
- $serviceExists = false;
- foreach ($services as $serviceName => $service) {
- $transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
- if ($transformedServiceName === $fqdnFor) {
- $serviceExists = true;
- break;
- }
- }
- // Only add domain if the service exists
- if ($serviceExists) {
- $domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
- $domainExists = data_get($domains->get($fqdnFor), 'domain');
- $envExists = $resource->environment_variables()->where('key', $key->value())->first();
- if (str($domainExists)->replace('http://', '')->replace('https://', '')->value() !== $envExists->value) {
- $envExists->update([
- 'value' => $url,
- ]);
- }
- if (is_null($domainExists)) {
- // Put URL in the domains array instead of FQDN
- $domains->put((string) $fqdnFor, [
- 'domain' => $url,
- ]);
- $resource->docker_compose_domains = $domains->toJson();
- $resource->save();
- }
- }
- }
- } elseif ($command->value() === 'URL') {
- $urlFor = $key->after('SERVICE_URL_')->lower()->value();
- $originalUrlFor = str($urlFor)->replace('_', '-');
- if (str($urlFor)->contains('-')) {
- $urlFor = str($urlFor)->replace('-', '_')->replace('.', '_');
- }
- $url = generateUrl(server: $server, random: "$originalUrlFor-$uuid");
+ // ALWAYS create base SERVICE_URL variable (with scheme)
$resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_URL_{$serviceNamePreserved}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
'value' => $url,
'is_preview' => false,
]);
+
+ // If port-specific, ALSO create port-specific pairs
+ if ($parsed['has_port'] && $port) {
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => "SERVICE_FQDN_{$serviceNamePreserved}_{$port}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdnValueForEnvWithPort,
+ 'is_preview' => false,
+ ]);
+
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => "SERVICE_URL_{$serviceNamePreserved}_{$port}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $urlWithPort,
+ 'is_preview' => false,
+ ]);
+ }
+
if ($resource->build_pack === 'dockercompose') {
// Check if a service with this name actually exists
$serviceExists = false;
- foreach ($services as $serviceName => $service) {
- $transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
- if ($transformedServiceName === $urlFor) {
+ foreach ($services as $serviceNameKey => $service) {
+ $transformedServiceName = str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value();
+ if ($transformedServiceName === $serviceName) {
$serviceExists = true;
break;
}
@@ -591,16 +612,14 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// Only add domain if the service exists
if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
- $domainExists = data_get($domains->get($urlFor), 'domain');
- $envExists = $resource->environment_variables()->where('key', $key->value())->first();
- if ($domainExists !== $envExists->value) {
- $envExists->update([
- 'value' => $url,
- ]);
- }
+ $domainExists = data_get($domains->get($serviceName), 'domain');
+
+ // Update domain using URL with port if applicable
+ $domainValue = $port ? $urlWithPort : $url;
+
if (is_null($domainExists)) {
- $domains->put((string) $urlFor, [
- 'domain' => $url,
+ $domains->put($serviceName, [
+ 'domain' => $domainValue,
]);
$resource->docker_compose_domains = $domains->toJson();
$resource->save();
@@ -711,9 +730,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// Validate source and target for command injection (array/long syntax)
if ($source !== null && ! empty($source->value())) {
$sourceValue = $source->value();
- // Allow simple environment variable references
+ // Allow environment variable references and env vars with path concatenation
$isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue);
- if (! $isSimpleEnvVar) {
+ $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $sourceValue);
+ $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceValue);
+
+ if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceValue, 'volume source');
} catch (\Exception $e) {
@@ -1164,13 +1186,21 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
- // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
- if (str($value)->isEmpty()) {
- if ($resource->environment_variables()->where('key', $key)->exists()) {
- $value = $resource->environment_variables()->where('key', $key)->first()->value;
- } else {
- $value = null;
+ // Preserve empty strings and null values with correct Docker Compose semantics:
+ // - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
+ // - Null: Variable is unset/removed from container environment (may inherit from host)
+ if ($value === null) {
+ // User explicitly wants variable unset - respect that
+ // NEVER override from database - null means "inherit from environment"
+ // Keep as null (will be excluded from container environment)
+ } elseif ($value === '') {
+ // Empty string - allow database override for backward compatibility
+ $dbEnv = $resource->environment_variables()->where('key', $key)->first();
+ // Only use database override if it exists AND has a non-empty value
+ if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
+ $value = $dbEnv->value;
}
+ // Otherwise keep empty string as-is
}
return $value;
@@ -1285,6 +1315,15 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($depends_on->count() > 0) {
$payload['depends_on'] = $depends_on;
}
+ // Auto-inject .env file so Coolify environment variables are available inside containers
+ // This makes Applications behave consistently with manual .env file usage
+ $existingEnvFiles = data_get($service, 'env_file');
+ $envFiles = collect(is_null($existingEnvFiles) ? [] : (is_array($existingEnvFiles) ? $existingEnvFiles : [$existingEnvFiles]))
+ ->push('.env')
+ ->unique()
+ ->values();
+
+ $payload['env_file'] = $envFiles;
if ($isPullRequest) {
$serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId);
}
@@ -1299,6 +1338,18 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
return array_search($key, $customOrder);
});
+ // Remove empty top-level sections (volumes, networks, configs, secrets)
+ // Keep only non-empty sections to match Docker Compose best practices
+ $topLevel = $topLevel->filter(function ($value, $key) {
+ // Always keep 'services' section
+ if ($key === 'services') {
+ return true;
+ }
+
+ // Keep section only if it has content
+ return $value instanceof Collection ? $value->isNotEmpty() : ! empty($value);
+ });
+
$cleanedCompose = Yaml::dump(convertToArray($topLevel), 10, 2);
$resource->docker_compose = $cleanedCompose;
@@ -1392,22 +1443,40 @@ function serviceParser(Service $resource): Collection
}
$image = data_get_str($service, 'image');
- $isDatabase = isDatabaseImage($image, $service);
- if ($isDatabase) {
- $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
- if ($applicationFound) {
- $savedService = $applicationFound;
+
+ // Check for manually migrated services first (respects user's conversion choice)
+ $migratedApp = ServiceApplication::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+ $migratedDb = ServiceDatabase::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+
+ if ($migratedApp || $migratedDb) {
+ // Use the migrated service type, ignoring image detection
+ $isDatabase = (bool) $migratedDb;
+ $savedService = $migratedApp ?: $migratedDb;
+ } else {
+ // Use image detection for non-migrated services
+ $isDatabase = isDatabaseImage($image, $service);
+ if ($isDatabase) {
+ $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
+ if ($applicationFound) {
+ $savedService = $applicationFound;
+ } else {
+ $savedService = ServiceDatabase::firstOrCreate([
+ 'name' => $serviceName,
+ 'service_id' => $resource->id,
+ ]);
+ }
} else {
- $savedService = ServiceDatabase::firstOrCreate([
+ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName,
'service_id' => $resource->id,
]);
}
- } else {
- $savedService = ServiceApplication::firstOrCreate([
- 'name' => $serviceName,
- 'service_id' => $resource->id,
- ]);
}
// Update image if it changed
if ($savedService->image !== $image) {
@@ -1422,7 +1491,24 @@ function serviceParser(Service $resource): Collection
$environment = collect(data_get($service, 'environment', []));
$buildArgs = collect(data_get($service, 'build.args', []));
$environment = $environment->merge($buildArgs);
- $isDatabase = isDatabaseImage($image, $service);
+
+ // Check for manually migrated services first (respects user's conversion choice)
+ $migratedApp = ServiceApplication::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+ $migratedDb = ServiceDatabase::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+
+ if ($migratedApp || $migratedDb) {
+ // Use the migrated service type, ignoring image detection
+ $isDatabase = (bool) $migratedDb;
+ } else {
+ // Use image detection for non-migrated services
+ $isDatabase = isDatabaseImage($image, $service);
+ }
$containerName = "$serviceName-{$resource->uuid}";
@@ -1442,7 +1528,11 @@ function serviceParser(Service $resource): Collection
if ($serviceName === 'plausible') {
$predefinedPort = '8000';
}
- if ($isDatabase) {
+
+ if ($migratedApp || $migratedDb) {
+ // Use the already determined migrated service
+ $savedService = $migratedApp ?: $migratedDb;
+ } elseif ($isDatabase) {
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) {
$savedService = $applicationFound;
@@ -1504,106 +1594,119 @@ function serviceParser(Service $resource): Collection
}
// Get magic environments where we need to preset the FQDN / URL
if ($key->startsWith('SERVICE_FQDN_') || $key->startsWith('SERVICE_URL_')) {
- // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
- if (substr_count(str($key)->value(), '_') === 3) {
- if ($key->startsWith('SERVICE_FQDN_')) {
- $urlFor = null;
- $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
+ // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 or SERVICE_URL_APP or SERVICE_URL_APP_3000
+ // ALWAYS create BOTH SERVICE_URL and SERVICE_FQDN pairs regardless of which one is in template
+ $parsed = parseServiceEnvironmentVariable($key->value());
+
+ // Extract service name preserving original case from template
+ $strKey = str($key->value());
+ if ($parsed['has_port']) {
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
+ } else {
+ continue;
}
- if ($key->startsWith('SERVICE_URL_')) {
- $fqdnFor = null;
- $urlFor = $key->after('SERVICE_URL_')->beforeLast('_')->lower()->value();
- }
- $port = $key->afterLast('_')->value();
} else {
- if ($key->startsWith('SERVICE_FQDN_')) {
- $urlFor = null;
- $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->value();
+ } else {
+ continue;
}
- if ($key->startsWith('SERVICE_URL_')) {
- $fqdnFor = null;
- $urlFor = $key->after('SERVICE_URL_')->lower()->value();
- }
- $port = null;
}
- if (blank($savedService->fqdn)) {
- if ($fqdnFor) {
- $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
- } else {
- $fqdn = generateFqdn(server: $server, random: "{$savedService->name}-$uuid", parserVersion: $resource->compose_parsing_version);
- }
- if ($urlFor) {
- $url = generateUrl($server, "$urlFor-$uuid");
- } else {
- $url = generateUrl($server, "{$savedService->name}-$uuid");
- }
- } else {
+
+ $port = $parsed['port'];
+ $fqdnFor = $parsed['service_name'];
+
+ // Only ServiceApplication has fqdn column, ServiceDatabase does not
+ $isServiceApplication = $savedService instanceof ServiceApplication;
+
+ if ($isServiceApplication && blank($savedService->fqdn)) {
+ $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
+ $url = generateUrl($server, "$fqdnFor-$uuid");
+ } elseif ($isServiceApplication) {
$fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
$url = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
+ } else {
+ // For ServiceDatabase, generate fqdn/url without saving to the model
+ $fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
+ $url = generateUrl($server, "$fqdnFor-$uuid");
}
+ // IMPORTANT: SERVICE_FQDN env vars should NOT contain scheme (host only)
+ // But $fqdn variable itself may contain scheme (used for database domain field)
+ // Strip scheme for environment variable values
+ $fqdnValueForEnv = str($fqdn)->after('://')->value();
+
if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
$url = "$url$path";
+ $fqdnValueForEnv = "$fqdnValueForEnv$path";
}
}
- $fqdnWithPort = $fqdn;
+
$urlWithPort = $url;
+ $fqdnValueForEnvWithPort = $fqdnValueForEnv;
if ($fqdn && $port) {
- $fqdnWithPort = "$fqdn:$port";
+ $fqdnValueForEnvWithPort = "$fqdnValueForEnv:$port";
}
if ($url && $port) {
$urlWithPort = "$url:$port";
}
- if (is_null($savedService->fqdn)) {
+
+ // Only save fqdn to ServiceApplication, not ServiceDatabase
+ if ($isServiceApplication && is_null($savedService->fqdn)) {
+ // Save URL (with scheme) to database, not FQDN
if ((int) $resource->compose_parsing_version >= 5 && version_compare(config('constants.coolify.version'), '4.0.0-beta.420.7', '>=')) {
- if ($fqdnFor) {
- $savedService->fqdn = $fqdnWithPort;
- }
- if ($urlFor) {
- $savedService->fqdn = $urlWithPort;
- }
+ $savedService->fqdn = $urlWithPort;
} else {
- $savedService->fqdn = $fqdnWithPort;
+ $savedService->fqdn = $urlWithPort;
}
$savedService->save();
}
- if (substr_count(str($key)->value(), '_') === 2) {
+
+ // ALWAYS create BOTH base SERVICE_URL and SERVICE_FQDN pairs (without port)
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => "SERVICE_FQDN_{$serviceName}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdnValueForEnv,
+ 'is_preview' => false,
+ ]);
+
+ $resource->environment_variables()->updateOrCreate([
+ 'key' => "SERVICE_URL_{$serviceName}",
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $url,
+ 'is_preview' => false,
+ ]);
+
+ // For port-specific variables, ALSO create port-specific pairs
+ // If template variable has port, create both URL and FQDN with port suffix
+ if ($parsed['has_port'] && $port) {
$resource->environment_variables()->updateOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_FQDN_{$serviceName}_{$port}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
- 'value' => $fqdn,
+ 'value' => $fqdnValueForEnvWithPort,
'is_preview' => false,
]);
+
$resource->environment_variables()->updateOrCreate([
- 'key' => $key->value(),
+ 'key' => "SERVICE_URL_{$serviceName}_{$port}",
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
], [
- 'value' => $url,
- 'is_preview' => false,
- ]);
- }
- if (substr_count(str($key)->value(), '_') === 3) {
- $newKey = str($key)->beforeLast('_');
- $resource->environment_variables()->updateOrCreate([
- 'key' => $newKey->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $fqdn,
- 'is_preview' => false,
- ]);
- $resource->environment_variables()->updateOrCreate([
- 'key' => $newKey->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $url,
+ 'value' => $urlWithPort,
'is_preview' => false,
]);
}
@@ -1621,8 +1724,17 @@ function serviceParser(Service $resource): Collection
$url = generateUrl(server: $server, random: str($fqdnFor)->replace('_', '-')->value()."-$uuid");
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
+ // Also check if a port-suffixed version exists (e.g., SERVICE_FQDN_UMAMI_3000)
+ $portSuffixedExists = $resource->environment_variables()
+ ->where('key', 'LIKE', $key->value().'_%')
+ ->whereRaw('key ~ ?', ['^'.$key->value().'_[0-9]+$'])
+ ->exists();
$serviceExists = ServiceApplication::where('name', str($fqdnFor)->replace('_', '-')->value())->where('service_id', $resource->id)->first();
- if (! $envExists && (data_get($serviceExists, 'name') === str($fqdnFor)->replace('_', '-')->value())) {
+ // Check if FQDN already has a port set (contains ':' after the domain)
+ $fqdnHasPort = $serviceExists && str($serviceExists->fqdn)->contains(':') && str($serviceExists->fqdn)->afterLast(':')->isMatch('/^\d+$/');
+ // Only set FQDN if it's for the current service being processed (prevent race conditions)
+ $isCurrentService = $serviceExists && $serviceExists->id === $savedService->id;
+ if (! $envExists && ! $portSuffixedExists && ! $fqdnHasPort && $isCurrentService && (data_get($serviceExists, 'name') === str($fqdnFor)->replace('_', '-')->value())) {
// Save URL otherwise it won't work.
$serviceExists->fqdn = $url;
$serviceExists->save();
@@ -1641,8 +1753,17 @@ function serviceParser(Service $resource): Collection
$url = generateUrl(server: $server, random: str($urlFor)->replace('_', '-')->value()."-$uuid");
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
+ // Also check if a port-suffixed version exists (e.g., SERVICE_URL_DASHBOARD_6791)
+ $portSuffixedExists = $resource->environment_variables()
+ ->where('key', 'LIKE', $key->value().'_%')
+ ->whereRaw('key ~ ?', ['^'.$key->value().'_[0-9]+$'])
+ ->exists();
$serviceExists = ServiceApplication::where('name', str($urlFor)->replace('_', '-')->value())->where('service_id', $resource->id)->first();
- if (! $envExists && (data_get($serviceExists, 'name') === str($urlFor)->replace('_', '-')->value())) {
+ // Check if FQDN already has a port set (contains ':' after the domain)
+ $fqdnHasPort = $serviceExists && str($serviceExists->fqdn)->contains(':') && str($serviceExists->fqdn)->afterLast(':')->isMatch('/^\d+$/');
+ // Only set FQDN if it's for the current service being processed (prevent race conditions)
+ $isCurrentService = $serviceExists && $serviceExists->id === $savedService->id;
+ if (! $envExists && ! $portSuffixedExists && ! $fqdnHasPort && $isCurrentService && (data_get($serviceExists, 'name') === str($urlFor)->replace('_', '-')->value())) {
$serviceExists->fqdn = $url;
$serviceExists->save();
}
@@ -1707,7 +1828,25 @@ function serviceParser(Service $resource): Collection
$environment = convertToKeyValueCollection($environment);
$coolifyEnvironments = collect([]);
- $isDatabase = isDatabaseImage($image, $service);
+ // Check for manually migrated services first (respects user's conversion choice)
+ $migratedApp = ServiceApplication::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+ $migratedDb = ServiceDatabase::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+
+ if ($migratedApp || $migratedDb) {
+ // Use the migrated service type, ignoring image detection
+ $isDatabase = (bool) $migratedDb;
+ $savedService = $migratedApp ?: $migratedDb;
+ } else {
+ // Use image detection for non-migrated services
+ $isDatabase = isDatabaseImage($image, $service);
+ }
+
$volumesParsed = collect([]);
$containerName = "$serviceName-{$resource->uuid}";
@@ -1729,7 +1868,10 @@ function serviceParser(Service $resource): Collection
$predefinedPort = '8000';
}
- if ($isDatabase) {
+ if ($migratedApp || $migratedDb) {
+ // Use the already determined migrated service
+ $savedService = $migratedApp ?: $migratedDb;
+ } elseif ($isDatabase) {
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) {
$savedService = $applicationFound;
@@ -1791,9 +1933,12 @@ function serviceParser(Service $resource): Collection
// Validate source and target for command injection (array/long syntax)
if ($source !== null && ! empty($source->value())) {
$sourceValue = $source->value();
- // Allow simple environment variable references
+ // Allow environment variable references and env vars with path concatenation
$isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue);
- if (! $isSimpleEnvVar) {
+ $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $sourceValue);
+ $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceValue);
+
+ if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceValue, 'volume source');
} catch (\Exception $e) {
@@ -2122,13 +2267,21 @@ function serviceParser(Service $resource): Collection
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
- // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
- if (str($value)->isEmpty()) {
- if ($resource->environment_variables()->where('key', $key)->exists()) {
- $value = $resource->environment_variables()->where('key', $key)->first()->value;
- } else {
- $value = null;
+ // Preserve empty strings and null values with correct Docker Compose semantics:
+ // - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
+ // - Null: Variable is unset/removed from container environment (may inherit from host)
+ if ($value === null) {
+ // User explicitly wants variable unset - respect that
+ // NEVER override from database - null means "inherit from environment"
+ // Keep as null (will be excluded from container environment)
+ } elseif ($value === '') {
+ // Empty string - allow database override for backward compatibility
+ $dbEnv = $resource->environment_variables()->where('key', $key)->first();
+ // Only use database override if it exists AND has a non-empty value
+ if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
+ $value = $dbEnv->value;
}
+ // Otherwise keep empty string as-is
}
return $value;
@@ -2240,6 +2393,15 @@ function serviceParser(Service $resource): Collection
if ($depends_on->count() > 0) {
$payload['depends_on'] = $depends_on;
}
+ // Auto-inject .env file so Coolify environment variables are available inside containers
+ // This makes Services behave consistently with Applications
+ $existingEnvFiles = data_get($service, 'env_file');
+ $envFiles = collect(is_null($existingEnvFiles) ? [] : (is_array($existingEnvFiles) ? $existingEnvFiles : [$existingEnvFiles]))
+ ->push('.env')
+ ->unique()
+ ->values();
+
+ $payload['env_file'] = $envFiles;
$parsedServices->put($serviceName, $payload);
}
@@ -2251,6 +2413,18 @@ function serviceParser(Service $resource): Collection
return array_search($key, $customOrder);
});
+ // Remove empty top-level sections (volumes, networks, configs, secrets)
+ // Keep only non-empty sections to match Docker Compose best practices
+ $topLevel = $topLevel->filter(function ($value, $key) {
+ // Always keep 'services' section
+ if ($key === 'services') {
+ return true;
+ }
+
+ // Keep section only if it has content
+ return $value instanceof Collection ? $value->isNotEmpty() : ! empty($value);
+ });
+
$cleanedCompose = Yaml::dump(convertToArray($topLevel), 10, 2);
$resource->docker_compose = $cleanedCompose;
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 924bad307..08fad4958 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -212,7 +212,7 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
'services' => [
'traefik' => [
'container_name' => 'coolify-proxy',
- 'image' => 'traefik:v3.1',
+ 'image' => 'traefik:v3.6',
'restart' => RESTART_MODE,
'extra_hosts' => [
'host.docker.internal:host-gateway',
@@ -334,3 +334,93 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
return $config;
}
+
+function getExactTraefikVersionFromContainer(Server $server): ?string
+{
+ try {
+ Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Checking for exact version");
+
+ // Method A: Execute traefik version command (most reliable)
+ $versionCommand = "docker exec coolify-proxy traefik version 2>/dev/null | grep -oP 'Version:\s+\K\d+\.\d+\.\d+'";
+ Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Running: {$versionCommand}");
+
+ $output = instant_remote_process([$versionCommand], $server, false);
+
+ if (! empty(trim($output))) {
+ $version = trim($output);
+ Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Detected exact version from command: {$version}");
+
+ return $version;
+ }
+
+ // Method B: Try OCI label as fallback
+ $labelCommand = "docker inspect coolify-proxy --format '{{index .Config.Labels \"org.opencontainers.image.version\"}}' 2>/dev/null";
+ Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Trying OCI label");
+
+ $label = instant_remote_process([$labelCommand], $server, false);
+
+ if (! empty(trim($label))) {
+ // Extract version number from label (might have 'v' prefix)
+ if (preg_match('/(\d+\.\d+\.\d+)/', trim($label), $matches)) {
+ Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Detected from OCI label: {$matches[1]}");
+
+ return $matches[1];
+ }
+ }
+
+ Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Could not detect exact version");
+
+ return null;
+ } catch (\Exception $e) {
+ Log::error("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage());
+
+ return null;
+ }
+}
+
+function getTraefikVersionFromDockerCompose(Server $server): ?string
+{
+ try {
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Starting version detection");
+
+ // Try to get exact version from running container (e.g., "3.6.0")
+ $exactVersion = getExactTraefikVersionFromContainer($server);
+ if ($exactVersion) {
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Using exact version: {$exactVersion}");
+
+ return $exactVersion;
+ }
+
+ // Fallback: Check image tag (current method)
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Falling back to image tag detection");
+
+ $containerName = 'coolify-proxy';
+ $inspectCommand = "docker inspect {$containerName} --format '{{.Config.Image}}' 2>/dev/null";
+
+ $image = instant_remote_process([$inspectCommand], $server, false);
+
+ if (empty(trim($image))) {
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Container '{$containerName}' not found or not running");
+
+ return null;
+ }
+
+ $image = trim($image);
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Running container image: {$image}");
+
+ // Extract version from image string (e.g., "traefik:v3.6" or "traefik:3.6.0" or "traefik:latest")
+ if (preg_match('/traefik:(v?\d+\.\d+(?:\.\d+)?|latest)/i', $image, $matches)) {
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Extracted version from image tag: {$matches[1]}");
+
+ return $matches[1];
+ }
+
+ Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Image format doesn't match expected pattern: {$image}");
+
+ return null;
+ } catch (\Exception $e) {
+ Log::error("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage());
+
+ return null;
+ }
+}
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index a124272a2..3fff2c090 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -115,65 +115,170 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$resource->save();
}
- $serviceName = str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
- $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete();
- $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete();
+ // Extract SERVICE_URL and SERVICE_FQDN variable names from the compose template
+ // to ensure we use the exact names defined in the template (which may be abbreviated)
+ // IMPORTANT: Only extract variables that are DIRECTLY DECLARED for this service,
+ // not variables that are merely referenced from other services
+ $serviceConfig = data_get($dockerCompose, "services.{$name}");
+ $environment = data_get($serviceConfig, 'environment', []);
+ $templateVariableNames = [];
+
+ foreach ($environment as $key => $value) {
+ if (is_int($key) && is_string($value)) {
+ // List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value"
+ // Extract variable name (before '=' if present)
+ $envVarName = str($value)->before('=')->trim();
+ // Only include if it's a direct declaration (not a reference like ${VAR})
+ // Direct declarations look like: SERVICE_URL_APP or SERVICE_URL_APP_3000
+ // References look like: NEXT_PUBLIC_URL=${SERVICE_URL_APP}
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ $templateVariableNames[] = $envVarName->value();
+ }
+ } elseif (is_string($key)) {
+ // Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost"
+ $envVarName = str($key);
+ if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
+ $templateVariableNames[] = $envVarName->value();
+ }
+ }
+ // DO NOT extract variables that are only referenced with ${VAR_NAME} syntax
+ // Those belong to other services and will be updated when THOSE services are updated
+ }
+
+ // Remove duplicates
+ $templateVariableNames = array_unique($templateVariableNames);
+
+ // Extract unique service names to process (preserving the original case from template)
+ // This allows us to create both URL and FQDN pairs regardless of which one is in the template
+ $serviceNamesToProcess = [];
+ foreach ($templateVariableNames as $templateVarName) {
+ $parsed = parseServiceEnvironmentVariable($templateVarName);
+
+ // Extract the original service name with case preserved from the template
+ $strKey = str($templateVarName);
+ if ($parsed['has_port']) {
+ // For port-specific variables, get the name between SERVICE_URL_/SERVICE_FQDN_ and the last underscore
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
+ } else {
+ continue;
+ }
+ } else {
+ // For base variables, get everything after SERVICE_URL_/SERVICE_FQDN_
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->value();
+ } else {
+ continue;
+ }
+ }
+
+ // Use lowercase key for array indexing (to group case variations together)
+ $serviceKey = str($serviceName)->lower()->value();
+
+ // Track both base service name and port-specific variant
+ if (! isset($serviceNamesToProcess[$serviceKey])) {
+ $serviceNamesToProcess[$serviceKey] = [
+ 'base' => $serviceName, // Preserve original case
+ 'ports' => [],
+ ];
+ }
+
+ // If this variable has a port, track it
+ if ($parsed['has_port'] && $parsed['port']) {
+ $serviceNamesToProcess[$serviceKey]['ports'][] = $parsed['port'];
+ }
+ }
+
+ // Delete all existing SERVICE_URL and SERVICE_FQDN variables for these service names
+ // We need to delete both URL and FQDN variants, with and without ports
+ foreach ($serviceNamesToProcess as $serviceInfo) {
+ $serviceName = $serviceInfo['base'];
+
+ // Delete base variables
+ $resource->service->environment_variables()->where('key', "SERVICE_URL_{$serviceName}")->delete();
+ $resource->service->environment_variables()->where('key', "SERVICE_FQDN_{$serviceName}")->delete();
+
+ // Delete port-specific variables
+ foreach ($serviceInfo['ports'] as $port) {
+ $resource->service->environment_variables()->where('key', "SERVICE_URL_{$serviceName}_{$port}")->delete();
+ $resource->service->environment_variables()->where('key', "SERVICE_FQDN_{$serviceName}_{$port}")->delete();
+ }
+ }
if ($resource->fqdn) {
$resourceFqdns = str($resource->fqdn)->explode(',');
$resourceFqdns = $resourceFqdns->first();
- $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
$url = Url::fromString($resourceFqdns);
$port = $url->getPort();
$path = $url->getPath();
+
+ // Prepare URL value (with scheme and host)
$urlValue = $url->getScheme().'://'.$url->getHost();
$urlValue = ($path === '/') ? $urlValue : $urlValue.$path;
- $resource->service->environment_variables()->updateOrCreate([
- 'resourceable_type' => Service::class,
- 'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
- ], [
- 'value' => $urlValue,
- 'is_preview' => false,
- ]);
- if ($port) {
- $variableName = $variableName."_$port";
+
+ // Prepare FQDN value (host only, no scheme)
+ $fqdnHost = $url->getHost();
+ $fqdnValue = str($fqdnHost)->after('://');
+ if ($path !== '/') {
+ $fqdnValue = $fqdnValue.$path;
+ }
+
+ // For each service name found in template, create BOTH SERVICE_URL and SERVICE_FQDN pairs
+ foreach ($serviceNamesToProcess as $serviceInfo) {
+ $serviceName = $serviceInfo['base'];
+ $ports = array_unique($serviceInfo['ports']);
+
+ // ALWAYS create base pair (without port)
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
+ 'key' => "SERVICE_URL_{$serviceName}",
], [
'value' => $urlValue,
'is_preview' => false,
]);
- }
- $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
- $fqdn = Url::fromString($resourceFqdns);
- $port = $fqdn->getPort();
- $path = $fqdn->getPath();
- $fqdn = $fqdn->getHost();
- $fqdnValue = str($fqdn)->after('://');
- if ($path !== '/') {
- $fqdnValue = $fqdnValue.$path;
- }
- $resource->service->environment_variables()->updateOrCreate([
- 'resourceable_type' => Service::class,
- 'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
- ], [
- 'value' => $fqdnValue,
- 'is_preview' => false,
- ]);
- if ($port) {
- $variableName = $variableName."_$port";
+
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
- 'key' => $variableName,
+ 'key' => "SERVICE_FQDN_{$serviceName}",
], [
'value' => $fqdnValue,
'is_preview' => false,
]);
+
+ // Create port-specific pairs for each port found in template or FQDN
+ $allPorts = $ports;
+ if ($port && ! in_array($port, $allPorts)) {
+ $allPorts[] = $port;
+ }
+
+ foreach ($allPorts as $portNum) {
+ $urlWithPort = $urlValue.':'.$portNum;
+ $fqdnWithPort = $fqdnValue.':'.$portNum;
+
+ $resource->service->environment_variables()->updateOrCreate([
+ 'resourceable_type' => Service::class,
+ 'resourceable_id' => $resource->service_id,
+ 'key' => "SERVICE_URL_{$serviceName}_{$portNum}",
+ ], [
+ 'value' => $urlWithPort,
+ 'is_preview' => false,
+ ]);
+
+ $resource->service->environment_variables()->updateOrCreate([
+ 'resourceable_type' => Service::class,
+ 'resourceable_id' => $resource->service_id,
+ 'key' => "SERVICE_FQDN_{$serviceName}_{$portNum}",
+ ], [
+ 'value' => $fqdnWithPort,
+ 'is_preview' => false,
+ ]);
+ }
}
}
} catch (\Throwable $e) {
@@ -184,3 +289,53 @@ function serviceKeys()
{
return get_service_templates()->keys();
}
+
+/**
+ * Parse a SERVICE_URL_* or SERVICE_FQDN_* variable to extract the service name and port.
+ *
+ * This function detects if a service environment variable has a port suffix by checking
+ * if the last segment after the underscore is numeric.
+ *
+ * Examples:
+ * - SERVICE_URL_APP_3000 → ['service_name' => 'app', 'port' => '3000', 'has_port' => true]
+ * - SERVICE_URL_MY_API_8080 → ['service_name' => 'my_api', 'port' => '8080', 'has_port' => true]
+ * - SERVICE_URL_MY_APP → ['service_name' => 'my_app', 'port' => null, 'has_port' => false]
+ * - SERVICE_FQDN_REDIS_CACHE_6379 → ['service_name' => 'redis_cache', 'port' => '6379', 'has_port' => true]
+ *
+ * @param string $key The environment variable key (e.g., SERVICE_URL_APP_3000)
+ * @return array{service_name: string, port: string|null, has_port: bool} Parsed service information
+ */
+function parseServiceEnvironmentVariable(string $key): array
+{
+ $strKey = str($key);
+ $lastSegment = $strKey->afterLast('_')->value();
+ $hasPort = is_numeric($lastSegment) && ctype_digit($lastSegment);
+
+ if ($hasPort) {
+ // Port-specific variable (e.g., SERVICE_URL_APP_3000)
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->lower()->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
+ } else {
+ $serviceName = '';
+ }
+ $port = $lastSegment;
+ } else {
+ // Base variable without port (e.g., SERVICE_URL_APP)
+ if ($strKey->startsWith('SERVICE_URL_')) {
+ $serviceName = $strKey->after('SERVICE_URL_')->lower()->value();
+ } elseif ($strKey->startsWith('SERVICE_FQDN_')) {
+ $serviceName = $strKey->after('SERVICE_FQDN_')->lower()->value();
+ } else {
+ $serviceName = '';
+ }
+ $port = null;
+ }
+
+ return [
+ 'service_name' => $serviceName,
+ 'port' => $port,
+ 'has_port' => $hasPort,
+ ];
+}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 0f5b6f553..1b23247fa 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -241,10 +241,9 @@ function get_latest_sentinel_version(): string
function get_latest_version_of_coolify(): string
{
try {
- $versions = File::get(base_path('versions.json'));
- $versions = json_decode($versions, true);
+ $versions = get_versions_data();
- return data_get($versions, 'coolify.v4.version');
+ return data_get($versions, 'coolify.v4.version', '0.0.0');
} catch (\Throwable $e) {
return '0.0.0';
@@ -1353,52 +1352,71 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Decide if the service is a database
$image = data_get_str($service, 'image');
- $isDatabase = isDatabaseImage($image, $service);
- data_set($service, 'is_database', $isDatabase);
- // Create new serviceApplication or serviceDatabase
- if ($isDatabase) {
- if ($isNew) {
- $savedService = ServiceDatabase::create([
- 'name' => $serviceName,
- 'image' => $image,
- 'service_id' => $resource->id,
- ]);
- } else {
- $savedService = ServiceDatabase::where([
- 'name' => $serviceName,
- 'service_id' => $resource->id,
- ])->first();
- if (is_null($savedService)) {
+ // Check for manually migrated services first (respects user's conversion choice)
+ $migratedApp = ServiceApplication::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+ $migratedDb = ServiceDatabase::where('name', $serviceName)
+ ->where('service_id', $resource->id)
+ ->where('is_migrated', true)
+ ->first();
+
+ if ($migratedApp || $migratedDb) {
+ // Use the migrated service type, ignoring image detection
+ $isDatabase = (bool) $migratedDb;
+ $savedService = $migratedApp ?: $migratedDb;
+ } else {
+ // Use image detection for non-migrated services
+ $isDatabase = isDatabaseImage($image, $service);
+
+ // Create new serviceApplication or serviceDatabase
+ if ($isDatabase) {
+ if ($isNew) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
+ } else {
+ $savedService = ServiceDatabase::where([
+ 'name' => $serviceName,
+ 'service_id' => $resource->id,
+ ])->first();
+ if (is_null($savedService)) {
+ $savedService = ServiceDatabase::create([
+ 'name' => $serviceName,
+ 'image' => $image,
+ 'service_id' => $resource->id,
+ ]);
+ }
}
- }
- } else {
- if ($isNew) {
- $savedService = ServiceApplication::create([
- 'name' => $serviceName,
- 'image' => $image,
- 'service_id' => $resource->id,
- ]);
} else {
- $savedService = ServiceApplication::where([
- 'name' => $serviceName,
- 'service_id' => $resource->id,
- ])->first();
- if (is_null($savedService)) {
+ if ($isNew) {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
+ } else {
+ $savedService = ServiceApplication::where([
+ 'name' => $serviceName,
+ 'service_id' => $resource->id,
+ ])->first();
+ if (is_null($savedService)) {
+ $savedService = ServiceApplication::create([
+ 'name' => $serviceName,
+ 'image' => $image,
+ 'service_id' => $resource->id,
+ ]);
+ }
}
}
}
+ data_set($service, 'is_database', $isDatabase);
+
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
@@ -2879,6 +2897,18 @@ function instanceSettings()
return InstanceSettings::get();
}
+function getHelperVersion(): string
+{
+ $settings = instanceSettings();
+
+ // In development mode, use the dev_helper_version if set, otherwise fallback to config
+ if (isDev() && ! empty($settings->dev_helper_version)) {
+ return $settings->dev_helper_version;
+ }
+
+ return config('constants.coolify.helper_version');
+}
+
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
{
$server = Server::find($server_id)->where('team_id', $team_id)->first();
@@ -3123,3 +3153,158 @@ function generateDockerComposeServiceName(mixed $services, int $pullRequestId =
return $collection;
}
+
+function formatBytes(?int $bytes, int $precision = 2): string
+{
+ if ($bytes === null || $bytes === 0) {
+ return '0 B';
+ }
+
+ // Handle negative numbers
+ if ($bytes < 0) {
+ return '0 B';
+ }
+
+ $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+ $base = 1024;
+ $exponent = floor(log($bytes) / log($base));
+ $exponent = min($exponent, count($units) - 1);
+
+ $value = $bytes / pow($base, $exponent);
+
+ return round($value, $precision).' '.$units[$exponent];
+}
+
+/**
+ * Validates that a file path is safely within the /tmp/ directory.
+ * Protects against path traversal attacks by resolving the real path
+ * and verifying it stays within /tmp/.
+ *
+ * Note: On macOS, /tmp is often a symlink to /private/tmp, which is handled.
+ */
+function isSafeTmpPath(?string $path): bool
+{
+ if (blank($path)) {
+ return false;
+ }
+
+ // URL decode to catch encoded traversal attempts
+ $decodedPath = urldecode($path);
+
+ // Minimum length check - /tmp/x is 6 chars
+ if (strlen($decodedPath) < 6) {
+ return false;
+ }
+
+ // Must start with /tmp/
+ if (! str($decodedPath)->startsWith('/tmp/')) {
+ return false;
+ }
+
+ // Quick check for obvious traversal attempts
+ if (str($decodedPath)->contains('..')) {
+ return false;
+ }
+
+ // Check for null bytes (directory traversal technique)
+ if (str($decodedPath)->contains("\0")) {
+ return false;
+ }
+
+ // Remove any trailing slashes for consistent validation
+ $normalizedPath = rtrim($decodedPath, '/');
+
+ // Normalize the path by removing redundant separators and resolving . and ..
+ // We'll do this manually since realpath() requires the path to exist
+ $parts = explode('/', $normalizedPath);
+ $resolvedParts = [];
+
+ foreach ($parts as $part) {
+ if ($part === '' || $part === '.') {
+ // Skip empty parts (from //) and current directory references
+ continue;
+ } elseif ($part === '..') {
+ // Parent directory - this should have been caught earlier but double-check
+ return false;
+ } else {
+ $resolvedParts[] = $part;
+ }
+ }
+
+ $resolvedPath = '/'.implode('/', $resolvedParts);
+
+ // Final check: resolved path must start with /tmp/
+ // And must have at least one component after /tmp/
+ if (! str($resolvedPath)->startsWith('/tmp/') || $resolvedPath === '/tmp') {
+ return false;
+ }
+
+ // Resolve the canonical /tmp path (handles symlinks like /tmp -> /private/tmp on macOS)
+ $canonicalTmpPath = realpath('/tmp');
+ if ($canonicalTmpPath === false) {
+ // If /tmp doesn't exist, something is very wrong, but allow non-existing paths
+ $canonicalTmpPath = '/tmp';
+ }
+
+ // Calculate dirname once to avoid redundant calls
+ $dirPath = dirname($resolvedPath);
+
+ // If the directory exists, resolve it via realpath to catch symlink attacks
+ if (is_dir($dirPath)) {
+ // For existing paths, resolve to absolute path to catch symlinks
+ $realDir = realpath($dirPath);
+ if ($realDir === false) {
+ return false;
+ }
+
+ // Check if the real directory is within /tmp (or its canonical path)
+ if (! str($realDir)->startsWith('/tmp') && ! str($realDir)->startsWith($canonicalTmpPath)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Transform colon-delimited status format to human-readable parentheses format.
+ *
+ * Handles Docker container status formats with optional health check status and exclusion modifiers.
+ *
+ * Examples:
+ * - running:healthy → Running (healthy)
+ * - running:unhealthy:excluded → Running (unhealthy, excluded)
+ * - exited:excluded → Exited (excluded)
+ * - Proxy:running → Proxy:running (preserved as-is for headline formatting)
+ * - running → Running
+ *
+ * @param string $status The status string to format
+ * @return string The formatted status string
+ */
+function formatContainerStatus(string $status): string
+{
+ // Preserve Proxy statuses as-is (they follow different format)
+ if (str($status)->startsWith('Proxy')) {
+ return str($status)->headline()->value();
+ }
+
+ // Check for :excluded suffix
+ $isExcluded = str($status)->endsWith(':excluded');
+ $parts = explode(':', $status);
+
+ if ($isExcluded) {
+ if (count($parts) === 3) {
+ // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded)
+ return str($parts[0])->headline().' ('.$parts[1].', excluded)';
+ } else {
+ // No health status: exited:excluded → Exited (excluded)
+ return str($parts[0])->headline().' (excluded)';
+ }
+ } elseif (count($parts) >= 2) {
+ // Regular colon format: running:healthy → Running (healthy)
+ return str($parts[0])->headline().' ('.$parts[1].')';
+ } else {
+ // Simple status: running → Running
+ return str($status)->headline()->value();
+ }
+}
diff --git a/bootstrap/helpers/sudo.php b/bootstrap/helpers/sudo.php
index ba252c64f..f7336beeb 100644
--- a/bootstrap/helpers/sudo.php
+++ b/bootstrap/helpers/sudo.php
@@ -58,16 +58,35 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
$commands = $commands->map(function ($line) {
$line = str($line);
+
+ // Detect complex piped commands that should be wrapped in bash -c
+ $isComplexPipeCommand = (
+ $line->contains(' | sh') ||
+ $line->contains(' | bash') ||
+ ($line->contains(' | ') && ($line->contains('||') || $line->contains('&&')))
+ );
+
+ // If it's a complex pipe command and starts with sudo, wrap it in bash -c
+ if ($isComplexPipeCommand && $line->startsWith('sudo ')) {
+ $commandWithoutSudo = $line->after('sudo ')->value();
+ // Escape single quotes for bash -c by replacing ' with '\''
+ $escapedCommand = str_replace("'", "'\\''", $commandWithoutSudo);
+
+ return "sudo bash -c '$escapedCommand'";
+ }
+
+ // For non-complex commands, apply the original logic
if (str($line)->contains('$(')) {
$line = $line->replace('$(', '$(sudo ');
}
- if (str($line)->contains('||')) {
+ if (! $isComplexPipeCommand && str($line)->contains('||')) {
$line = $line->replace('||', '|| sudo');
}
- if (str($line)->contains('&&')) {
+ if (! $isComplexPipeCommand && str($line)->contains('&&')) {
$line = $line->replace('&&', '&& sudo');
}
- if (str($line)->contains(' | ')) {
+ // Don't insert sudo into pipes for complex commands
+ if (! $isComplexPipeCommand && str($line)->contains(' | ')) {
$line = $line->replace(' | ', ' | sudo ');
}
diff --git a/bootstrap/helpers/versions.php b/bootstrap/helpers/versions.php
new file mode 100644
index 000000000..bb4694de5
--- /dev/null
+++ b/bootstrap/helpers/versions.php
@@ -0,0 +1,53 @@
+ '3.5.6'])
+ */
+function get_traefik_versions(): ?array
+{
+ $versions = get_versions_data();
+
+ if (! $versions) {
+ return null;
+ }
+
+ $traefikVersions = data_get($versions, 'traefik');
+
+ return is_array($traefikVersions) ? $traefikVersions : null;
+}
+
+/**
+ * Invalidate the versions cache.
+ * Call this after updating versions.json to ensure fresh data is loaded.
+ */
+function invalidate_versions_cache(): void
+{
+ Cache::forget('coolify:versions:all');
+}
diff --git a/composer.json b/composer.json
index ea466049d..1db389a57 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,7 @@
"poliander/cron": "^3.2.1",
"purplepixie/phpdns": "^2.2",
"pusher/pusher-php-server": "^7.2.7",
- "resend/resend-laravel": "^0.19.0",
+ "resend/resend-laravel": "^0.20.0",
"sentry/sentry-laravel": "^4.15.1",
"socialiteproviders/authentik": "^5.2",
"socialiteproviders/clerk": "^5.0",
diff --git a/composer.lock b/composer.lock
index 6320db071..b2923a240 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a993799242581bd06b5939005ee458d9",
+ "content-hash": "423b7d10901b9f31c926d536ff163a22",
"packages": [
{
"name": "amphp/amp",
@@ -7048,16 +7048,16 @@
},
{
"name": "resend/resend-laravel",
- "version": "v0.19.0",
+ "version": "v0.20.0",
"source": {
"type": "git",
"url": "https://github.com/resend/resend-laravel.git",
- "reference": "ce11e363c42c1d6b93983dfebbaba3f906863c3a"
+ "reference": "f32c2f484df2bc65fba8ea9ab9b210cd42d9f3ed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/resend/resend-laravel/zipball/ce11e363c42c1d6b93983dfebbaba3f906863c3a",
- "reference": "ce11e363c42c1d6b93983dfebbaba3f906863c3a",
+ "url": "https://api.github.com/repos/resend/resend-laravel/zipball/f32c2f484df2bc65fba8ea9ab9b210cd42d9f3ed",
+ "reference": "f32c2f484df2bc65fba8ea9ab9b210cd42d9f3ed",
"shasum": ""
},
"require": {
@@ -7111,9 +7111,9 @@
],
"support": {
"issues": "https://github.com/resend/resend-laravel/issues",
- "source": "https://github.com/resend/resend-laravel/tree/v0.19.0"
+ "source": "https://github.com/resend/resend-laravel/tree/v0.20.0"
},
- "time": "2025-05-06T21:36:51+00:00"
+ "time": "2025-08-04T19:26:47+00:00"
},
{
"name": "resend/resend-php",
@@ -9514,16 +9514,16 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.3.2",
+ "version": "v7.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6"
+ "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6",
- "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4",
+ "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4",
"shasum": ""
},
"require": {
@@ -9573,7 +9573,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.3.2"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.3.7"
},
"funding": [
{
@@ -9593,7 +9593,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:47:49+00:00"
+ "time": "2025-11-08T16:41:12+00:00"
},
{
"name": "symfony/http-kernel",
@@ -9799,16 +9799,16 @@
},
{
"name": "symfony/mime",
- "version": "v7.3.2",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1"
+ "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1",
- "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
+ "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
"shasum": ""
},
"require": {
@@ -9863,7 +9863,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.3.2"
+ "source": "https://github.com/symfony/mime/tree/v7.3.4"
},
"funding": [
{
@@ -9883,7 +9883,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-15T13:41:35+00:00"
+ "time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/options-resolver",
@@ -10195,7 +10195,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -10258,7 +10258,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -10269,6 +10269,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10278,7 +10282,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -10339,7 +10343,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -10350,6 +10354,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10359,7 +10367,7 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -10420,7 +10428,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -10431,6 +10439,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10440,7 +10452,7 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@@ -10500,7 +10512,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -10511,6 +10523,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -10520,16 +10536,16 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"shasum": ""
},
"require": {
@@ -10576,7 +10592,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
},
"funding": [
{
@@ -10587,12 +10603,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-07-08T02:45:35+00:00"
},
{
"name": "symfony/polyfill-uuid",
diff --git a/conductor.json b/conductor.json
index 851d13ed0..688de3a90 100644
--- a/conductor.json
+++ b/conductor.json
@@ -1,7 +1,7 @@
{
"scripts": {
"setup": "./scripts/conductor-setup.sh",
- "run": "docker rm -f coolify coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite; spin up; spin down"
+ "run": "docker rm -f coolify coolify-minio-init coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite; spin up; spin down"
},
"runScriptMode": "nonconcurrent"
-}
+}
\ No newline at end of file
diff --git a/config/constants.php b/config/constants.php
index 503fe3808..24aae9c81 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,8 +2,8 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.439',
- 'helper_version' => '1.0.11',
+ 'version' => '4.0.0-beta.448',
+ 'helper_version' => '1.0.12',
'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
@@ -12,7 +12,7 @@
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
- 'releases_url' => 'https://cdn.coollabs.io/coolify/releases.json',
+ 'releases_url' => 'https://cdn.coolify.io/releases.json',
],
'urls' => [
@@ -95,4 +95,27 @@
'storage_api_key' => env('BUNNY_STORAGE_API_KEY'),
'api_key' => env('BUNNY_API_KEY'),
],
+
+ 'server_checks' => [
+ // Notification delay configuration for parallel server checks
+ // Used for Traefik version checks and other future server check jobs
+ // These settings control how long to wait before sending notifications
+ // after dispatching parallel check jobs for all servers
+
+ // Minimum delay in seconds (120s = 2 minutes)
+ // Accounts for job processing time, retries, and network latency
+ 'notification_delay_min' => 120,
+
+ // Maximum delay in seconds (300s = 5 minutes)
+ // Prevents excessive waiting for very large server counts
+ 'notification_delay_max' => 300,
+
+ // Scaling factor: seconds to add per server (0.2)
+ // Formula: delay = min(max, max(min, serverCount * scaling))
+ // Examples:
+ // - 100 servers: 120s (uses minimum)
+ // - 1000 servers: 200s
+ // - 2000 servers: 300s (hits maximum)
+ 'notification_delay_scaling' => 0.2,
+ ],
];
diff --git a/config/logging.php b/config/logging.php
index 488327414..1a75978f3 100644
--- a/config/logging.php
+++ b/config/logging.php
@@ -129,8 +129,8 @@
'scheduled-errors' => [
'driver' => 'daily',
'path' => storage_path('logs/scheduled-errors.log'),
- 'level' => 'debug',
- 'days' => 7,
+ 'level' => 'warning',
+ 'days' => 14,
],
],
diff --git a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php
deleted file mode 100644
index fe216a57d..000000000
--- a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php
+++ /dev/null
@@ -1,32 +0,0 @@
-id();
- $table->foreignId('team_id')->constrained()->onDelete('cascade');
- $table->string('name');
- $table->text('script'); // Encrypted in the model
- $table->timestamps();
-
- $table->index('team_id');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('cloud_init_scripts');
- }
-};
diff --git a/database/migrations/2025_10_10_120000_create_webhook_notification_settings_table.php b/database/migrations/2025_10_10_120000_create_webhook_notification_settings_table.php
deleted file mode 100644
index a3edacbf9..000000000
--- a/database/migrations/2025_10_10_120000_create_webhook_notification_settings_table.php
+++ /dev/null
@@ -1,46 +0,0 @@
-id();
- $table->foreignId('team_id')->constrained()->cascadeOnDelete();
-
- $table->boolean('webhook_enabled')->default(false);
- $table->text('webhook_url')->nullable();
-
- $table->boolean('deployment_success_webhook_notifications')->default(false);
- $table->boolean('deployment_failure_webhook_notifications')->default(true);
- $table->boolean('status_change_webhook_notifications')->default(false);
- $table->boolean('backup_success_webhook_notifications')->default(false);
- $table->boolean('backup_failure_webhook_notifications')->default(true);
- $table->boolean('scheduled_task_success_webhook_notifications')->default(false);
- $table->boolean('scheduled_task_failure_webhook_notifications')->default(true);
- $table->boolean('docker_cleanup_success_webhook_notifications')->default(false);
- $table->boolean('docker_cleanup_failure_webhook_notifications')->default(true);
- $table->boolean('server_disk_usage_webhook_notifications')->default(true);
- $table->boolean('server_reachable_webhook_notifications')->default(false);
- $table->boolean('server_unreachable_webhook_notifications')->default(true);
- $table->boolean('server_patch_webhook_notifications')->default(false);
-
- $table->unique(['team_id']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('webhook_notification_settings');
- }
-};
diff --git a/database/migrations/2025_10_10_120001_populate_webhook_notification_settings_for_existing_teams.php b/database/migrations/2025_10_10_120001_populate_webhook_notification_settings_for_existing_teams.php
deleted file mode 100644
index de2707557..000000000
--- a/database/migrations/2025_10_10_120001_populate_webhook_notification_settings_for_existing_teams.php
+++ /dev/null
@@ -1,47 +0,0 @@
-get();
-
- foreach ($teams as $team) {
- DB::table('webhook_notification_settings')->updateOrInsert(
- ['team_id' => $team->id],
- [
- 'webhook_enabled' => false,
- 'webhook_url' => null,
- 'deployment_success_webhook_notifications' => false,
- 'deployment_failure_webhook_notifications' => true,
- 'status_change_webhook_notifications' => false,
- 'backup_success_webhook_notifications' => false,
- 'backup_failure_webhook_notifications' => true,
- 'scheduled_task_success_webhook_notifications' => false,
- 'scheduled_task_failure_webhook_notifications' => true,
- 'docker_cleanup_success_webhook_notifications' => false,
- 'docker_cleanup_failure_webhook_notifications' => true,
- 'server_disk_usage_webhook_notifications' => true,
- 'server_reachable_webhook_notifications' => false,
- 'server_unreachable_webhook_notifications' => true,
- 'server_patch_webhook_notifications' => false,
- ]
- );
- }
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- // We don't need to do anything in down() since the webhook_notification_settings
- // table will be dropped by the create migration's down() method
- }
-};
diff --git a/database/migrations/2025_11_02_161923_add_dev_helper_version_to_instance_settings.php b/database/migrations/2025_11_02_161923_add_dev_helper_version_to_instance_settings.php
new file mode 100644
index 000000000..56ed2239a
--- /dev/null
+++ b/database/migrations/2025_11_02_161923_add_dev_helper_version_to_instance_settings.php
@@ -0,0 +1,28 @@
+string('dev_helper_version')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('instance_settings', function (Blueprint $table) {
+ $table->dropColumn('dev_helper_version');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php b/database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php
new file mode 100644
index 000000000..067861e16
--- /dev/null
+++ b/database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php
@@ -0,0 +1,28 @@
+integer('timeout')->default(300)->after('frequency');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('scheduled_tasks', function (Blueprint $table) {
+ $table->dropColumn('timeout');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php b/database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php
new file mode 100644
index 000000000..14fdd5998
--- /dev/null
+++ b/database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php
@@ -0,0 +1,31 @@
+timestamp('started_at')->nullable()->after('scheduled_task_id');
+ $table->integer('retry_count')->default(0)->after('status');
+ $table->decimal('duration', 10, 2)->nullable()->after('retry_count')->comment('Duration in seconds');
+ $table->text('error_details')->nullable()->after('message');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('scheduled_task_executions', function (Blueprint $table) {
+ $table->dropColumn(['started_at', 'retry_count', 'duration', 'error_details']);
+ });
+ }
+};
diff --git a/database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php b/database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php
new file mode 100644
index 000000000..329ac7af9
--- /dev/null
+++ b/database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php
@@ -0,0 +1,30 @@
+integer('restart_count')->default(0)->after('status');
+ $table->timestamp('last_restart_at')->nullable()->after('restart_count');
+ $table->string('last_restart_type', 10)->nullable()->after('last_restart_at');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn(['restart_count', 'last_restart_at', 'last_restart_type']);
+ });
+ }
+};
diff --git a/database/migrations/2025_11_12_130931_add_traefik_version_tracking_to_servers_table.php b/database/migrations/2025_11_12_130931_add_traefik_version_tracking_to_servers_table.php
new file mode 100644
index 000000000..3bab33368
--- /dev/null
+++ b/database/migrations/2025_11_12_130931_add_traefik_version_tracking_to_servers_table.php
@@ -0,0 +1,28 @@
+string('detected_traefik_version')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropColumn('detected_traefik_version');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_12_131252_add_traefik_outdated_to_email_notification_settings.php b/database/migrations/2025_11_12_131252_add_traefik_outdated_to_email_notification_settings.php
new file mode 100644
index 000000000..ac509dc71
--- /dev/null
+++ b/database/migrations/2025_11_12_131252_add_traefik_outdated_to_email_notification_settings.php
@@ -0,0 +1,28 @@
+boolean('traefik_outdated_email_notifications')->default(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('email_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_email_notifications');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_12_133400_add_traefik_outdated_thread_id_to_telegram_notification_settings.php b/database/migrations/2025_11_12_133400_add_traefik_outdated_thread_id_to_telegram_notification_settings.php
new file mode 100644
index 000000000..b7d69e634
--- /dev/null
+++ b/database/migrations/2025_11_12_133400_add_traefik_outdated_thread_id_to_telegram_notification_settings.php
@@ -0,0 +1,28 @@
+text('telegram_notifications_traefik_outdated_thread_id')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('telegram_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('telegram_notifications_traefik_outdated_thread_id');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_14_114632_add_traefik_outdated_info_to_servers_table.php b/database/migrations/2025_11_14_114632_add_traefik_outdated_info_to_servers_table.php
new file mode 100644
index 000000000..99e10707d
--- /dev/null
+++ b/database/migrations/2025_11_14_114632_add_traefik_outdated_info_to_servers_table.php
@@ -0,0 +1,28 @@
+json('traefik_outdated_info')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_info');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_16_000001_create_webhook_notification_settings_table.php b/database/migrations/2025_11_16_000001_create_webhook_notification_settings_table.php
new file mode 100644
index 000000000..9e9a6303f
--- /dev/null
+++ b/database/migrations/2025_11_16_000001_create_webhook_notification_settings_table.php
@@ -0,0 +1,88 @@
+id();
+ $table->foreignId('team_id')->constrained()->cascadeOnDelete();
+
+ $table->boolean('webhook_enabled')->default(false);
+ $table->text('webhook_url')->nullable();
+
+ $table->boolean('deployment_success_webhook_notifications')->default(false);
+ $table->boolean('deployment_failure_webhook_notifications')->default(true);
+ $table->boolean('status_change_webhook_notifications')->default(false);
+ $table->boolean('backup_success_webhook_notifications')->default(false);
+ $table->boolean('backup_failure_webhook_notifications')->default(true);
+ $table->boolean('scheduled_task_success_webhook_notifications')->default(false);
+ $table->boolean('scheduled_task_failure_webhook_notifications')->default(true);
+ $table->boolean('docker_cleanup_success_webhook_notifications')->default(false);
+ $table->boolean('docker_cleanup_failure_webhook_notifications')->default(true);
+ $table->boolean('server_disk_usage_webhook_notifications')->default(true);
+ $table->boolean('server_reachable_webhook_notifications')->default(false);
+ $table->boolean('server_unreachable_webhook_notifications')->default(true);
+ $table->boolean('server_patch_webhook_notifications')->default(false);
+
+ $table->unique(['team_id']);
+ });
+ }
+
+ // Populate webhook notification settings for existing teams (only if they don't already have settings)
+ DB::table('teams')->chunkById(100, function ($teams) {
+ foreach ($teams as $team) {
+ try {
+ // Check if settings already exist for this team
+ $exists = DB::table('webhook_notification_settings')
+ ->where('team_id', $team->id)
+ ->exists();
+
+ if (! $exists) {
+ // Only insert if no settings exist - don't overwrite existing preferences
+ DB::table('webhook_notification_settings')->insert([
+ 'team_id' => $team->id,
+ 'webhook_enabled' => false,
+ 'webhook_url' => null,
+ 'deployment_success_webhook_notifications' => false,
+ 'deployment_failure_webhook_notifications' => true,
+ 'status_change_webhook_notifications' => false,
+ 'backup_success_webhook_notifications' => false,
+ 'backup_failure_webhook_notifications' => true,
+ 'scheduled_task_success_webhook_notifications' => false,
+ 'scheduled_task_failure_webhook_notifications' => true,
+ 'docker_cleanup_success_webhook_notifications' => false,
+ 'docker_cleanup_failure_webhook_notifications' => true,
+ 'server_disk_usage_webhook_notifications' => true,
+ 'server_reachable_webhook_notifications' => false,
+ 'server_unreachable_webhook_notifications' => true,
+ 'server_patch_webhook_notifications' => false,
+ 'traefik_outdated_webhook_notifications' => true,
+ ]);
+ }
+ } catch (\Throwable $e) {
+ Log::error('Error creating webhook notification settings for team '.$team->id.': '.$e->getMessage());
+ }
+ }
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('webhook_notification_settings');
+ }
+};
diff --git a/database/migrations/2025_11_16_000002_create_cloud_init_scripts_table.php b/database/migrations/2025_11_16_000002_create_cloud_init_scripts_table.php
new file mode 100644
index 000000000..11c5b99a3
--- /dev/null
+++ b/database/migrations/2025_11_16_000002_create_cloud_init_scripts_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->foreignId('team_id')->constrained()->cascadeOnDelete();
+ $table->string('name');
+ $table->text('script'); // Encrypted in the model
+ $table->timestamps();
+ });
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('cloud_init_scripts');
+ }
+};
diff --git a/database/migrations/2025_11_17_092707_add_traefik_outdated_to_notification_settings.php b/database/migrations/2025_11_17_092707_add_traefik_outdated_to_notification_settings.php
new file mode 100644
index 000000000..b5cad28b0
--- /dev/null
+++ b/database/migrations/2025_11_17_092707_add_traefik_outdated_to_notification_settings.php
@@ -0,0 +1,60 @@
+boolean('traefik_outdated_discord_notifications')->default(true);
+ });
+
+ Schema::table('slack_notification_settings', function (Blueprint $table) {
+ $table->boolean('traefik_outdated_slack_notifications')->default(true);
+ });
+
+ Schema::table('webhook_notification_settings', function (Blueprint $table) {
+ $table->boolean('traefik_outdated_webhook_notifications')->default(true);
+ });
+
+ Schema::table('telegram_notification_settings', function (Blueprint $table) {
+ $table->boolean('traefik_outdated_telegram_notifications')->default(true);
+ });
+
+ Schema::table('pushover_notification_settings', function (Blueprint $table) {
+ $table->boolean('traefik_outdated_pushover_notifications')->default(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('discord_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_discord_notifications');
+ });
+
+ Schema::table('slack_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_slack_notifications');
+ });
+
+ Schema::table('webhook_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_webhook_notifications');
+ });
+
+ Schema::table('telegram_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_telegram_notifications');
+ });
+
+ Schema::table('pushover_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('traefik_outdated_pushover_notifications');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_18_083747_cleanup_dockerfile_data_for_non_dockerfile_buildpacks.php b/database/migrations/2025_11_18_083747_cleanup_dockerfile_data_for_non_dockerfile_buildpacks.php
new file mode 100644
index 000000000..959662cd5
--- /dev/null
+++ b/database/migrations/2025_11_18_083747_cleanup_dockerfile_data_for_non_dockerfile_buildpacks.php
@@ -0,0 +1,31 @@
+where('build_pack', '!=', 'dockerfile')
+ ->update([
+ 'dockerfile' => null,
+ 'dockerfile_location' => null,
+ 'dockerfile_target_build' => null,
+ 'custom_healthcheck_found' => false,
+ ]);
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ // No rollback needed - we're cleaning up corrupt data
+ }
+};
diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php
index adada458e..511af1a9f 100644
--- a/database/seeders/ProductionSeeder.php
+++ b/database/seeders/ProductionSeeder.php
@@ -113,6 +113,8 @@ public function run(): void
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
+ 'last_saved_settings' => null,
+ 'last_applied_settings' => null,
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
@@ -177,6 +179,8 @@ public function run(): void
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
+ 'last_saved_settings' => null,
+ 'last_applied_settings' => null,
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
diff --git a/database/seeders/S3StorageSeeder.php b/database/seeders/S3StorageSeeder.php
index de7cef6dc..9fa531447 100644
--- a/database/seeders/S3StorageSeeder.php
+++ b/database/seeders/S3StorageSeeder.php
@@ -20,6 +20,7 @@ public function run(): void
'bucket' => 'local',
'endpoint' => 'http://coolify-minio:9000',
'team_id' => 0,
+ 'is_usable' => true,
]);
}
}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index d76c91aa2..4f41f1c63 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -118,6 +118,26 @@ services:
- dev_minio_data:/data
networks:
- coolify
+ minio-init:
+ image: minio/mc:latest
+ pull_policy: always
+ container_name: coolify-minio-init
+ restart: no
+ depends_on:
+ - minio
+ entrypoint: >
+ /bin/sh -c "
+ echo 'Waiting for MinIO to be ready...';
+ until mc alias set local http://coolify-minio:9000 minioadmin minioadmin 2>/dev/null; do
+ echo 'MinIO not ready yet, waiting...';
+ sleep 2;
+ done;
+ echo 'MinIO is ready, creating bucket if needed...';
+ mc mb local/local --ignore-existing;
+ echo 'MinIO initialization complete - bucket local is ready';
+ "
+ networks:
+ - coolify
volumes:
dev_backups_data:
diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile
index 212703798..14879eb96 100644
--- a/docker/coolify-helper/Dockerfile
+++ b/docker/coolify-helper/Dockerfile
@@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.25.0
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.38.2
# https://github.com/railwayapp/nixpacks/releases
-ARG NIXPACKS_VERSION=1.40.0
+ARG NIXPACKS_VERSION=1.41.0
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2025-08-13T08-35-41Z
diff --git a/docker/development/etc/nginx/site-opts.d/http.conf b/docker/development/etc/nginx/site-opts.d/http.conf
index a5bbd78a3..d7855ae80 100644
--- a/docker/development/etc/nginx/site-opts.d/http.conf
+++ b/docker/development/etc/nginx/site-opts.d/http.conf
@@ -13,6 +13,9 @@ charset utf-8;
# Set max upload to 2048M
client_max_body_size 2048M;
+# Set client body buffer to handle Sentinel payloads in memory
+client_body_buffer_size 256k;
+
# Healthchecks: Set /healthcheck to be the healthcheck URL
location /healthcheck {
access_log off;
diff --git a/docker/production/etc/nginx/site-opts.d/http.conf b/docker/production/etc/nginx/site-opts.d/http.conf
index a5bbd78a3..d7855ae80 100644
--- a/docker/production/etc/nginx/site-opts.d/http.conf
+++ b/docker/production/etc/nginx/site-opts.d/http.conf
@@ -13,6 +13,9 @@ charset utf-8;
# Set max upload to 2048M
client_max_body_size 2048M;
+# Set client body buffer to handle Sentinel payloads in memory
+client_body_buffer_size 256k;
+
# Healthchecks: Set /healthcheck to be the healthcheck URL
location /healthcheck {
access_log off;
diff --git a/gcool.json b/gcool.json
deleted file mode 100644
index 629d8569a..000000000
--- a/gcool.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "scripts": {
- "onWorktreeCreate": "cp $GCOOL_ROOT_PATH/.env .",
- "run": "docker rm -f coolify coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite && spin up"
- }
-}
diff --git a/jean.json b/jean.json
new file mode 100644
index 000000000..4e5c788ed
--- /dev/null
+++ b/jean.json
@@ -0,0 +1,5 @@
+{
+ "scripts": {
+ "setup": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json"
+ }
+}
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index 2e5cc5e84..e946d3bb6 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,19 +1,29 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.435"
+ "version": "4.0.0-beta.448"
},
"nightly": {
- "version": "4.0.0-beta.436"
+ "version": "4.0.0-beta.449"
},
"helper": {
- "version": "1.0.11"
+ "version": "1.0.12"
},
"realtime": {
"version": "1.0.10"
},
"sentinel": {
- "version": "0.0.16"
+ "version": "0.0.18"
}
+ },
+ "traefik": {
+ "v3.6": "3.6.1",
+ "v3.5": "3.5.6",
+ "v3.4": "3.4.5",
+ "v3.3": "3.3.7",
+ "v3.2": "3.2.5",
+ "v3.1": "3.1.7",
+ "v3.0": "3.0.4",
+ "v2.11": "2.11.31"
}
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9e8fe7328..b076800e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2664,11 +2664,11 @@
}
},
"node_modules/tar": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
- "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
+ "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
diff --git a/public/svgs/opnform.svg b/public/svgs/opnform.svg
new file mode 100644
index 000000000..70562a4bf
--- /dev/null
+++ b/public/svgs/opnform.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/palworld.svg b/public/svgs/palworld.svg
new file mode 100644
index 000000000..f5fff5bc8
--- /dev/null
+++ b/public/svgs/palworld.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/pangolin-logo.png b/public/svgs/pangolin-logo.png
new file mode 100644
index 000000000..fb7a252d9
Binary files /dev/null and b/public/svgs/pangolin-logo.png differ
diff --git a/public/svgs/postgresus.svg b/public/svgs/postgresus.svg
new file mode 100644
index 000000000..a45e81167
--- /dev/null
+++ b/public/svgs/postgresus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/tailscale.svg b/public/svgs/tailscale.svg
new file mode 100644
index 000000000..cde7dbd50
--- /dev/null
+++ b/public/svgs/tailscale.svg
@@ -0,0 +1,7 @@
+
+
+ Tailscale Streamline Icon: https://streamlinehq.com
+
+ Tailscale
+
+
\ No newline at end of file
diff --git a/resources/css/app.css b/resources/css/app.css
index fa1e61cb2..70759e542 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -82,7 +82,7 @@ @keyframes lds-heart {
*/
html,
body {
- @apply w-full min-h-full bg-neutral-50 dark:bg-base dark:text-neutral-400;
+ @apply w-full min-h-full bg-gray-50 dark:bg-base dark:text-neutral-400;
}
body {
diff --git a/resources/css/utilities.css b/resources/css/utilities.css
index f819280d5..2899ea1e5 100644
--- a/resources/css/utilities.css
+++ b/resources/css/utilities.css
@@ -32,7 +32,20 @@ @utility apexcharts-tooltip-custom-title {
}
@utility input-sticky {
- @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
+ @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 dark:bg-coolgray-100 dark:text-white disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 focus-visible:outline-none;
+ box-shadow: inset 4px 0 0 transparent, inset 0 0 0 1px #e5e5e5;
+
+ &:where(.dark, .dark *) {
+ box-shadow: inset 4px 0 0 transparent, inset 0 0 0 1px #242424;
+ }
+
+ &:focus-visible {
+ box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 1px #e5e5e5;
+ }
+
+ &:where(.dark, .dark *):focus-visible {
+ box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 1px #242424;
+ }
}
@utility input-sticky-active {
@@ -46,20 +59,49 @@ @utility input-focus {
/* input, select before */
@utility input-select {
- @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-2 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 dark:disabled:ring-transparent;
+ @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 dark:bg-coolgray-100 dark:text-white disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40;
+ box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;
+
+ &:where(.dark, .dark *) {
+ box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #242424;
+ }
+
+ &:disabled {
+ box-shadow: none;
+ }
+
+ &:where(.dark, .dark *):disabled {
+ box-shadow: none;
+ }
}
/* Readonly */
@utility input {
- @apply dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200;
+ @apply dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200;
@apply input-select;
- @apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
+ @apply focus-visible:outline-none;
+
+ &:focus-visible {
+ box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
+ }
+
+ &:where(.dark, .dark *):focus-visible {
+ box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
+ }
+
+ &:read-only {
+ box-shadow: none;
+ }
+
+ &:where(.dark, .dark *):read-only {
+ box-shadow: none;
+ }
}
@utility select {
@apply w-full;
@apply input-select;
- @apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
+ @apply focus-visible:outline-none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23000000'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
@@ -69,6 +111,14 @@ @utility select {
&:where(.dark, .dark *) {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23ffffff'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
}
+
+ &:focus-visible {
+ box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
+ }
+
+ &:where(.dark, .dark *):focus-visible {
+ box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
+ }
}
@utility button {
@@ -152,7 +202,7 @@ @utility custom-modal {
}
@utility navbar-main {
- @apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center;
+ @apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center text-neutral-700 dark:text-neutral-400;
}
@utility loading {
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index f85dc268e..ede49117a 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -61,7 +61,7 @@ class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning