diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03ddc913c..eae126054 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,20 +4,166 @@ All notable changes to this project will be documented in this file.
## [unreleased]
+### π Bug Fixes
+
+- *(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
+
+## [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
+- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README
+- *(core)* Enable magic env variables for compose based applications
+
+### π 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
+- *(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
+- *(application)* Append base directory to git branch URLs for improved path handling
+- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON
+
+### πΌ 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
+- *(source)* Conditionally display connected source and change source options based on private key presence
+
+### π Documentation
+
+- Update changelog
+- Update changelog
+- Update changelog
+
### βοΈ Miscellaneous Tasks
-- *(versions)* Bump version to 404
+- *(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
+
+## [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)
+
+### π 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
+
+### βοΈ Miscellaneous Tasks
+
+- *(versions)* Bump version to 406
+- *(versions)* Bump version to 407
## [4.0.0-beta.404] - 2025-04-03
### π Features
+- *(proxy)* Enhance proxy handling and port conflict detection
- *(lang)* Added Azerbaijani language updated turkish language. (#5497)
- *(lang)* Added Portuguese from Brazil language (#5500)
- *(lang)* Add Indonesian language translations (#5513)
### π Bug Fixes
+- *(database)* Custom config for MongoDB (#5471)
+- *(ui)* Instance Backup settings
- *(docs)* Comment out execute for now
- *(installation)* Mount the docker config
- *(installation)* Path to config file for docker login
@@ -27,10 +173,13 @@ All notable changes to this project will be documented in this file.
- *(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
- Update changelog
- Update changelog
- Update changelog
@@ -38,9 +187,11 @@ All notable changes to this project will be documented in this file.
### βοΈ Miscellaneous Tasks
+- *(versions)* Bump version to 403 (#5520)
+- *(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
## [4.0.0-beta.402] - 2025-04-01
@@ -59,7 +210,6 @@ All notable changes to this project will be documented in this file.
- *(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
@@ -73,7 +223,6 @@ All notable changes to this project will be documented in this file.
- *(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
@@ -144,6 +293,14 @@ All notable changes to this project will be documented in this file.
### π Features
+- *(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
- *(service)* Neon
- *(migration)* Add `ssl_certificates` table and model
- *(migration)* Add ssl setting to `standalone_postgresqls` table
@@ -185,14 +342,6 @@ All notable changes to this project will be documented in this file.
- *(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
@@ -210,6 +359,18 @@ All notable changes to this project will be documented in this file.
- *(api)* Docker compose based apps creationg through api
- *(database)* Improve database type detection for Supabase Postgres images
+- *(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
- *(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
@@ -249,18 +410,6 @@ All notable changes to this project will be documented in this file.
- *(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
@@ -309,6 +458,7 @@ All notable changes to this project will be documented in this file.
### βοΈ Miscellaneous Tasks
+- *(supabase)* Update Supabase service template and Postgres image version
- *(migration)* Remove unused columns
- *(ssl)* Improve code in ssl helper
- *(migration)* Ssl cert and key should not be nullable
@@ -316,7 +466,6 @@ All notable changes to this project will be documented in this file.
- 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
@@ -709,6 +858,14 @@ All notable changes to this project will be documented in this file.
### π 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
- Able to import full db backups for pg/mysql/mariadb
- Restore backup from server file
- Docker volume data cloning
@@ -744,6 +901,35 @@ All notable changes to this project will be documented in this file.
### π Bug Fixes
+- Fallback for copy button
+- Copy the right text
+- Maybe fallback is now working
+- Only show copy button on secure context
+- 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
- Compose envs
- Scheduled tasks and backups are executed by server timezone.
- Show backup timezone on the UI
@@ -843,6 +1029,7 @@ All notable changes to this project will be documented in this file.
### π Refactor
+- Rename `coolify.environment` to `coolify.environmentName`
- Rename parameter in DatabaseBackupJob for clarity
- Improve checkbox component accessibility and styling
- Remove unused tags method from ApplicationDeploymentJob
@@ -858,6 +1045,9 @@ All notable changes to this project will be documented in this file.
### βοΈ Miscellaneous Tasks
+- 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
@@ -881,44 +1071,11 @@ All notable changes to this project will be documented in this file.
### π 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
@@ -928,15 +1085,11 @@ All notable changes to this project will be documented in this file.
- Error message
- Update healthcheck and port configurations to use port 8080
-### π Refactor
+## [4.0.0-beta.379] - 2024-12-13
-- Rename `coolify.environment` to `coolify.environmentName`
+### π Bug Fixes
-### βοΈ Miscellaneous Tasks
-
-- Regenerate API spec, removing notification fields
-- Remove ray debugging
-- Version ++
+- Saving oauth
## [4.0.0-beta.378] - 2024-12-13
@@ -945,11 +1098,6 @@ All notable changes to this project will be documented in this file.
- 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
diff --git a/README.md b/README.md
index 8670e9c76..6e245aa22 100644
--- a/README.md
+++ b/README.md
@@ -29,99 +29,6 @@ You can find the installation script source [here](./scripts/install.sh).
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
-# 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!
-
-Special thanks to our biggest sponsors!
-
-### Special Sponsors
-
-
-
-
-* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
-* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
-* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
-* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform.
-* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
-* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
-* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
-* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
-* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
-* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
-* [Convex](https://convex.link/coolify.io) - Convex is the open-source reactive database for web app developers.
-* [Cloudify.ro](https://cloudify.ro/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
-* [Syntaxfm](https://syntax.fm/?ref=coolify.io) - Podcast for web developers.
-* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang.
-* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
-* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
-* [Brand Dev](https://brand.dev/?ref=coolify.io) - The #1 Brand API for B2B software startups - instantly pull logos, fonts, descriptions, social links, slogans, and so much more from any domain via a single api call.
-* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
-* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
-* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
-* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - An open-source alternative to hyperscale cloud providers, offering high-performance cloud computing services.
-* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
-* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
-* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
-* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - A Fast web hosting provider.
-
-
-## Github Sponsors ($40+)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## Organizations
-
-
-
-
-
-
-
-
-
-
-
-
-## Individuals
-
-
-
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io)
@@ -137,6 +44,96 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
+# 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
+
+* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management
+* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
+* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
+* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions
+* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers
+* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions
+* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers
+* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase
+* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers
+* [Trieve](https://trieve.ai?ref=coolify.io) - AI-powered search and analytics
+* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data
+* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions
+* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
+* [COMIT](https://comit.international?ref=coolify.io) - New York Times awardβwinning contractor
+* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform
+* [WZ-IT](https://wz-it.com/?ref=coolify.io) - German agency for customised cloud solutions
+* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner
+* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform
+* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions
+* [QuantCDN](https://www.quantcdn.io?ref=coolify.io) - Enterprise-grade content delivery network
+* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang
+* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
+* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital transformation and web solutions
+* [Cloudify.ro](https://cloudify.ro?ref=coolify.io) - Cloud hosting solutions
+* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half
+* [American Cloud](https://americancloud.com?ref=coolify.io) - US-based cloud infrastructure services
+* [MassiveGrid](https://massivegrid.com?ref=coolify.io) - Enterprise cloud hosting solutions
+* [Syntax.fm](https://syntax.fm?ref=coolify.io) - Podcast for web developers
+* [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform
+* [CompAI](https://www.trycomp.ai?ref=coolify.io) - Open source compliance automation platform
+* [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions
+
+## Small Sponsors
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
+
# Recognitions
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index a42f03eb5..870b5b7e5 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -217,6 +217,10 @@ class StartMongodb
if ($this->database->enable_ssl) {
$commandParts = ['mongod'];
+ if (! empty($this->database->mongo_conf)) {
+ $commandParts = ['mongod', '--config', '/etc/mongo/mongod.conf'];
+ }
+
$sslConfig = match ($this->database->ssl_mode) {
'allow' => [
'--tlsMode=allowTLS',
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 27608547a..5a2562073 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -27,7 +27,7 @@ class CheckProxy
return false;
}
$proxyType = $server->proxyType();
- if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
+ if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
return false;
}
if (! $server->isProxyShouldRun()) {
@@ -37,8 +37,12 @@ class CheckProxy
return false;
}
}
+
+ // Determine proxy container name based on environment
+ $proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
+
if ($server->isSwarm()) {
- $status = getContainerStatus($server, 'coolify-proxy_traefik');
+ $status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
@@ -47,7 +51,7 @@ class CheckProxy
return true;
} else {
- $status = getContainerStatus($server, 'coolify-proxy');
+ $status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
@@ -61,34 +65,11 @@ class CheckProxy
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
-
$portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) {
- // Try multiple methods to check port availability
- $commands = [
- // Method 1: Check /proc/net/tcp directly (convert port to hex)
- "cat /proc/net/tcp | grep -q '00000000:".str_pad(dechex($port), 4, '0', STR_PAD_LEFT)."'",
- // Method 2: Use ss command (modern alternative to netstat)
- "ss -tuln | grep -q ':$port '",
- // Method 3: Use lsof if available
- "lsof -i :$port >/dev/null 2>&1",
- // Method 4: Use fuser if available
- "fuser $port/tcp >/dev/null 2>&1",
- ];
-
- $portInUse = false;
- foreach ($commands as $command) {
- try {
- instant_remote_process([$command], $server);
- $portInUse = true;
- break;
- } catch (\Throwable $e) {
-
- continue;
- }
- }
- if ($portInUse) {
+ // Use the smart port checker that handles dual-stack properly
+ if ($this->isPortConflict($server, $port, $proxyContainerName)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coolify.io/discord");
} else {
@@ -126,4 +107,144 @@ class CheckProxy
return true;
}
}
+
+ /**
+ * Smart port checker that handles dual-stack configurations
+ * Returns true only if there's a real port conflict (not just dual-stack)
+ */
+ private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
+ {
+ // First check if our own proxy is using this port (which is fine)
+ try {
+ $getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
+ $containerId = trim(instant_remote_process([$getProxyContainerId], $server));
+
+ if (! empty($containerId)) {
+ $checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
+ try {
+ instant_remote_process([$checkProxyPort], $server);
+
+ // Our proxy is using the port, which is fine
+ return false;
+ } catch (\Throwable $e) {
+ // Our container exists but not using this port
+ }
+ }
+ } catch (\Throwable $e) {
+ // Container not found or error checking, continue with regular checks
+ }
+
+ // Command sets for different ways to check ports, ordered by preference
+ $commandSets = [
+ // Set 1: Use ss to check listener counts by protocol stack
+ [
+ 'available' => 'command -v ss >/dev/null 2>&1',
+ 'check' => [
+ // Get listening process details
+ "ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
+ // Count IPv4 listeners
+ "echo \"\$ss_output\" | grep -c ':$port '",
+ ],
+ ],
+ // Set 2: Use netstat as alternative to ss
+ [
+ 'available' => 'command -v netstat >/dev/null 2>&1',
+ 'check' => [
+ // Get listening process details
+ "netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
+ // Count listeners
+ "echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
+ ],
+ ],
+ // Set 3: Use lsof as last resort
+ [
+ 'available' => 'command -v lsof >/dev/null 2>&1',
+ 'check' => [
+ // Get process using the port
+ "lsof -i :$port -P -n | grep 'LISTEN'",
+ // Count listeners
+ "lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
+ ],
+ ],
+ ];
+
+ // Try each command set until we find one available
+ foreach ($commandSets as $set) {
+ try {
+ // Check if the command is available
+ instant_remote_process([$set['available']], $server);
+
+ // Run the actual check commands
+ $output = instant_remote_process($set['check'], $server, true);
+
+ // Parse the output lines
+ $lines = explode("\n", trim($output));
+
+ // Get the detailed output and listener count
+ $details = trim($lines[0] ?? '');
+ $count = intval(trim($lines[1] ?? '0'));
+
+ // If no listeners or empty result, port is free
+ if ($count == 0 || empty($details)) {
+ return false;
+ }
+
+ // Try to detect if this is our coolify-proxy
+ if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
+ // It's likely our docker or proxy, which is fine
+ return false;
+ }
+
+ // Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
+ // If exactly 2 listeners and both have same port, likely dual-stack
+ if ($count <= 2) {
+ // Check if it looks like a standard dual-stack setup
+ $isDualStack = false;
+
+ // Look for IPv4 and IPv6 in the listing (ss output format)
+ if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
+ (preg_match('/\*:'.$port.'\s/', $details) ||
+ preg_match('/:::'.$port.'\s/', $details))) {
+ $isDualStack = true;
+ }
+
+ // For netstat format
+ if (strpos($details, '0.0.0.0:'.$port) !== false &&
+ strpos($details, ':::'.$port) !== false) {
+ $isDualStack = true;
+ }
+
+ // For lsof format (IPv4 and IPv6)
+ if (strpos($details, '*:'.$port) !== false &&
+ preg_match('/\*:'.$port.'.*IPv4/', $details) &&
+ preg_match('/\*:'.$port.'.*IPv6/', $details)) {
+ $isDualStack = true;
+ }
+
+ if ($isDualStack) {
+ return false; // This is just a normal dual-stack setup
+ }
+ }
+
+ // If we get here, it's likely a real port conflict
+ return true;
+
+ } catch (\Throwable $e) {
+ // This command set failed, try the next one
+ continue;
+ }
+ }
+
+ // Fallback to simpler check if all above methods fail
+ try {
+ // Just try to bind to the port directly to see if it's available
+ $checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
+ $result = instant_remote_process([$checkCommand], $server, true);
+
+ return trim($result) === 'in-use';
+ } catch (\Throwable $e) {
+ // If everything fails, assume the port is free to avoid false positives
+ return false;
+ }
+ }
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index ba4c2311a..754feecb1 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -14,15 +14,26 @@ class CleanupDocker
public function handle(Server $server)
{
$settings = instanceSettings();
+ $realtimeImage = config('constants.coolify.realtime_image');
+ $realtimeImageVersion = config('constants.coolify.realtime_version');
+ $realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion";
+ $realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime';
+ $realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
+
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
+ $helperImageWithoutPrefix = 'coollabsio/coolify-helper';
+ $helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
$commands = [
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
'docker image prune -af --filter "label!=coolify.managed=true"',
'docker builder prune -af',
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
+ "docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f",
+ "docker images --filter before=$helperImageWithoutPrefixVersion --filter reference=$helperImageWithoutPrefix | grep $helperImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
+ "docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
];
if ($server->settings->delete_unused_volumes) {
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/Generate/OpenApi.php
similarity index 89%
rename from app/Console/Commands/OpenApi.php
rename to app/Console/Commands/Generate/OpenApi.php
index 3cef85477..2b266c258 100644
--- a/app/Console/Commands/OpenApi.php
+++ b/app/Console/Commands/Generate/OpenApi.php
@@ -1,6 +1,6 @@
json([
+ 'message' => $result['message'],
+ ], 200);
+ }
} else {
if ($application->build_pack === 'dockercompose') {
LoadComposeFile::dispatch($application);
@@ -1004,12 +1009,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
no_questions_asked: true,
is_api: true,
);
+ if ($result['status'] === 'skipped') {
+ return response()->json([
+ 'message' => $result['message'],
+ ], 200);
+ }
} else {
if ($application->build_pack === 'dockercompose') {
LoadComposeFile::dispatch($application);
@@ -1101,12 +1111,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
no_questions_asked: true,
is_api: true,
);
+ if ($result['status'] === 'skipped') {
+ return response()->json([
+ 'message' => $result['message'],
+ ], 200);
+ }
} else {
if ($application->build_pack === 'dockercompose') {
LoadComposeFile::dispatch($application);
@@ -1190,12 +1205,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
no_questions_asked: true,
is_api: true,
);
+ if ($result['status'] === 'skipped') {
+ return response()->json([
+ 'message' => $result['message'],
+ ], 200);
+ }
}
return response()->json(serializeApiResponse([
@@ -1254,12 +1274,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
no_questions_asked: true,
is_api: true,
);
+ if ($result['status'] === 'skipped') {
+ return response()->json([
+ 'message' => $result['message'],
+ ], 200);
+ }
}
return response()->json(serializeApiResponse([
@@ -1610,6 +1635,18 @@ class ApplicationsController extends Controller
['bearerAuth' => []],
],
tags: ['Applications'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the application.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
requestBody: new OA\RequestBody(
description: 'Application updated.',
required: true,
@@ -1884,11 +1921,16 @@ class ApplicationsController extends Controller
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
is_api: true,
);
+ if ($result['status'] === 'skipped') {
+ return response()->json([
+ 'message' => $result['message'],
+ ], 200);
+ }
}
return response()->json([
@@ -2520,10 +2562,6 @@ class ApplicationsController extends Controller
])->setStatusCode(201);
}
}
-
- return response()->json([
- 'message' => 'Something went wrong.',
- ], 500);
}
#[OA\Delete(
@@ -2705,13 +2743,21 @@ class ApplicationsController extends Controller
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
is_api: true,
no_questions_asked: $instant_deploy
);
+ if ($result['status'] === 'skipped') {
+ return response()->json(
+ [
+ 'message' => $result['message'],
+ ],
+ 200
+ );
+ }
return response()->json(
[
@@ -2866,12 +2912,17 @@ class ApplicationsController extends Controller
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
restart_only: true,
is_api: true,
);
+ if ($result['status'] === 'skipped') {
+ return response()->json([
+ 'message' => $result['message'],
+ ], 200);
+ }
return response()->json(
[
@@ -3006,73 +3057,73 @@ class ApplicationsController extends Controller
// ]);
// }
- // private function validateDataApplications(Request $request, Server $server)
- // {
- // $teamId = getTeamIdFromToken();
+ private function validateDataApplications(Request $request, Server $server)
+ {
+ $teamId = getTeamIdFromToken();
- // // Validate ports_mappings
- // if ($request->has('ports_mappings')) {
- // $ports = [];
- // foreach (explode(',', $request->ports_mappings) as $portMapping) {
- // $port = explode(':', $portMapping);
- // if (in_array($port[0], $ports)) {
- // return response()->json([
- // 'message' => 'Validation failed.',
- // 'errors' => [
- // 'ports_mappings' => 'The first number before : should be unique between mappings.',
- // ],
- // ], 422);
- // }
- // $ports[] = $port[0];
- // }
- // }
- // // Validate custom_labels
- // if ($request->has('custom_labels')) {
- // if (! isBase64Encoded($request->custom_labels)) {
- // return response()->json([
- // 'message' => 'Validation failed.',
- // 'errors' => [
- // 'custom_labels' => 'The custom_labels should be base64 encoded.',
- // ],
- // ], 422);
- // }
- // $customLabels = base64_decode($request->custom_labels);
- // if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
- // return response()->json([
- // 'message' => 'Validation failed.',
- // 'errors' => [
- // 'custom_labels' => 'The custom_labels should be base64 encoded.',
- // ],
- // ], 422);
- // }
- // }
- // if ($request->has('domains') && $server->isProxyShouldRun()) {
- // $uuid = $request->uuid;
- // $fqdn = $request->domains;
- // $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
- // $fqdn = str($fqdn)->replaceStart(',', '')->trim();
- // $errors = [];
- // $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
- // if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
- // $errors[] = 'Invalid domain: '.$domain;
- // }
+ // Validate ports_mappings
+ if ($request->has('ports_mappings')) {
+ $ports = [];
+ foreach (explode(',', $request->ports_mappings) as $portMapping) {
+ $port = explode(':', $portMapping);
+ if (in_array($port[0], $ports)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'ports_mappings' => 'The first number before : should be unique between mappings.',
+ ],
+ ], 422);
+ }
+ $ports[] = $port[0];
+ }
+ }
+ // Validate custom_labels
+ if ($request->has('custom_labels')) {
+ if (! isBase64Encoded($request->custom_labels)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_labels' => 'The custom_labels should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customLabels = base64_decode($request->custom_labels);
+ if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_labels' => 'The custom_labels should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
+ if ($request->has('domains') && $server->isProxyShouldRun()) {
+ $uuid = $request->uuid;
+ $fqdn = $request->domains;
+ $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
+ $fqdn = str($fqdn)->replaceStart(',', '')->trim();
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
- // return str($domain)->trim()->lower();
- // });
- // if (count($errors) > 0) {
- // return response()->json([
- // 'message' => 'Validation failed.',
- // 'errors' => $errors,
- // ], 422);
- // }
- // if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
- // return response()->json([
- // 'message' => 'Validation failed.',
- // 'errors' => [
- // 'domains' => 'One of the domain is already used.',
- // ],
- // ], 422);
- // }
- // }
- // }
+ return str($domain)->trim()->lower();
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
+ }
+ }
+ }
}
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 424c2cc76..46606e24a 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
+use App\Models\Service;
use App\Models\Tag;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
@@ -132,7 +133,7 @@ class DeployController extends Controller
#[OA\Get(
summary: 'Deploy',
- description: 'Deploy by tag or uuid. `Post` request also accepted.',
+ description: 'Deploy by tag or uuid. `Post` request also accepted with `uuid` and `tag` json body.',
path: '/deploy',
operationId: 'deploy-by-tag-or-uuid',
security: [
@@ -191,10 +192,10 @@ class DeployController extends Controller
return invalidTokenResponse();
}
- $uuids = $request->query->get('uuid');
- $tags = $request->query->get('tag');
- $force = $request->query->get('force') ?? false;
- $pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0;
+ $uuids = $request->input('uuid');
+ $tags = $request->input('tag');
+ $force = $request->input('force') ?? false;
+ $pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0;
if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
@@ -297,17 +298,21 @@ class DeployController extends Controller
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
switch ($resource?->getMorphClass()) {
- case \App\Models\Application::class:
+ case Application::class:
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
pull_request_id: $pr,
);
- $message = "Application {$resource->name} deployment queued.";
+ if ($result['status'] === 'skipped') {
+ $message = $result['message'];
+ } else {
+ $message = "Application {$resource->name} deployment queued.";
+ }
break;
- case \App\Models\Service::class:
+ case Service::class:
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
break;
@@ -333,6 +338,40 @@ class DeployController extends Controller
['bearerAuth' => []],
],
tags: ['Deployments'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the application.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ new OA\Parameter(
+ name: 'skip',
+ in: 'query',
+ description: 'Number of records to skip.',
+ required: false,
+ schema: new OA\Schema(
+ type: 'integer',
+ minimum: 0,
+ default: 0,
+ )
+ ),
+ new OA\Parameter(
+ name: 'take',
+ in: 'query',
+ description: 'Number of records to take.',
+ required: false,
+ schema: new OA\Schema(
+ type: 'integer',
+ minimum: 1,
+ default: 10,
+ )
+ ),
+ ],
responses: [
new OA\Response(
response: 200,
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index b94ce9c67..98637c3e8 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -267,6 +267,18 @@ class ProjectController extends Controller
['bearerAuth' => []],
],
tags: ['Projects'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the project.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
requestBody: new OA\RequestBody(
required: true,
description: 'Project updated.',
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index a9a0a2e53..cbd20400a 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -809,6 +809,6 @@ class ServersController extends Controller
}
ValidateServer::dispatch($server);
- return response()->json(['message' => 'Validation started.']);
+ return response()->json(['message' => 'Validation started.'], 201);
}
}
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 027bd5c1c..e792779e1 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -380,6 +380,9 @@ class ServicesController extends Controller
$service = new Service;
$result = $this->upsert_service($request, $service, $teamId);
+ if ($result instanceof \Illuminate\Http\JsonResponse) {
+ return $result;
+ }
return response()->json(serializeApiResponse($result))->setStatusCode(201);
} else {
@@ -527,6 +530,18 @@ class ServicesController extends Controller
['bearerAuth' => []],
],
tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
requestBody: new OA\RequestBody(
description: 'Service updated.',
required: true,
@@ -596,12 +611,14 @@ class ServicesController extends Controller
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
-
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$result = $this->upsert_service($request, $service, $teamId);
+ if ($result instanceof \Illuminate\Http\JsonResponse) {
+ return $result;
+ }
return response()->json(serializeApiResponse($result))->setStatusCode(200);
}
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index 33d8f8532..490b66e58 100644
--- a/app/Http/Controllers/Webhook/Bitbucket.php
+++ b/app/Http/Controllers/Webhook/Bitbucket.php
@@ -100,18 +100,26 @@ class Bitbucket extends Controller
if ($x_bitbucket_event === 'repo:push') {
if ($application->isDeployable()) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: $commit,
force_rebuild: false,
is_webhook: true
);
- $return_payloads->push([
- 'application' => $application->name,
- 'status' => 'success',
- 'message' => 'Preview deployment queued.',
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Deployment queued.',
+ ]);
+ }
} else {
$return_payloads->push([
'application' => $application->name,
@@ -143,7 +151,7 @@ class Bitbucket extends Controller
]);
}
}
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
@@ -152,11 +160,19 @@ class Bitbucket extends Controller
is_webhook: true,
git_type: 'bitbucket'
);
- $return_payloads->push([
- 'application' => $application->name,
- 'status' => 'success',
- 'message' => 'Preview deployment queued.',
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Preview deployment queued.',
+ ]);
+ }
} else {
$return_payloads->push([
'application' => $application->name,
diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php
index 87fd2255f..3c3d6e0b6 100644
--- a/app/Http/Controllers/Webhook/Gitea.php
+++ b/app/Http/Controllers/Webhook/Gitea.php
@@ -116,19 +116,27 @@ class Gitea extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
- $return_payloads->push([
- 'status' => 'success',
- 'message' => 'Deployment queued.',
- 'application_uuid' => $application->uuid,
- 'application_name' => $application->name,
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'status' => 'success',
+ 'message' => 'Deployment queued.',
+ 'application_uuid' => $application->uuid,
+ 'application_name' => $application->name,
+ ]);
+ }
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
@@ -175,7 +183,7 @@ class Gitea extends Controller
]);
}
}
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
@@ -184,11 +192,19 @@ class Gitea extends Controller
is_webhook: true,
git_type: 'gitea'
);
- $return_payloads->push([
- 'application' => $application->name,
- 'status' => 'success',
- 'message' => 'Preview deployment queued.',
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Preview deployment queued.',
+ ]);
+ }
} else {
$return_payloads->push([
'application' => $application->name,
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 882f2be8b..597ec023f 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -122,19 +122,29 @@ class Github extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
- $return_payloads->push([
- 'status' => 'success',
- 'message' => 'Deployment queued.',
- 'application_uuid' => $application->uuid,
- 'application_name' => $application->name,
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Deployment queued.',
+ 'application_uuid' => $application->uuid,
+ 'application_name' => $application->name,
+ 'deployment_uuid' => $result['deployment_uuid'],
+ ]);
+ }
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
@@ -181,7 +191,8 @@ class Github extends Controller
]);
}
}
- queue_application_deployment(
+
+ $result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
@@ -190,11 +201,19 @@ class Github extends Controller
is_webhook: true,
git_type: 'github'
);
- $return_payloads->push([
- 'application' => $application->name,
- 'status' => 'success',
- 'message' => 'Preview deployment queued.',
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Preview deployment queued.',
+ ]);
+ }
} else {
$return_payloads->push([
'application' => $application->name,
@@ -341,7 +360,7 @@ class Github extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
@@ -349,10 +368,11 @@ class Github extends Controller
is_webhook: true,
);
$return_payloads->push([
- 'status' => 'success',
- 'message' => 'Deployment queued.',
+ 'status' => $result['status'],
+ 'message' => $result['message'],
'application_uuid' => $application->uuid,
'application_name' => $application->name,
+ 'deployment_uuid' => $result['deployment_uuid'],
]);
} else {
$paths = str($application->watch_paths)->explode("\n");
@@ -389,7 +409,7 @@ class Github extends Controller
'pull_request_html_url' => $pull_request_html_url,
]);
}
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
@@ -398,11 +418,19 @@ class Github extends Controller
is_webhook: true,
git_type: 'github'
);
- $return_payloads->push([
- 'application' => $application->name,
- 'status' => 'success',
- 'message' => 'Preview deployment queued.',
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Preview deployment queued.',
+ ]);
+ }
} else {
$return_payloads->push([
'application' => $application->name,
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index cf6874b8c..d6d12a05f 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -142,19 +142,28 @@ class Gitlab extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
- $return_payloads->push([
- 'status' => 'success',
- 'message' => 'Deployment queued.',
- 'application_uuid' => $application->uuid,
- 'application_name' => $application->name,
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'status' => $result['status'],
+ 'message' => $result['message'],
+ 'application_uuid' => $application->uuid,
+ 'application_name' => $application->name,
+ ]);
+ } else {
+ $return_payloads->push([
+ 'status' => 'success',
+ 'message' => 'Deployment queued.',
+ 'application_uuid' => $application->uuid,
+ 'application_name' => $application->name,
+ ]);
+ }
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
@@ -201,7 +210,7 @@ class Gitlab extends Controller
]);
}
}
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
@@ -210,11 +219,19 @@ class Gitlab extends Controller
is_webhook: true,
git_type: 'gitlab'
);
- $return_payloads->push([
- 'application' => $application->name,
- 'status' => 'success',
- 'message' => 'Preview Deployment queued',
- ]);
+ if ($result['status'] === 'skipped') {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'skipped',
+ 'message' => $result['message'],
+ ]);
+ } else {
+ $return_payloads->push([
+ 'application' => $application->name,
+ 'status' => 'success',
+ 'message' => 'Preview Deployment queued',
+ ]);
+ }
} else {
$return_payloads->push([
'application' => $application->name,
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 5dbdbf215..c29093ce0 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -329,7 +329,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$this->write_deployment_configurations();
}
- $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
+ $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
@@ -899,100 +899,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
}
$ports = $this->application->main_port();
- if ($this->pull_request_id !== 0) {
- $this->env_filename = ".env-pr-$this->pull_request_id";
- // Add SOURCE_COMMIT if not exists
- if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
- if (! is_null($this->commit)) {
- $envs->push("SOURCE_COMMIT={$this->commit}");
- } else {
- $envs->push('SOURCE_COMMIT=unknown');
- }
- }
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
- $envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
- $envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
- }
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
- $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
- $envs->push("COOLIFY_URL={$url}");
- $envs->push("COOLIFY_DOMAIN_FQDN={$url}");
- }
- if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
- $envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
- }
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
- $envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
- }
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
- $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
- }
- }
-
- add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
-
- foreach ($sorted_environment_variables_preview as $env) {
- $real_value = $env->real_value;
- if ($env->version === '4.0.0-beta.239') {
- $real_value = $env->real_value;
- } else {
- if ($env->is_literal || $env->is_multiline) {
- $real_value = '\''.$real_value.'\'';
- } else {
- $real_value = escapeEnvVariables($env->real_value);
- }
- }
- $envs->push($env->key.'='.$real_value);
- }
- // Add PORT if not exists, use the first port as default
- if ($this->build_pack !== 'dockercompose') {
- if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
- $envs->push("PORT={$ports[0]}");
- }
- }
- // Add HOST if not exists
- if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
- $envs->push('HOST=0.0.0.0');
- }
- } else {
+ $coolify_envs = $this->generate_coolify_env_variables();
+ $coolify_envs->each(function ($item, $key) use ($envs) {
+ $envs->push($key.'='.$item);
+ });
+ if ($this->pull_request_id === 0) {
$this->env_filename = '.env';
- // Add SOURCE_COMMIT if not exists
- if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
- if (! is_null($this->commit)) {
- $envs->push("SOURCE_COMMIT={$this->commit}");
- } else {
- $envs->push('SOURCE_COMMIT=unknown');
- }
- }
- if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
- if ((int) $this->application->compose_parsing_version >= 3) {
- $envs->push("COOLIFY_URL={$this->application->fqdn}");
- } else {
- $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
- }
- }
- if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
- $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
- if ((int) $this->application->compose_parsing_version >= 3) {
- $envs->push("COOLIFY_FQDN={$url}");
- } else {
- $envs->push("COOLIFY_URL={$url}");
- }
- }
- if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
- if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
- $envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
- }
- if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
- $envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
- }
- if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
- $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
- }
- }
-
- add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
@@ -1017,6 +929,32 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0');
}
+ } else {
+ $this->env_filename = ".env-pr-$this->pull_request_id";
+ foreach ($sorted_environment_variables_preview as $env) {
+ $real_value = $env->real_value;
+ if ($env->version === '4.0.0-beta.239') {
+ $real_value = $env->real_value;
+ } else {
+ if ($env->is_literal || $env->is_multiline) {
+ $real_value = '\''.$real_value.'\'';
+ } else {
+ $real_value = escapeEnvVariables($env->real_value);
+ }
+ }
+ $envs->push($env->key.'='.$real_value);
+ }
+ // Add PORT if not exists, use the first port as default
+ if ($this->build_pack !== 'dockercompose') {
+ if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
+ $envs->push("PORT={$ports[0]}");
+ }
+ }
+ // Add HOST if not exists
+ if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
+ $envs->push('HOST=0.0.0.0');
+ }
+
}
if ($envs->isEmpty()) {
$this->env_filename = null;
@@ -1361,7 +1299,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
- $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command(
[
@@ -1394,6 +1331,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
+ if (! $destination) {
+ continue;
+ }
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
@@ -1437,6 +1377,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_git_if_build_needed()
{
+ if ($this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) {
+ $repository = githubApi($this->source, "repos/{$this->customRepository}");
+ $data = data_get($repository, 'data');
+ if (isset($data->id)) {
+ $repository_project_id = $data->id;
+ if (blank($this->application->repository_project_id) || $this->application->repository_project_id !== $repository_project_id) {
+ $this->application->repository_project_id = $repository_project_id;
+ $this->application->save();
+ }
+ }
+ }
$this->generate_git_import_commands();
$local_branch = $this->branch;
if ($this->pull_request_id !== 0) {
@@ -1626,20 +1577,128 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
}
+ private function generate_coolify_env_variables(): Collection
+ {
+ $coolify_envs = collect([]);
+ $local_branch = $this->branch;
+ if ($this->pull_request_id !== 0) {
+ // Add SOURCE_COMMIT if not exists
+ if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
+ if (! is_null($this->commit)) {
+ $coolify_envs->put('SOURCE_COMMIT', $this->commit);
+ } else {
+ $coolify_envs->put('SOURCE_COMMIT', 'unknown');
+ }
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
+ $coolify_envs->put('COOLIFY_FQDN', $this->preview->fqdn);
+ $coolify_envs->put('COOLIFY_DOMAIN_URL', $this->preview->fqdn);
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
+ $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
+ $coolify_envs->put('COOLIFY_URL', $url);
+ $coolify_envs->put('COOLIFY_DOMAIN_FQDN', $url);
+ }
+ if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $coolify_envs->put('COOLIFY_BRANCH', $local_branch);
+ }
+ 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);
+ }
+ }
+
+ add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables_preview);
+
+ } else {
+ // Add SOURCE_COMMIT if not exists
+ if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
+ if (! is_null($this->commit)) {
+ $coolify_envs->put('SOURCE_COMMIT', $this->commit);
+ } else {
+ $coolify_envs->put('SOURCE_COMMIT', 'unknown');
+ }
+ }
+ if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
+ if ((int) $this->application->compose_parsing_version >= 3) {
+ $coolify_envs->put('COOLIFY_URL', $this->application->fqdn);
+ } else {
+ $coolify_envs->put('COOLIFY_FQDN', $this->application->fqdn);
+ }
+ }
+ if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
+ $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
+ if ((int) $this->application->compose_parsing_version >= 3) {
+ $coolify_envs->put('COOLIFY_FQDN', $url);
+ } else {
+ $coolify_envs->put('COOLIFY_URL', $url);
+ }
+ }
+ if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
+ if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $coolify_envs->put('COOLIFY_BRANCH', $local_branch);
+ }
+ 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);
+ }
+ }
+
+ add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables);
+
+ }
+
+ return $coolify_envs;
+ }
+
private function generate_env_variables()
{
$this->env_args = collect([]);
$this->env_args->put('SOURCE_COMMIT', $this->commit);
+ $coolify_envs = $this->generate_coolify_env_variables();
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
if (! is_null($env->real_value)) {
$this->env_args->put($env->key, $env->real_value);
+ if (str($env->real_value)->startsWith('$')) {
+ $variable_key = str($env->real_value)->after('$');
+ if ($variable_key->startsWith('COOLIFY_')) {
+ $variable = $coolify_envs->get($variable_key->value());
+ if (filled($variable)) {
+ $this->env_args->prepend($variable, $variable_key->value());
+ }
+ } else {
+ $variable = $this->application->environment_variables()->where('key', $variable_key)->first();
+ if ($variable) {
+ $this->env_args->prepend($variable->real_value, $env->key);
+ }
+ }
+ }
}
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
if (! is_null($env->real_value)) {
$this->env_args->put($env->key, $env->real_value);
+ if (str($env->real_value)->startsWith('$')) {
+ $variable_key = str($env->real_value)->after('$');
+ if ($variable_key->startsWith('COOLIFY_')) {
+ $variable = $coolify_envs->get($variable_key->value());
+ if (filled($variable)) {
+ $this->env_args->prepend($variable, $variable_key->value());
+ }
+ } else {
+ $variable = $this->application->environment_variables_preview()->where('key', $variable_key)->first();
+ if ($variable) {
+ $this->env_args->prepend($variable->real_value, $env->key);
+ }
+ }
+ }
}
}
}
@@ -1664,25 +1723,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$labels = $labels->filter(function ($value, $key) {
return ! Str::startsWith($value, 'coolify.');
});
- $found_caddy_labels = $labels->filter(function ($value, $key) {
- return Str::startsWith($value, 'caddy_');
- });
- if ($found_caddy_labels->count() === 0) {
- if ($this->pull_request_id !== 0) {
- $domains = str(data_get($this->preview, 'fqdn'))->explode(',');
- } else {
- $domains = str(data_get($this->application, 'fqdn'))->explode(',');
- }
- $labels = $labels->merge(fqdnLabelsForCaddy(
- network: $this->application->destination->network,
- uuid: $this->application->uuid,
- domains: $domains,
- onlyPort: $onlyPort,
- is_force_https_enabled: $this->application->isForceHttpsEnabled(),
- is_gzip_enabled: $this->application->isGzipEnabled(),
- is_stripprefix_enabled: $this->application->isStripprefixEnabled()
- ));
- }
$this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save();
} else {
@@ -1710,6 +1750,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]);
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
}
+ $custom_network_aliases = [];
+ if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) {
+ $custom_network_aliases = $this->application->custom_network_aliases;
+ }
$docker_compose = [
'services' => [
$this->container_name => [
@@ -1719,9 +1763,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'expose' => $ports,
'networks' => [
$this->destination->network => [
- 'aliases' => [
- $this->container_name,
- ],
+ 'aliases' => array_merge(
+ [$this->container_name],
+ $custom_network_aliases
+ ),
],
],
'mem_limit' => $this->application->limits_memory,
@@ -2409,20 +2454,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function next(string $status)
{
queue_next_deployment($this->application);
- // If the deployment is cancelled by the user, don't update the status
- if (
- $this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
- $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
- ) {
- $this->application_deployment_queue->update([
- 'status' => $status,
- ]);
+
+ // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
+ if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value ||
+ $this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
+ return;
}
- if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
+
+ $this->application_deployment_queue->update([
+ 'status' => $status,
+ ]);
+
+ if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
+
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php
index 84f14ed02..008492342 100644
--- a/app/Jobs/CleanupInstanceStuffsJob.php
+++ b/app/Jobs/CleanupInstanceStuffsJob.php
@@ -17,11 +17,13 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ public $timeout = 60;
+
public function __construct() {}
public function middleware(): array
{
- return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
+ return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)];
}
public function handle(): void
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 05a4aa8de..7e246649d 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -31,7 +31,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
- return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ return [(new WithoutOverlapping($this->server->uuid))->expireAfter(600)];
}
public function __construct(public Server $server, public bool $manualCleanup = false) {}
diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php
index 93b203fcb..4d40240f9 100644
--- a/app/Jobs/PushServerUpdateJob.php
+++ b/app/Jobs/PushServerUpdateJob.php
@@ -71,7 +71,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
- return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ return [(new WithoutOverlapping($this->server->uuid))->expireAfter(30)];
}
public function backoff(): int
diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php
index 7fc716f70..4e1ade0da 100644
--- a/app/Jobs/RestartProxyJob.php
+++ b/app/Jobs/RestartProxyJob.php
@@ -24,7 +24,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
- return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)];
}
public function __construct(public Server $server) {}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 9818d5c6a..ffa298390 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
- return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)];
}
public function __construct(public Server $server) {}
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 3ed20f907..c5f518e16 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -269,7 +269,7 @@ class Email extends Component
} catch (\Throwable $e) {
$this->smtpEnabled = false;
- return handleError($e);
+ return handleError($e, $this);
}
}
@@ -337,32 +337,29 @@ class Email extends Component
public function copyFromInstanceSettings()
{
$settings = instanceSettings();
+ $this->smtpFromAddress = $settings->smtp_from_address;
+ $this->smtpFromName = $settings->smtp_from_name;
if ($settings->smtp_enabled) {
$this->smtpEnabled = true;
- $this->smtpFromAddress = $settings->smtp_from_address;
- $this->smtpFromName = $settings->smtp_from_name;
- $this->smtpRecipients = $settings->smtp_recipients;
- $this->smtpHost = $settings->smtp_host;
- $this->smtpPort = $settings->smtp_port;
- $this->smtpEncryption = $settings->smtp_encryption;
- $this->smtpUsername = $settings->smtp_username;
- $this->smtpPassword = $settings->smtp_password;
- $this->smtpTimeout = $settings->smtp_timeout;
$this->resendEnabled = false;
- $this->saveModel();
-
- return;
}
+
+ $this->smtpRecipients = $settings->smtp_recipients;
+ $this->smtpHost = $settings->smtp_host;
+ $this->smtpPort = $settings->smtp_port;
+ $this->smtpEncryption = $settings->smtp_encryption;
+ $this->smtpUsername = $settings->smtp_username;
+ $this->smtpPassword = $settings->smtp_password;
+ $this->smtpTimeout = $settings->smtp_timeout;
+
if ($settings->resend_enabled) {
$this->resendEnabled = true;
- $this->resendApiKey = $settings->resend_api_key;
$this->smtpEnabled = false;
- $this->saveModel();
-
- return;
}
- $this->dispatch('error', 'Instance SMTP/Resend settings are not enabled.');
+ $this->resendApiKey = $settings->resend_api_key;
+ $this->saveModel();
+
}
public function render()
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index b85023a0c..b7cb693b6 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -68,6 +68,7 @@ class General extends Component
'application.publish_directory' => 'nullable',
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
+ 'application.custom_network_aliases' => 'nullable',
'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
@@ -93,6 +94,9 @@ class General extends Component
'application.settings.is_preserve_repository_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required',
+ 'application.http_basic_auth_enabled' => 'boolean|required',
+ 'application.http_basic_auth_username' => 'nullable',
+ 'application.http_basic_auth_password' => 'nullable',
];
protected $validationAttributes = [
@@ -121,6 +125,7 @@ class General extends Component
'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',
@@ -455,7 +460,6 @@ class General extends Component
{
$config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json');
- dd($config);
return response()->streamDownload(function () use ($config) {
echo $config;
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 0d7d7755f..475d2dfa8 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -84,11 +84,16 @@ class Heading extends Component
return;
}
$this->setDeploymentUuid();
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
+ if ($result['status'] === 'skipped') {
+ $this->dispatch('success', 'Deployment skipped', $result['message']);
+
+ return;
+ }
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
@@ -126,11 +131,16 @@ class Heading extends Component
return;
}
$this->setDeploymentUuid();
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
+ if ($result['status'] === 'skipped') {
+ $this->dispatch('success', 'Deployment skipped', $result['message']);
+
+ return;
+ }
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index bdf62706c..88ce65c53 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -159,13 +159,18 @@ class Previews extends Component
'pull_request_html_url' => $pull_request_html_url,
]);
}
- queue_application_deployment(
+ $result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deployment_uuid,
force_rebuild: false,
pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
);
+ if ($result['status'] === 'skipped') {
+ $this->dispatch('success', 'Deployment skipped', $result['message']);
+
+ return;
+ }
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php
index ade297d50..e27a550c1 100644
--- a/app/Livewire/Project/Application/Source.php
+++ b/app/Livewire/Project/Application/Source.php
@@ -30,11 +30,15 @@ class Source extends Component
#[Validate(['nullable', 'string'])]
public ?string $gitCommitSha = null;
+ #[Locked]
+ public $sources;
+
public function mount()
{
try {
$this->syncData();
$this->getPrivateKeys();
+ $this->getSources();
} catch (\Throwable $e) {
handleError($e, $this);
}
@@ -66,6 +70,14 @@ class Source extends Component
});
}
+ private function getSources()
+ {
+ // filter the current source out
+ $this->sources = currentTeam()->sources()->whereNotNull('app_id')->reject(function ($source) {
+ return $source->id === $this->application->source_id;
+ })->sortBy('name');
+ }
+
public function setPrivateKey(int $privateKeyId)
{
try {
@@ -92,4 +104,20 @@ class Source extends Component
return handleError($e, $this);
}
}
+
+ public function changeSource($sourceId, $sourceType)
+ {
+ try {
+ $this->application->update([
+ 'source_id' => $sourceId,
+ 'source_type' => $sourceType,
+ 'repository_project_id' => null,
+ ]);
+ $this->application->refresh();
+ $this->getSources();
+ $this->dispatch('success', 'Source updated!');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
}
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index f301d912e..f03f1256d 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -21,7 +21,7 @@ class General extends Component
public string $redis_username;
- public string $redis_password;
+ public ?string $redis_password;
public string $redis_version;
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index 1759fe08a..71a913add 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -79,7 +79,7 @@ class Destination extends Component
$deployment_uuid = new Cuid2;
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
- queue_application_deployment(
+ $result = queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
@@ -87,6 +87,11 @@ class Destination extends Component
only_this_server: true,
no_questions_asked: true,
);
+ if ($result['status'] === 'skipped') {
+ $this->dispatch('success', 'Deployment skipped', $result['message']);
+
+ return;
+ }
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 35e585c82..699dca187 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
+use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class All extends Component
{
+ use EnvironmentVariableProtection;
+
public $resource;
public string $resourceClass;
@@ -138,17 +141,57 @@ class All extends Component
private function handleBulkSubmit()
{
$variables = parseEnvFormatToArray($this->variables);
+ $changesMade = false;
+ $errorOccurred = false;
- $this->deleteRemovedVariables(false, $variables);
- $this->updateOrCreateVariables(false, $variables);
+ // Try to delete removed variables
+ $deletedCount = $this->deleteRemovedVariables(false, $variables);
+ if ($deletedCount > 0) {
+ $changesMade = true;
+ } elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
+ // If we tried to delete but couldn't (due to Docker Compose), mark as error
+ $errorOccurred = true;
+ }
+
+ // Update or create variables
+ $updatedCount = $this->updateOrCreateVariables(false, $variables);
+ if ($updatedCount > 0) {
+ $changesMade = true;
+ }
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
- $this->deleteRemovedVariables(true, $previewVariables);
- $this->updateOrCreateVariables(true, $previewVariables);
+
+ // Try to delete removed preview variables
+ $deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
+ if ($deletedPreviewCount > 0) {
+ $changesMade = true;
+ } elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
+ // If we tried to delete but couldn't (due to Docker Compose), mark as error
+ $errorOccurred = true;
+ }
+
+ // Update or create preview variables
+ $updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables);
+ if ($updatedPreviewCount > 0) {
+ $changesMade = true;
+ }
}
- $this->dispatch('success', 'Environment variables updated.');
+ // Debug information
+ \Log::info('Environment variables update status', [
+ 'deletedCount' => $deletedCount,
+ 'updatedCount' => $updatedCount,
+ 'deletedPreviewCount' => $deletedPreviewCount ?? 0,
+ 'updatedPreviewCount' => $updatedPreviewCount ?? 0,
+ 'changesMade' => $changesMade,
+ 'errorOccurred' => $errorOccurred,
+ ]);
+
+ // Only show success message if changes were actually made and no errors occurred
+ if ($changesMade && ! $errorOccurred) {
+ $this->dispatch('success', 'Environment variables updated.');
+ }
}
private function handleSingleSubmit($data)
@@ -184,11 +227,37 @@ class All extends Component
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
+
+ // Get all environment variables that will be deleted
+ $variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
+
+ // If there are no variables to delete, return 0
+ if ($variablesToDelete->isEmpty()) {
+ return 0;
+ }
+
+ // Check if any of these variables are used in Docker Compose
+ if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') {
+ foreach ($variablesToDelete as $envVar) {
+ [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose);
+
+ if ($isUsed) {
+ $this->dispatch('error', "Cannot delete environment variable '{$envVar->key}'
Please remove it from the Docker Compose file first.");
+
+ return 0;
+ }
+ }
+ }
+
+ // If we get here, no variables are used in Docker Compose, so we can delete them
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
+
+ return $variablesToDelete->count();
}
private function updateOrCreateVariables($isPreview, $variables)
{
+ $count = 0;
foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
continue;
@@ -198,8 +267,12 @@ class All extends Component
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
- $found->value = $value;
- $found->save();
+ // Only count as a change if the value actually changed
+ if ($found->value !== $value) {
+ $found->value = $value;
+ $found->save();
+ $count++;
+ }
}
} else {
$environment = new EnvironmentVariable;
@@ -212,8 +285,11 @@ class All extends Component
$environment->resourceable_type = $this->resource->getMorphClass();
$environment->save();
+ $count++;
}
}
+
+ return $count;
}
public function refreshEnvs()
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index 3a7d0faa5..535ac6c67 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
+use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class Show extends Component
{
+ use EnvironmentVariableProtection;
+
public $parameters;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
@@ -40,6 +43,8 @@ class Show extends Component
public bool $is_really_required = false;
+ public bool $is_redis_credential = false;
+
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -65,7 +70,9 @@ class Show extends Component
}
$this->parameters = get_route_parameters();
$this->checkEnvs();
-
+ if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
+ $this->is_redis_credential = true;
+ }
}
public function refresh()
@@ -171,6 +178,17 @@ class Show extends Component
public function delete()
{
try {
+ // Check if the variable is used in Docker Compose
+ if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) {
+ [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose);
+
+ if ($isUsed) {
+ $this->dispatch('error', "Cannot delete environment variable '{$this->env->key}'
Please remove it from the Docker Compose file first.");
+
+ return;
+ }
+ }
+
$this->env->delete();
$this->dispatch('environmentVariableDeleted');
$this->dispatch('success', 'Environment variable deleted successfully.');
diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php
index 6277a24bd..7db890638 100644
--- a/app/Livewire/Server/Proxy/DynamicConfigurations.php
+++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php
@@ -38,7 +38,8 @@ class DynamicConfigurations extends Component
$contents = collect([]);
foreach ($files as $file) {
$without_extension = str_replace('.', '|', $file);
- $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
+ $content = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
+ $contents[$without_extension] = $content ?? '';
}
$this->contents = $contents;
$this->dispatch('$refresh');
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index b2394d7b0..73e8c7398 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -177,7 +177,7 @@ class SettingsEmail extends Component
} catch (\Throwable $e) {
$this->smtpEnabled = false;
- return handleError($e);
+ return handleError($e, $this);
}
}
@@ -207,7 +207,7 @@ class SettingsEmail extends Component
} catch (\Throwable $e) {
$this->resendEnabled = false;
- return handleError($e);
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php
index df450cf7e..8a9cc456f 100644
--- a/app/Livewire/Subscription/Index.php
+++ b/app/Livewire/Subscription/Index.php
@@ -12,19 +12,30 @@ class Index extends Component
public bool $alreadySubscribed = false;
+ public bool $isUnpaid = false;
+
+ public bool $isCancelled = false;
+
+ public bool $isMember = false;
+
+ public bool $loading = true;
+
public function mount()
{
if (! isCloud()) {
return redirect(RouteServiceProvider::HOME);
}
if (auth()->user()?->isMember()) {
- return redirect()->route('dashboard');
+ $this->isMember = true;
}
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}
$this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
+ if (! $this->alreadySubscribed) {
+ $this->loading = false;
+ }
}
public function stripeCustomerPortal()
@@ -37,6 +48,41 @@ class Index extends Component
return redirect($session->url);
}
+ public function getStripeStatus()
+ {
+ try {
+ $subscription = currentTeam()->subscription;
+ $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
+ $customer = $stripe->customers->retrieve(currentTeam()->subscription->stripe_customer_id);
+ if ($customer) {
+ $subscriptions = $stripe->subscriptions->all(['customer' => $customer->id]);
+ $currentTeam = currentTeam()->id ?? null;
+ if (count($subscriptions->data) > 0 && $currentTeam) {
+ $foundSubscription = collect($subscriptions->data)->firstWhere('metadata.team_id', $currentTeam);
+ if ($foundSubscription) {
+ $status = data_get($foundSubscription, 'status');
+ $subscription->update([
+ 'stripe_subscription_id' => $foundSubscription->id,
+ ]);
+ if ($status === 'unpaid') {
+ $this->isUnpaid = true;
+ }
+ }
+ }
+ if (count($subscriptions->data) === 0) {
+ $this->isCancelled = true;
+ }
+ }
+ } catch (\Exception $e) {
+ // Log the error
+ logger()->error('Stripe API error: ' . $e->getMessage());
+ // Set a flag to show an error message to the user
+ $this->addError('stripe', 'Could not retrieve subscription information. Please try again later.');
+ } finally {
+ $this->loading = false;
+ }
+ }
+
public function render()
{
return view('livewire.subscription.index');
diff --git a/app/Models/Application.php b/app/Models/Application.php
index d07577cc7..3306510d1 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -45,6 +45,7 @@ use Visus\Cuid2\Cuid2;
'start_command' => ['type' => 'string', 'description' => 'Start command.'],
'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'],
+ 'custom_network_aliases' => ['type' => 'string', 'nullable' => true, 'description' => 'Network aliases for Docker container.'],
'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
@@ -102,6 +103,9 @@ use Visus\Cuid2\Cuid2;
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'],
'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'],
'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'],
+ 'http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
+ 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
+ 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
]
)]
@@ -115,6 +119,68 @@ class Application extends BaseModel
protected $appends = ['server_status'];
+ protected $casts = ['custom_network_aliases' => 'array'];
+
+ public function customNetworkAliases(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ if (is_null($value) || $value === '') {
+ return null;
+ }
+
+ // If it's already a JSON string, decode it
+ if (is_string($value) && $this->isJson($value)) {
+ $value = json_decode($value, true);
+ }
+
+ // If it's a string but not JSON, treat it as a comma-separated list
+ if (is_string($value) && ! is_array($value)) {
+ $value = explode(',', $value);
+ }
+
+ $value = collect($value)
+ ->map(function ($alias) {
+ if (is_string($alias)) {
+ return str_replace(' ', '-', trim($alias));
+ }
+
+ return null;
+ })
+ ->filter()
+ ->unique() // Remove duplicate values
+ ->values()
+ ->toArray();
+
+ return empty($value) ? null : json_encode($value);
+ },
+ get: function ($value) {
+ if (is_null($value)) {
+ return null;
+ }
+
+ if (is_string($value) && $this->isJson($value)) {
+ return json_decode($value, true);
+ }
+
+ return is_array($value) ? $value : [];
+ }
+ );
+ }
+
+ /**
+ * Check if a string is a valid JSON
+ */
+ private function isJson($string)
+ {
+ if (! is_string($string)) {
+ return false;
+ }
+ json_decode($string);
+
+ return json_last_error() === JSON_ERROR_NONE;
+ }
+
protected static function booted()
{
static::addGlobalScope('withRelations', function ($builder) {
@@ -392,22 +458,23 @@ class Application extends BaseModel
{
return Attribute::make(
get: function () {
+ $base_dir = $this->base_directory ?? '/';
if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
if (str($this->git_repository)->contains('bitbucket')) {
- return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}";
+ return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}{$base_dir}";
}
- return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
+ return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}{$base_dir}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
if (str($this->git_repository)->contains('bitbucket')) {
- return "https://{$git_repository}/src/{$this->git_branch}";
+ return "https://{$git_repository}/src/{$this->git_branch}{$base_dir}";
}
- return "https://{$git_repository}/tree/{$this->git_branch}";
+ return "https://{$git_repository}/tree/{$this->git_branch}{$base_dir}";
}
return $this->git_repository;
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index fd8f1cba2..2a9bea67a 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -101,17 +102,23 @@ class ApplicationDeploymentQueue extends Model
'hidden' => $hidden,
'batch' => 1,
];
- if ($this->logs) {
- $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
- $newLogEntry['order'] = count($previousLogs) + 1;
- $previousLogs[] = $newLogEntry;
- $this->update([
- 'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR),
- ]);
- } else {
- $this->update([
- 'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR),
- ]);
- }
+
+ // Use a transaction to ensure atomicity
+ DB::transaction(function () use ($newLogEntry) {
+ // Reload the model to get the latest logs
+ $this->refresh();
+
+ if ($this->logs) {
+ $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
+ $newLogEntry['order'] = count($previousLogs) + 1;
+ $previousLogs[] = $newLogEntry;
+ $this->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
+ } else {
+ $this->logs = json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR);
+ }
+
+ // Save without triggering events to prevent potential race conditions
+ $this->saveQuietly();
+ });
}
}
diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php
index 0e702e460..97c32fa31 100644
--- a/app/Models/PrivateKey.php
+++ b/app/Models/PrivateKey.php
@@ -17,6 +17,8 @@ use phpseclib3\Crypt\PublicKeyLoader;
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'private_key' => ['type' => 'string', 'format' => 'private-key'],
+ 'public_key' => ['type' => 'string'],
+ 'fingerprint' => ['type' => 'string'],
'is_git_related' => ['type' => 'boolean'],
'team_id' => ['type' => 'integer'],
'created_at' => ['type' => 'string'],
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 56aa58e87..caf65cc58 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -20,7 +20,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA;
@@ -493,11 +492,7 @@ $schema://$host {
if ($proxyType === ProxyTypes::TRAEFIK->value) {
// Do nothing
} elseif ($proxyType === ProxyTypes::CADDY->value) {
- if (isDev()) {
- $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
- } else {
- $proxy_path = $proxy_path.'/caddy';
- }
+ $proxy_path = $proxy_path.'/caddy';
} elseif ($proxyType === ProxyTypes::NGINX->value) {
if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
@@ -925,7 +920,7 @@ $schema://$host {
public function isFunctional()
{
- $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
+ $isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && $this->ip !== '1.2.3.4';
if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename());
@@ -1026,22 +1021,11 @@ $schema://$host {
$this->refresh();
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable;
-
- Log::debug('Server reachability check', [
- 'server_id' => $this->id,
- 'is_reachable' => $isReachable,
- 'notification_sent' => $unreachableNotificationSent,
- 'unreachable_count' => $this->unreachable_count,
- ]);
-
if ($isReachable === true) {
$this->unreachable_count = 0;
$this->save();
if ($unreachableNotificationSent === true) {
- Log::debug('Server is now reachable, sending notification', [
- 'server_id' => $this->id,
- ]);
$this->sendReachableNotification();
}
@@ -1049,17 +1033,10 @@ $schema://$host {
}
$this->increment('unreachable_count');
- Log::debug('Incremented unreachable count', [
- 'server_id' => $this->id,
- 'new_count' => $this->unreachable_count,
- ]);
if ($this->unreachable_count === 1) {
$this->settings->is_reachable = true;
$this->settings->save();
- Log::debug('First unreachable attempt, marking as reachable', [
- 'server_id' => $this->id,
- ]);
return;
}
@@ -1068,11 +1045,6 @@ $schema://$host {
$failedChecks = 0;
for ($i = 0; $i < 3; $i++) {
$status = $this->serverStatus();
- Log::debug('Additional reachability check', [
- 'server_id' => $this->id,
- 'attempt' => $i + 1,
- 'status' => $status,
- ]);
sleep(5);
if (! $status) {
$failedChecks++;
@@ -1080,9 +1052,6 @@ $schema://$host {
}
if ($failedChecks === 3 && ! $unreachableNotificationSent) {
- Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [
- 'server_id' => $this->id,
- ]);
$this->sendUnreachableNotification();
}
}
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index c2a0df8cd..40d183033 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -141,6 +141,6 @@ class ServiceDatabase extends BaseModel
str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') ||
- str($this->databaseType())->contains('mongodb');
+ str($this->databaseType())->contains('mongo');
}
}
diff --git a/app/Models/Team.php b/app/Models/Team.php
index d36f8c1ab..42b88f9e7 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -192,8 +192,6 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
public function subscriptionEnded()
{
$this->subscription->update([
- 'stripe_subscription_id' => null,
- 'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
diff --git a/app/Traits/EnvironmentVariableProtection.php b/app/Traits/EnvironmentVariableProtection.php
new file mode 100644
index 000000000..b6b8d2687
--- /dev/null
+++ b/app/Traits/EnvironmentVariableProtection.php
@@ -0,0 +1,63 @@
+startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL');
+ }
+
+ /**
+ * Check if an environment variable is used in Docker Compose
+ *
+ * @param string $key The environment variable key to check
+ * @param string|null $dockerCompose The Docker Compose YAML content
+ * @return array [bool $isUsed, string $reason] Whether the variable is used and the reason if it is
+ */
+ protected function isEnvironmentVariableUsedInDockerCompose(string $key, ?string $dockerCompose): array
+ {
+ if (empty($dockerCompose)) {
+ return [false, ''];
+ }
+
+ try {
+ $dockerComposeData = Yaml::parse($dockerCompose);
+ $dockerEnvVars = data_get($dockerComposeData, 'services.*.environment');
+
+ foreach ($dockerEnvVars as $serviceEnvs) {
+ if (! is_array($serviceEnvs)) {
+ continue;
+ }
+
+ // Check for direct variable usage
+ foreach ($serviceEnvs as $env => $value) {
+ if ($env === $key) {
+ return [true, "Environment variable '{$key}' is used directly in the Docker Compose file."];
+ }
+ }
+
+ // Check for variable references in values
+ foreach ($serviceEnvs as $env => $value) {
+ if (is_string($value) && str_contains($value, '$'.$key)) {
+ return [true, "Environment variable '{$key}' is referenced in the Docker Compose file."];
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ // If there's an error parsing the Docker Compose file, we'll assume it's not used
+ return [false, ''];
+ }
+
+ return [false, ''];
+ }
+}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index d5283898e..3f1e8513c 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -24,6 +24,26 @@ function queue_application_deployment(Application $application, string $deployme
if ($destination) {
$destination_id = $destination->id;
}
+
+ // Check if there's already a deployment in progress or queued for this application and commit
+ $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
+ ->where('commit', $commit)
+ ->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])
+ ->first();
+
+ if ($existing_deployment) {
+ // If force_rebuild is true or rollback is true or no_questions_asked is true, we'll still create a new deployment
+ if (! $force_rebuild && ! $rollback && ! $no_questions_asked) {
+ // Return the existing deployment's details
+ return [
+ 'status' => 'skipped',
+ 'message' => 'Deployment already queued for this commit.',
+ 'deployment_uuid' => $existing_deployment->deployment_uuid,
+ 'existing_deployment' => $existing_deployment,
+ ];
+ }
+ }
+
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
'application_name' => $application->name,
@@ -47,11 +67,17 @@ function queue_application_deployment(Application $application, string $deployme
ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id,
);
- } elseif (next_queuable($server_id, $application_id)) {
+ } elseif (next_queuable($server_id, $application_id, $commit)) {
ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id,
);
}
+
+ return [
+ 'status' => 'queued',
+ 'message' => 'Deployment queued.',
+ 'deployment_uuid' => $deployment_uuid,
+ ];
}
function force_start_deployment(ApplicationDeploymentQueue $deployment)
{
@@ -78,20 +104,35 @@ function queue_next_deployment(Application $application)
}
}
-function next_queuable(string $server_id, string $application_id): bool
+function next_queuable(string $server_id, string $application_id, string $commit = 'HEAD'): bool
{
- $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at');
- $same_application_deployments = $deployments->where('application_id', $application_id);
- $in_progress = $same_application_deployments->filter(function ($value, $key) {
- return $value->status === 'in_progress';
- });
- if ($in_progress->count() > 0) {
+ // Check if there's already a deployment in progress for this application and commit
+ $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
+ ->where('commit', $commit)
+ ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
+ ->first();
+
+ if ($existing_deployment) {
return false;
}
+
+ // Check if there's any deployment in progress for this application
+ $in_progress = ApplicationDeploymentQueue::where('application_id', $application_id)
+ ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
+ ->exists();
+
+ if ($in_progress) {
+ return false;
+ }
+
+ // Check server's concurrent build limit
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
+ $active_deployments = ApplicationDeploymentQueue::where('server_id', $server_id)
+ ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
+ ->count();
- if ($deployments->count() > $concurrent_builds) {
+ if ($active_deployments >= $concurrent_builds) {
return false;
}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 8bad79708..d094b0f57 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -296,7 +296,8 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
return $payload;
}
-function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null)
+
+function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null)
{
$labels = collect([]);
if ($serviceLabels) {
@@ -304,6 +305,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
} else {
$labels->push("caddy_ingress_network={$network}");
}
+
+ $http_basic_auth_enabled = $http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
+
foreach ($domains as $loop => $domain) {
$url = Url::fromString($domain);
$host = $url->getHost();
@@ -340,20 +344,30 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
$labels->push("caddy_{$loop}.redir={$schema}://{$host_without_www}{uri}");
}
- if (isDev()) {
- // $labels->push("caddy_{$loop}.tls=internal");
+ if ($http_basic_auth_enabled) {
+ $http_basic_auth_password = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
+ $labels->push("caddy_{$loop}.basicauth.{$http_basic_auth_username}=\"{$http_basic_auth_password}\"");
}
}
return $labels->sort();
}
-function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both')
+
+function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
$labels->push('traefik.http.middlewares.gzip.compress=true');
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
+ $http_basic_auth_enabled = $http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
+ $http_basic_auth_label = "http-basic-auth-{$uuid}";
+
+ if ($http_basic_auth_enabled) {
+ $http_basic_auth_password = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
+ $labels->push("traefik.http.middlewares.{$http_basic_auth_label}.basicauth.users={$http_basic_auth_username}:{$http_basic_auth_password}");
+ }
+
$middlewares_from_labels = collect([]);
if ($serviceLabels) {
@@ -511,6 +525,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ if ($http_basic_auth_enabled) {
+ $middlewares->push($http_basic_auth_label);
+ }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
@@ -534,6 +551,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ if ($http_basic_auth_enabled) {
+ $middlewares->push($http_basic_auth_label);
+ }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
@@ -562,6 +582,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
if ($pull_request_id !== 0) {
$appUuid = $appUuid.'-pr-'.$pull_request_id;
}
+ ray($application);
$labels = collect([]);
if ($pull_request_id === 0) {
if ($application->fqdn) {
@@ -577,7 +598,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(),
- redirect_direction: $application->redirect
+ redirect_direction: $application->redirect,
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
break;
case ProxyTypes::CADDY->value:
@@ -589,7 +613,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(),
- redirect_direction: $application->redirect
+ redirect_direction: $application->redirect,
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
break;
}
@@ -601,7 +628,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(),
- redirect_direction: $application->redirect
+ redirect_direction: $application->redirect,
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -611,7 +641,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(),
- redirect_direction: $application->redirect
+ redirect_direction: $application->redirect,
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
}
}
@@ -631,7 +664,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled()
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
break;
case ProxyTypes::CADDY->value:
@@ -642,7 +678,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled()
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
break;
}
@@ -653,7 +692,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled()
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -662,7 +704,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled()
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ http_basic_auth_enabled: $application->http_basic_auth_enabled,
+ http_basic_auth_username: $application->http_basic_auth_username,
+ http_basic_auth_password: $application->http_basic_auth_password,
));
}
}
@@ -682,8 +727,10 @@ function isDatabaseImage(?string $image = null)
$image = str($image)->append(':latest');
}
$imageName = $image->before(':');
- if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
- return true;
+ foreach (DATABASE_DOCKER_IMAGES as $database_docker_image) {
+ if (str($imageName)->contains($database_docker_image)) {
+ return true;
+ }
}
return false;
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 81f8ff18a..0de2f2fd9 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -52,6 +52,9 @@ function generateGithubToken(GithubApp $source, string $type)
if (! $response->successful()) {
$error = data_get($response->json(), 'message', 'no error message found');
+ if ($error === 'Not Found') {
+ $error = 'Repository not found. Is it moved or deleted?';
+ }
throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error);
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index b90de4dbc..44e20c9b3 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -2987,7 +2987,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$predefinedPort = '8000';
}
if ($isDatabase) {
- $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
+ $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) {
$savedService = $applicationFound;
$savedService = ServiceDatabase::firstOrCreate([
@@ -2999,178 +2999,174 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
- 'image' => $image,
'service_id' => $resource->id,
]);
}
} else {
$savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName,
- 'image' => $image,
'service_id' => $resource->id,
]);
}
- $environment = collect(data_get($service, 'environment', []));
- $buildArgs = collect(data_get($service, 'build.args', []));
- $environment = $environment->merge($buildArgs);
- // convert environment variables to one format
- $environment = convertToKeyValueCollection($environment);
+ // Check if image changed
+ if ($savedService->image !== $image) {
+ $savedService->image = $image;
+ $savedService->save();
+ }
+ }
+ $environment = collect(data_get($service, 'environment', []));
+ $buildArgs = collect(data_get($service, 'build.args', []));
+ $environment = $environment->merge($buildArgs);
- // Add Coolify defined environments
- $allEnvironments = $resource->environment_variables()->get(['key', 'value']);
+ // convert environment variables to one format
+ $environment = convertToKeyValueCollection($environment);
- $allEnvironments = $allEnvironments->mapWithKeys(function ($item) {
- return [$item['key'] => $item['value']];
- });
- // filter and add magic environments
- foreach ($environment as $key => $value) {
- // Get all SERVICE_ variables from keys and values
- $key = str($key);
- $value = str($value);
+ // Add Coolify defined environments
+ $allEnvironments = $resource->environment_variables()->get(['key', 'value']);
- $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/';
- preg_match_all($regex, $value, $valueMatches);
- if (count($valueMatches[1]) > 0) {
- foreach ($valueMatches[1] as $match) {
- $match = replaceVariables($match);
- if ($match->startsWith('SERVICE_')) {
- if ($magicEnvironments->has($match->value())) {
- continue;
- }
- $magicEnvironments->put($match->value(), '');
+ $allEnvironments = $allEnvironments->mapWithKeys(function ($item) {
+ return [$item['key'] => $item['value']];
+ });
+ // filter and add magic environments
+ foreach ($environment as $key => $value) {
+ // Get all SERVICE_ variables from keys and values
+ $key = str($key);
+ $value = str($value);
+
+ $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/';
+ preg_match_all($regex, $value, $valueMatches);
+ if (count($valueMatches[1]) > 0) {
+ foreach ($valueMatches[1] as $match) {
+ $match = replaceVariables($match);
+ if ($match->startsWith('SERVICE_')) {
+ if ($magicEnvironments->has($match->value())) {
+ continue;
}
- }
- }
-
- // Get magic environments where we need to preset the FQDN
- 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;
- }
- if ($isApplication) {
- $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
- } elseif ($isService) {
- if ($fqdnFor) {
- $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
- } else {
- $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
- }
- }
-
- if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
- $path = $value->value();
- if ($path !== '/') {
- $fqdn = "$fqdn$path";
- }
- }
- $fqdnWithPort = $fqdn;
- if ($port) {
- $fqdnWithPort = "$fqdn:$port";
- }
- if ($isApplication && is_null($resource->fqdn)) {
- data_forget($resource, 'environment_variables');
- data_forget($resource, 'environment_variables_preview');
- $resource->fqdn = $fqdnWithPort;
- $resource->save();
- } elseif ($isService && is_null($savedService->fqdn)) {
- $savedService->fqdn = $fqdnWithPort;
- $savedService->save();
- }
-
- if (substr_count(str($key)->value(), '_') === 2) {
- $resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $fqdn,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
- }
- if (substr_count(str($key)->value(), '_') === 3) {
- $newKey = str($key)->beforeLast('_');
- $resource->environment_variables()->firstOrCreate([
- 'key' => $newKey->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $fqdn,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
+ $magicEnvironments->put($match->value(), '');
}
}
}
- $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
- if ($magicEnvironments->count() > 0) {
- foreach ($magicEnvironments as $key => $value) {
- $key = str($key);
- $value = replaceVariables($value);
- $command = parseCommandFromMagicEnvVariable($key);
- $found = $resource->environment_variables()->where('key', $key->value())->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first();
- if ($found) {
- continue;
- }
- if ($command->value() === 'FQDN') {
- $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
- if (str($fqdnFor)->contains('_')) {
- $fqdnFor = str($fqdnFor)->before('_');
- }
- if ($isApplication) {
- $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
- } elseif ($isService) {
- $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
- }
- $resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $fqdn,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
- } elseif ($command->value() === 'URL') {
- $fqdnFor = $key->after('SERVICE_URL_')->lower()->value();
- if (str($fqdnFor)->contains('_')) {
- $fqdnFor = str($fqdnFor)->before('_');
- }
- if ($isApplication) {
- $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
- } elseif ($isService) {
- $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
- }
- $fqdn = str($fqdn)->replace('http://', '')->replace('https://', '');
- $resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $fqdn,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
+ // Get magic environments where we need to preset the FQDN
+ 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;
+ }
+ if ($isApplication) {
+ $fqdn = generateFqdn($server, "$uuid");
+ } elseif ($isService) {
+ if ($fqdnFor) {
+ $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
} else {
- $value = generateEnvValue($command, $resource);
- $resource->environment_variables()->firstOrCreate([
- 'key' => $key->value(),
- 'resourceable_type' => get_class($resource),
- 'resourceable_id' => $resource->id,
- ], [
- 'value' => $value,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
+ $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
}
}
+
+ if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
+ $path = $value->value();
+ if ($path !== '/') {
+ $fqdn = "$fqdn$path";
+ }
+ }
+ $fqdnWithPort = $fqdn;
+ if ($port) {
+ $fqdnWithPort = "$fqdn:$port";
+ }
+ if ($isApplication && is_null($resource->fqdn)) {
+ data_forget($resource, 'environment_variables');
+ data_forget($resource, 'environment_variables_preview');
+ $resource->fqdn = $fqdnWithPort;
+ $resource->save();
+ } elseif ($isService && is_null($savedService->fqdn)) {
+ $savedService->fqdn = $fqdnWithPort;
+ $savedService->save();
+ }
+
+ if (substr_count(str($key)->value(), '_') === 2) {
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $key->value(),
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdn,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ }
+ if (substr_count(str($key)->value(), '_') === 3) {
+ $newKey = str($key)->beforeLast('_');
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $newKey->value(),
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdn,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ }
+ }
+ }
+
+ $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
+ if ($magicEnvironments->count() > 0) {
+ foreach ($magicEnvironments as $key => $value) {
+ $key = str($key);
+ $value = replaceVariables($value);
+ $command = parseCommandFromMagicEnvVariable($key);
+ $found = $resource->environment_variables()->where('key', $key->value())->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first();
+ if ($found) {
+ continue;
+ }
+ if ($command->value() === 'FQDN') {
+ $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
+ if (str($fqdnFor)->contains('_')) {
+ $fqdnFor = str($fqdnFor)->before('_');
+ }
+ $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $key->value(),
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdn,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ } elseif ($command->value() === 'URL') {
+ $fqdnFor = $key->after('SERVICE_URL_')->lower()->value();
+ if (str($fqdnFor)->contains('_')) {
+ $fqdnFor = str($fqdnFor)->before('_');
+ }
+ $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
+ $fqdn = str($fqdn)->replace('http://', '')->replace('https://', '');
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $key->value(),
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $fqdn,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ } else {
+ $value = generateEnvValue($command, $resource);
+ $resource->environment_variables()->firstOrCreate([
+ 'key' => $key->value(),
+ 'resourceable_type' => get_class($resource),
+ 'resourceable_id' => $resource->id,
+ ], [
+ 'value' => $value,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ }
}
}
}
@@ -3201,6 +3197,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$use_network_mode = data_get($service, 'network_mode') !== null;
$depends_on = collect(data_get($service, 'depends_on', []));
$labels = collect(data_get($service, 'labels', []));
+ if ($labels->count() > 0) {
+ if (isAssociativeArray($labels)) {
+ $newLabels = collect([]);
+ $labels->each(function ($value, $key) use ($newLabels) {
+ $newLabels->push("$key=$value");
+ });
+ $labels = $newLabels;
+ }
+ }
$environment = collect(data_get($service, 'environment', []));
$ports = collect(data_get($service, 'ports', []));
$buildArgs = collect(data_get($service, 'build.args', []));
diff --git a/config/constants.php b/config/constants.php
index 3f23a191b..c057a85db 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,14 +2,15 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.405',
+ 'version' => '4.0.0-beta.410',
'helper_version' => '1.0.8',
- 'realtime_version' => '1.0.6',
+ 'realtime_version' => '1.0.7',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
'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),
],
diff --git a/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php b/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php
new file mode 100644
index 000000000..61fadd0e5
--- /dev/null
+++ b/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php
@@ -0,0 +1,22 @@
+text('custom_network_aliases')->nullable();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('custom_network_aliases');
+ });
+ }
+};
diff --git a/database/migrations/2025_04_01_124212_stripe_comment_nullable.php b/database/migrations/2025_04_01_124212_stripe_comment_nullable.php
new file mode 100644
index 000000000..7f61c202e
--- /dev/null
+++ b/database/migrations/2025_04_01_124212_stripe_comment_nullable.php
@@ -0,0 +1,28 @@
+longText('stripe_comment')->nullable()->change();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('subscriptions', function (Blueprint $table) {
+ $table->longText('stripe_comment')->nullable(false)->change();
+ });
+ }
+};
diff --git a/database/migrations/2025_04_17_110026_add_application_http_basic_auth_fields.php b/database/migrations/2025_04_17_110026_add_application_http_basic_auth_fields.php
new file mode 100644
index 000000000..247300abd
--- /dev/null
+++ b/database/migrations/2025_04_17_110026_add_application_http_basic_auth_fields.php
@@ -0,0 +1,32 @@
+boolean('http_basic_auth_enabled')->default(false);
+ $table->string('http_basic_auth_username')->nullable(true)->default(null);
+ $table->string('http_basic_auth_password')->nullable(true)->default(null);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('http_basic_auth_enabled');
+ $table->dropColumn('http_basic_auth_username');
+ $table->dropColumn('http_basic_auth_password');
+ });
+ }
+};
diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json
index aea3952c0..1c329e47f 100644
--- a/docker/coolify-realtime/package-lock.json
+++ b/docker/coolify-realtime/package-lock.json
@@ -7,9 +7,9 @@
"dependencies": {
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
- "axios": "1.7.9",
+ "axios": "1.8.4",
"cookie": "1.0.2",
- "dotenv": "16.4.7",
+ "dotenv": "16.5.0",
"node-pty": "1.0.0",
"ws": "8.18.1"
}
@@ -36,9 +36,9 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
- "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
+ "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -90,9 +90,9 @@
}
},
"node_modules/dotenv": {
- "version": "16.4.7",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
- "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json
index 0a9b80cb5..7851d7f4d 100644
--- a/docker/coolify-realtime/package.json
+++ b/docker/coolify-realtime/package.json
@@ -5,9 +5,9 @@
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"cookie": "1.0.2",
- "axios": "1.7.9",
- "dotenv": "16.4.7",
+ "axios": "1.8.4",
+ "dotenv": "16.5.0",
"node-pty": "1.0.0",
"ws": "8.18.1"
}
-}
+}
\ No newline at end of file
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile
index 38bb50f3f..8d74ba107 100644
--- a/docker/production/Dockerfile
+++ b/docker/production/Dockerfile
@@ -89,9 +89,9 @@ RUN echo "alias ll='ls -al'" >> /etc/profile && \
# Install Cloudflared based on architecture
RUN mkdir -p /usr/local/bin && \
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
- curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \
+ curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \
elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
- curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \
+ curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \
fi && \
chmod +x /usr/local/bin/cloudflared
diff --git a/hooks/pre-commit b/hooks/pre-commit
index 69a5a9d41..029f67917 100644
--- a/hooks/pre-commit
+++ b/hooks/pre-commit
@@ -1,7 +1,7 @@
#!/bin/sh
# Detect whether /dev/tty is available & functional
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then
- exec < /dev/tty
+ exec Dashboard