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]
### 🐛 Bug Fixes
- *(navbar)* Update error message link to use route for environment variables navigation
- Unsend template
- Replace ports with expose
- *(templates)* Update Unsend compose configuration for improved service integration
### 🚜 Refactor
- *(jobs)* Update WithoutOverlapping middleware to use expireAfter for better queue management
### 📚 Documentation
- Update changelog
## [4.0.0-beta.408] - 2025-04-14
### 🚀 Features
- *(OpenApi)* Enhance OpenAPI specifications by adding UUID parameters for application, project, and service updates; improve deployment listing with pagination parameters; update command signature for OpenApi generation
- *(subscription)* Enhance subscription management with loading states and Stripe status checks
- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README
- *(core)* Enable magic env variables for compose based applications
### 🐛 Bug Fixes
- *(pre-commit)* Correct input redirection for /dev/tty and add OpenAPI generation command
- *(pricing-plans)* Adjust grid class for improved layout consistency in subscription pricing plans
- *(migrations)* Make stripe_comment field nullable in subscriptions table
- *(mongodb)* Also apply custom config when SSL is enabled
- *(templates)* Correct casing of denoKV references in service templates and YAML files
- *(deployment)* Handle missing destination in deployment process to prevent errors
- *(parser)* Transform associative array labels into key=value format for better compatibility
- *(redis)* Update username and password input handling to clarify database sync requirements
- *(source)* Update connected source display to handle cases with no source connected
- *(application)* Append base directory to git branch URLs for improved path handling
- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON
### 💼 Other
- Add missing openapi items to PrivateKey
### 🚜 Refactor
- *(commands)* Reorganize OpenAPI and Services generation commands into a new namespace for better structure; remove old command files
- *(Dockerfile)* Remove service generation command from the build process to streamline Dockerfile and improve build efficiency
- *(navbar-delete-team)* Simplify modal confirmation layout and enhance button styling for better user experience
- *(Server)* Remove debug logging from isReachableChanged method to clean up code and improve performance
- *(source)* Conditionally display connected source and change source options based on private key presence
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump version to 404
- *(versions)* Update nightly version to 4.0.0-beta.410
- *(pre-commit)* Remove OpenAPI generation command from pre-commit hook
- *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json
- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files
- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files
- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service
## [4.0.0-beta.407] - 2025-04-09
### 📚 Documentation
- Update changelog
## [4.0.0-beta.406] - 2025-04-05
### 🚀 Features
- *(Deploy)* Add info dispatch for proxy check initiation
- *(EnvironmentVariable)* Add handling for Redis credentials in the environment variable component
- *(EnvironmentVariable)* Implement protection for critical environment variables and enhance deletion logic
- *(Application)* Add networkAliases attribute for handling network aliases as JSON or comma-separated values
- *(GithubApp)* Update default events to include 'pull_request' and streamline event handling
- *(CleanupDocker)* Add support for realtime image management in Docker cleanup process
- *(Deployment)* Enhance queue_application_deployment to handle existing deployments and return appropriate status messages
- *(SourceManagement)* Add functionality to change Git source and display current source in the application settings
### 🐛 Bug Fixes
- *(CheckProxy)* Update port conflict check to ensure accurate grep matching
- *(CheckProxy)* Refine port conflict detection with improved grep patterns
- *(CheckProxy)* Enhance port conflict detection by adjusting ss command for better output
- *(api)* Add back validateDataApplications (#5539)
- *(CheckProxy, Status)* Prevent proxy checks when force_stop is active; remove debug statement in General
- *(Status)* Conditionally check proxy status and refresh button based on force_stop state
- *(General)* Change redis_password property to nullable string
- *(DeployController)* Update request handling to use input method and enhance OpenAPI description for deployment endpoint
### 💼 Other
- Add missing UUID to openapi spec
### 🚜 Refactor
- *(Server)* Use data_get for safer access to settings properties in isFunctional method
- *(Application)* Rename network_aliases to custom_network_aliases across the application for clarity and consistency
- *(ApplicationDeploymentJob)* Streamline environment variable handling by introducing generate_coolify_env_variables method and consolidating logic for pull request and main branch scenarios
- *(ApplicationDeploymentJob, ApplicationDeploymentQueue)* Improve deployment status handling and log entry management with transaction support
- *(SourceManagement)* Sort sources by name and improve UI for changing Git source with better error handling
- *(Email)* Streamline SMTP and resend settings handling in copyFromInstanceSettings method
- *(Email)* Enhance error handling in SMTP and resend methods by passing context to handleError function
- *(DynamicConfigurations)* Improve handling of dynamic configuration content by ensuring fallback to empty string when content is null
- *(ServicesGenerate)* Update command signature from 'services:generate' to 'generate:services' for consistency; update Dockerfile to run service generation during build; update Odoo image version to 18 and add extra addons volume in compose configuration
- *(Dockerfile)* Streamline RUN commands for improved readability and maintainability by adding line continuations
- *(Dockerfile)* Reintroduce service generation command in the build process for consistency and ensure proper asset compilation
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump version to 406
- *(versions)* Bump version to 407 and 408 for coolify and nightly
- *(versions)* Bump version to 408 for coolify and 409 for nightly
## [4.0.0-beta.405] - 2025-04-04
### 🚀 Features
- *(api)* Update OpenAPI spec for services (#5448)
### 🐛 Bug Fixes
- *(api)* Used ssh keys can be deleted
- *(email)* Transactional emails not sending
### 🚜 Refactor
- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump version to 406
- *(versions)* Bump version to 407
## [4.0.0-beta.404] - 2025-04-03
### 🚀 Features
- *(proxy)* Enhance proxy handling and port conflict detection
- *(lang)* Added Azerbaijani language updated turkish language. (#5497)
- *(lang)* Added Portuguese from Brazil language (#5500)
- *(lang)* Add Indonesian language translations (#5513)
### 🐛 Bug Fixes
- *(database)* Custom config for MongoDB (#5471)
- *(ui)* Instance Backup settings
- *(docs)* Comment out execute for now
- *(installation)* Mount the docker config
- *(installation)* Path to config file for docker login
@@ -27,10 +173,13 @@ All notable changes to this project will be documented in this file.
- *(docs)* Contribute service url (#5517)
- *(proxy)* Proxy restart does not work on domain
- *(ui)* Only show copy button on https
- *(database)* Custom config for MongoDB (#5471)
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
@@ -38,9 +187,11 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump version to 403 (#5520)
- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
- *(service)* Remove unused code in Bugsink service
- *(versions)* Update version to 404
- *(versions)* Bump version to 403 (#5520)
- *(versions)* Bump version to 404
## [4.0.0-beta.402] - 2025-04-01
@@ -59,7 +210,6 @@ All notable changes to this project will be documented in this file.
- *(DeployController)* Cast 'pr' query parameter to integer
- *(deploy)* Validate team ID before deployment
- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424)
- *(ui)* Instance Backup settings
### 🚜 Refactor
@@ -73,7 +223,6 @@ All notable changes to this project will be documented in this file.
- *(service)* Add google variables to plausible.yaml (#5429)
- *(service)* Update authentik.yaml versions (#5373)
- *(core)* Remove redocs
- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
## [4.0.0-beta.401] - 2025-03-28
@@ -144,6 +293,14 @@ All notable changes to this project will be documented in this file.
### 🚀 Features
- *(github-source)* Enhance GitHub App configuration with manual and private key support
- *(ui)* Improve GitHub repository selection and styling
- *(database)* Implement two-step confirmation for database deletion
- *(assets)* Add new SVG logo for Coolify
- *(install)* Enhance Docker address pool configuration and validation
- *(install)* Improve Docker address pool management and service restart logic
- *(install)* Add missing env variable to install script
- *(LocalFileVolume)* Add binary file detection and update UI logic
- *(service)* Neon
- *(migration)* Add `ssl_certificates` table and model
- *(migration)* Add ssl setting to `standalone_postgresqls` table
@@ -185,14 +342,6 @@ All notable changes to this project will be documented in this file.
- *(ssl)* Improve Redis and remove modes
- Full SSL support for DrangonflyDB
- SSL notification
- *(github-source)* Enhance GitHub App configuration with manual and private key support
- *(ui)* Improve GitHub repository selection and styling
- *(database)* Implement two-step confirmation for database deletion
- *(assets)* Add new SVG logo for Coolify
- *(install)* Enhance Docker address pool configuration and validation
- *(install)* Improve Docker address pool management and service restart logic
- *(install)* Add missing env variable to install script
- *(LocalFileVolume)* Add binary file detection and update UI logic
- *(templates)* Change glance for v0.7
- *(templates)* Add Freescout service template
- *(service)* Add Evolution API template
@@ -210,6 +359,18 @@ All notable changes to this project will be documented in this file.
- *(api)* Docker compose based apps creationg through api
- *(database)* Improve database type detection for Supabase Postgres images
- *(ui)* Correct grammatical error in 404 page
- *(seeder)* Update GitHub app name in GithubAppSeeder
- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration
- *(domain)* Dispatch refreshStatus event after successful domain update
- *(database)* Correct container name generation for service databases
- *(database)* Limit container name length for database proxy
- *(database)* Handle unsupported database types in StartDatabaseProxy
- *(database)* Simplify container name generation in StartDatabaseProxy
- *(install)* Handle potential errors in Docker address pool configuration
- *(backups)* Retention settings
- *(redis)* Set default redis_username for new instances
- *(core)* Improve instantSave logic and error handling
- *(ssl)* Permission of ssl crt and key inside the container
- *(ui)* Make sure file mounts do not showing the encrypted values
- *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert
@@ -249,18 +410,6 @@ All notable changes to this project will be documented in this file.
- *(ssl)* Add `--tls` arg to DrangflyDB
- *(notification)* Always send SSL notifications
- *(database)* Change default value of enable_ssl to false for multiple tables
- *(ui)* Correct grammatical error in 404 page
- *(seeder)* Update GitHub app name in GithubAppSeeder
- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration
- *(domain)* Dispatch refreshStatus event after successful domain update
- *(database)* Correct container name generation for service databases
- *(database)* Limit container name length for database proxy
- *(database)* Handle unsupported database types in StartDatabaseProxy
- *(database)* Simplify container name generation in StartDatabaseProxy
- *(install)* Handle potential errors in Docker address pool configuration
- *(backups)* Retention settings
- *(redis)* Set default redis_username for new instances
- *(core)* Improve instantSave logic and error handling
- *(general)* Correct link to framework specific documentation
- *(core)* Redirect healthcheck route for dockercompose applications
- *(api)* Use name from request payload
@@ -309,6 +458,7 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(supabase)* Update Supabase service template and Postgres image version
- *(migration)* Remove unused columns
- *(ssl)* Improve code in ssl helper
- *(migration)* Ssl cert and key should not be nullable
@@ -316,7 +466,6 @@ All notable changes to this project will be documented in this file.
- Rename ca crt folder to ssl
- *(ui)* Improve valid until handling
- Improve code quality suggested by code rabbit
- *(supabase)* Update Supabase service template and Postgres image version
- *(versions)* Update version numbers for coolify and nightly
## [4.0.0-beta.398] - 2025-03-01
@@ -709,6 +858,14 @@ All notable changes to this project will be documented in this file.
### 🚀 Features
- New ServerReachabilityChanged event
- Use new ServerReachabilityChanged event instead of isDirty
- Add infomaniak oauth
- Add server disk usage check frequency
- Add environment_uuid support and update API documentation
- Add service/resource/project labels
- Add coolify.environment label
- Add database subtype
- Able to import full db backups for pg/mysql/mariadb
- Restore backup from server file
- Docker volume data cloning
@@ -744,6 +901,35 @@ All notable changes to this project will be documented in this file.
### 🐛 Bug Fixes
- Fallback for copy button
- Copy the right text
- Maybe fallback is now working
- Only show copy button on secure context
- Render html on error page correctly
- Invalid API response on missing project
- Applications API response code + schema
- Applications API writing to unavailable models
- If an init script is renamed the old version is still on the server
- Oauthseeder
- Compose loading seq
- Resource clone name + volume name generation
- Update Dockerfile entrypoint path to /etc/entrypoint.d
- Debug mode
- Unreachable notifications
- Remove duplicated ServerCheckJob call
- Few fixes and use new ServerReachabilityChanged event
- Use serverStatus not just status
- Oauth seeder
- Service ui structure
- Check port 8080 and fallback to 80
- Refactor database view
- Always use docker cleanup frequency
- Advanced server UI
- Html css
- Fix domain being override when update application
- Use nixpacks predefined build variables, but still could update the default values from Coolify
- Use local monaco-editor instead of Cloudflare
- N8n timezone
- Compose envs
- Scheduled tasks and backups are executed by server timezone.
- Show backup timezone on the UI
@@ -843,6 +1029,7 @@ All notable changes to this project will be documented in this file.
### 🚜 Refactor
- Rename `coolify.environment` to `coolify.environmentName`
- Rename parameter in DatabaseBackupJob for clarity
- Improve checkbox component accessibility and styling
- Remove unused tags method from ApplicationDeploymentJob
@@ -858,6 +1045,9 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- Regenerate API spec, removing notification fields
- Remove ray debugging
- Version ++
- Improve Penpot healthchecks
- Switch up readonly lables to make more sense
- Remove unused computed fields
@@ -881,44 +1071,11 @@ All notable changes to this project will be documented in this file.
### 🚀 Features
- New ServerReachabilityChanged event
- Use new ServerReachabilityChanged event instead of isDirty
- Add infomaniak oauth
- Add server disk usage check frequency
- Add environment_uuid support and update API documentation
- Add service/resource/project labels
- Add coolify.environment label
- Add database subtype
- Migrate to new encryption options
- New encryption options
### 🐛 Bug Fixes
- Render html on error page correctly
- Invalid API response on missing project
- Applications API response code + schema
- Applications API writing to unavailable models
- If an init script is renamed the old version is still on the server
- Oauthseeder
- Compose loading seq
- Resource clone name + volume name generation
- Update Dockerfile entrypoint path to /etc/entrypoint.d
- Debug mode
- Unreachable notifications
- Remove duplicated ServerCheckJob call
- Few fixes and use new ServerReachabilityChanged event
- Use serverStatus not just status
- Oauth seeder
- Service ui structure
- Check port 8080 and fallback to 80
- Refactor database view
- Always use docker cleanup frequency
- Advanced server UI
- Html css
- Fix domain being override when update application
- Use nixpacks predefined build variables, but still could update the default values from Coolify
- Use local monaco-editor instead of Cloudflare
- N8n timezone
- Smtp encryption
- Bind() to 0.0.0.0:80 failed
- Oauth seeder
@@ -928,15 +1085,11 @@ All notable changes to this project will be documented in this file.
- Error message
- Update healthcheck and port configurations to use port 8080
### 🚜 Refactor
## [4.0.0-beta.379] - 2024-12-13
- Rename `coolify.environment` to `coolify.environmentName`
### 🐛 Bug Fixes
### ⚙️ Miscellaneous Tasks
- Regenerate API spec, removing notification fields
- Remove ray debugging
- Version ++
- Saving oauth
## [4.0.0-beta.378] - 2024-12-13
@@ -945,11 +1098,6 @@ All notable changes to this project will be documented in this file.
- Monaco editor light and dark mode switching
- Service status indicator + oauth saving
- Socialite for azure and authentik
- Saving oauth
- Fallback for copy button
- Copy the right text
- Maybe fallback is now working
- Only show copy button on secure context
## [4.0.0-beta.377] - 2024-12-13

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).
# 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
If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io)
@@ -137,6 +44,96 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
# Donations
To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development.
[coolify.io/sponsorships](https://coolify.io/sponsorships)
Thank you so much!
## Big Sponsors
* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management
* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions
* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers
* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions
* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers
* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase
* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers
* [Trieve](https://trieve.ai?ref=coolify.io) - AI-powered search and analytics
* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data
* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions
* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
* [COMIT](https://comit.international?ref=coolify.io) - New York Times 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
<p>

View File

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

View File

@@ -27,7 +27,7 @@ class CheckProxy
return false;
}
$proxyType = $server->proxyType();
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
return false;
}
if (! $server->isProxyShouldRun()) {
@@ -37,8 +37,12 @@ class CheckProxy
return false;
}
}
// Determine proxy container name based on environment
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
if ($server->isSwarm()) {
$status = getContainerStatus($server, 'coolify-proxy_traefik');
$status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
@@ -47,7 +51,7 @@ class CheckProxy
return true;
} else {
$status = getContainerStatus($server, 'coolify-proxy');
$status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
@@ -61,34 +65,11 @@ class CheckProxy
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) {
// Try multiple methods to check port availability
$commands = [
// Method 1: Check /proc/net/tcp directly (convert port to hex)
"cat /proc/net/tcp | grep -q '00000000:".str_pad(dechex($port), 4, '0', STR_PAD_LEFT)."'",
// Method 2: Use ss command (modern alternative to netstat)
"ss -tuln | grep -q ':$port '",
// Method 3: Use lsof if available
"lsof -i :$port >/dev/null 2>&1",
// Method 4: Use fuser if available
"fuser $port/tcp >/dev/null 2>&1",
];
$portInUse = false;
foreach ($commands as $command) {
try {
instant_remote_process([$command], $server);
$portInUse = true;
break;
} catch (\Throwable $e) {
continue;
}
}
if ($portInUse) {
// Use the smart port checker that handles dual-stack properly
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<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 {
@@ -126,4 +107,144 @@ class CheckProxy
return true;
}
}
/**
* Smart port checker that handles dual-stack configurations
* Returns true only if there's a real port conflict (not just dual-stack)
*/
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
{
// First check if our own proxy is using this port (which is fine)
try {
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
if (! empty($containerId)) {
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
try {
instant_remote_process([$checkProxyPort], $server);
// Our proxy is using the port, which is fine
return false;
} catch (\Throwable $e) {
// Our container exists but not using this port
}
}
} catch (\Throwable $e) {
// Container not found or error checking, continue with regular checks
}
// Command sets for different ways to check ports, ordered by preference
$commandSets = [
// Set 1: Use ss to check listener counts by protocol stack
[
'available' => 'command -v ss >/dev/null 2>&1',
'check' => [
// Get listening process details
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
// Count IPv4 listeners
"echo \"\$ss_output\" | grep -c ':$port '",
],
],
// Set 2: Use netstat as alternative to ss
[
'available' => 'command -v netstat >/dev/null 2>&1',
'check' => [
// Get listening process details
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
// Count listeners
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
],
],
// Set 3: Use lsof as last resort
[
'available' => 'command -v lsof >/dev/null 2>&1',
'check' => [
// Get process using the port
"lsof -i :$port -P -n | grep 'LISTEN'",
// Count listeners
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
],
],
];
// Try each command set until we find one available
foreach ($commandSets as $set) {
try {
// Check if the command is available
instant_remote_process([$set['available']], $server);
// Run the actual check commands
$output = instant_remote_process($set['check'], $server, true);
// Parse the output lines
$lines = explode("\n", trim($output));
// Get the detailed output and listener count
$details = trim($lines[0] ?? '');
$count = intval(trim($lines[1] ?? '0'));
// If no listeners or empty result, port is free
if ($count == 0 || empty($details)) {
return false;
}
// Try to detect if this is our coolify-proxy
if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
// It's likely our docker or proxy, which is fine
return false;
}
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
// If exactly 2 listeners and both have same port, likely dual-stack
if ($count <= 2) {
// Check if it looks like a standard dual-stack setup
$isDualStack = false;
// Look for IPv4 and IPv6 in the listing (ss output format)
if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
(preg_match('/\*:'.$port.'\s/', $details) ||
preg_match('/:::'.$port.'\s/', $details))) {
$isDualStack = true;
}
// For netstat format
if (strpos($details, '0.0.0.0:'.$port) !== false &&
strpos($details, ':::'.$port) !== false) {
$isDualStack = true;
}
// For lsof format (IPv4 and IPv6)
if (strpos($details, '*:'.$port) !== false &&
preg_match('/\*:'.$port.'.*IPv4/', $details) &&
preg_match('/\*:'.$port.'.*IPv6/', $details)) {
$isDualStack = true;
}
if ($isDualStack) {
return false; // This is just a normal dual-stack setup
}
}
// If we get here, it's likely a real port conflict
return true;
} catch (\Throwable $e) {
// This command set failed, try the next one
continue;
}
}
// Fallback to simpler check if all above methods fail
try {
// Just try to bind to the port directly to see if it's available
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
$result = instant_remote_process([$checkCommand], $server, true);
return trim($result) === 'in-use';
} catch (\Throwable $e) {
// If everything fails, assume the port is free to avoid false positives
return false;
}
}
}

View File

@@ -14,15 +14,26 @@ class CleanupDocker
public function handle(Server $server)
{
$settings = instanceSettings();
$realtimeImage = config('constants.coolify.realtime_image');
$realtimeImageVersion = config('constants.coolify.realtime_version');
$realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion";
$realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime';
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
$commands = [
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
'docker image prune -af --filter "label!=coolify.managed=true"',
'docker builder prune -af',
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
"docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f",
"docker images --filter before=$helperImageWithoutPrefixVersion --filter reference=$helperImageWithoutPrefix | grep $helperImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
"docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
];
if ($server->settings->delete_unused_volumes) {

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Service;
use App\Models\Tag;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
@@ -132,7 +133,7 @@ class DeployController extends Controller
#[OA\Get(
summary: 'Deploy',
description: 'Deploy by tag or uuid. `Post` request also accepted.',
description: 'Deploy by tag or uuid. `Post` request also accepted with `uuid` and `tag` json body.',
path: '/deploy',
operationId: 'deploy-by-tag-or-uuid',
security: [
@@ -191,10 +192,10 @@ class DeployController extends Controller
return invalidTokenResponse();
}
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
$pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0;
$uuids = $request->input('uuid');
$tags = $request->input('tag');
$force = $request->input('force') ?? false;
$pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0;
if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
@@ -297,17 +298,21 @@ class DeployController extends Controller
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
switch ($resource?->getMorphClass()) {
case \App\Models\Application::class:
case Application::class:
$deployment_uuid = new Cuid2;
queue_application_deployment(
$result = queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
pull_request_id: $pr,
);
if ($result['status'] === 'skipped') {
$message = $result['message'];
} else {
$message = "Application {$resource->name} deployment queued.";
}
break;
case \App\Models\Service::class:
case Service::class:
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
break;
@@ -333,6 +338,40 @@ class DeployController extends Controller
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
new OA\Parameter(
name: 'skip',
in: 'query',
description: 'Number of records to skip.',
required: false,
schema: new OA\Schema(
type: 'integer',
minimum: 0,
default: 0,
)
),
new OA\Parameter(
name: 'take',
in: 'query',
description: 'Number of records to take.',
required: false,
schema: new OA\Schema(
type: 'integer',
minimum: 1,
default: 10,
)
),
],
responses: [
new OA\Response(
response: 200,

View File

@@ -267,6 +267,18 @@ class ProjectController extends Controller
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the project.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Project updated.',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -329,7 +329,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$this->write_deployment_configurations();
}
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
@@ -899,100 +899,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
}
$ports = $this->application->main_port();
if ($this->pull_request_id !== 0) {
$this->env_filename = ".env-pr-$this->pull_request_id";
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (! is_null($this->commit)) {
$envs->push("SOURCE_COMMIT={$this->commit}");
} else {
$envs->push('SOURCE_COMMIT=unknown');
}
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
$envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
$envs->push("COOLIFY_DOMAIN_FQDN={$url}");
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
}
}
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
if ($env->is_literal || $env->is_multiline) {
$real_value = '\''.$real_value.'\'';
} else {
$real_value = escapeEnvVariables($env->real_value);
}
}
$envs->push($env->key.'='.$real_value);
}
// Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
$envs->push("PORT={$ports[0]}");
}
}
// Add HOST if not exists
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0');
}
} else {
$coolify_envs = $this->generate_coolify_env_variables();
$coolify_envs->each(function ($item, $key) use ($envs) {
$envs->push($key.'='.$item);
});
if ($this->pull_request_id === 0) {
$this->env_filename = '.env';
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (! is_null($this->commit)) {
$envs->push("SOURCE_COMMIT={$this->commit}");
} else {
$envs->push('SOURCE_COMMIT=unknown');
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_URL={$this->application->fqdn}");
} else {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_FQDN={$url}");
} else {
$envs->push("COOLIFY_URL={$url}");
}
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
}
}
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
@@ -1017,6 +929,32 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0');
}
} else {
$this->env_filename = ".env-pr-$this->pull_request_id";
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
if ($env->is_literal || $env->is_multiline) {
$real_value = '\''.$real_value.'\'';
} else {
$real_value = escapeEnvVariables($env->real_value);
}
}
$envs->push($env->key.'='.$real_value);
}
// Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
$envs->push("PORT={$ports[0]}");
}
}
// Add HOST if not exists
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0');
}
}
if ($envs->isEmpty()) {
$this->env_filename = null;
@@ -1361,7 +1299,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command(
[
@@ -1394,6 +1331,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
if (! $destination) {
continue;
}
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
@@ -1437,6 +1377,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_git_if_build_needed()
{
if ($this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) {
$repository = githubApi($this->source, "repos/{$this->customRepository}");
$data = data_get($repository, 'data');
if (isset($data->id)) {
$repository_project_id = $data->id;
if (blank($this->application->repository_project_id) || $this->application->repository_project_id !== $repository_project_id) {
$this->application->repository_project_id = $repository_project_id;
$this->application->save();
}
}
}
$this->generate_git_import_commands();
$local_branch = $this->branch;
if ($this->pull_request_id !== 0) {
@@ -1626,20 +1577,128 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
}
private function generate_coolify_env_variables(): Collection
{
$coolify_envs = collect([]);
$local_branch = $this->branch;
if ($this->pull_request_id !== 0) {
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (! is_null($this->commit)) {
$coolify_envs->put('SOURCE_COMMIT', $this->commit);
} else {
$coolify_envs->put('SOURCE_COMMIT', 'unknown');
}
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$coolify_envs->put('COOLIFY_FQDN', $this->preview->fqdn);
$coolify_envs->put('COOLIFY_DOMAIN_URL', $this->preview->fqdn);
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$coolify_envs->put('COOLIFY_URL', $url);
$coolify_envs->put('COOLIFY_DOMAIN_FQDN', $url);
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$coolify_envs->put('COOLIFY_BRANCH', $local_branch);
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid);
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
}
}
add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables_preview);
} else {
// Add SOURCE_COMMIT if not exists
if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (! is_null($this->commit)) {
$coolify_envs->put('SOURCE_COMMIT', $this->commit);
} else {
$coolify_envs->put('SOURCE_COMMIT', 'unknown');
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
if ((int) $this->application->compose_parsing_version >= 3) {
$coolify_envs->put('COOLIFY_URL', $this->application->fqdn);
} else {
$coolify_envs->put('COOLIFY_FQDN', $this->application->fqdn);
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
if ((int) $this->application->compose_parsing_version >= 3) {
$coolify_envs->put('COOLIFY_FQDN', $url);
} else {
$coolify_envs->put('COOLIFY_URL', $url);
}
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$coolify_envs->put('COOLIFY_BRANCH', $local_branch);
}
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid);
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name);
}
}
add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables);
}
return $coolify_envs;
}
private function generate_env_variables()
{
$this->env_args = collect([]);
$this->env_args->put('SOURCE_COMMIT', $this->commit);
$coolify_envs = $this->generate_coolify_env_variables();
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
if (! is_null($env->real_value)) {
$this->env_args->put($env->key, $env->real_value);
if (str($env->real_value)->startsWith('$')) {
$variable_key = str($env->real_value)->after('$');
if ($variable_key->startsWith('COOLIFY_')) {
$variable = $coolify_envs->get($variable_key->value());
if (filled($variable)) {
$this->env_args->prepend($variable, $variable_key->value());
}
} else {
$variable = $this->application->environment_variables()->where('key', $variable_key)->first();
if ($variable) {
$this->env_args->prepend($variable->real_value, $env->key);
}
}
}
}
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
if (! is_null($env->real_value)) {
$this->env_args->put($env->key, $env->real_value);
if (str($env->real_value)->startsWith('$')) {
$variable_key = str($env->real_value)->after('$');
if ($variable_key->startsWith('COOLIFY_')) {
$variable = $coolify_envs->get($variable_key->value());
if (filled($variable)) {
$this->env_args->prepend($variable, $variable_key->value());
}
} else {
$variable = $this->application->environment_variables_preview()->where('key', $variable_key)->first();
if ($variable) {
$this->env_args->prepend($variable->real_value, $env->key);
}
}
}
}
}
}
@@ -1664,25 +1723,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$labels = $labels->filter(function ($value, $key) {
return ! Str::startsWith($value, 'coolify.');
});
$found_caddy_labels = $labels->filter(function ($value, $key) {
return Str::startsWith($value, 'caddy_');
});
if ($found_caddy_labels->count() === 0) {
if ($this->pull_request_id !== 0) {
$domains = str(data_get($this->preview, 'fqdn'))->explode(',');
} else {
$domains = str(data_get($this->application, 'fqdn'))->explode(',');
}
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $this->application->destination->network,
uuid: $this->application->uuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $this->application->isForceHttpsEnabled(),
is_gzip_enabled: $this->application->isGzipEnabled(),
is_stripprefix_enabled: $this->application->isStripprefixEnabled()
));
}
$this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save();
} else {
@@ -1710,6 +1750,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]);
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
}
$custom_network_aliases = [];
if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) {
$custom_network_aliases = $this->application->custom_network_aliases;
}
$docker_compose = [
'services' => [
$this->container_name => [
@@ -1719,9 +1763,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'expose' => $ports,
'networks' => [
$this->destination->network => [
'aliases' => [
$this->container_name,
],
'aliases' => array_merge(
[$this->container_name],
$custom_network_aliases
),
],
],
'mem_limit' => $this->application->limits_memory,
@@ -2409,20 +2454,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function next(string $status)
{
queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status
if (
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
) {
// Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value ||
$this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
return;
}
$this->application_deployment_queue->update([
'status' => $status,
]);
}
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();

View File

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

View File

@@ -31,7 +31,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))->expireAfter(600)];
}
public function __construct(public Server $server, public bool $manualCleanup = false) {}

View File

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

View File

@@ -24,7 +24,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)];
}
public function __construct(public Server $server) {}

View File

@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)];
}
public function __construct(public Server $server) {}

View File

@@ -269,7 +269,7 @@ class Email extends Component
} catch (\Throwable $e) {
$this->smtpEnabled = false;
return handleError($e);
return handleError($e, $this);
}
}
@@ -337,11 +337,14 @@ class Email extends Component
public function copyFromInstanceSettings()
{
$settings = instanceSettings();
$this->smtpFromAddress = $settings->smtp_from_address;
$this->smtpFromName = $settings->smtp_from_name;
if ($settings->smtp_enabled) {
$this->smtpEnabled = true;
$this->smtpFromAddress = $settings->smtp_from_address;
$this->smtpFromName = $settings->smtp_from_name;
$this->resendEnabled = false;
}
$this->smtpRecipients = $settings->smtp_recipients;
$this->smtpHost = $settings->smtp_host;
$this->smtpPort = $settings->smtp_port;
@@ -349,20 +352,14 @@ class Email extends Component
$this->smtpUsername = $settings->smtp_username;
$this->smtpPassword = $settings->smtp_password;
$this->smtpTimeout = $settings->smtp_timeout;
$this->resendEnabled = false;
$this->saveModel();
return;
}
if ($settings->resend_enabled) {
$this->resendEnabled = true;
$this->resendApiKey = $settings->resend_api_key;
$this->smtpEnabled = false;
}
$this->resendApiKey = $settings->resend_api_key;
$this->saveModel();
return;
}
$this->dispatch('error', 'Instance SMTP/Resend settings are not enabled.');
}
public function render()

View File

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

View File

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

View File

@@ -159,13 +159,18 @@ class Previews extends Component
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deployment_uuid,
force_rebuild: false,
pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
);
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],

View File

@@ -30,11 +30,15 @@ class Source extends Component
#[Validate(['nullable', 'string'])]
public ?string $gitCommitSha = null;
#[Locked]
public $sources;
public function mount()
{
try {
$this->syncData();
$this->getPrivateKeys();
$this->getSources();
} catch (\Throwable $e) {
handleError($e, $this);
}
@@ -66,6 +70,14 @@ class Source extends Component
});
}
private function getSources()
{
// filter the current source out
$this->sources = currentTeam()->sources()->whereNotNull('app_id')->reject(function ($source) {
return $source->id === $this->application->source_id;
})->sortBy('name');
}
public function setPrivateKey(int $privateKeyId)
{
try {
@@ -92,4 +104,20 @@ class Source extends Component
return handleError($e, $this);
}
}
public function changeSource($sourceId, $sourceType)
{
try {
$this->application->update([
'source_id' => $sourceId,
'source_type' => $sourceType,
'repository_project_id' => null,
]);
$this->application->refresh();
$this->getSources();
$this->dispatch('success', 'Source updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

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

View File

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

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class All extends Component
{
use EnvironmentVariableProtection;
public $resource;
public string $resourceClass;
@@ -138,18 +141,58 @@ class All extends Component
private function handleBulkSubmit()
{
$variables = parseEnvFormatToArray($this->variables);
$changesMade = false;
$errorOccurred = false;
$this->deleteRemovedVariables(false, $variables);
$this->updateOrCreateVariables(false, $variables);
// Try to delete removed variables
$deletedCount = $this->deleteRemovedVariables(false, $variables);
if ($deletedCount > 0) {
$changesMade = true;
} elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
// If we tried to delete but couldn't (due to Docker Compose), mark as error
$errorOccurred = true;
}
// Update or create variables
$updatedCount = $this->updateOrCreateVariables(false, $variables);
if ($updatedCount > 0) {
$changesMade = true;
}
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
$this->deleteRemovedVariables(true, $previewVariables);
$this->updateOrCreateVariables(true, $previewVariables);
// Try to delete removed preview variables
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
if ($deletedPreviewCount > 0) {
$changesMade = true;
} elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
// If we tried to delete but couldn't (due to Docker Compose), mark as error
$errorOccurred = true;
}
// Update or create preview variables
$updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables);
if ($updatedPreviewCount > 0) {
$changesMade = true;
}
}
// Debug information
\Log::info('Environment variables update status', [
'deletedCount' => $deletedCount,
'updatedCount' => $updatedCount,
'deletedPreviewCount' => $deletedPreviewCount ?? 0,
'updatedPreviewCount' => $updatedPreviewCount ?? 0,
'changesMade' => $changesMade,
'errorOccurred' => $errorOccurred,
]);
// Only show success message if changes were actually made and no errors occurred
if ($changesMade && ! $errorOccurred) {
$this->dispatch('success', 'Environment variables updated.');
}
}
private function handleSingleSubmit($data)
{
@@ -184,11 +227,37 @@ class All extends Component
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
// Get all environment variables that will be deleted
$variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
// If there are no variables to delete, return 0
if ($variablesToDelete->isEmpty()) {
return 0;
}
// Check if any of these variables are used in Docker Compose
if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') {
foreach ($variablesToDelete as $envVar) {
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose);
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$envVar->key}' <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();
return $variablesToDelete->count();
}
private function updateOrCreateVariables($isPreview, $variables)
{
$count = 0;
foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
continue;
@@ -198,8 +267,12 @@ class All extends Component
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
// Only count as a change if the value actually changed
if ($found->value !== $value) {
$found->value = $value;
$found->save();
$count++;
}
}
} else {
$environment = new EnvironmentVariable;
@@ -212,8 +285,11 @@ class All extends Component
$environment->resourceable_type = $this->resource->getMorphClass();
$environment->save();
$count++;
}
}
return $count;
}
public function refreshEnvs()

View File

@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class Show extends Component
{
use EnvironmentVariableProtection;
public $parameters;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
@@ -40,6 +43,8 @@ class Show extends Component
public bool $is_really_required = false;
public bool $is_redis_credential = false;
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -65,7 +70,9 @@ class Show extends Component
}
$this->parameters = get_route_parameters();
$this->checkEnvs();
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
$this->is_redis_credential = true;
}
}
public function refresh()
@@ -171,6 +178,17 @@ class Show extends Component
public function delete()
{
try {
// Check if the variable is used in Docker Compose
if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) {
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose);
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$this->env->key}' <br><br>Please remove it from the Docker Compose file first.");
return;
}
}
$this->env->delete();
$this->dispatch('environmentVariableDeleted');
$this->dispatch('success', 'Environment variable deleted successfully.');

View File

@@ -38,7 +38,8 @@ class DynamicConfigurations extends Component
$contents = collect([]);
foreach ($files as $file) {
$without_extension = str_replace('.', '|', $file);
$contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
$content = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
$contents[$without_extension] = $content ?? '';
}
$this->contents = $contents;
$this->dispatch('$refresh');

View File

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

View File

@@ -12,19 +12,30 @@ class Index extends Component
public bool $alreadySubscribed = false;
public bool $isUnpaid = false;
public bool $isCancelled = false;
public bool $isMember = false;
public bool $loading = true;
public function mount()
{
if (! isCloud()) {
return redirect(RouteServiceProvider::HOME);
}
if (auth()->user()?->isMember()) {
return redirect()->route('dashboard');
$this->isMember = true;
}
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}
$this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
if (! $this->alreadySubscribed) {
$this->loading = false;
}
}
public function stripeCustomerPortal()
@@ -37,6 +48,41 @@ class Index extends Component
return redirect($session->url);
}
public function getStripeStatus()
{
try {
$subscription = currentTeam()->subscription;
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$customer = $stripe->customers->retrieve(currentTeam()->subscription->stripe_customer_id);
if ($customer) {
$subscriptions = $stripe->subscriptions->all(['customer' => $customer->id]);
$currentTeam = currentTeam()->id ?? null;
if (count($subscriptions->data) > 0 && $currentTeam) {
$foundSubscription = collect($subscriptions->data)->firstWhere('metadata.team_id', $currentTeam);
if ($foundSubscription) {
$status = data_get($foundSubscription, 'status');
$subscription->update([
'stripe_subscription_id' => $foundSubscription->id,
]);
if ($status === 'unpaid') {
$this->isUnpaid = true;
}
}
}
if (count($subscriptions->data) === 0) {
$this->isCancelled = true;
}
}
} catch (\Exception $e) {
// Log the error
logger()->error('Stripe API error: ' . $e->getMessage());
// Set a flag to show an error message to the user
$this->addError('stripe', 'Could not retrieve subscription information. Please try again later.');
} finally {
$this->loading = false;
}
}
public function render()
{
return view('livewire.subscription.index');

View File

@@ -45,6 +45,7 @@ use Visus\Cuid2\Cuid2;
'start_command' => ['type' => 'string', 'description' => 'Start command.'],
'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'],
'custom_network_aliases' => ['type' => 'string', 'nullable' => true, 'description' => 'Network aliases for Docker container.'],
'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
@@ -102,6 +103,9 @@ use Visus\Cuid2\Cuid2;
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'],
'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'],
'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'],
'http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
]
)]
@@ -115,6 +119,68 @@ class Application extends BaseModel
protected $appends = ['server_status'];
protected $casts = ['custom_network_aliases' => 'array'];
public function customNetworkAliases(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return null;
}
// If it's already a JSON string, decode it
if (is_string($value) && $this->isJson($value)) {
$value = json_decode($value, true);
}
// If it's a string but not JSON, treat it as a comma-separated list
if (is_string($value) && ! is_array($value)) {
$value = explode(',', $value);
}
$value = collect($value)
->map(function ($alias) {
if (is_string($alias)) {
return str_replace(' ', '-', trim($alias));
}
return null;
})
->filter()
->unique() // Remove duplicate values
->values()
->toArray();
return empty($value) ? null : json_encode($value);
},
get: function ($value) {
if (is_null($value)) {
return null;
}
if (is_string($value) && $this->isJson($value)) {
return json_decode($value, true);
}
return is_array($value) ? $value : [];
}
);
}
/**
* Check if a string is a valid JSON
*/
private function isJson($string)
{
if (! is_string($string)) {
return false;
}
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
protected static function booted()
{
static::addGlobalScope('withRelations', function ($builder) {
@@ -392,22 +458,23 @@ class Application extends BaseModel
{
return Attribute::make(
get: function () {
$base_dir = $this->base_directory ?? '/';
if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
if (str($this->git_repository)->contains('bitbucket')) {
return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}";
return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}{$base_dir}";
}
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}{$base_dir}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
if (str($this->git_repository)->contains('bitbucket')) {
return "https://{$git_repository}/src/{$this->git_branch}";
return "https://{$git_repository}/src/{$this->git_branch}{$base_dir}";
}
return "https://{$git_repository}/tree/{$this->git_branch}";
return "https://{$git_repository}/tree/{$this->git_branch}{$base_dir}";
}
return $this->git_repository;

View File

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

View File

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

View File

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

View File

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

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) {
$destination_id = $destination->id;
}
// Check if there's already a deployment in progress or queued for this application and commit
$existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
->where('commit', $commit)
->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])
->first();
if ($existing_deployment) {
// If force_rebuild is true or rollback is true or no_questions_asked is true, we'll still create a new deployment
if (! $force_rebuild && ! $rollback && ! $no_questions_asked) {
// Return the existing deployment's details
return [
'status' => 'skipped',
'message' => 'Deployment already queued for this commit.',
'deployment_uuid' => $existing_deployment->deployment_uuid,
'existing_deployment' => $existing_deployment,
];
}
}
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
'application_name' => $application->name,
@@ -47,11 +67,17 @@ function queue_application_deployment(Application $application, string $deployme
ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id,
);
} elseif (next_queuable($server_id, $application_id)) {
} elseif (next_queuable($server_id, $application_id, $commit)) {
ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id,
);
}
return [
'status' => 'queued',
'message' => 'Deployment queued.',
'deployment_uuid' => $deployment_uuid,
];
}
function force_start_deployment(ApplicationDeploymentQueue $deployment)
{
@@ -78,20 +104,35 @@ function queue_next_deployment(Application $application)
}
}
function next_queuable(string $server_id, string $application_id): bool
function next_queuable(string $server_id, string $application_id, string $commit = 'HEAD'): bool
{
$deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at');
$same_application_deployments = $deployments->where('application_id', $application_id);
$in_progress = $same_application_deployments->filter(function ($value, $key) {
return $value->status === 'in_progress';
});
if ($in_progress->count() > 0) {
// Check if there's already a deployment in progress for this application and commit
$existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
->where('commit', $commit)
->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
->first();
if ($existing_deployment) {
return false;
}
// Check if there's any deployment in progress for this application
$in_progress = ApplicationDeploymentQueue::where('application_id', $application_id)
->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
->exists();
if ($in_progress) {
return false;
}
// Check server's concurrent build limit
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
$active_deployments = ApplicationDeploymentQueue::where('server_id', $server_id)
->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)
->count();
if ($deployments->count() > $concurrent_builds) {
if ($active_deployments >= $concurrent_builds) {
return false;
}

View File

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

View File

@@ -52,6 +52,9 @@ function generateGithubToken(GithubApp $source, string $type)
if (! $response->successful()) {
$error = data_get($response->json(), 'message', 'no error message found');
if ($error === 'Not Found') {
$error = 'Repository not found. Is it moved or deleted?';
}
throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error);
}

View File

@@ -2987,7 +2987,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$predefinedPort = '8000';
}
if ($isDatabase) {
$applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) {
$savedService = $applicationFound;
$savedService = ServiceDatabase::firstOrCreate([
@@ -2999,17 +2999,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else {
$savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
}
$environment = collect(data_get($service, 'environment', []));
$buildArgs = collect(data_get($service, 'build.args', []));
$environment = $environment->merge($buildArgs);
@@ -3054,7 +3059,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$port = null;
}
if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid");
$fqdn = generateFqdn($server, "$uuid");
} elseif ($isService) {
if ($fqdnFor) {
$fqdn = generateFqdn($server, "$fqdnFor-$uuid");
@@ -3124,11 +3129,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if (str($fqdnFor)->contains('_')) {
$fqdnFor = str($fqdnFor)->before('_');
}
if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid");
} elseif ($isService) {
$fqdn = generateFqdn($server, "$fqdnFor-$uuid");
}
$resource->environment_variables()->firstOrCreate([
'key' => $key->value(),
'resourceable_type' => get_class($resource),
@@ -3143,11 +3144,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
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(),
@@ -3173,7 +3170,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
}
}
}
$serviceAppsLogDrainEnabledMap = collect([]);
if ($resource instanceof Service) {
@@ -3201,6 +3197,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$use_network_mode = data_get($service, 'network_mode') !== null;
$depends_on = collect(data_get($service, 'depends_on', []));
$labels = collect(data_get($service, 'labels', []));
if ($labels->count() > 0) {
if (isAssociativeArray($labels)) {
$newLabels = collect([]);
$labels->each(function ($value, $key) use ($newLabels) {
$newLabels->push("$key=$value");
});
$labels = $newLabels;
}
}
$environment = collect(data_get($service, 'environment', []));
$ports = collect(data_get($service, 'ports', []));
$buildArgs = collect(data_get($service, 'build.args', []));

View File

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

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

View File

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

View File

@@ -1798,6 +1798,18 @@
"summary": "Update",
"description": "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": {
"description": "Application updated.",
"required": true,
@@ -4441,7 +4453,7 @@
"Deployments"
],
"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",
"parameters": [
{
@@ -4529,6 +4541,40 @@
"summary": "List application deployments",
"description": "List application deployments by using the 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": {
"200": {
"description": "List application deployments by using the app uuid.",
@@ -4921,6 +4967,18 @@
"summary": "Update",
"description": "Update Project.",
"operationId": "update-project-by-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the project.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"description": "Project updated.",
"required": true,
@@ -5321,6 +5379,22 @@
},
"404": {
"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": [
@@ -6222,6 +6296,18 @@
"summary": "Update",
"description": "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": {
"description": "Service updated.",
"required": true,
@@ -7198,6 +7284,11 @@
"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."
@@ -7638,6 +7729,12 @@
"type": "string",
"format": "private-key"
},
"public_key": {
"type": "string"
},
"fingerprint": {
"type": "string"
},
"is_git_related": {
"type": "boolean"
},

View File

@@ -1277,6 +1277,15 @@ paths:
summary: Update
description: '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:
description: 'Application updated.'
required: true
@@ -3085,7 +3094,7 @@ paths:
tags:
- Deployments
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
parameters:
-
@@ -3135,6 +3144,33 @@ paths:
summary: 'List application deployments'
description: 'List application deployments by using the 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:
'200':
description: 'List application deployments by using the app uuid.'
@@ -3377,6 +3413,15 @@ paths:
summary: Update
description: 'Update Project.'
operationId: update-project-by-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the project.'
required: true
schema:
type: string
format: uuid
requestBody:
description: 'Project updated.'
required: true
@@ -3630,6 +3675,14 @@ paths:
$ref: '#/components/responses/400'
'404':
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:
-
bearerAuth: []
@@ -4145,6 +4198,15 @@ paths:
summary: Update
description: '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:
description: 'Service updated.'
required: true
@@ -4769,6 +4831,10 @@ components:
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.'
@@ -5093,6 +5159,10 @@ components:
private_key:
type: string
format: private-key
public_key:
type: string
fingerprint:
type: string
is_git_related:
type: boolean
team_id:

View File

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

8
package-lock.json generated
View File

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

View File

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

View File

@@ -8,13 +8,8 @@
<h1>Dashboard</h1>
<div class="subtitle">Your self-hosted infrastructure.</div>
@if (request()->query->get('success'))
<div class="items-center justify-center mb-10 font-bold rounded alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
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
<div class=" mb-10 font-bold alert alert-success">
Your subscription has been activated! Welcome onboard! It could take a few seconds before your
subscription is activated.<br> Please be patient.
</div>
@endif

View File

@@ -1,13 +1,6 @@
<div>
<x-modal-confirmation
title="Confirm Team Deletion?"
buttonTitle="Delete Team"
isErrorButton
submitAction="delete"
:actions="['The current Team will be permanently deleted.']"
confirmationText="{{ $team }}"
<div class="w-full px-2">
<x-modal-confirmation buttonFullWidth title="Confirm Team Deletion?" 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"
shortConfirmationLabel="Team Name"
step3ButtonText="Permanently Delete"
/>
shortConfirmationLabel="Team Name" step3ButtonText="Permanently Delete" />
</div>

View File

@@ -342,6 +342,24 @@
<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." />
@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>
@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="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">
<x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" />
<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" />
</div>
</div>
@if ($privateKeyId)
<h3 class="pt-4">Deploy Key</h3>
<div class="py-2 pt-4">Currently attached Private Key: <span
@@ -47,6 +53,38 @@
</x-forms.button>
@endforeach
</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
</form>
</div>

View File

@@ -20,7 +20,12 @@
</div>
<div class="w-48 pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@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>
@if ($backup->save_s3)
<div class="pb-6">

View File

@@ -13,6 +13,23 @@
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 class="flex flex-col gap-2">
@if ($database->started_at)
<div class="pt-2 dark:text-warning">If you change the values in the database, please sync it here,
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.
@@ -30,6 +47,8 @@
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')" />
</div>
@endif
</div>
<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>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"

View File

@@ -129,7 +129,7 @@
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
<div class="text-error">
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>
</div>
</div>

View File

@@ -31,18 +31,19 @@
@else
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($is_multiline)
<x-forms.input isMultiline="{{ $is_multiline }}" id="key" />
<x-forms.textarea type="password" id="value" />
<x-forms.input :required="$is_redis_credential" isMultiline="{{ $is_multiline }}" id="key" />
<x-forms.textarea :required="$is_redis_credential" type="password" id="value" />
@else
<x-forms.input id="key" />
<x-forms.input type="password" id="value" />
<x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" id="key" />
<x-forms.input :required="$is_redis_credential" type="password" id="value" />
@endif
@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
</div>
@endif
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if (!$is_redis_credential)
@if ($type === 'service')
<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}'`"
@@ -75,6 +76,7 @@
@endif
@endif
@endif
@endif
<div class="flex-1"></div>
@if ($isDisabled)
<x-forms.button disabled type="submit">

View File

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

View File

@@ -38,7 +38,7 @@
wire:model="contents.{{ $fileName }}" rows="5" />
@else
<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 }}" />
<x-forms.textarea disabled wire:model="contents.{{ $fileName }}"
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">
@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')
<x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting')

View File

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

View File

@@ -3,13 +3,6 @@
Subscribe | Coolify
</x-slot>
@if (auth()->user()->isAdminFromSession())
<div>
<div class="flex gap-2">
<h1>Subscriptions</h1>
@if (subscriptionProvider() === 'stripe' && $alreadySubscribed)
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button>
@endif
</div>
@if (request()->query->get('cancelled'))
<div class="mb-6 rounded alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
@@ -21,19 +14,51 @@
support.</span>
</div>
@endif
<div class="flex gap-2">
<h1>Subscriptions</h1>
</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')
<livewire:subscription.pricing-plans />
<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
<div class="flex flex-col justify-center mx-10">
<div class="flex gap-2">
<h1>Subscription</h1>
</div>
<div>You are not an admin or have been removed from this team. If this does not make sense, please <span
class="underline cursor-pointer dark:text-white" wire:click="help">contact
us</span>.</div>
<div>You are not an admin so you cannot manage your Team's subscription. If this does not make sense, please
<span class="underline cursor-pointer dark:text-white" wire:click="help">contact
us</span>.
</div>
</div>
@endif
</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="flex justify-center">
<fieldset
@@ -20,7 +20,7 @@
</div>
<div class="flow-root mt-12">
<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">
<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">
@@ -72,14 +72,16 @@
</div>
</div>
</div>
<div class="flex pt-4 h-14">
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-monthly')">
class="w-full" wire:click="subscribeStripe('dynamic-monthly')">
Subscribe
</x-forms.button>
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-yearly')">
class="w-full" wire:click="subscribeStripe('dynamic-yearly')">
Subscribe
</x-forms.button>
</div>
<ul role="list" class="mt-8 space-y-3 text-sm leading-6 dark:text-neutral-400">
<li class="flex">
<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;
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([
'middleware' => ['auth:sanctum', 'api.ability:write'],
'prefix' => 'v1',
@@ -117,7 +122,7 @@ Route::group([
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::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::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/
# slogan: The Denoland key-value database
# tags: deno, kv, key-value, database
# logo: svgs/denoKV.svg
# logo: svgs/denokv.svg
# port: 4512
services:

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
services:
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"'
environment:
- SERVICE_FQDN_PLAUSIBLE
@@ -17,13 +17,19 @@ services:
- TOTP_VAULT_KEY=${SERVICE_REALBASE64_32_TOTP}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- 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:
plausible-db:
condition: service_healthy
plausible-events-db:
condition: service_healthy
mail:
condition: service_healthy
healthcheck:
test:
[
@@ -39,15 +45,6 @@ services:
retries: 5
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:
image: "postgres:16-alpine"
volumes:
@@ -63,7 +60,9 @@ services:
retries: 10
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:
- plausible-events-data:/var/lib/clickhouse
- type: bind

View File

@@ -35,6 +35,8 @@ services:
unsend:
image: unsend/unsend:latest
expose:
- 3000
environment:
- SERVICE_FQDN_UNSEND_3000
- 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
- NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false}
- API_RATE_LIMIT=${API_RATE_LIMIT:-1}
- HOSTNAME=0.0.0.0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
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
retries: 10
timeout: 2s

File diff suppressed because one or more lines are too long

View File

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