Merge branch 'next' into add-hostname

This commit is contained in:
Andras Bacsai
2025-04-22 22:03:45 +02:00
committed by GitHub
80 changed files with 2164 additions and 883 deletions

View File

@@ -4,20 +4,166 @@ All notable changes to this project will be documented in this file.
## [unreleased] ## [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 ### ⚙️ 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 ## [4.0.0-beta.404] - 2025-04-03
### 🚀 Features ### 🚀 Features
- *(proxy)* Enhance proxy handling and port conflict detection
- *(lang)* Added Azerbaijani language updated turkish language. (#5497) - *(lang)* Added Azerbaijani language updated turkish language. (#5497)
- *(lang)* Added Portuguese from Brazil language (#5500) - *(lang)* Added Portuguese from Brazil language (#5500)
- *(lang)* Add Indonesian language translations (#5513) - *(lang)* Add Indonesian language translations (#5513)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- *(database)* Custom config for MongoDB (#5471)
- *(ui)* Instance Backup settings
- *(docs)* Comment out execute for now - *(docs)* Comment out execute for now
- *(installation)* Mount the docker config - *(installation)* Mount the docker config
- *(installation)* Path to config file for docker login - *(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) - *(docs)* Contribute service url (#5517)
- *(proxy)* Proxy restart does not work on domain - *(proxy)* Proxy restart does not work on domain
- *(ui)* Only show copy button on https - *(ui)* Only show copy button on https
- *(database)* Custom config for MongoDB (#5471)
### 📚 Documentation ### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
- 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 ### ⚙️ 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 - *(service)* Remove unused code in Bugsink service
- *(versions)* Update version to 404 - *(versions)* Update version to 404
- *(versions)* Bump version to 403 (#5520) - *(versions)* Bump version to 404
## [4.0.0-beta.402] - 2025-04-01 ## [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 - *(DeployController)* Cast 'pr' query parameter to integer
- *(deploy)* Validate team ID before deployment - *(deploy)* Validate team ID before deployment
- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424) - *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424)
- *(ui)* Instance Backup settings
### 🚜 Refactor ### 🚜 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)* Add google variables to plausible.yaml (#5429)
- *(service)* Update authentik.yaml versions (#5373) - *(service)* Update authentik.yaml versions (#5373)
- *(core)* Remove redocs - *(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 ## [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 ### 🚀 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 - *(service)* Neon
- *(migration)* Add `ssl_certificates` table and model - *(migration)* Add `ssl_certificates` table and model
- *(migration)* Add ssl setting to `standalone_postgresqls` table - *(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 - *(ssl)* Improve Redis and remove modes
- Full SSL support for DrangonflyDB - Full SSL support for DrangonflyDB
- SSL notification - 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)* Change glance for v0.7
- *(templates)* Add Freescout service template - *(templates)* Add Freescout service template
- *(service)* Add Evolution API 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 - *(api)* Docker compose based apps creationg through api
- *(database)* Improve database type detection for Supabase Postgres images - *(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 - *(ssl)* Permission of ssl crt and key inside the container
- *(ui)* Make sure file mounts do not showing the encrypted values - *(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 - *(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 - *(ssl)* Add `--tls` arg to DrangflyDB
- *(notification)* Always send SSL notifications - *(notification)* Always send SSL notifications
- *(database)* Change default value of enable_ssl to false for multiple tables - *(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 - *(general)* Correct link to framework specific documentation
- *(core)* Redirect healthcheck route for dockercompose applications - *(core)* Redirect healthcheck route for dockercompose applications
- *(api)* Use name from request payload - *(api)* Use name from request payload
@@ -309,6 +458,7 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks ### ⚙️ Miscellaneous Tasks
- *(supabase)* Update Supabase service template and Postgres image version
- *(migration)* Remove unused columns - *(migration)* Remove unused columns
- *(ssl)* Improve code in ssl helper - *(ssl)* Improve code in ssl helper
- *(migration)* Ssl cert and key should not be nullable - *(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 - Rename ca crt folder to ssl
- *(ui)* Improve valid until handling - *(ui)* Improve valid until handling
- Improve code quality suggested by code rabbit - Improve code quality suggested by code rabbit
- *(supabase)* Update Supabase service template and Postgres image version
- *(versions)* Update version numbers for coolify and nightly - *(versions)* Update version numbers for coolify and nightly
## [4.0.0-beta.398] - 2025-03-01 ## [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 ### 🚀 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 - Able to import full db backups for pg/mysql/mariadb
- Restore backup from server file - Restore backup from server file
- Docker volume data cloning - Docker volume data cloning
@@ -744,6 +901,35 @@ All notable changes to this project will be documented in this file.
### 🐛 Bug Fixes ### 🐛 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 - Compose envs
- Scheduled tasks and backups are executed by server timezone. - Scheduled tasks and backups are executed by server timezone.
- Show backup timezone on the UI - Show backup timezone on the UI
@@ -843,6 +1029,7 @@ All notable changes to this project will be documented in this file.
### 🚜 Refactor ### 🚜 Refactor
- Rename `coolify.environment` to `coolify.environmentName`
- Rename parameter in DatabaseBackupJob for clarity - Rename parameter in DatabaseBackupJob for clarity
- Improve checkbox component accessibility and styling - Improve checkbox component accessibility and styling
- Remove unused tags method from ApplicationDeploymentJob - Remove unused tags method from ApplicationDeploymentJob
@@ -858,6 +1045,9 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks ### ⚙️ Miscellaneous Tasks
- Regenerate API spec, removing notification fields
- Remove ray debugging
- Version ++
- Improve Penpot healthchecks - Improve Penpot healthchecks
- Switch up readonly lables to make more sense - Switch up readonly lables to make more sense
- Remove unused computed fields - Remove unused computed fields
@@ -881,44 +1071,11 @@ All notable changes to this project will be documented in this file.
### 🚀 Features ### 🚀 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 - Migrate to new encryption options
- New encryption options - New encryption options
### 🐛 Bug Fixes ### 🐛 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 - Smtp encryption
- Bind() to 0.0.0.0:80 failed - Bind() to 0.0.0.0:80 failed
- Oauth seeder - Oauth seeder
@@ -928,15 +1085,11 @@ All notable changes to this project will be documented in this file.
- Error message - Error message
- Update healthcheck and port configurations to use port 8080 - 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 - Saving oauth
- Regenerate API spec, removing notification fields
- Remove ray debugging
- Version ++
## [4.0.0-beta.378] - 2024-12-13 ## [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 - Monaco editor light and dark mode switching
- Service status indicator + oauth saving - Service status indicator + oauth saving
- Socialite for azure and authentik - 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 ## [4.0.0-beta.377] - 2024-12-13

183
README.md
View File

@@ -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). 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
![image](https://github.com/user-attachments/assets/6022bc9c-8435-4d14-9497-8be230ed8cb1)
* [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+)
<a href="https://serpapi.com/?ref=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></a>
<a href="https://typebot.io/?ref=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://www.runpod.io/?ref=coolify.io">
<svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a>
<a href="https://lightspeed.run/?ref=coolify.io"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://dartnode.com/?ref=coolify.io"><img src="https://github.com/DartNode-com.png" width="60px" alt="DartNode"/></a>
<a href="https://www.flint.sh/en/home?ref=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a>
<a href="https://americancloud.com/?ref=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?ref=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://codext.link/coolify-io?ref=coolify.io"><img src="./other/logos/codext.jpg" width="60px" alt="Codext" /></a>
<a href="https://x.com/mrsmith9ja?ref=coolify.io"><img width="60px" alt="Thompson Edolo" src="https://github.com/verygreenboi.png"/></a>
<a href="https://www.uxwizz.com/?ref=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
<a href="https://github.com/Flowko"><img src="https://barrad.me/_ipx/f_webp&s_300x300/younes.jpg" width="60px" alt="Younes Barrad" /></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Automaze" /></a>
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>
<a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a>
<a href="https://github.com/whitesidest"><img src="https://avatars.githubusercontent.com/u/12365916?s=52&v=4" width="60px" alt="Tyler Whitesides" /></a>
<a href="https://github.com/aniftyco"><img src="https://github.com/aniftyco.png" width="60px" alt="NiftyCo" /></a>
<a href="https://github.com/iujlaki"><img src="https://github.com/iujlaki.png" width="60px" alt="Imre Ujlaki" /></a>
<a href="https://il.ly"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://www.breakcold.com/?utm_source=coolify.io"><img src="https://github.com/breakcold.png" width="60px" alt="Breakcold" /></a>
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a>
<a href="https://startupfa.me?utm_source=coolify.io"><img src="https://github.com/startupfame.png" width="60px" alt="StartupFame" /></a>
<a href="https://bsky.app/profile/jyc.dev"><img src="https://github.com/jycouet.png" width="60px" alt="jyc.dev" /></a>
<a href="https://bitlaunch.io/?utm_source=coolify.io"><img src="https://github.com/bitlaunchio.png" width="60px" alt="BitLaunch" /></a>
<a href="https://internetgarden.co/?utm_source=coolify.io"><img src="./other/logos/internetgarden.ico" width="60px" alt="Internet Garden" /></a>
<a href="https://jonasjaeger.com?utm_source=coolify.io"><img src="https://github.com/toxin20.png" width="60px" alt="Jonas Jaeger" /></a>
<a href="https://github.com/therealjp?utm_source=coolify.io"><img src="https://github.com/therealjp.png" width="60px" alt="JP" /></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img src="https://github.com/evercam.png" width="60px" alt="Evercam" /></a>
<a href="https://web3.career/?utm_source=coolify.io"><img src="https://web3.career/favicon1.png" width="60px" alt="Web3 Career" /></a>
## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/1/website"><img src="https://opencollective.com/coollabsio/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/2/website"><img src="https://opencollective.com/coollabsio/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/3/website"><img src="https://opencollective.com/coollabsio/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/4/website"><img src="https://opencollective.com/coollabsio/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/5/website"><img src="https://opencollective.com/coollabsio/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/6/website"><img src="https://opencollective.com/coollabsio/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
# Cloud # Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io) 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 - Better support
- Less maintenance for you - 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 awardwinning 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
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img width="60px" alt="Evercam" src="https://github.com/evercam.png"/></a>
<a href="https://github.com/iujlaki"><img width="60px" alt="Imre Ujlaki" src="https://github.com/iujlaki.png"/></a>
<a href="https://bsky.app/profile/jyc.dev"><img width="60px" alt="jyc.dev" src="https://github.com/jycouet.png"/></a>
<a href="https://github.com/therealjp?utm_source=coolify.io"><img width="60px" alt="TheRealJP" src="https://github.com/therealjp.png"/></a>
<a href="https://360creators.com/?utm_source=coolify.io"><img width="60px" alt="360Creators" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/503e0953-bff7-4296-b4cc-5e36d40eecc0/icon-360creators.png"/></a>
<a href="https://github.com/aniftyco"><img width="60px" alt="NiftyCo" src="https://github.com/aniftyco.png"/></a>
<a href="https://dry.software/?utm_source=coolify.io"><img width="60px" alt="Dry Software" src="https://github.com/dry-software.png"/></a>
<a href="https://lightspeed.run/?utm_source=coolify.io"><img width="60px" alt="Lightspeed.run" src="https://github.com/lightspeedrun.png"/></a>
<a href="https://linkdr.com?utm_source=coolify.io"><img width="60px" alt="LinkDr" src="https://github.com/LLM-Inc.png"/></a>
<a href="http://gravitywiz.com/?utm_source=coolify.io"><img width="60px" alt="Gravity Wiz" src="https://github.com/gravitywiz.png"/></a>
<a href="https://bitlaunch.io/?utm_source=coolify.io"><img width="60px" alt="BitLaunch" src="https://github.com/bitlaunchio.png"/></a>
<a href="https://bestforandroid.com/?utm_source=coolify.io"><img width="60px" alt="Best for Android" src="https://github.com/bestforandroid.png"/></a>
<a href="https://il.ly/?utm_source=coolify.io"><img width="60px" alt="Ilias Ism" src="https://github.com/Illyism.png"/></a>
<a href="https://formbricks.com/?utm_source=coolify.io"><img width="60px" alt="Formbricks" src="https://github.com/formbricks.png"/></a>
<a href="https://www.serversearcher.com/"><img width="60px" alt="Server Searcher" src="https://github.com/serversearcher.png"/></a>
<a href="https://www.reshot.ai/?utm_source=coolify.io"><img width="60px" alt="Reshot" src="https://coolify.io/images/reshotai.png"/></a>
<a href="https://cirun.io/?utm_source=coolify.io"><img width="60px" alt="Cirun" src="https://coolify.io/images/cirun-logo.png"/></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img width="60px" alt="Typebot" src="https://cdn.bsky.app/img/avatar/plain/did:plc:gwxcta3pccyim4z5vuultdqx/bafkreig23hci7e2qpdxicsshnuzujbcbcgmydxhbybkewszdezhdodv42m@jpeg"/></a>
<a href="https://cccareers.org/?utm_source=coolify.io"><img width="60px" alt="Creating Coding Careers" src="https://github.com/cccareers.png"/></a>
<a href="https://internetgarden.co/?utm_source=coolify.io"><img width="60px" alt="Internet Garden" src="https://coolify.io/images/internetgarden.ico"/></a>
<a href="https://web3.career/?utm_source=coolify.io"><img width="60px" alt="Web3 Jobs" src="https://coolify.io/images/web3jobs.png"/></a>
<a href="https://codext.link/coolify-io?utm_source=coolify.io"><img width="60px" alt="Codext" src="https://coolify.io/images/codext.jpg"/></a>
<a href="https://github.com/monocursive"><img width="60px" alt="Michael Mazurczak" src="https://github.com/monocursive.png"/></a>
<a href="https://fider.io/?utm_source=coolify.io"><img width="60px" alt="Fider" src="https://github.com/getfider.png"/></a>
<a href="https://www.flint.sh/en/home?utm_source=coolify.io"><img width="60px" alt="Flint" src="https://github.com/Flint-company.png"/></a>
<a href="https://github.com/urtho"><img width="60px" alt="Paweł Pierścionek" src="https://github.com/urtho.png"/></a>
<a href="https://www.runpod.io/?utm_source=coolify.io"><img width="60px" alt="RunPod" src="https://coolify.io/images/runpod.svg"/></a>
<a href="https://dartnode.com/?utm_source=coolify.io"><img width="60px" alt="DartNode" src="https://github.com/dartnode.png"/></a>
<a href="https://github.com/whitesidest"><img width="60px" alt="Tyler Whitesides" src="https://avatars.githubusercontent.com/u/12365916?s=52&v=4"/></a>
<a href="https://serpapi.com/?utm_source=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></a>
<a href="https://aquarela.io"><img width="60px" alt="Aquarela" src="https://github.com/aquarela-io.png"/></a>
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img width="60px" alt="Crypto Jobs List" src="https://github.com/cryptojobslist.png"/></a>
<a href="https://www.youtube.com/@AlfredNutile?utm_source=coolify.io"><img width="60px" alt="Alfred Nutile" src="https://github.com/alnutile.png"/></a>
<a href="https://startupfa.me?utm_source=coolify.io"><img width="60px" alt="Startup Fame" src="https://github.com/startupfame.png"/></a>
<a href="https://barrad.me/?utm_source=coolify.io"><img width="60px" alt="Younes Barrad" src="https://github.com/Flowko.png"/></a>
<a href="https://jonasjaeger.com?utm_source=coolify.io"><img width="60px" alt="Jonas Jaeger" src="https://github.com/toxin20.png"/></a>
<a href="https://pixel.ao/?utm_source=coolify.io"><img width="60px" alt="Pixel Infinito" src="https://github.com/pixelinfinito.png"/></a>
<a href="https://github.com/corentinclichy"><img width="60px" alt="Corentin Clichy" src="https://github.com/corentinclichy.png"/></a>
<a href="https://x.com/mrsmith9ja?utm_source=coolify.io"><img width="60px" alt="Thompson Edolo" src="https://github.com/verygreenboi.png"/></a>
<a href="https://devhuset.no?utm_source=coolify.io"><img width="60px" alt="Devhuset" src="https://github.com/devhuset.png"/></a>
<a href="https://arvensis.systems/?utm_source=coolify.io"><img width="60px" alt="Arvensis Systems" src="https://coolify.io/images/arvensis.png"/></a>
<a href="https://github.com/Niki2k1"><img width="60px" alt="Niklas Lausch" src="https://github.com/Niki2k1.png"/></a>
<a href="https://capgo.app/?utm_source=coolify.io"><img width="60px" alt="Cap-go" src="https://github.com/cap-go.png"/></a>
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
# Recognitions # Recognitions
<p> <p>

View File

@@ -217,6 +217,10 @@ class StartMongodb
if ($this->database->enable_ssl) { if ($this->database->enable_ssl) {
$commandParts = ['mongod']; $commandParts = ['mongod'];
if (! empty($this->database->mongo_conf)) {
$commandParts = ['mongod', '--config', '/etc/mongo/mongod.conf'];
}
$sslConfig = match ($this->database->ssl_mode) { $sslConfig = match ($this->database->ssl_mode) {
'allow' => [ 'allow' => [
'--tlsMode=allowTLS', '--tlsMode=allowTLS',

View File

@@ -27,7 +27,7 @@ class CheckProxy
return false; return false;
} }
$proxyType = $server->proxyType(); $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; return false;
} }
if (! $server->isProxyShouldRun()) { if (! $server->isProxyShouldRun()) {
@@ -37,8 +37,12 @@ class CheckProxy
return false; return false;
} }
} }
// Determine proxy container name based on environment
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
if ($server->isSwarm()) { if ($server->isSwarm()) {
$status = getContainerStatus($server, 'coolify-proxy_traefik'); $status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status); $server->proxy->set('status', $status);
$server->save(); $server->save();
if ($status === 'running') { if ($status === 'running') {
@@ -47,7 +51,7 @@ class CheckProxy
return true; return true;
} else { } else {
$status = getContainerStatus($server, 'coolify-proxy'); $status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') { if ($status === 'running') {
$server->proxy->set('status', 'running'); $server->proxy->set('status', 'running');
$server->save(); $server->save();
@@ -61,34 +65,11 @@ class CheckProxy
if ($server->id === 0) { if ($server->id === 0) {
$ip = 'host.docker.internal'; $ip = 'host.docker.internal';
} }
$portsToCheck = ['80', '443']; $portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) { foreach ($portsToCheck as $port) {
// Try multiple methods to check port availability // Use the smart port checker that handles dual-stack properly
$commands = [ if ($this->isPortConflict($server, $port, $proxyContainerName)) {
// 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) {
if ($fromUI) { if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>"); throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
} else { } else {
@@ -126,4 +107,144 @@ class CheckProxy
return true; 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;
}
}
} }

View File

@@ -14,15 +14,26 @@ class CleanupDocker
public function handle(Server $server) public function handle(Server $server)
{ {
$settings = instanceSettings(); $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'); $helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('constants.coolify.helper_image'); $helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion"; $helperImageWithVersion = "$helperImage:$helperImageVersion";
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
$commands = [ $commands = [
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"', 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
'docker image prune -af --filter "label!=coolify.managed=true"', 'docker image prune -af --filter "label!=coolify.managed=true"',
'docker builder prune -af', '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=$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) { if ($server->settings->delete_unused_volumes) {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\Generate;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
@@ -8,7 +8,7 @@ use Symfony\Component\Yaml\Yaml;
class OpenApi extends Command class OpenApi extends Command
{ {
protected $signature = 'openapi'; protected $signature = 'generate:openapi';
protected $description = 'Generate OpenApi file.'; protected $description = 'Generate OpenApi file.';
@@ -18,7 +18,7 @@ class OpenApi extends Command
echo "Generating OpenAPI documentation.\n"; echo "Generating OpenAPI documentation.\n";
// https://github.com/OAI/OpenAPI-Specification/releases // https://github.com/OAI/OpenAPI-Specification/releases
$process = Process::run([ $process = Process::run([
'/var/www/html/vendor/bin/openapi', './vendor/bin/openapi',
'app', 'app',
'-o', '-o',
'openapi.yaml', 'openapi.yaml',

View File

@@ -1,17 +1,17 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands\Generate;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
class ServicesGenerate extends Command class Services extends Command
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected $signature = 'services:generate'; protected $signature = 'generate:services';
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -880,12 +880,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
no_questions_asked: true, no_questions_asked: true,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
} else { } else {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
LoadComposeFile::dispatch($application); LoadComposeFile::dispatch($application);
@@ -1004,12 +1009,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
no_questions_asked: true, no_questions_asked: true,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
} else { } else {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
LoadComposeFile::dispatch($application); LoadComposeFile::dispatch($application);
@@ -1101,12 +1111,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
no_questions_asked: true, no_questions_asked: true,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
} else { } else {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
LoadComposeFile::dispatch($application); LoadComposeFile::dispatch($application);
@@ -1190,12 +1205,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
no_questions_asked: true, no_questions_asked: true,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
} }
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
@@ -1254,12 +1274,17 @@ class ApplicationsController extends Controller
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
no_questions_asked: true, no_questions_asked: true,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
} }
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
@@ -1610,6 +1635,18 @@ class ApplicationsController extends Controller
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Applications'], 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( requestBody: new OA\RequestBody(
description: 'Application updated.', description: 'Application updated.',
required: true, required: true,
@@ -1884,11 +1921,16 @@ class ApplicationsController extends Controller
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
} }
return response()->json([ return response()->json([
@@ -2520,10 +2562,6 @@ class ApplicationsController extends Controller
])->setStatusCode(201); ])->setStatusCode(201);
} }
} }
return response()->json([
'message' => 'Something went wrong.',
], 500);
} }
#[OA\Delete( #[OA\Delete(
@@ -2705,13 +2743,21 @@ class ApplicationsController extends Controller
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: $force, force_rebuild: $force,
is_api: true, is_api: true,
no_questions_asked: $instant_deploy no_questions_asked: $instant_deploy
); );
if ($result['status'] === 'skipped') {
return response()->json(
[
'message' => $result['message'],
],
200
);
}
return response()->json( return response()->json(
[ [
@@ -2866,12 +2912,17 @@ class ApplicationsController extends Controller
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
restart_only: true, restart_only: true,
is_api: true, is_api: true,
); );
if ($result['status'] === 'skipped') {
return response()->json([
'message' => $result['message'],
], 200);
}
return response()->json( return response()->json(
[ [
@@ -3006,73 +3057,73 @@ class ApplicationsController extends Controller
// ]); // ]);
// } // }
// private function validateDataApplications(Request $request, Server $server) private function validateDataApplications(Request $request, Server $server)
// { {
// $teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
// // Validate ports_mappings // Validate ports_mappings
// if ($request->has('ports_mappings')) { if ($request->has('ports_mappings')) {
// $ports = []; $ports = [];
// foreach (explode(',', $request->ports_mappings) as $portMapping) { foreach (explode(',', $request->ports_mappings) as $portMapping) {
// $port = explode(':', $portMapping); $port = explode(':', $portMapping);
// if (in_array($port[0], $ports)) { if (in_array($port[0], $ports)) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'ports_mappings' => 'The first number before : should be unique between mappings.', 'ports_mappings' => 'The first number before : should be unique between mappings.',
// ], ],
// ], 422); ], 422);
// } }
// $ports[] = $port[0]; $ports[] = $port[0];
// } }
// } }
// // Validate custom_labels // Validate custom_labels
// if ($request->has('custom_labels')) { if ($request->has('custom_labels')) {
// if (! isBase64Encoded($request->custom_labels)) { if (! isBase64Encoded($request->custom_labels)) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.', 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ], ],
// ], 422); ], 422);
// } }
// $customLabels = base64_decode($request->custom_labels); $customLabels = base64_decode($request->custom_labels);
// if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.', 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ], ],
// ], 422); ], 422);
// } }
// } }
// if ($request->has('domains') && $server->isProxyShouldRun()) { if ($request->has('domains') && $server->isProxyShouldRun()) {
// $uuid = $request->uuid; $uuid = $request->uuid;
// $fqdn = $request->domains; $fqdn = $request->domains;
// $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
// $fqdn = str($fqdn)->replaceStart(',', '')->trim(); $fqdn = str($fqdn)->replaceStart(',', '')->trim();
// $errors = []; $errors = [];
// $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
// if (filter_var($domain, FILTER_VALIDATE_URL) === false) { if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
// $errors[] = 'Invalid domain: '.$domain; $errors[] = 'Invalid domain: '.$domain;
// } }
// return str($domain)->trim()->lower(); return str($domain)->trim()->lower();
// }); });
// if (count($errors) > 0) { if (count($errors) > 0) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => $errors, 'errors' => $errors,
// ], 422); ], 422);
// } }
// if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'domains' => 'One of the domain is already used.', 'domains' => 'One of the domain is already used.',
// ], ],
// ], 422); ], 422);
// } }
// } }
// } }
} }

View File

@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use App\Models\Tag; use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -132,7 +133,7 @@ class DeployController extends Controller
#[OA\Get( #[OA\Get(
summary: 'Deploy', 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', path: '/deploy',
operationId: 'deploy-by-tag-or-uuid', operationId: 'deploy-by-tag-or-uuid',
security: [ security: [
@@ -191,10 +192,10 @@ class DeployController extends Controller
return invalidTokenResponse(); return invalidTokenResponse();
} }
$uuids = $request->query->get('uuid'); $uuids = $request->input('uuid');
$tags = $request->query->get('tag'); $tags = $request->input('tag');
$force = $request->query->get('force') ?? false; $force = $request->input('force') ?? false;
$pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0; $pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0;
if ($uuids && $tags) { if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); 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]; return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
} }
switch ($resource?->getMorphClass()) { switch ($resource?->getMorphClass()) {
case \App\Models\Application::class: case Application::class:
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $resource, application: $resource,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: $force, force_rebuild: $force,
pull_request_id: $pr, 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; break;
case \App\Models\Service::class: case Service::class:
StartService::run($resource); StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient."; $message = "Service {$resource->name} started. It could take a while, be patient.";
break; break;
@@ -333,6 +338,40 @@ class DeployController extends Controller
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Deployments'], 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: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 200,

View File

@@ -267,6 +267,18 @@ class ProjectController extends Controller
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Projects'], 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( requestBody: new OA\RequestBody(
required: true, required: true,
description: 'Project updated.', description: 'Project updated.',

View File

@@ -809,6 +809,6 @@ class ServersController extends Controller
} }
ValidateServer::dispatch($server); ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.']); return response()->json(['message' => 'Validation started.'], 201);
} }
} }

View File

@@ -380,6 +380,9 @@ class ServicesController extends Controller
$service = new Service; $service = new Service;
$result = $this->upsert_service($request, $service, $teamId); $result = $this->upsert_service($request, $service, $teamId);
if ($result instanceof \Illuminate\Http\JsonResponse) {
return $result;
}
return response()->json(serializeApiResponse($result))->setStatusCode(201); return response()->json(serializeApiResponse($result))->setStatusCode(201);
} else { } else {
@@ -527,6 +530,18 @@ class ServicesController extends Controller
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Services'], 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( requestBody: new OA\RequestBody(
description: 'Service updated.', description: 'Service updated.',
required: true, required: true,
@@ -596,12 +611,14 @@ class ServicesController extends Controller
} }
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) { if (! $service) {
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
$result = $this->upsert_service($request, $service, $teamId); $result = $this->upsert_service($request, $service, $teamId);
if ($result instanceof \Illuminate\Http\JsonResponse) {
return $result;
}
return response()->json(serializeApiResponse($result))->setStatusCode(200); return response()->json(serializeApiResponse($result))->setStatusCode(200);
} }

View File

@@ -100,18 +100,26 @@ class Bitbucket extends Controller
if ($x_bitbucket_event === 'repo:push') { if ($x_bitbucket_event === 'repo:push') {
if ($application->isDeployable()) { if ($application->isDeployable()) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: $commit, commit: $commit,
force_rebuild: false, force_rebuild: false,
is_webhook: true is_webhook: true
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'application' => $application->name, $return_payloads->push([
'status' => 'success', 'application' => $application->name,
'message' => 'Preview deployment queued.', 'status' => 'skipped',
]); 'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Deployment queued.',
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
@@ -143,7 +151,7 @@ class Bitbucket extends Controller
]); ]);
} }
} }
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -152,11 +160,19 @@ class Bitbucket extends Controller
is_webhook: true, is_webhook: true,
git_type: 'bitbucket' git_type: 'bitbucket'
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'application' => $application->name, $return_payloads->push([
'status' => 'success', 'application' => $application->name,
'message' => 'Preview deployment queued.', 'status' => 'skipped',
]); 'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -116,19 +116,27 @@ class Gitea extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) { if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: false, force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'), commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true, is_webhook: true,
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'status' => 'success', $return_payloads->push([
'message' => 'Deployment queued.', 'application' => $application->name,
'application_uuid' => $application->uuid, 'status' => 'skipped',
'application_name' => $application->name, 'message' => $result['message'],
]); ]);
} else {
$return_payloads->push([
'status' => 'success',
'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
}
} else { } else {
$paths = str($application->watch_paths)->explode("\n"); $paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([ $return_payloads->push([
@@ -175,7 +183,7 @@ class Gitea extends Controller
]); ]);
} }
} }
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -184,11 +192,19 @@ class Gitea extends Controller
is_webhook: true, is_webhook: true,
git_type: 'gitea' git_type: 'gitea'
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'application' => $application->name, $return_payloads->push([
'status' => 'success', 'application' => $application->name,
'message' => 'Preview deployment queued.', 'status' => 'skipped',
]); 'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -122,19 +122,29 @@ class Github extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) { if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: false, force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'), commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true, is_webhook: true,
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'status' => 'success', $return_payloads->push([
'message' => 'Deployment queued.', 'application' => $application->name,
'application_uuid' => $application->uuid, 'status' => 'skipped',
'application_name' => $application->name, '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 { } else {
$paths = str($application->watch_paths)->explode("\n"); $paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([ $return_payloads->push([
@@ -181,7 +191,8 @@ class Github extends Controller
]); ]);
} }
} }
queue_application_deployment(
$result = queue_application_deployment(
application: $application, application: $application,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -190,11 +201,19 @@ class Github extends Controller
is_webhook: true, is_webhook: true,
git_type: 'github' git_type: 'github'
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'application' => $application->name, $return_payloads->push([
'status' => 'success', 'application' => $application->name,
'message' => 'Preview deployment queued.', 'status' => 'skipped',
]); 'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
@@ -341,7 +360,7 @@ class Github extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) { if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'), commit: data_get($payload, 'after', 'HEAD'),
@@ -349,10 +368,11 @@ class Github extends Controller
is_webhook: true, is_webhook: true,
); );
$return_payloads->push([ $return_payloads->push([
'status' => 'success', 'status' => $result['status'],
'message' => 'Deployment queued.', 'message' => $result['message'],
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'application_name' => $application->name, 'application_name' => $application->name,
'deployment_uuid' => $result['deployment_uuid'],
]); ]);
} else { } else {
$paths = str($application->watch_paths)->explode("\n"); $paths = str($application->watch_paths)->explode("\n");
@@ -389,7 +409,7 @@ class Github extends Controller
'pull_request_html_url' => $pull_request_html_url, 'pull_request_html_url' => $pull_request_html_url,
]); ]);
} }
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -398,11 +418,19 @@ class Github extends Controller
is_webhook: true, is_webhook: true,
git_type: 'github' git_type: 'github'
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'application' => $application->name, $return_payloads->push([
'status' => 'success', 'application' => $application->name,
'message' => 'Preview deployment queued.', 'status' => 'skipped',
]); 'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -142,19 +142,28 @@ class Gitlab extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) { if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'), commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false, force_rebuild: false,
is_webhook: true, is_webhook: true,
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'status' => 'success', $return_payloads->push([
'message' => 'Deployment queued.', 'status' => $result['status'],
'application_uuid' => $application->uuid, 'message' => $result['message'],
'application_name' => $application->name, '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 { } else {
$paths = str($application->watch_paths)->explode("\n"); $paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([ $return_payloads->push([
@@ -201,7 +210,7 @@ class Gitlab extends Controller
]); ]);
} }
} }
queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -210,11 +219,19 @@ class Gitlab extends Controller
is_webhook: true, is_webhook: true,
git_type: 'gitlab' git_type: 'gitlab'
); );
$return_payloads->push([ if ($result['status'] === 'skipped') {
'application' => $application->name, $return_payloads->push([
'status' => 'success', 'application' => $application->name,
'message' => 'Preview Deployment queued', 'status' => 'skipped',
]); 'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview Deployment queued',
]);
}
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -329,7 +329,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else { } else {
$this->write_deployment_configurations(); $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); $this->graceful_shutdown_container($this->deployment_uuid);
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); 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'); $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
} }
$ports = $this->application->main_port(); $ports = $this->application->main_port();
if ($this->pull_request_id !== 0) { $coolify_envs = $this->generate_coolify_env_variables();
$this->env_filename = ".env-pr-$this->pull_request_id"; $coolify_envs->each(function ($item, $key) use ($envs) {
// Add SOURCE_COMMIT if not exists $envs->push($key.'='.$item);
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { });
if (! is_null($this->commit)) { if ($this->pull_request_id === 0) {
$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 {
$this->env_filename = '.env'; $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) { foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value; $real_value = $env->real_value;
@@ -1017,6 +929,32 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0'); $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()) { if ($envs->isEmpty()) {
$this->env_filename = null; $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("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->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -1394,6 +1331,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
foreach ($destination_ids as $destination_id) { foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id); $destination = StandaloneDocker::find($destination_id);
if (! $destination) {
continue;
}
$server = $destination->server; $server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) { if ($server->team_id !== $this->mainServer->team_id) {
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); $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() 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(); $this->generate_git_import_commands();
$local_branch = $this->branch; $local_branch = $this->branch;
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
@@ -1626,20 +1577,128 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' '); $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() private function generate_env_variables()
{ {
$this->env_args = collect([]); $this->env_args = collect([]);
$this->env_args->put('SOURCE_COMMIT', $this->commit); $this->env_args->put('SOURCE_COMMIT', $this->commit);
$coolify_envs = $this->generate_coolify_env_variables();
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) { foreach ($this->application->build_environment_variables as $env) {
if (! is_null($env->real_value)) { if (! is_null($env->real_value)) {
$this->env_args->put($env->key, $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 { } else {
foreach ($this->application->build_environment_variables_preview as $env) { foreach ($this->application->build_environment_variables_preview as $env) {
if (! is_null($env->real_value)) { if (! is_null($env->real_value)) {
$this->env_args->put($env->key, $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) { $labels = $labels->filter(function ($value, $key) {
return ! Str::startsWith($value, 'coolify.'); 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->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save(); $this->application->save();
} else { } else {
@@ -1710,6 +1750,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]); ]);
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo')); $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 = [ $docker_compose = [
'services' => [ 'services' => [
$this->container_name => [ $this->container_name => [
@@ -1719,9 +1763,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network => [ $this->destination->network => [
'aliases' => [ 'aliases' => array_merge(
$this->container_name, [$this->container_name],
], $custom_network_aliases
),
], ],
], ],
'mem_limit' => $this->application->limits_memory, 'mem_limit' => $this->application->limits_memory,
@@ -2409,20 +2454,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function next(string $status) private function next(string $status)
{ {
queue_next_deployment($this->application); queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status
if ( // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value ||
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value $this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
) { return;
$this->application_deployment_queue->update([
'status' => $status,
]);
} }
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)); $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return; return;
} }
if ($status === ApplicationDeploymentStatus::FINISHED->value) { if ($status === ApplicationDeploymentStatus::FINISHED->value) {
if (! $this->only_this_server) { if (! $this->only_this_server) {
$this->deploy_to_additional_destinations(); $this->deploy_to_additional_destinations();

View File

@@ -17,11 +17,13 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 60;
public function __construct() {} public function __construct() {}
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)];
} }
public function handle(): void public function handle(): void

View File

@@ -31,7 +31,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array 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) {} public function __construct(public Server $server, public bool $manualCleanup = false) {}

View File

@@ -71,7 +71,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))->expireAfter(30)];
} }
public function backoff(): int public function backoff(): int

View File

@@ -24,7 +24,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array 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) {} public function __construct(public Server $server) {}

View File

@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array 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) {} public function __construct(public Server $server) {}

View File

@@ -269,7 +269,7 @@ class Email extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->smtpEnabled = false; $this->smtpEnabled = false;
return handleError($e); return handleError($e, $this);
} }
} }
@@ -337,32 +337,29 @@ class Email extends Component
public function copyFromInstanceSettings() public function copyFromInstanceSettings()
{ {
$settings = instanceSettings(); $settings = instanceSettings();
$this->smtpFromAddress = $settings->smtp_from_address;
$this->smtpFromName = $settings->smtp_from_name;
if ($settings->smtp_enabled) { if ($settings->smtp_enabled) {
$this->smtpEnabled = true; $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->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) { if ($settings->resend_enabled) {
$this->resendEnabled = true; $this->resendEnabled = true;
$this->resendApiKey = $settings->resend_api_key;
$this->smtpEnabled = false; $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() public function render()

View File

@@ -68,6 +68,7 @@ class General extends Component
'application.publish_directory' => 'nullable', 'application.publish_directory' => 'nullable',
'application.ports_exposes' => 'required', 'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable', 'application.ports_mappings' => 'nullable',
'application.custom_network_aliases' => 'nullable',
'application.dockerfile' => 'nullable', 'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable', 'application.docker_registry_image_tag' => 'nullable',
@@ -93,6 +94,9 @@ class General extends Component
'application.settings.is_preserve_repository_enabled' => 'boolean|required', 'application.settings.is_preserve_repository_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable', 'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required', '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 = [ protected $validationAttributes = [
@@ -121,6 +125,7 @@ class General extends Component
'application.custom_labels' => 'Custom labels', 'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build', 'application.dockerfile_target_build' => 'Dockerfile target build',
'application.custom_docker_run_options' => 'Custom docker run commands', '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_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.custom_nginx_configuration' => 'Custom Nginx configuration', 'application.custom_nginx_configuration' => 'Custom Nginx configuration',
@@ -455,7 +460,6 @@ class General extends Component
{ {
$config = GenerateConfig::run($this->application, true); $config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json'); $fileName = str($this->application->name)->slug()->append('_config.json');
dd($config);
return response()->streamDownload(function () use ($config) { return response()->streamDownload(function () use ($config) {
echo $config; echo $config;

View File

@@ -84,11 +84,16 @@ class Heading extends Component
return; return;
} }
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( $result = queue_application_deployment(
application: $this->application, application: $this->application,
deployment_uuid: $this->deploymentUuid, deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild, force_rebuild: $force_rebuild,
); );
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [ return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
@@ -126,11 +131,16 @@ class Heading extends Component
return; return;
} }
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( $result = queue_application_deployment(
application: $this->application, application: $this->application,
deployment_uuid: $this->deploymentUuid, deployment_uuid: $this->deploymentUuid,
restart_only: true, restart_only: true,
); );
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [ return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],

View File

@@ -159,13 +159,18 @@ class Previews extends Component
'pull_request_html_url' => $pull_request_html_url, 'pull_request_html_url' => $pull_request_html_url,
]); ]);
} }
queue_application_deployment( $result = queue_application_deployment(
application: $this->application, application: $this->application,
deployment_uuid: $this->deployment_uuid, deployment_uuid: $this->deployment_uuid,
force_rebuild: false, force_rebuild: false,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null, 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', [ return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],

View File

@@ -30,11 +30,15 @@ class Source extends Component
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $gitCommitSha = null; public ?string $gitCommitSha = null;
#[Locked]
public $sources;
public function mount() public function mount()
{ {
try { try {
$this->syncData(); $this->syncData();
$this->getPrivateKeys(); $this->getPrivateKeys();
$this->getSources();
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); 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) public function setPrivateKey(int $privateKeyId)
{ {
try { try {
@@ -92,4 +104,20 @@ class Source extends Component
return handleError($e, $this); 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);
}
}
} }

View File

@@ -21,7 +21,7 @@ class General extends Component
public string $redis_username; public string $redis_username;
public string $redis_password; public ?string $redis_password;
public string $redis_version; public string $redis_version;

View File

@@ -79,7 +79,7 @@ class Destination extends Component
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$server = Server::ownedByCurrentTeam()->findOrFail($server_id); $server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail(); $destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
queue_application_deployment( $result = queue_application_deployment(
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
application: $this->resource, application: $this->resource,
server: $server, server: $server,
@@ -87,6 +87,11 @@ class Destination extends Component
only_this_server: true, only_this_server: true,
no_questions_asked: true, no_questions_asked: true,
); );
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return redirect()->route('project.application.deployment.show', [ return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'), 'project_uuid' => data_get($this->resource, 'environment.project.uuid'),

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable; namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component; use Livewire\Component;
class All extends Component class All extends Component
{ {
use EnvironmentVariableProtection;
public $resource; public $resource;
public string $resourceClass; public string $resourceClass;
@@ -138,17 +141,57 @@ class All extends Component
private function handleBulkSubmit() private function handleBulkSubmit()
{ {
$variables = parseEnvFormatToArray($this->variables); $variables = parseEnvFormatToArray($this->variables);
$changesMade = false;
$errorOccurred = false;
$this->deleteRemovedVariables(false, $variables); // Try to delete removed variables
$this->updateOrCreateVariables(false, $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) { if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview); $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) private function handleSingleSubmit($data)
@@ -184,11 +227,37 @@ class All extends Component
private function deleteRemovedVariables($isPreview, $variables) private function deleteRemovedVariables($isPreview, $variables)
{ {
$method = $isPreview ? 'environment_variables_preview' : 'environment_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}' <br><br>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(); $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
return $variablesToDelete->count();
} }
private function updateOrCreateVariables($isPreview, $variables) private function updateOrCreateVariables($isPreview, $variables)
{ {
$count = 0;
foreach ($variables as $key => $value) { foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) { if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
continue; continue;
@@ -198,8 +267,12 @@ class All extends Component
if ($found) { if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) { if (! $found->is_shown_once && ! $found->is_multiline) {
$found->value = $value; // Only count as a change if the value actually changed
$found->save(); if ($found->value !== $value) {
$found->value = $value;
$found->save();
$count++;
}
} }
} else { } else {
$environment = new EnvironmentVariable; $environment = new EnvironmentVariable;
@@ -212,8 +285,11 @@ class All extends Component
$environment->resourceable_type = $this->resource->getMorphClass(); $environment->resourceable_type = $this->resource->getMorphClass();
$environment->save(); $environment->save();
$count++;
} }
} }
return $count;
} }
public function refreshEnvs() public function refreshEnvs()

View File

@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable; use App\Models\SharedEnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
use EnvironmentVariableProtection;
public $parameters; public $parameters;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env; public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
@@ -40,6 +43,8 @@ class Show extends Component
public bool $is_really_required = false; public bool $is_really_required = false;
public bool $is_redis_credential = false;
protected $listeners = [ protected $listeners = [
'refreshEnvs' => 'refresh', 'refreshEnvs' => 'refresh',
'refresh', 'refresh',
@@ -65,7 +70,9 @@ class Show extends Component
} }
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->checkEnvs(); $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() public function refresh()
@@ -171,6 +178,17 @@ class Show extends Component
public function delete() public function delete()
{ {
try { 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}' <br><br>Please remove it from the Docker Compose file first.");
return;
}
}
$this->env->delete(); $this->env->delete();
$this->dispatch('environmentVariableDeleted'); $this->dispatch('environmentVariableDeleted');
$this->dispatch('success', 'Environment variable deleted successfully.'); $this->dispatch('success', 'Environment variable deleted successfully.');

View File

@@ -38,7 +38,8 @@ class DynamicConfigurations extends Component
$contents = collect([]); $contents = collect([]);
foreach ($files as $file) { foreach ($files as $file) {
$without_extension = str_replace('.', '|', $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->contents = $contents;
$this->dispatch('$refresh'); $this->dispatch('$refresh');

View File

@@ -177,7 +177,7 @@ class SettingsEmail extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->smtpEnabled = false; $this->smtpEnabled = false;
return handleError($e); return handleError($e, $this);
} }
} }
@@ -207,7 +207,7 @@ class SettingsEmail extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->resendEnabled = false; $this->resendEnabled = false;
return handleError($e); return handleError($e, $this);
} }
} }

View File

@@ -12,19 +12,30 @@ class Index extends Component
public bool $alreadySubscribed = false; public bool $alreadySubscribed = false;
public bool $isUnpaid = false;
public bool $isCancelled = false;
public bool $isMember = false;
public bool $loading = true;
public function mount() public function mount()
{ {
if (! isCloud()) { if (! isCloud()) {
return redirect(RouteServiceProvider::HOME); return redirect(RouteServiceProvider::HOME);
} }
if (auth()->user()?->isMember()) { if (auth()->user()?->isMember()) {
return redirect()->route('dashboard'); $this->isMember = true;
} }
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show'); return redirect()->route('subscription.show');
} }
$this->settings = instanceSettings(); $this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists(); $this->alreadySubscribed = currentTeam()->subscription()->exists();
if (! $this->alreadySubscribed) {
$this->loading = false;
}
} }
public function stripeCustomerPortal() public function stripeCustomerPortal()
@@ -37,6 +48,41 @@ class Index extends Component
return redirect($session->url); 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() public function render()
{ {
return view('livewire.subscription.index'); return view('livewire.subscription.index');

View File

@@ -45,6 +45,7 @@ use Visus\Cuid2\Cuid2;
'start_command' => ['type' => 'string', 'description' => 'Start command.'], 'start_command' => ['type' => 'string', 'description' => 'Start command.'],
'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'], 'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'], '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.'], 'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'], 'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], '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.'], '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.'], '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.'], '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 $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() protected static function booted()
{ {
static::addGlobalScope('withRelations', function ($builder) { static::addGlobalScope('withRelations', function ($builder) {
@@ -392,22 +458,23 @@ class Application extends BaseModel
{ {
return Attribute::make( return Attribute::make(
get: function () { 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 (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
if (str($this->git_repository)->contains('bitbucket')) { 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 // Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) { if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
if (str($this->git_repository)->contains('bitbucket')) { 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; return $this->git_repository;

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
#[OA\Schema( #[OA\Schema(
@@ -101,17 +102,23 @@ class ApplicationDeploymentQueue extends Model
'hidden' => $hidden, 'hidden' => $hidden,
'batch' => 1, 'batch' => 1,
]; ];
if ($this->logs) {
$previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR); // Use a transaction to ensure atomicity
$newLogEntry['order'] = count($previousLogs) + 1; DB::transaction(function () use ($newLogEntry) {
$previousLogs[] = $newLogEntry; // Reload the model to get the latest logs
$this->update([ $this->refresh();
'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR),
]); if ($this->logs) {
} else { $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$this->update([ $newLogEntry['order'] = count($previousLogs) + 1;
'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR), $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();
});
} }
} }

View File

@@ -17,6 +17,8 @@ use phpseclib3\Crypt\PublicKeyLoader;
'name' => ['type' => 'string'], 'name' => ['type' => 'string'],
'description' => ['type' => 'string'], 'description' => ['type' => 'string'],
'private_key' => ['type' => 'string', 'format' => 'private-key'], 'private_key' => ['type' => 'string', 'format' => 'private-key'],
'public_key' => ['type' => 'string'],
'fingerprint' => ['type' => 'string'],
'is_git_related' => ['type' => 'boolean'], 'is_git_related' => ['type' => 'boolean'],
'team_id' => ['type' => 'integer'], 'team_id' => ['type' => 'integer'],
'created_at' => ['type' => 'string'], 'created_at' => ['type' => 'string'],

View File

@@ -20,7 +20,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -493,11 +492,7 @@ $schema://$host {
if ($proxyType === ProxyTypes::TRAEFIK->value) { if ($proxyType === ProxyTypes::TRAEFIK->value) {
// Do nothing // Do nothing
} elseif ($proxyType === ProxyTypes::CADDY->value) { } elseif ($proxyType === ProxyTypes::CADDY->value) {
if (isDev()) { $proxy_path = $proxy_path.'/caddy';
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
} else {
$proxy_path = $proxy_path.'/caddy';
}
} elseif ($proxyType === ProxyTypes::NGINX->value) { } elseif ($proxyType === ProxyTypes::NGINX->value) {
if (isDev()) { if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx'; $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
@@ -925,7 +920,7 @@ $schema://$host {
public function isFunctional() 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) { if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
@@ -1026,22 +1021,11 @@ $schema://$host {
$this->refresh(); $this->refresh();
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent; $unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable; $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) { if ($isReachable === true) {
$this->unreachable_count = 0; $this->unreachable_count = 0;
$this->save(); $this->save();
if ($unreachableNotificationSent === true) { if ($unreachableNotificationSent === true) {
Log::debug('Server is now reachable, sending notification', [
'server_id' => $this->id,
]);
$this->sendReachableNotification(); $this->sendReachableNotification();
} }
@@ -1049,17 +1033,10 @@ $schema://$host {
} }
$this->increment('unreachable_count'); $this->increment('unreachable_count');
Log::debug('Incremented unreachable count', [
'server_id' => $this->id,
'new_count' => $this->unreachable_count,
]);
if ($this->unreachable_count === 1) { if ($this->unreachable_count === 1) {
$this->settings->is_reachable = true; $this->settings->is_reachable = true;
$this->settings->save(); $this->settings->save();
Log::debug('First unreachable attempt, marking as reachable', [
'server_id' => $this->id,
]);
return; return;
} }
@@ -1068,11 +1045,6 @@ $schema://$host {
$failedChecks = 0; $failedChecks = 0;
for ($i = 0; $i < 3; $i++) { for ($i = 0; $i < 3; $i++) {
$status = $this->serverStatus(); $status = $this->serverStatus();
Log::debug('Additional reachability check', [
'server_id' => $this->id,
'attempt' => $i + 1,
'status' => $status,
]);
sleep(5); sleep(5);
if (! $status) { if (! $status) {
$failedChecks++; $failedChecks++;
@@ -1080,9 +1052,6 @@ $schema://$host {
} }
if ($failedChecks === 3 && ! $unreachableNotificationSent) { if ($failedChecks === 3 && ! $unreachableNotificationSent) {
Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [
'server_id' => $this->id,
]);
$this->sendUnreachableNotification(); $this->sendUnreachableNotification();
} }
} }

View File

@@ -141,6 +141,6 @@ class ServiceDatabase extends BaseModel
str($this->databaseType())->contains('postgres') || str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') || str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') || str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongodb'); str($this->databaseType())->contains('mongo');
} }
} }

View File

@@ -192,8 +192,6 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
public function subscriptionEnded() public function subscriptionEnded()
{ {
$this->subscription->update([ $this->subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false, 'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false, 'stripe_trial_already_ended' => false,

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Traits;
use Symfony\Component\Yaml\Yaml;
trait EnvironmentVariableProtection
{
/**
* Check if an environment variable is protected from deletion
*
* @param string $key The environment variable key to check
* @return bool True if the variable is protected, false otherwise
*/
protected function isProtectedEnvironmentVariable(string $key): bool
{
return str($key)->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, ''];
}
}

View File

@@ -24,6 +24,26 @@ function queue_application_deployment(Application $application, string $deployme
if ($destination) { if ($destination) {
$destination_id = $destination->id; $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([ $deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id, 'application_id' => $application_id,
'application_name' => $application->name, 'application_name' => $application->name,
@@ -47,11 +67,17 @@ function queue_application_deployment(Application $application, string $deployme
ApplicationDeploymentJob::dispatch( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
); );
} elseif (next_queuable($server_id, $application_id)) { } elseif (next_queuable($server_id, $application_id, $commit)) {
ApplicationDeploymentJob::dispatch( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
); );
} }
return [
'status' => 'queued',
'message' => 'Deployment queued.',
'deployment_uuid' => $deployment_uuid,
];
} }
function force_start_deployment(ApplicationDeploymentQueue $deployment) 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'); // Check if there's already a deployment in progress for this application and commit
$same_application_deployments = $deployments->where('application_id', $application_id); $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
$in_progress = $same_application_deployments->filter(function ($value, $key) { ->where('commit', $commit)
return $value->status === 'in_progress'; ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
}); ->first();
if ($in_progress->count() > 0) {
if ($existing_deployment) {
return false; 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); $server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds; $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; return false;
} }

View File

@@ -296,7 +296,8 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
return $payload; 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([]); $labels = collect([]);
if ($serviceLabels) { if ($serviceLabels) {
@@ -304,6 +305,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
} else { } else {
$labels->push("caddy_ingress_network={$network}"); $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) { foreach ($domains as $loop => $domain) {
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
@@ -340,20 +344,30 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
$labels->push("caddy_{$loop}.redir={$schema}://{$host_without_www}{uri}"); $labels->push("caddy_{$loop}.redir={$schema}://{$host_without_www}{uri}");
} }
if (isDev()) { if ($http_basic_auth_enabled) {
// $labels->push("caddy_{$loop}.tls=internal"); $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(); 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 = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
$labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.gzip.compress=true');
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); $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([]); $middlewares_from_labels = collect([]);
if ($serviceLabels) { if ($serviceLabels) {
@@ -511,6 +525,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $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_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name); $middlewares->push($middleware_name);
}); });
@@ -534,6 +551,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $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_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name); $middlewares->push($middleware_name);
}); });
@@ -562,6 +582,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$appUuid = $appUuid.'-pr-'.$pull_request_id; $appUuid = $appUuid.'-pr-'.$pull_request_id;
} }
ray($application);
$labels = collect([]); $labels = collect([]);
if ($pull_request_id === 0) { if ($pull_request_id === 0) {
if ($application->fqdn) { if ($application->fqdn) {
@@ -577,7 +598,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(), 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; break;
case ProxyTypes::CADDY->value: case ProxyTypes::CADDY->value:
@@ -589,7 +613,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(), 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; break;
} }
@@ -601,7 +628,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(), 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( $labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network, network: $application->destination->network,
@@ -611,7 +641,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled(), 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, onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), 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; break;
case ProxyTypes::CADDY->value: case ProxyTypes::CADDY->value:
@@ -642,7 +678,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort, onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), 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; break;
} }
@@ -653,7 +692,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort, onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), 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( $labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network, network: $application->destination->network,
@@ -662,7 +704,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort, onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(), is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(), 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'); $image = str($image)->append(':latest');
} }
$imageName = $image->before(':'); $imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { foreach (DATABASE_DOCKER_IMAGES as $database_docker_image) {
return true; if (str($imageName)->contains($database_docker_image)) {
return true;
}
} }
return false; return false;

View File

@@ -52,6 +52,9 @@ function generateGithubToken(GithubApp $source, string $type)
if (! $response->successful()) { if (! $response->successful()) {
$error = data_get($response->json(), 'message', 'no error message found'); $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); throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error);
} }

View File

@@ -2987,7 +2987,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { 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) { if ($applicationFound) {
$savedService = $applicationFound; $savedService = $applicationFound;
$savedService = ServiceDatabase::firstOrCreate([ $savedService = ServiceDatabase::firstOrCreate([
@@ -2999,178 +2999,174 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} else { } else {
$savedService = ServiceDatabase::firstOrCreate([ $savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id, 'service_id' => $resource->id,
]); ]);
} }
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id, '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 // Check if image changed
$environment = convertToKeyValueCollection($environment); 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 // convert environment variables to one format
$allEnvironments = $resource->environment_variables()->get(['key', 'value']); $environment = convertToKeyValueCollection($environment);
$allEnvironments = $allEnvironments->mapWithKeys(function ($item) { // Add Coolify defined environments
return [$item['key'] => $item['value']]; $allEnvironments = $resource->environment_variables()->get(['key', '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]*)\}?)/'; $allEnvironments = $allEnvironments->mapWithKeys(function ($item) {
preg_match_all($regex, $value, $valueMatches); return [$item['key'] => $item['value']];
if (count($valueMatches[1]) > 0) { });
foreach ($valueMatches[1] as $match) { // filter and add magic environments
$match = replaceVariables($match); foreach ($environment as $key => $value) {
if ($match->startsWith('SERVICE_')) { // Get all SERVICE_ variables from keys and values
if ($magicEnvironments->has($match->value())) { $key = str($key);
continue; $value = str($value);
}
$magicEnvironments->put($match->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(), '');
}
// 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,
]);
} }
} }
} }
$allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); // Get magic environments where we need to preset the FQDN
if ($magicEnvironments->count() > 0) { if ($key->startsWith('SERVICE_FQDN_')) {
foreach ($magicEnvironments as $key => $value) { // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
$key = str($key); if (substr_count(str($key)->value(), '_') === 3) {
$value = replaceVariables($value); $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
$command = parseCommandFromMagicEnvVariable($key); $port = $key->afterLast('_')->value();
$found = $resource->environment_variables()->where('key', $key->value())->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first(); } else {
if ($found) { $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
continue; $port = null;
} }
if ($command->value() === 'FQDN') { if ($isApplication) {
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); $fqdn = generateFqdn($server, "$uuid");
if (str($fqdnFor)->contains('_')) { } elseif ($isService) {
$fqdnFor = str($fqdnFor)->before('_'); if ($fqdnFor) {
} $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
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,
]);
} else { } else {
$value = generateEnvValue($command, $resource); $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
$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,
]);
} }
} }
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; $use_network_mode = data_get($service, 'network_mode') !== null;
$depends_on = collect(data_get($service, 'depends_on', [])); $depends_on = collect(data_get($service, 'depends_on', []));
$labels = collect(data_get($service, 'labels', [])); $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', [])); $environment = collect(data_get($service, 'environment', []));
$ports = collect(data_get($service, 'ports', [])); $ports = collect(data_get($service, 'ports', []));
$buildArgs = collect(data_get($service, 'build.args', [])); $buildArgs = collect(data_get($service, 'build.args', []));

View File

@@ -2,14 +2,15 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.405', 'version' => '4.0.0-beta.410',
'helper_version' => '1.0.8', 'helper_version' => '1.0.8',
'realtime_version' => '1.0.6', 'realtime_version' => '1.0.7',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'), 'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'registry_url' => env('REGISTRY_URL', 'ghcr.io'), 'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'), '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), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
], ],

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_network_aliases')->nullable();
});
}
public function down()
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('custom_network_aliases');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->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();
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->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');
});
}
};

View File

@@ -7,9 +7,9 @@
"dependencies": { "dependencies": {
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0", "@xterm/xterm": "5.5.0",
"axios": "1.7.9", "axios": "1.8.4",
"cookie": "1.0.2", "cookie": "1.0.2",
"dotenv": "16.4.7", "dotenv": "16.5.0",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"ws": "8.18.1" "ws": "8.18.1"
} }
@@ -36,9 +36,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.9", "version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@@ -90,9 +90,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"

View File

@@ -5,9 +5,9 @@
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0", "@xterm/xterm": "5.5.0",
"cookie": "1.0.2", "cookie": "1.0.2",
"axios": "1.7.9", "axios": "1.8.4",
"dotenv": "16.4.7", "dotenv": "16.5.0",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"ws": "8.18.1" "ws": "8.18.1"
} }
} }

View File

@@ -89,9 +89,9 @@ RUN echo "alias ll='ls -al'" >> /etc/profile && \
# Install Cloudflared based on architecture # Install Cloudflared based on architecture
RUN mkdir -p /usr/local/bin && \ RUN mkdir -p /usr/local/bin && \
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ 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 \ 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 && \ fi && \
chmod +x /usr/local/bin/cloudflared chmod +x /usr/local/bin/cloudflared

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Detect whether /dev/tty is available & functional # Detect whether /dev/tty is available & functional
if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then
exec < /dev/tty exec </dev/tty
fi fi
# Get list of stashed PHP files # Get list of stashed PHP files

View File

@@ -1798,6 +1798,18 @@
"summary": "Update", "summary": "Update",
"description": "Update application by UUID.", "description": "Update application by UUID.",
"operationId": "update-application-by-uuid", "operationId": "update-application-by-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": { "requestBody": {
"description": "Application updated.", "description": "Application updated.",
"required": true, "required": true,
@@ -4441,7 +4453,7 @@
"Deployments" "Deployments"
], ],
"summary": "Deploy", "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.",
"operationId": "deploy-by-tag-or-uuid", "operationId": "deploy-by-tag-or-uuid",
"parameters": [ "parameters": [
{ {
@@ -4529,6 +4541,40 @@
"summary": "List application deployments", "summary": "List application deployments",
"description": "List application deployments by using the app uuid", "description": "List application deployments by using the app uuid",
"operationId": "list-deployments-by-app-uuid", "operationId": "list-deployments-by-app-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "skip",
"in": "query",
"description": "Number of records to skip.",
"required": false,
"schema": {
"type": "integer",
"default": 0,
"minimum": 0
}
},
{
"name": "take",
"in": "query",
"description": "Number of records to take.",
"required": false,
"schema": {
"type": "integer",
"default": 10,
"minimum": 1
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "List application deployments by using the app uuid.", "description": "List application deployments by using the app uuid.",
@@ -4921,6 +4967,18 @@
"summary": "Update", "summary": "Update",
"description": "Update Project.", "description": "Update Project.",
"operationId": "update-project-by-uuid", "operationId": "update-project-by-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the project.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": { "requestBody": {
"description": "Project updated.", "description": "Project updated.",
"required": true, "required": true,
@@ -5321,6 +5379,22 @@
}, },
"404": { "404": {
"description": "Private Key not found." "description": "Private Key not found."
},
"422": {
"description": "Private Key is in use and cannot be deleted.",
"content": {
"application\/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "Private Key is in use and cannot be deleted."
}
},
"type": "object"
}
}
}
} }
}, },
"security": [ "security": [
@@ -6222,6 +6296,18 @@
"summary": "Update", "summary": "Update",
"description": "Update service by UUID.", "description": "Update service by UUID.",
"operationId": "update-service-by-uuid", "operationId": "update-service-by-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the service.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": { "requestBody": {
"description": "Service updated.", "description": "Service updated.",
"required": true, "required": true,
@@ -7198,6 +7284,11 @@
"nullable": true, "nullable": true,
"description": "Ports mappings." "description": "Ports mappings."
}, },
"custom_network_aliases": {
"type": "string",
"nullable": true,
"description": "Network aliases for Docker container."
},
"base_directory": { "base_directory": {
"type": "string", "type": "string",
"description": "Base directory for all commands." "description": "Base directory for all commands."
@@ -7638,6 +7729,12 @@
"type": "string", "type": "string",
"format": "private-key" "format": "private-key"
}, },
"public_key": {
"type": "string"
},
"fingerprint": {
"type": "string"
},
"is_git_related": { "is_git_related": {
"type": "boolean" "type": "boolean"
}, },

View File

@@ -1277,6 +1277,15 @@ paths:
summary: Update summary: Update
description: 'Update application by UUID.' description: 'Update application by UUID.'
operationId: update-application-by-uuid operationId: update-application-by-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
requestBody: requestBody:
description: 'Application updated.' description: 'Application updated.'
required: true required: true
@@ -3085,7 +3094,7 @@ paths:
tags: tags:
- Deployments - Deployments
summary: Deploy 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.'
operationId: deploy-by-tag-or-uuid operationId: deploy-by-tag-or-uuid
parameters: parameters:
- -
@@ -3135,6 +3144,33 @@ paths:
summary: 'List application deployments' summary: 'List application deployments'
description: 'List application deployments by using the app uuid' description: 'List application deployments by using the app uuid'
operationId: list-deployments-by-app-uuid operationId: list-deployments-by-app-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
-
name: skip
in: query
description: 'Number of records to skip.'
required: false
schema:
type: integer
default: 0
minimum: 0
-
name: take
in: query
description: 'Number of records to take.'
required: false
schema:
type: integer
default: 10
minimum: 1
responses: responses:
'200': '200':
description: 'List application deployments by using the app uuid.' description: 'List application deployments by using the app uuid.'
@@ -3377,6 +3413,15 @@ paths:
summary: Update summary: Update
description: 'Update Project.' description: 'Update Project.'
operationId: update-project-by-uuid operationId: update-project-by-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the project.'
required: true
schema:
type: string
format: uuid
requestBody: requestBody:
description: 'Project updated.' description: 'Project updated.'
required: true required: true
@@ -3630,6 +3675,14 @@ paths:
$ref: '#/components/responses/400' $ref: '#/components/responses/400'
'404': '404':
description: 'Private Key not found.' description: 'Private Key not found.'
'422':
description: 'Private Key is in use and cannot be deleted.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Private Key is in use and cannot be deleted.' }
type: object
security: security:
- -
bearerAuth: [] bearerAuth: []
@@ -4145,6 +4198,15 @@ paths:
summary: Update summary: Update
description: 'Update service by UUID.' description: 'Update service by UUID.'
operationId: update-service-by-uuid operationId: update-service-by-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the service.'
required: true
schema:
type: string
format: uuid
requestBody: requestBody:
description: 'Service updated.' description: 'Service updated.'
required: true required: true
@@ -4769,6 +4831,10 @@ components:
type: string type: string
nullable: true nullable: true
description: 'Ports mappings.' description: 'Ports mappings.'
custom_network_aliases:
type: string
nullable: true
description: 'Network aliases for Docker container.'
base_directory: base_directory:
type: string type: string
description: 'Base directory for all commands.' description: 'Base directory for all commands.'
@@ -5093,6 +5159,10 @@ components:
private_key: private_key:
type: string type: string
format: private-key format: private-key
public_key:
type: string
fingerprint:
type: string
is_git_related: is_git_related:
type: boolean type: boolean
team_id: team_id:

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.405" "version": "4.0.0-beta.410"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.406" "version": "4.0.0-beta.411"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"
}, },
"realtime": { "realtime": {
"version": "1.0.6" "version": "1.0.7"
}, },
"sentinel": { "sentinel": {
"version": "0.0.15" "version": "0.0.15"

8
package-lock.json generated
View File

@@ -22,7 +22,7 @@
"pusher-js": "8.4.0", "pusher-js": "8.4.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"vite": "^6.2.4", "vite": "^6.2.6",
"vue": "3.5.13" "vue": "3.5.13"
} }
}, },
@@ -2994,9 +2994,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.4", "version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -16,7 +16,7 @@
"pusher-js": "8.4.0", "pusher-js": "8.4.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"vite": "^6.2.4", "vite": "^6.2.6",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"dependencies": { "dependencies": {

View File

@@ -8,13 +8,8 @@
<h1>Dashboard</h1> <h1>Dashboard</h1>
<div class="subtitle">Your self-hosted infrastructure.</div> <div class="subtitle">Your self-hosted infrastructure.</div>
@if (request()->query->get('success')) @if (request()->query->get('success'))
<div class="items-center justify-center mb-10 font-bold rounded alert alert-success"> <div class=" mb-10 font-bold alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" Your subscription has been activated! Welcome onboard! It could take a few seconds before your
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Your subscription has been activated! Welcome onboard! <br>It could take a few seconds before your
subscription is activated.<br> Please be patient. subscription is activated.<br> Please be patient.
</div> </div>
@endif @endif

View File

@@ -1,13 +1,6 @@
<div> <div class="w-full px-2">
<x-modal-confirmation <x-modal-confirmation buttonFullWidth title="Confirm Team Deletion?" buttonTitle="Delete Team" isErrorButton
title="Confirm Team Deletion?" submitAction="delete" :actions="['The current Team will be permanently deleted.']" confirmationText="{{ $team }}"
buttonTitle="Delete Team"
isErrorButton
submitAction="delete"
:actions="['The current Team will be permanently deleted.']"
confirmationText="{{ $team }}"
confirmationLabel="Please confirm the execution of the actions by entering the Team Name below" confirmationLabel="Please confirm the execution of the actions by entering the Team Name below"
shortConfirmationLabel="Team Name" shortConfirmationLabel="Team Name" step3ButtonText="Permanently Delete" />
step3ButtonText="Permanently Delete"
/>
</div> </div>

View File

@@ -342,6 +342,24 @@
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" <x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." /> helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@endif @endif
@if (!$application->destination->server->isSwarm())
<x-forms.input id="application.custom_network_aliases" label="Network Aliases"
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>api.internal,api.local"
wire:model="application.custom_network_aliases" />
@endif
</div>
<h3 class="pt-8">HTTP Basic Authentication</h3>
<div x-data="{ enabled: {{ $application->http_basic_auth_enabled ? 'true' : 'false' }} }">
<div class="w-96">
<x-forms.checkbox helper="This will add the proper proxy labels to the container."
label="Enable" id="application.http_basic_auth_enabled" x-model="enabled" />
</div>
<div class="flex gap-2 py-2" x-show="enabled">
<x-forms.input id="application.http_basic_auth_username" label="Username" />
<x-forms.input id="application.http_basic_auth_password" type="password" label="Password" />
</div>
</div> </div>
@if ($application->settings->is_container_label_readonly_enabled) @if ($application->settings->is_container_label_readonly_enabled)

View File

@@ -26,6 +26,11 @@
<div class="pb-4">Code source of your application.</div> <div class="pb-4">Code source of your application.</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@if (!$privateKeyId)
<div>Currently connected source: <span
class="font-bold text-warning">{{ data_get($application, 'source.name', 'No source connected') }}</span>
</div>
@endif
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" /> <x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" />
<x-forms.input placeholder="main" id="gitBranch" label="Branch" /> <x-forms.input placeholder="main" id="gitBranch" label="Branch" />
@@ -34,6 +39,7 @@
<x-forms.input placeholder="HEAD" id="gitCommitSha" placeholder="HEAD" label="Commit SHA" /> <x-forms.input placeholder="HEAD" id="gitCommitSha" placeholder="HEAD" label="Commit SHA" />
</div> </div>
</div> </div>
@if ($privateKeyId) @if ($privateKeyId)
<h3 class="pt-4">Deploy Key</h3> <h3 class="pt-4">Deploy Key</h3>
<div class="py-2 pt-4">Currently attached Private Key: <span <div class="py-2 pt-4">Currently attached Private Key: <span
@@ -47,6 +53,38 @@
</x-forms.button> </x-forms.button>
@endforeach @endforeach
</div> </div>
@else
<div class="pt-4">
<h3 class="pb-2">Change Git Source</h3>
<div class="grid grid-cols-1 gap-2">
@forelse ($sources as $source)
<div wire:key="{{ $source->name }}">
<x-modal-confirmation title="Change Git Source" :actions="['Change git source to ' . $source->name]" :buttonFullWidth="true"
:isHighlightedButton="$application->source_id === $source->id" :disabled="$application->source_id === $source->id"
submitAction="changeSource({{ $source->id }}, {{ $source->getMorphClass() }})"
:confirmWithText="true" confirmationText="Change Git Source"
confirmationLabel="Please confirm changing the git source by entering the text below"
shortConfirmationLabel="Confirmation Text" :confirmWithPassword="false">
<x-slot:customButton>
<div class="flex items-center gap-2">
<div class="box-title">
{{ $source->name }}
@if ($application->source_id === $source->id)
<span class="text-xs">(current)</span>
@endif
</div>
<div class="box-description">
{{ $source->organization ?? 'Personal Account' }}
</div>
</div>
</x-slot:customButton>
</x-modal-confirmation>
</div>
@empty
<div>No other sources found</div>
@endforelse
</div>
</div>
@endif @endif
</form> </form>
</div> </div>

View File

@@ -20,7 +20,12 @@
</div> </div>
<div class="w-48 pb-2"> <div class="w-48 pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" /> <x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" /> @if ($s3s->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@else
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled />
@endif
</div> </div>
@if ($backup->save_s3) @if ($backup->save_s3)
<div class="pb-6"> <div class="pb-6">

View File

@@ -13,22 +13,41 @@
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/redis'>https://hub.docker.com/_/redis</a>" /> helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/redis'>https://hub.docker.com/_/redis</a>" />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@if (version_compare($redis_version, '6.0', '>=')) @if ($database->started_at)
<x-forms.input label="Username" id="redis_username" required <div class="pt-2 dark:text-warning">If you change the values in the database, please sync it here,
helper="You can change the Redis Username in the input field below or by editing the value of the REDIS_USERNAME environment variable. otherwise
automations won't work. <br>Changing them here will not change the values in the database.
</div>
<div class="flex gap-2">
@if (version_compare($redis_version, '6.0', '>='))
<x-forms.input label="Username" id="redis_username"
helper="You can only change this in the database." />
@endif
<x-forms.input label="Password" id="redis_password" type="password"
helper="You can only change this in the database." />
</div>
@else
<div class="pt-2 dark:text-warning">You can only change the username and password in the database after
initial start.</div>
<div class="flex gap-2">
@if (version_compare($redis_version, '6.0', '>='))
<x-forms.input label="Username" id="redis_username" required
helper="You can change the Redis Username in the input field below or by editing the value of the REDIS_USERNAME environment variable.
<br><br> <br><br>
If you change the Redis Username in the database, please sync it here, otherwise automations (like backups) won't work. If you change the Redis Username in the database, please sync it here, otherwise automations (like backups) won't work.
<br><br> <br><br>
Note: If the environment variable REDIS_USERNAME is set as a shared variable (environment, project, or team-based), this input field will become read-only." Note: If the environment variable REDIS_USERNAME is set as a shared variable (environment, project, or team-based), this input field will become read-only."
:disabled="$this->isSharedVariable('REDIS_USERNAME')" /> :disabled="$this->isSharedVariable('REDIS_USERNAME')" />
@endif @endif
<x-forms.input label="Password" id="redis_password" type="password" required <x-forms.input label="Password" id="redis_password" type="password" required
helper="You can change the Redis Password in the input field below or by editing the value of the REDIS_PASSWORD environment variable. helper="You can change the Redis Password in the input field below or by editing the value of the REDIS_PASSWORD environment variable.
<br><br> <br><br>
If you change the Redis Password in the database, please sync it here, otherwise automations (like backups) won't work. If you change the Redis Password in the database, please sync it here, otherwise automations (like backups) won't work.
<br><br> <br><br>
Note: If the environment variable REDIS_PASSWORD is set as a shared variable (environment, project, or team-based), this input field will become read-only." Note: If the environment variable REDIS_PASSWORD is set as a shared variable (environment, project, or team-based), this input field will become read-only."
:disabled="$this->isSharedVariable('REDIS_PASSWORD')" /> :disabled="$this->isSharedVariable('REDIS_PASSWORD')" />
</div>
@endif
</div> </div>
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"

View File

@@ -129,7 +129,7 @@
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last"> <div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
<div class="text-error"> <div class="text-error">
Unable to deploy. <a class="underline font-bold cursor-pointer" Unable to deploy. <a class="underline font-bold cursor-pointer"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"> href="{{ route('project.service.environment-variables', $parameters) }}" wire:navigate>
Required environment variables missing.</a> Required environment variables missing.</a>
</div> </div>
</div> </div>

View File

@@ -31,46 +31,48 @@
@else @else
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($is_multiline) @if ($is_multiline)
<x-forms.input isMultiline="{{ $is_multiline }}" id="key" /> <x-forms.input :required="$is_redis_credential" isMultiline="{{ $is_multiline }}" id="key" />
<x-forms.textarea type="password" id="value" /> <x-forms.textarea :required="$is_redis_credential" type="password" id="value" />
@else @else
<x-forms.input id="key" /> <x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" id="key" />
<x-forms.input type="password" id="value" /> <x-forms.input :required="$is_redis_credential" type="password" id="value" />
@endif @endif
@if ($is_shared) @if ($is_shared)
<x-forms.input disabled type="password" id="real_value" /> <x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" disabled type="password" id="real_value" />
@endif @endif
</div> </div>
@endif @endif
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($type === 'service') @if (!$is_redis_credential)
<x-forms.checkbox instantSave id="is_build_time" @if ($type === 'service')
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" />
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@else
@if ($is_shared)
<x-forms.checkbox instantSave id="is_build_time" <x-forms.checkbox instantSave id="is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" /> label="Build Variable?" />
<x-forms.checkbox instantSave id="is_literal" <x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" /> label="Is Literal?" />
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@else @else
@if ($isSharedVariable) @if ($is_shared)
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@else
<x-forms.checkbox instantSave id="is_build_time" <x-forms.checkbox instantSave id="is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for dockerfile, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" /> label="Build Variable?" />
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox instantSave id="is_literal"
@if ($is_multiline === false) helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
<x-forms.checkbox instantSave id="is_literal" label="Is Literal?" />
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." @else
label="Is Literal?" /> @if ($isSharedVariable)
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@else
<x-forms.checkbox instantSave id="is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for dockerfile, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
label="Build Variable?" />
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@if ($is_multiline === false)
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@endif
@endif @endif
@endif @endif
@endif @endif

View File

@@ -72,6 +72,7 @@
@script @script
<script> <script>
$wire.$on('checkProxyEvent', () => { $wire.$on('checkProxyEvent', () => {
$wire.$dispatch('info', 'Checking proxy.');
$wire.$call('checkProxy'); $wire.$call('checkProxy');
}); });
$wire.$on('restartEvent', () => { $wire.$on('restartEvent', () => {

View File

@@ -38,7 +38,7 @@
wire:model="contents.{{ $fileName }}" rows="5" /> wire:model="contents.{{ $fileName }}" rows="5" />
@else @else
<livewire:server.proxy.dynamic-configuration-navbar :server_id="$server->id" <livewire:server.proxy.dynamic-configuration-navbar :server_id="$server->id"
:fileName="$fileName" :value="$value" :newFile="false" :fileName="$fileName" :value="$value ?? ''" :newFile="false"
wire:key="{{ $fileName }}-{{ $loop->index }}" /> wire:key="{{ $fileName }}-{{ $loop->index }}" />
<x-forms.textarea disabled wire:model="contents.{{ $fileName }}" <x-forms.textarea disabled wire:model="contents.{{ $fileName }}"
rows="10" /> rows="10" />

View File

@@ -1,5 +1,7 @@
<div x-init="$wire.checkProxy()" class="flex gap-2"> <div @if (data_get($server, 'proxy.force_stop', false) === false) x-init="$wire.checkProxy()" @endif class="flex gap-2">
<x-forms.button wire:click='checkProxy(true)' :showLoadingIndicator="false">Refresh</x-forms.button> @if (data_get($server, 'proxy.force_stop', false) === false)
<x-forms.button wire:click='checkProxy(true)' :showLoadingIndicator="false">Refresh</x-forms.button>
@endif
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" /> <x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting') @elseif (data_get($server, 'proxy.status') === 'restarting')

View File

@@ -277,12 +277,15 @@
emails: 'read', emails: 'read',
administration: 'read' administration: 'read'
}; };
const default_events = ['push'];
if (preview_deployment_permissions) { if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write'; default_permissions.pull_requests = 'write';
default_events.push('pull_request');
} }
if (administration) { if (administration) {
default_permissions.administration = 'write'; default_permissions.administration = 'write';
} }
const data = { const data = {
name, name,
url: baseUrl, url: baseUrl,
@@ -297,7 +300,7 @@
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`, setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
setup_on_update: true, setup_on_update: true,
default_permissions, default_permissions,
default_events: ['pull_request', 'push'] default_events
}; };
const form = document.createElement('form'); const form = document.createElement('form');
form.setAttribute('method', 'post'); form.setAttribute('method', 'post');

View File

@@ -3,37 +3,62 @@
Subscribe | Coolify Subscribe | Coolify
</x-slot> </x-slot>
@if (auth()->user()->isAdminFromSession()) @if (auth()->user()->isAdminFromSession())
<div> @if (request()->query->get('cancelled'))
<div class="flex gap-2"> <div class="mb-6 rounded alert-error">
<h1>Subscriptions</h1> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
@if (subscriptionProvider() === 'stripe' && $alreadySubscribed) viewBox="0 0 24 24">
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@endif d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact
support.</span>
</div> </div>
@if (request()->query->get('cancelled')) @endif
<div class="mb-6 rounded alert-error"> <div class="flex gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" <h1>Subscriptions</h1>
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact
support.</span>
</div>
@endif
@if (config('subscription.provider') === 'stripe')
<livewire:subscription.pricing-plans />
@endif
</div> </div>
@if ($loading)
<div class="flex gap-2" wire:init="getStripeStatus">
Loading your subscription status...
</div>
@else
@if ($isUnpaid)
<div class="mb-6 rounded alert-error">
<span>Your last payment was failed for Coolify Cloud.</span>
</div>
<div>
<p class="mb-2">Open the following link, navigate to the button and pay your unpaid/past due
subscription.
</p>
<x-forms.button wire:click='stripeCustomerPortal'>Billing Portal</x-forms.button>
</div>
@else
@if (config('subscription.provider') === 'stripe')
<div @class([
'pb-4' => $isCancelled,
'pb-10' => !$isCancelled,
])>
@if ($isCancelled)
<div class="alert-error">
<span>It looks like your previous subscription has been cancelled, because you forgot to
pay
the bills.<br />Please subscribe again to continue using Coolify.</span>
</div>
@endif
</div>
<livewire:subscription.pricing-plans />
@endif
@endif
@endif
@else @else
<div class="flex flex-col justify-center mx-10"> <div class="flex flex-col justify-center mx-10">
<div class="flex gap-2"> <div class="flex gap-2">
<h1>Subscription</h1> <h1>Subscription</h1>
</div> </div>
<div>You are not an admin or have been removed from this team. If this does not make sense, please <span <div>You are not an admin so you cannot manage your Team's subscription. If this does not make sense, please
class="underline cursor-pointer dark:text-white" wire:click="help">contact <span class="underline cursor-pointer dark:text-white" wire:click="help">contact
us</span>.</div> us</span>.
</div>
</div> </div>
@endif @endif
</div> </div>

View File

@@ -1,4 +1,4 @@
<div x-data="{ selected: 'monthly' }" class="w-full pb-20 pt-10"> <div x-data="{ selected: 'monthly' }" class="w-full pb-20">
<div class="px-6 mx-auto lg:px-8"> <div class="px-6 mx-auto lg:px-8">
<div class="flex justify-center"> <div class="flex justify-center">
<fieldset <fieldset
@@ -20,7 +20,7 @@
</div> </div>
<div class="flow-root mt-12"> <div class="flow-root mt-12">
<div <div
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-1 lg:divide-x lg:divide-y-0 xl:-mx-4"> class="grid grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-1 lg:divide-x lg:divide-y-0 xl:-mx-4">
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14"> <div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
<h3 id="tier-dynamic" class="text-4xl font-semibold leading-7 dark:text-white">Pay-as-you-go</h3> <h3 id="tier-dynamic" class="text-4xl font-semibold leading-7 dark:text-white">Pay-as-you-go</h3>
<p class="mt-4 text-sm leading-6 dark:text-neutral-400"> <p class="mt-4 text-sm leading-6 dark:text-neutral-400">
@@ -72,14 +72,16 @@
</div> </div>
</div> </div>
</div> </div>
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" <div class="flex pt-4 h-14">
class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-monthly')"> <x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
Subscribe class="w-full" wire:click="subscribeStripe('dynamic-monthly')">
</x-forms.button> Subscribe
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" </x-forms.button>
class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-yearly')"> <x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
Subscribe class="w-full" wire:click="subscribeStripe('dynamic-yearly')">
</x-forms.button> Subscribe
</x-forms.button>
</div>
<ul role="list" class="mt-8 space-y-3 text-sm leading-6 dark:text-neutral-400"> <ul role="list" class="mt-8 space-y-3 text-sm leading-6 dark:text-neutral-400">
<li class="flex"> <li class="flex">
<svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 mr-3 text-warning" viewBox="0 0 20 20" fill="currentColor"

View File

@@ -16,8 +16,13 @@ use App\Models\Server;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/health', [OtherController::class, 'healthcheck']); Route::get('/health', [OtherController::class, 'healthcheck']);
Route::post('/feedback', [OtherController::class, 'feedback']); Route::group([
'prefix' => 'v1',
], function () {
Route::get('/health', [OtherController::class, 'healthcheck']);
});
Route::post('/feedback', [OtherController::class, 'feedback']);
Route::group([ Route::group([
'middleware' => ['auth:sanctum', 'api.ability:write'], 'middleware' => ['auth:sanctum', 'api.ability:write'],
'prefix' => 'v1', 'prefix' => 'v1',
@@ -117,7 +122,7 @@ Route::group([
Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']); Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']);
Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['api.ability:read']); Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['api.ability:read']);
Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['ability:write']); Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs'])->middleware(['api.ability:read']); Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs'])->middleware(['api.ability:read']);

View File

@@ -1,7 +1,7 @@
# documentation: https://docs.deno.com/deploy/kv/manual/ # documentation: https://docs.deno.com/deploy/kv/manual/
# slogan: The Denoland key-value database # slogan: The Denoland key-value database
# tags: deno, kv, key-value, database # tags: deno, kv, key-value, database
# logo: svgs/denoKV.svg # logo: svgs/denokv.svg
# port: 4512 # port: 4512
services: services:

View File

@@ -126,6 +126,7 @@ services:
- S3_SECRET_KEY=${S3_SECRET_KEY:-} - S3_SECRET_KEY=${S3_SECRET_KEY:-}
- S3_BUCKET=${S3_BUCKET:-evolution} - S3_BUCKET=${S3_BUCKET:-evolution}
- S3_PORT=${S3_PORT:-443} - S3_PORT=${S3_PORT:-443}
- S3_REGION=${S3_REGION:-us-east-1}
- S3_ENDPOINT=${S3_ENDPOINT:-files.site.com} - S3_ENDPOINT=${S3_ENDPOINT:-files.site.com}
- S3_USE_SSL=${S3_USE_SSL:-true} - S3_USE_SSL=${S3_USE_SSL:-true}
- 'AUTHENTICATION_API_KEY=${SERVICE_PASSWORD_AUTHENTICATIONAPIKEY}' - 'AUTHENTICATION_API_KEY=${SERVICE_PASSWORD_AUTHENTICATIONAPIKEY}'

View File

@@ -6,7 +6,7 @@
services: services:
odoo: odoo:
image: odoo:17 image: odoo:18
environment: environment:
- SERVICE_FQDN_ODOO_8069 - SERVICE_FQDN_ODOO_8069
- HOST=postgresql - HOST=postgresql
@@ -14,6 +14,7 @@ services:
- PASSWORD=$SERVICE_PASSWORD_POSTGRES - PASSWORD=$SERVICE_PASSWORD_POSTGRES
volumes: volumes:
- odoo-web-data:/var/lib/odoo - odoo-web-data:/var/lib/odoo
- odoo-extra-addons:/mnt/extra-addons
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8069"] test: ["CMD", "curl", "-f", "http://127.0.0.1:8069"]
interval: 2s interval: 2s

View File

@@ -6,7 +6,7 @@
services: services:
plausible: plausible:
image: "ghcr.io/plausible/community-edition:v2.1.4" image: "ghcr.io/plausible/community-edition:v3.0.1"
command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"' command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"'
environment: environment:
- SERVICE_FQDN_PLAUSIBLE - SERVICE_FQDN_PLAUSIBLE
@@ -17,13 +17,19 @@ services:
- TOTP_VAULT_KEY=${SERVICE_REALBASE64_32_TOTP} - TOTP_VAULT_KEY=${SERVICE_REALBASE64_32_TOTP}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- MAILER_ADAPTER=${MAILER_ADAPTER:-Bamboo.LocalAdapter}
- MAILER_EMAIL=${MAILER_EMAIL}
- MAILER_NAME=${MAILER_NAME}
- SMTP_HOST_ADDR=${SMTP_HOST_ADDR}
- SMTP_HOST_PORT=${SMTP_HOST_PORT}
- SMTP_USER_NAME=${SMTP_USER_NAME}
- SMTP_USER_PWD=${SMTP_USER_PWD}
- SMTP_HOST_SSL_ENABLED=${SMTP_HOST_SSL_ENABLED}
depends_on: depends_on:
plausible-db: plausible-db:
condition: service_healthy condition: service_healthy
plausible-events-db: plausible-events-db:
condition: service_healthy condition: service_healthy
mail:
condition: service_healthy
healthcheck: healthcheck:
test: test:
[ [
@@ -39,15 +45,6 @@ services:
retries: 5 retries: 5
start_period: 45s start_period: 45s
mail:
image: bytemark/smtp
platform: linux/amd64
healthcheck:
test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/25' || exit 1"]
interval: 5s
timeout: 10s
retries: 20
plausible-db: plausible-db:
image: "postgres:16-alpine" image: "postgres:16-alpine"
volumes: volumes:
@@ -63,7 +60,9 @@ services:
retries: 10 retries: 10
plausible-events-db: plausible-events-db:
image: "clickhouse/clickhouse-server:24.3.3.102-alpine" image: "clickhouse/clickhouse-server:24.12-alpine"
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
volumes: volumes:
- plausible-events-data:/var/lib/clickhouse - plausible-events-data:/var/lib/clickhouse
- type: bind - type: bind

View File

@@ -35,6 +35,8 @@ services:
unsend: unsend:
image: unsend/unsend:latest image: unsend/unsend:latest
expose:
- 3000
environment: environment:
- SERVICE_FQDN_UNSEND_3000 - SERVICE_FQDN_UNSEND_3000
- DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${SERVICE_DB_POSTGRES:-unsend} - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${SERVICE_DB_POSTGRES:-unsend}
@@ -48,13 +50,14 @@ services:
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false} - NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false}
- API_RATE_LIMIT=${API_RATE_LIMIT:-1} - API_RATE_LIMIT=${API_RATE_LIMIT:-1}
- HOSTNAME=0.0.0.0
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:3000 || exit 1" ] test: [ "CMD-SHELL", "wget -qO- http://unsend:3000 || exit 1" ]
interval: 5s interval: 5s
retries: 10 retries: 10
timeout: 2s timeout: 2s

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.405" "version": "4.0.0-beta.410"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.406" "version": "4.0.0-beta.411"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"
}, },
"realtime": { "realtime": {
"version": "1.0.6" "version": "1.0.7"
}, },
"sentinel": { "sentinel": {
"version": "0.0.15" "version": "0.0.15"