Merge branch 'next' into feature/signoz

This commit is contained in:
🏔️ Peak 2025-05-14 14:30:52 +02:00 committed by GitHub
commit 4075c26eed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
387 changed files with 14280 additions and 6431 deletions

View file

@ -14,3 +14,5 @@ PUSHER_APP_SECRET=
ROOT_USERNAME=
ROOT_USER_EMAIL=
ROOT_USER_PASSWORD=
REGISTRY_URL=ghcr.io

View file

@ -1,65 +0,0 @@
name: Dusk
on:
push:
branches: [ "not-existing" ]
jobs:
dusk:
runs-on: ubuntu-latest
services:
redis:
image: redis
env:
REDIS_HOST: localhost
REDIS_PORT: 6379
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up PostgreSQL
run: |
sudo systemctl start postgresql
sudo -u postgres psql -c "CREATE DATABASE coolify;"
sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';"
sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';"
sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';"
sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;"
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Copy .env
run: cp .env.dusk.ci .env
- name: Install Dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Generate key
run: php artisan key:generate
- name: Install Chrome binaries
run: php artisan dusk:chrome-driver --detect
- name: Start Chrome Driver
run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 &
- name: Build assets
run: npm install && npm run build
- name: Run Laravel Server
run: php artisan serve --no-reload &
- name: Execute tests
run: php artisan dusk
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tests/Browser/screenshots
- name: Upload Console Logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: console
path: tests/Browser/console

View file

@ -21,7 +21,7 @@ jobs:
async function processIssue(issueNumber, isFromPR = false, prBaseBranch = null) {
try {
if (isFromPR && prBaseBranch !== 'main') {
if (isFromPR && prBaseBranch !== 'v4.x') {
return;
}
@ -70,7 +70,7 @@ jobs:
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const pr = context.payload.pull_request;
await processIssue(pr.number);
if (pr.merged && pr.base.ref === 'main' && pr.body) {
if (pr.merged && pr.base.ref === 'v4.x' && pr.body) {
const issueReferences = pr.body.match(/#(\d+)/g);
if (issueReferences) {
for (const reference of issueReferences) {

View file

@ -2,7 +2,7 @@ name: Coolify Helper Image
on:
push:
branches: [ "main" ]
branches: [ "v4.x" ]
paths:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile

View file

@ -2,7 +2,7 @@ name: Production Build (v4)
on:
push:
branches: ["main"]
branches: ["v4.x"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- .github/workflows/coolify-helper-next.yml

View file

@ -2,7 +2,7 @@ name: Coolify Realtime
on:
push:
branches: [ "main" ]
branches: [ "v4.x" ]
paths:
- .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile

View file

@ -2,7 +2,10 @@ name: Staging Build
on:
push:
branches-ignore: ["main", "v3"]
branches-ignore:
- v4.x
- v3.x
- '**v5.x**'
paths-ignore:
- .github/workflows/coolify-helper.yml
- .github/workflows/coolify-helper-next.yml

View file

@ -2,7 +2,7 @@ name: Generate Changelog
on:
push:
branches: [ main ]
branches: [ v4.x ]
workflow_dispatch:
permissions:
@ -33,4 +33,4 @@ jobs:
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add CHANGELOG.md
git commit -m "docs: update changelog"
git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git main
git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git v4.x

View file

@ -4,6 +4,565 @@ # Changelog
## [unreleased]
### 📚 Documentation
- Update changelog
## [4.0.0-beta.415] - 2025-04-29
### 📚 Documentation
- Update changelog
## [4.0.0-beta.413] - 2025-04-28
### 💼 Other
- Adjust Workflows for v5 (#5689)
### 📚 Documentation
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(workflows)* Adjust workflow for announcement
## [4.0.0-beta.412] - 2025-04-23
### ⚙️ Miscellaneous Tasks
- *(versions)* Update coolify version to 4.0.0-beta.412 and nightly version to 4.0.0-beta.413 in configuration files
## [4.0.0-beta.411] - 2025-04-23
### 🚀 Features
- *(deployment)* Add repository_project_id handling for private GitHub apps and clean up unused Caddy label logic
- *(api)* Enhance OpenAPI specifications with token variable and additional key attributes
- *(docker)* Add HTTP Basic Authentication support and enhance hostname parsing in Docker run conversion
- *(api)* Add HTTP Basic Authentication fields to OpenAPI specifications and enhance PrivateKey model descriptions
### 🐛 Bug Fixes
- *(backup-edit)* Conditionally enable S3 checkbox based on available validated S3 storage
- *(source)* Update no sources found message for clarity
- *(api)* Correct middleware for service update route to ensure proper permissions
- *(api)* Handle JSON response in service creation and update methods for improved error handling
- Add 201 json code to servers validate api response
- *(docker)* Ensure password hashing only occurs when HTTP Basic Authentication is enabled
- *(docker)* Enhance hostname and GPU option validation in Docker run to compose conversion
### 🚜 Refactor
- *(jobs)* Comment out unused Caddy label handling in ApplicationDeploymentJob and simplify proxy path logic in Server model
- *(database)* Simplify database type checks in ServiceDatabase and enhance image validation in Docker helper
- *(shared)* Remove unused ray debugging statement from newParser function
- *(applications)* Remove redundant error response in create_env method
- *(api)* Restructure routes to include versioning and maintain existing feedback endpoint
- *(api)* Remove token variable from OpenAPI specifications for clarity
- *(environment-variables)* Remove protected variable checks from delete methods for cleaner logic
- *(http-basic-auth)* Rename 'http_basic_auth_enable' to 'http_basic_auth_enabled' across application files for consistency
- *(docker)* Remove debug statement and enhance hostname handling in Docker run conversion
- *(server)* Simplify proxy path logic and remove unnecessary conditions
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Update coolify version to 4.0.0-beta.411 and nightly version to 4.0.0-beta.412 in configuration files
## [4.0.0-beta.410] - 2025-04-18
### 🚀 Features
- Add HTTP Basic Authentication
- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README
- *(core)* Enable magic env variables for compose based applications
### 🐛 Bug Fixes
- *(application)* Append base directory to git branch URLs for improved path handling
- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON
- *(navbar)* Update error message link to use route for environment variables navigation
- Unsend template
- Replace ports with expose
- *(templates)* Update Unsend compose configuration for improved service integration
### 🚜 Refactor
- *(jobs)* Update WithoutOverlapping middleware to use expireAfter for better queue management
### 📚 Documentation
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files
- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service
## [4.0.0-beta.409] - 2025-04-16
### 🐛 Bug Fixes
- *(parser)* Transform associative array labels into key=value format for better compatibility
- *(redis)* Update username and password input handling to clarify database sync requirements
- *(source)* Update connected source display to handle cases with no source connected
### 🚜 Refactor
- *(source)* Conditionally display connected source and change source options based on private key presence
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files
## [4.0.0-beta.408] - 2025-04-14
### 🚀 Features
- *(OpenApi)* Enhance OpenAPI specifications by adding UUID parameters for application, project, and service updates; improve deployment listing with pagination parameters; update command signature for OpenApi generation
- *(subscription)* Enhance subscription management with loading states and Stripe status checks
### 🐛 Bug Fixes
- *(pre-commit)* Correct input redirection for /dev/tty and add OpenAPI generation command
- *(pricing-plans)* Adjust grid class for improved layout consistency in subscription pricing plans
- *(migrations)* Make stripe_comment field nullable in subscriptions table
- *(mongodb)* Also apply custom config when SSL is enabled
- *(templates)* Correct casing of denoKV references in service templates and YAML files
- *(deployment)* Handle missing destination in deployment process to prevent errors
### 💼 Other
- Add missing openapi items to PrivateKey
### 🚜 Refactor
- *(commands)* Reorganize OpenAPI and Services generation commands into a new namespace for better structure; remove old command files
- *(Dockerfile)* Remove service generation command from the build process to streamline Dockerfile and improve build efficiency
- *(navbar-delete-team)* Simplify modal confirmation layout and enhance button styling for better user experience
- *(Server)* Remove debug logging from isReachableChanged method to clean up code and improve performance
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Update nightly version to 4.0.0-beta.410
- *(pre-commit)* Remove OpenAPI generation command from pre-commit hook
- *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json
## [4.0.0-beta.407] - 2025-04-09
### 📚 Documentation
- Update changelog
## [4.0.0-beta.406] - 2025-04-05
### 🚀 Features
- *(Deploy)* Add info dispatch for proxy check initiation
- *(EnvironmentVariable)* Add handling for Redis credentials in the environment variable component
- *(EnvironmentVariable)* Implement protection for critical environment variables and enhance deletion logic
- *(Application)* Add networkAliases attribute for handling network aliases as JSON or comma-separated values
- *(GithubApp)* Update default events to include 'pull_request' and streamline event handling
- *(CleanupDocker)* Add support for realtime image management in Docker cleanup process
- *(Deployment)* Enhance queue_application_deployment to handle existing deployments and return appropriate status messages
- *(SourceManagement)* Add functionality to change Git source and display current source in the application settings
### 🐛 Bug Fixes
- *(CheckProxy)* Update port conflict check to ensure accurate grep matching
- *(CheckProxy)* Refine port conflict detection with improved grep patterns
- *(CheckProxy)* Enhance port conflict detection by adjusting ss command for better output
- *(api)* Add back validateDataApplications (#5539)
- *(CheckProxy, Status)* Prevent proxy checks when force_stop is active; remove debug statement in General
- *(Status)* Conditionally check proxy status and refresh button based on force_stop state
- *(General)* Change redis_password property to nullable string
- *(DeployController)* Update request handling to use input method and enhance OpenAPI description for deployment endpoint
### 💼 Other
- Add missing UUID to openapi spec
### 🚜 Refactor
- *(Server)* Use data_get for safer access to settings properties in isFunctional method
- *(Application)* Rename network_aliases to custom_network_aliases across the application for clarity and consistency
- *(ApplicationDeploymentJob)* Streamline environment variable handling by introducing generate_coolify_env_variables method and consolidating logic for pull request and main branch scenarios
- *(ApplicationDeploymentJob, ApplicationDeploymentQueue)* Improve deployment status handling and log entry management with transaction support
- *(SourceManagement)* Sort sources by name and improve UI for changing Git source with better error handling
- *(Email)* Streamline SMTP and resend settings handling in copyFromInstanceSettings method
- *(Email)* Enhance error handling in SMTP and resend methods by passing context to handleError function
- *(DynamicConfigurations)* Improve handling of dynamic configuration content by ensuring fallback to empty string when content is null
- *(ServicesGenerate)* Update command signature from 'services:generate' to 'generate:services' for consistency; update Dockerfile to run service generation during build; update Odoo image version to 18 and add extra addons volume in compose configuration
- *(Dockerfile)* Streamline RUN commands for improved readability and maintainability by adding line continuations
- *(Dockerfile)* Reintroduce service generation command in the build process for consistency and ensure proper asset compilation
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump version to 406
- *(versions)* Bump version to 407 and 408 for coolify and nightly
- *(versions)* Bump version to 408 for coolify and 409 for nightly
## [4.0.0-beta.405] - 2025-04-04
### 🚀 Features
- *(api)* Update OpenAPI spec for services (#5448)
- *(proxy)* Enhance proxy handling and port conflict detection
### 🐛 Bug Fixes
- *(api)* Used ssh keys can be deleted
- *(email)* Transactional emails not sending
### 🚜 Refactor
- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Bump version to 406
- *(versions)* Bump version to 407
## [4.0.0-beta.404] - 2025-04-03
### 🚀 Features
- *(lang)* Added Azerbaijani language updated turkish language. (#5497)
- *(lang)* Added Portuguese from Brazil language (#5500)
- *(lang)* Add Indonesian language translations (#5513)
### 🐛 Bug Fixes
- *(docs)* Comment out execute for now
- *(installation)* Mount the docker config
- *(installation)* Path to config file for docker login
- *(service)* Add health check to Bugsink service (#5512)
- *(email)* Emails are not sent in multiple cases
- *(deployments)* Use graceful shutdown instead of `rm`
- *(docs)* Contribute service url (#5517)
- *(proxy)* Proxy restart does not work on domain
- *(ui)* Only show copy button on https
- *(database)* Custom config for MongoDB (#5471)
### 📚 Documentation
- Update changelog
- Update changelog
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(service)* Remove unused code in Bugsink service
- *(versions)* Update version to 404
- *(versions)* Bump version to 403 (#5520)
- *(versions)* Bump version to 404
## [4.0.0-beta.402] - 2025-04-01
### 🚀 Features
- *(deployments)* Add list application deployments api route
- *(deploy)* Add pull request ID parameter to deploy endpoint
- *(api)* Add pull request ID parameter to applications endpoint
- *(api)* Add endpoints for retrieving application logs and deployments
- *(lang)* Added Norwegian language (#5280)
- *(dep)* Bump all dependencies
### 🐛 Bug Fixes
- Only get apps for the current team
- *(DeployController)* Cast 'pr' query parameter to integer
- *(deploy)* Validate team ID before deployment
- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424)
- *(ui)* Instance Backup settings
### 🚜 Refactor
- *(dev)* Remove OpenAPI generation functionality
- *(migration)* Enhance local file volumes migration with logging
### ⚙️ Miscellaneous Tasks
- *(service)* Update minecraft service ENVs
- *(service)* Add more vars to infisical.yaml (#5418)
- *(service)* Add google variables to plausible.yaml (#5429)
- *(service)* Update authentik.yaml versions (#5373)
- *(core)* Remove redocs
- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
## [4.0.0-beta.401] - 2025-03-28
### 📚 Documentation
- Update changelog
- Update changelog
## [4.0.0-beta.400] - 2025-03-27
### 🚀 Features
- *(database)* Disable MongoDB SSL by default in migration
- *(database)* Add CA certificate generation for database servers
- *(application)* Add SPA configuration and update Nginx generation logic
### 🐛 Bug Fixes
- *(file-storage)* Double save on compose volumes
- *(parser)* Add logging support for applications in services
### 🚜 Refactor
- *(proxy)* Improve port availability checks with multiple methods
- *(database)* Update MongoDB SSL configuration for improved security
- *(database)* Enhance SSL configuration handling for various databases
- *(notifications)* Update Telegram button URL for staging environment
- *(models)* Remove unnecessary cloud check in isEnabled method
- *(database)* Streamline event listeners in Redis General component
- *(database)* Remove redundant database status display in MongoDB view
- *(database)* Update import statements for Auth in database components
- *(database)* Require PEM key file for SSL certificate regeneration
- *(database)* Change MySQL daemon command to MariaDB daemon
- *(nightly)* Update version numbers and enhance upgrade script
- *(versions)* Update version numbers for coolify and nightly
- *(email)* Validate team membership for email recipients
- *(shared)* Simplify deployment status check logic
- *(shared)* Add logging for running deployment jobs
- *(shared)* Enhance job status check to include 'reserved'
- *(email)* Improve error handling by passing context to handleError
- *(email)* Streamline email sending logic and improve configuration handling
- *(email)* Remove unnecessary whitespace in email sending logic
- *(email)* Allow custom email recipients in email sending logic
- *(email)* Enhance sender information formatting in email logic
- *(proxy)* Remove redundant stop call in restart method
- *(file-storage)* Add loadStorageOnServer method for improved error handling
- *(docker)* Parse and sanitize YAML compose file before encoding
- *(file-storage)* Improve layout and structure of input fields
- *(email)* Update label for test email recipient input
- *(database-backup)* Remove existing Docker container before backup upload
- *(database)* Improve decryption and deduplication of local file volumes
- *(database)* Remove debug output from volume update process
### 📚 Documentation
- Update changelog
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(versions)* Update version numbers for coolify and nightly
### ◀️ Revert
- Encrypting mount and fs_path
## [4.0.0-beta.399] - 2025-03-25
### 🚀 Features
- *(service)* Neon
- *(migration)* Add `ssl_certificates` table and model
- *(migration)* Add ssl setting to `standalone_postgresqls` table
- *(ui)* Add ssl settings to Postgres ui
- *(db)* Add ssl mode to Postgres URLs
- *(db)* Setup ssl during Postgres start
- *(migration)* Encrypt local file volumes content and paths
- *(ssl)* Ssl generation helper
- *(ssl)* Migrate to `ECC`certificates using `secp521r1`
- *(ssl)* Improve SSL helper
- *(ssl)* Add a Coolify CA Certificate to all servers
- *(seeder)* Call CA SSL seeder in prod and dev
- *(ssl)* Add Coolify CA Certificate when adding a new server
- *(installer)* Create CA folder during installation
- *(ssl)* Improve SSL helper
- *(ssl)* Use new improved helper for SSL generation
- *(ui)* Add CA cert UI
- *(ui)* New copy button component
- *(ui)* Use new copy button component everywhere
- *(ui)* Improve server advanced view
- *(migration)* Add CN and alternative names to DB
- *(databases)* Add CA SSL crt location to Postgres URLs
- *(ssl)* Improve ssl generation
- *(ssl)* Regenerate SSL certs job
- *(ssl)* Regenerate certificate and valid until UI
- *(ssl)* Regenerate CA cert and all other certs logic
- *(ssl)* Add full MySQL SSL Support
- *(ssl)* Add full MariaDB SSL support
- *(ssl)* Add `openssl.conf` to configure SSL extension properly
- *(ssl)* Improve SSL generation and security a lot
- *(ssl)* Check for SSL renewal twice daily
- *(ssl)* Add SSL relationships to all DBs
- Add full SSL support to MongoDB
- *(ssl)* Fix some issues and improve ssl generation helper
- *(ssl)* Ability to create `.pem` certs and add `clientAuth` to `extendedKeyUsage`
- *(ssl)* New modes for MongoDB and get `caCert` and `mountPath` correctly
- *(ssl)* Full SSL support for Redis
- New mode implementation for MongoDB
- *(ssl)* Improve Redis and remove modes
- Full SSL support for DrangonflyDB
- SSL notification
- *(github-source)* Enhance GitHub App configuration with manual and private key support
- *(ui)* Improve GitHub repository selection and styling
- *(database)* Implement two-step confirmation for database deletion
- *(assets)* Add new SVG logo for Coolify
- *(install)* Enhance Docker address pool configuration and validation
- *(install)* Improve Docker address pool management and service restart logic
- *(install)* Add missing env variable to install script
- *(LocalFileVolume)* Add binary file detection and update UI logic
- *(templates)* Change glance for v0.7
- *(templates)* Add Freescout service template
- *(service)* Add Evolution API template
- *(service)* Add evolution-api and neon-ws-proxy templates
- *(svg)* Add coolify and evolution-api SVG logos
- *(api)* Add api to create custom services
- *(api)* Separate create and one-click routes
- *(api)* Update Services api routes and handlers
- *(api)* Unify service creation endpoint and enhance validation
- *(notifications)* Add discord ping functionality and settings
- *(user)* Implement session deletion on password reset
- *(github)* Enhance repository loading and validation in applications
### 🐛 Bug Fixes
- *(api)* Docker compose based apps creationg through api
- *(database)* Improve database type detection for Supabase Postgres images
- *(ssl)* Permission of ssl crt and key inside the container
- *(ui)* Make sure file mounts do not showing the encrypted values
- *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert
- *(ui)* Select component should not always uses title case
- *(db)* SSL certificates table and model
- *(migration)* Ssl certificates table
- *(databases)* Fix database name users new `uuid` instead of DB one
- *(database)* Fix volume and file mounts and naming
- *(migration)* Store subjectAlternativeNames as a json array in the db
- *(ssl)* Make sure the subjectAlternativeNames are unique and stored correctly
- *(ui)* Certificate expiration data is null before starting the DB
- *(deletion)* Fix DB deletion
- *(ssl)* Improve SSL cert file mounts
- *(ssl)* Always create ca crt on disk even if it is already there
- *(ssl)* Use mountPath parameter not a hardcoded path
- *(ssl)* Use 1 instead of on for mysql
- *(ssl)* Do not remove SSL directory
- *(ssl)* Wrong ssl cert is loaded to the server and UI error when regenerating SSL
- *(ssl)* Make sure when regenerating the CA cert it is not overwritten with a server cert
- *(ssl)* Regenerating certs for a specific DB
- *(ssl)* Fix MariaDB and MySQL need CA cert
- *(ssl)* Add mount path to DB to fix regeneration of certs
- *(ssl)* Fix SSL regeneration to sign with CA cert and use mount path
- *(ssl)* Get caCert correctly
- *(ssl)* Remove caCert even if it is a folder by accident
- *(ssl)* Ger caCert and `mountPath` correctly
- *(ui)* Only show Regenerate SSL Certificates button when there is a cert
- *(ssl)* Server id
- *(ssl)* When regenerating SSL certs the cert is not singed with the new CN
- *(ssl)* Adjust ca paths for MySQL
- *(ssl)* Remove mode selection for MariaDB as it is not supported
- *(ssl)* Permission issue with MariDB cert and key and paths
- *(ssl)* Rename Redis mode to verify-ca as it is not verify-full
- *(ui)* Remove unused mode for MongoDB
- *(ssl)* KeyDB port and caCert args are missing
- *(ui)* Enable SSL is not working correctly for KeyDB
- *(ssl)* Add `--tls` arg to DrangflyDB
- *(notification)* Always send SSL notifications
- *(database)* Change default value of enable_ssl to false for multiple tables
- *(ui)* Correct grammatical error in 404 page
- *(seeder)* Update GitHub app name in GithubAppSeeder
- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration
- *(domain)* Dispatch refreshStatus event after successful domain update
- *(database)* Correct container name generation for service databases
- *(database)* Limit container name length for database proxy
- *(database)* Handle unsupported database types in StartDatabaseProxy
- *(database)* Simplify container name generation in StartDatabaseProxy
- *(install)* Handle potential errors in Docker address pool configuration
- *(backups)* Retention settings
- *(redis)* Set default redis_username for new instances
- *(core)* Improve instantSave logic and error handling
- *(general)* Correct link to framework specific documentation
- *(core)* Redirect healthcheck route for dockercompose applications
- *(api)* Use name from request payload
- *(issue#4746)* Do not use setGitImportSettings inside of generateGitLsRemoteCommands
- Correct some spellings
- *(service)* Replace deprecated credentials env variables on keycloak service
- *(keycloak)* Update keycloak image version to 26.1
- *(console)* Handle missing root user in password reset command
- *(ssl)* Handle missing CA certificate in SSL regeneration job
- *(copy-button)* Ensure text is safely passed to clipboard
### 💼 Other
- Bump Coolify to 4.0.0-beta.400
- *(migration)* Add SSL fields to database tables
- SSL Support for KeyDB
### 🚜 Refactor
- *(ui)* Unhide log toggle in application settings
- *(nginx)* Streamline default Nginx configuration and improve error handling
- *(install)* Clean up install script and enhance Docker installation logic
- *(ScheduledTask)* Clean up code formatting and remove unused import
- *(app)* Remove unused MagicBar component and related code
- *(database)* Streamline SSL configuration handling across database types
- *(application)* Streamline healthcheck parsing from Dockerfile
- *(notifications)* Standardize getRecipients method signatures
- *(configuration)* Centralize configuration management in ConfigurationRepository
- *(docker)* Update image references to use centralized registry URL
- *(env)* Add centralized registry URL to environment configuration
- *(storage)* Simplify file storage iteration in Blade template
- *(models)* Add is_directory attribute to LocalFileVolume model
- *(modal)* Add ignoreWire attribute to modal-confirmation component
- *(invite-link)* Adjust layout for better responsiveness in form
- *(invite-link)* Enhance form layout for improved responsiveness
- *(network)* Enhance docker network creation with ipv6 fallback
- *(network)* Check for existing coolify network before creation
- *(database)* Enhance encryption process for local file volumes
### 📚 Documentation
- Update changelog
- Update changelog
- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility
- Update changelog
### ⚙️ Miscellaneous Tasks
- *(migration)* Remove unused columns
- *(ssl)* Improve code in ssl helper
- *(migration)* Ssl cert and key should not be nullable
- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts
- Rename ca crt folder to ssl
- *(ui)* Improve valid until handling
- Improve code quality suggested by code rabbit
- *(supabase)* Update Supabase service template and Postgres image version
- *(versions)* Update version numbers for coolify and nightly
## [4.0.0-beta.398] - 2025-03-01
### 🚀 Features
- *(billing)* Add Stripe past due subscription status tracking

View file

@ -136,6 +136,7 @@ ## 6. Start Development
- Password: `password`
2. Additional development tools:
| Tool | URL | Note |
|------|-----|------|
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
@ -237,9 +238,9 @@ ## Additional Contribution Guidelines
### Contributing a New Service
To add a new service to Coolify, please refer to our documentation:
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
[Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
### Contributing to Documentation
To contribute to the Coolify documentation, please refer to this guide:
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md)
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/readme.md)

View file

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2022] [Andras Bacsai]
Copyright [2025] [Andras Bacsai]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

187
README.md
View file

@ -29,99 +29,6 @@ # Support
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,100 @@ ## Why should I use the Cloud version?
- 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
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity
## 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>
<a href="https://interviewpal.com/?utm_source=coolify.io"><img width="60px" alt="InterviewPal" src="/public/svgs/interviewpal.svg"/></a>
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
# Recognitions
<p>

View file

@ -30,7 +30,7 @@ public function handle(Application $application, bool $previewDeployments = fals
$application->stopContainers($containersToStop, $server);
if ($application->build_pack === 'dockercompose') {
$application->delete_connected_networks($application->uuid);
$application->deleteConnectedNetworks();
}
if ($dockerCleanup) {

View file

@ -85,7 +85,6 @@ public function __invoke(): ProcessResult
]);
$processResult = $process->wait();
// $processResult = Process::timeout($timeout)->run($this->getCommand(), $this->handleOutput(...));
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
$status = ProcessStatus::ERROR;
} else {

View file

@ -22,74 +22,27 @@ class StartDatabaseProxy
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;
$type = $database->getMorphClass();
$databaseType = $database->database_type;
$network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$databaseType = $database->databaseType();
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
$network = $database->service->uuid;
$server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) {
case 'standalone-mariadb':
$type = \App\Models\StandaloneMariadb::class;
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
$type = \App\Models\StandaloneMongodb::class;
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
$type = \App\Models\StandaloneMysql::class;
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
$type = \App\Models\StandalonePostgresql::class;
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
$type = \App\Models\StandaloneRedis::class;
$containerName = "redis-{$database->service->uuid}";
break;
case 'standalone-keydb':
$type = \App\Models\StandaloneKeydb::class;
$containerName = "keydb-{$database->service->uuid}";
break;
case 'standalone-dragonfly':
$type = \App\Models\StandaloneDragonfly::class;
$containerName = "dragonfly-{$database->service->uuid}";
break;
case 'standalone-clickhouse':
$type = \App\Models\StandaloneClickhouse::class;
$containerName = "clickhouse-{$database->service->uuid}";
break;
case 'standalone-supabase/postgres':
$type = \App\Models\StandalonePostgresql::class;
$containerName = "supabase-db-{$database->service->uuid}";
break;
}
}
if ($type === \App\Models\StandaloneRedis::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandalonePostgresql::class) {
$internalPort = 5432;
} elseif ($type === \App\Models\StandaloneMongodb::class) {
$internalPort = 27017;
} elseif ($type === \App\Models\StandaloneMysql::class) {
$internalPort = 3306;
} elseif ($type === \App\Models\StandaloneMariadb::class) {
$internalPort = 3306;
} elseif ($type === \App\Models\StandaloneKeydb::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
$internalPort = 9000;
$containerName = "{$database->name}-{$database->service->uuid}";
}
$internalPort = match ($databaseType) {
'standalone-mariadb', 'standalone-mysql' => 3306,
'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
'standalone-clickhouse' => 9000,
'standalone-mongodb' => 27017,
default => throw new \Exception("Unsupported database type: $databaseType"),
};
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@ -16,24 +18,81 @@ class StartDragonfly
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneDragonfly $database)
{
$this->database = $database;
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/etc/dragonfly/certs/server.crt',
'/etc/dragonfly/certs/server.key',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/dragonfly/certs',
);
}
}
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$startCommand = $this->buildStartCommand();
$docker_compose = [
'services' => [
@ -70,27 +129,55 @@ public function handle(StandaloneDragonfly $database)
],
],
];
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
'target' => '/etc/dragonfly/certs/coolify-ca.crt',
'read_only' => true,
],
]
);
}
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
@ -102,12 +189,32 @@ public function handle(StandaloneDragonfly $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}
private function buildStartCommand(): string
{
$command = "dragonfly --requirepass {$this->database->dragonfly_password}";
if ($this->database->enable_ssl) {
$sslArgs = [
'--tls',
'--tls_cert_file /etc/dragonfly/certs/server.crt',
'--tls_key_file /etc/dragonfly/certs/server.key',
'--tls_ca_cert_file /etc/dragonfly/certs/coolify-ca.crt',
];
$command .= ' '.implode(' ', $sslArgs);
}
return $command;
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
@ -17,26 +19,84 @@ class StartKeydb
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneKeydb $database)
{
$this->database = $database;
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/etc/keydb/certs/server.crt',
'/etc/keydb/certs/server.key',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/keydb/certs',
);
}
}
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_keydb();
$startCommand = $this->buildStartCommand();
$docker_compose = [
'services' => [
$container_name => [
@ -72,34 +132,67 @@ public function handle(StandaloneKeydb $database)
],
],
];
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => $this->configuration_dir.'/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
],
]
);
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
'target' => '/etc/keydb/certs/coolify-ca.crt',
'read_only' => true,
],
]
);
}
// Add custom docker run options
@ -112,6 +205,9 @@ public function handle(StandaloneKeydb $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
@ -177,4 +273,36 @@ private function add_custom_keydb()
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
}
private function buildStartCommand(): string
{
$hasKeydbConf = ! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf);
$keydbConfPath = '/etc/keydb/keydb.conf';
if ($hasKeydbConf) {
$confContent = $this->database->keydb_conf;
$hasRequirePass = str_contains($confContent, 'requirepass');
if ($hasRequirePass) {
$command = "keydb-server $keydbConfPath";
} else {
$command = "keydb-server $keydbConfPath --requirepass {$this->database->keydb_password}";
}
} else {
$command = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
}
if ($this->database->enable_ssl) {
$sslArgs = [
'--tls-port 6380',
'--tls-cert-file /etc/keydb/certs/server.crt',
'--tls-key-file /etc/keydb/certs/server.key',
'--tls-ca-cert-file /etc/keydb/certs/coolify-ca.crt',
'--tls-auth-clients optional',
];
$command .= ' '.implode(' ', $sslArgs);
}
return $command;
}
}

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneMariadb;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@ -16,6 +18,8 @@ class StartMariadb
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneMariadb $database)
{
$this->database = $database;
@ -25,9 +29,64 @@ public function handle(StandaloneMariadb $database)
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/etc/mysql/certs/server.crt',
'/etc/mysql/certs/server.key',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/mysql/certs',
);
}
}
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@ -67,38 +126,81 @@ public function handle(StandaloneMariadb $database)
],
],
];
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
'target' => '/etc/mysql/certs/coolify-ca.crt',
'read_only' => true,
],
]
);
}
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
[
[
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
],
]
);
}
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['command'] = [
'mariadbd',
'--ssl-cert=/etc/mysql/certs/server.crt',
'--ssl-key=/etc/mysql/certs/server.key',
'--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
'--require-secure-transport=1',
];
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
@ -109,6 +211,9 @@ public function handle(StandaloneMariadb $database)
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
if ($this->database->enable_ssl) {
$this->commands[] = executeInDocker($this->database->uuid, 'chown mysql:mysql /etc/mysql/certs/server.crt /etc/mysql/certs/server.key');
}
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@ -16,6 +18,8 @@ class StartMongodb
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneMongodb $database)
{
$this->database = $database;
@ -24,16 +28,69 @@ public function handle(StandaloneMongodb $database)
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
}
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/etc/mongo/certs/server.pem',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/mongo/certs',
isPemKeyFileRequired: true,
);
}
}
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@ -79,47 +136,123 @@ public function handle(StandaloneMongodb $database)
],
],
];
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
if (! empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[[
'type' => 'bind',
'source' => $this->configuration_dir.'/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
]]
);
$docker_compose['services'][$container_name]['command'] = ['mongod', '--config', '/etc/mongo/mongod.conf'];
}
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[[
'type' => 'bind',
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
]]
);
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
'target' => '/etc/mongo/certs/ca.pem',
'read_only' => true,
],
]
);
}
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
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',
'--tlsAllowConnectionsWithoutCertificates',
'--tlsAllowInvalidHostnames',
],
'prefer' => [
'--tlsMode=preferTLS',
'--tlsAllowConnectionsWithoutCertificates',
'--tlsAllowInvalidHostnames',
],
'require' => [
'--tlsMode=requireTLS',
'--tlsAllowConnectionsWithoutCertificates',
'--tlsAllowInvalidHostnames',
],
'verify-full' => [
'--tlsMode=requireTLS',
'--tlsAllowInvalidHostnames',
],
default => [],
};
$commandParts = [...$commandParts, ...$sslConfig];
$commandParts[] = '--tlsCAFile';
$commandParts[] = '/etc/mongo/certs/ca.pem';
$commandParts[] = '--tlsCertificateKeyFile';
$commandParts[] = '/etc/mongo/certs/server.pem';
$docker_compose['services'][$container_name]['command'] = $commandParts;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@ -128,6 +261,9 @@ public function handle(StandaloneMongodb $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
if ($this->database->enable_ssl) {
$this->commands[] = executeInDocker($this->database->uuid, 'chown mongodb:mongodb /etc/mongo/certs/server.pem');
}
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneMysql;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@ -16,6 +18,8 @@ class StartMysql
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneMysql $database)
{
$this->database = $database;
@ -25,9 +29,64 @@ public function handle(StandaloneMysql $database)
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/etc/mysql/certs/server.crt',
'/etc/mysql/certs/server.key',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/mysql/certs',
);
}
}
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@ -67,39 +126,83 @@ public function handle(StandaloneMysql $database)
],
],
];
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
'target' => '/etc/mysql/certs/coolify-ca.crt',
'read_only' => true,
],
]
);
}
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
],
]
);
}
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['command'] = [
'mysqld',
'--ssl-cert=/etc/mysql/certs/server.crt',
'--ssl-key=/etc/mysql/certs/server.key',
'--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
'--require-secure-transport=1',
];
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@ -108,6 +211,11 @@ public function handle(StandaloneMysql $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
if ($this->database->enable_ssl) {
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mysql_user}:{$this->database->mysql_user} /etc/mysql/certs/server.crt /etc/mysql/certs/server.key");
}
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandalonePostgresql;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@ -18,6 +20,8 @@ class StartPostgresql
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandalonePostgresql $database)
{
$this->database = $database;
@ -29,10 +33,65 @@ public function handle(StandalonePostgresql $database)
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/var/lib/postgresql/certs/server.crt',
'/var/lib/postgresql/certs/server.key',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/var/lib/postgresql/certs',
);
}
}
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@ -77,49 +136,84 @@ public function handle(StandalonePostgresql $database)
],
],
];
if (filled($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (count($this->init_scripts) > 0) {
foreach ($this->init_scripts as $init_script) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $init_script,
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
'read_only' => true,
];
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
[[
'type' => 'bind',
'source' => $init_script,
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
'read_only' => true,
]]
);
}
}
if (filled($this->database->postgres_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-postgres.conf',
'target' => '/etc/postgresql/postgresql.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
[[
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-postgres.conf',
'target' => '/etc/postgresql/postgresql.conf',
'read_only' => true,
]]
);
$docker_compose['services'][$container_name]['command'] = [
'postgres',
'-c',
'config_file=/etc/postgresql/postgresql.conf',
];
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['command'] = [
'postgres',
'-c',
'ssl=on',
'-c',
'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
'-c',
'ssl_key_file=/var/lib/postgresql/certs/server.key',
];
}
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
@ -132,6 +226,9 @@ public function handle(StandalonePostgresql $database)
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
if ($this->database->enable_ssl) {
$this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->postgres_user}:{$this->database->postgres_user} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt");
}
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');

View file

@ -2,6 +2,8 @@
namespace App\Actions\Database;
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
@ -17,6 +19,8 @@ class StartRedis
public string $configuration_dir;
private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneRedis $database)
{
$this->database = $database;
@ -26,9 +30,62 @@ public function handle(StandaloneRedis $database)
$this->commands = [
"echo 'Starting database.'",
"echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
"echo 'Directories created successfully.'",
];
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
'/etc/redis/certs/server.crt',
'/etc/redis/certs/server.key',
]);
})
->each(function ($storage) {
$storage->delete();
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
$this->ssl_certificate = SslHelper::generateSslCertificate(
commonName: $this->database->uuid,
resourceType: $this->database->getMorphClass(),
resourceId: $this->database->id,
serverId: $server->id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/redis/certs',
);
}
}
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@ -76,26 +133,55 @@ public function handle(StandaloneRedis $database)
],
],
];
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
$docker_compose['services'][$container_name]['volumes'] ??= [];
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_storages
);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
$persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray()
);
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
[
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
'target' => '/etc/redis/certs/coolify-ca.crt',
'read_only' => true,
],
]
);
}
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
@ -116,6 +202,9 @@ public function handle(StandaloneRedis $database)
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
if ($this->database->enable_ssl) {
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
}
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
@ -202,6 +291,20 @@ private function buildStartCommand(): string
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
}
if ($this->database->enable_ssl) {
$sslArgs = [
'--tls-port 6380',
'--tls-cert-file /etc/redis/certs/server.crt',
'--tls-key-file /etc/redis/certs/server.key',
'--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
'--tls-auth-clients optional',
];
}
if (! empty($sslArgs)) {
$command .= ' '.implode(' ', $sslArgs);
}
return $command;
}

View file

@ -11,7 +11,6 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
@ -25,8 +24,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
return 'Server is not functional';
}
$this->stopContainer($database, $database->uuid, 300);
if (! $isDeleteOperation) {
$this->stopContainer($database, $database->uuid, 30);
if ($isDeleteOperation) {
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
@ -39,37 +38,12 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
return 'Database stopped successfully';
}
private function stopContainer($database, string $containerName, int $timeout = 300): void
private function stopContainer($database, string $containerName, int $timeout = 30): void
{
$server = $database->destination->server;
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
}
private function forceStopContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
}
private function removeContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
private function deleteConnectedNetworks($uuid, $server)
{
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
instant_remote_process(command: [
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
}
}

View file

@ -24,5 +24,6 @@ public function reset(User $user, array $input): void
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
$user->deleteAllSessions();
}
}

View file

@ -27,13 +27,9 @@ public function handle(Server $server, $fromUI = false): bool
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;
}
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) {
throw new \Exception($error);
}
if (! $server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
@ -41,8 +37,12 @@ public function handle(Server $server, $fromUI = false): bool
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') {
@ -51,7 +51,7 @@ public function handle(Server $server, $fromUI = false): bool
return true;
} else {
$status = getContainerStatus($server, 'coolify-proxy');
$status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
@ -65,9 +65,18 @@ public function handle(Server $server, $fromUI = false): bool
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) {
// 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 {
return false;
}
}
}
try {
if ($server->proxyType() !== ProxyTypes::NONE->value) {
$proxyCompose = CheckConfiguration::run($server);
@ -94,18 +103,148 @@ public function handle(Server $server, $fromUI = false): bool
if (count($portsToCheck) === 0) {
return false;
}
foreach ($portsToCheck as $port) {
$connection = @fsockopen($ip, $port);
if (is_resource($connection) && fclose($connection)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
}
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

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class StopProxy
{
use AsAction;
public function handle(Server $server, bool $forceStop = true, int $timeout = 30)
{
try {
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
instant_remote_process(command: [
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
$server->proxy->force_stop = $forceStop;
$server->proxy->status = 'exited';
$server->save();
} catch (\Throwable $e) {
return handleError($e);
}
}
}

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

@ -2,7 +2,9 @@
namespace App\Actions\Server;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneDocker;
use Lorisleiva\Actions\Concerns\AsAction;
@ -17,6 +19,27 @@ public function handle(Server $server)
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
}
if (! SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->exists()) {
$serverCert = SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
serverId: $server->id,
isCaCertificate: true,
validityDays: 10 * 365
);
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
remote_process($commands, $server);
}
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {

View file

@ -99,7 +99,8 @@ public function handle(Server $server, $data = null)
return data_get($value, 'Name') === '/coolify-proxy';
}
})->first();
if (! $foundProxyContainer) {
$proxyStatus = data_get($foundProxyContainer, 'State.Status', 'exited');
if (! $foundProxyContainer || $proxyStatus !== 'running') {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {

View file

@ -15,19 +15,18 @@ public function handle(Server $server)
{
if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom';
StopLogDrain::run($server);
} else {
$type = 'none';
}
if ($type !== 'none') {
StopLogDrain::run($server);
}
try {
if ($type === 'none') {
return 'No log drain is enabled.';
@ -186,7 +185,6 @@ public function handle(Server $server)
"echo '{$compose}' | base64 -d | tee $compose_path > /dev/null",
"echo '{$readme}' | base64 -d | tee $readme_path > /dev/null",
"test -f $config_path/.env && rm $config_path/.env",
];
if ($type === 'newrelic') {
$add_envs_command = [

View file

@ -25,7 +25,7 @@ public function handle(Server $server, bool $restart = false, ?string $latestVer
$endpoint = data_get($server, 'settings.sentinel_custom_url');
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
$mountDir = '/data/coolify/sentinel';
$image = "ghcr.io/coollabsio/sentinel:$version";
$image = config('constants.coolify.registry_url').'/coollabsio/sentinel:'.$version;
if (! $endpoint) {
throw new \Exception('You should set FQDN in Instance Settings.');
}

View file

@ -52,7 +52,8 @@ private function update()
{
PullHelperImageJob::dispatch($this->server);
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
instant_remote_process(["docker pull -q $image"], $this->server, false);
remote_process([
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',

View file

@ -48,7 +48,7 @@ public function handle(Service $service, bool $deleteConfigurations, bool $delet
}
if ($deleteConnectedNetworks) {
$service->delete_connected_networks($service->uuid);
$service->deleteConnectedNetworks();
}
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
@ -56,7 +56,7 @@ public function handle(Service $service, bool $deleteConfigurations, bool $delet
throw new \Exception($e->getMessage());
} finally {
if ($deleteConfigurations) {
$service->delete_configurations();
$service->deleteConfigurations();
}
foreach ($service->applications()->get() as $application) {
$application->forceDelete();

View file

@ -23,8 +23,8 @@ public function handle(Service $service, bool $isDeleteOperation = false, bool $
$containersToStop = $service->getContainersToStop();
$service->stopContainers($containersToStop, $server);
if (! $isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($isDeleteOperation) {
$service->deleteConnectedNetworks();
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}

View file

@ -13,17 +13,20 @@ class CleanupRedis extends Command
public function handle()
{
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
collect($keys)->each(function ($key) use ($prefix) {
$redis = Redis::connection('horizon');
$keys = $redis->keys('*');
$prefix = config('horizon.prefix');
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
});
$type = $redis->command('type', [$keyWithoutPrefix]);
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
collect($queueOverlaps)->each(function ($key) {
Redis::connection()->del($key);
});
if ($type === 5) {
$data = $redis->command('hgetall', [$keyWithoutPrefix]);
$status = data_get($data, 'status');
if ($status === 'completed') {
$redis->command('del', [$keyWithoutPrefix]);
}
}
}
}
}

View file

@ -5,12 +5,10 @@
use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
class Dev extends Command
{
protected $signature = 'dev {--init} {--generate-openapi}';
protected $signature = 'dev {--init}';
protected $description = 'Helper commands for development.';
@ -21,36 +19,6 @@ public function handle()
return;
}
if ($this->option('generate-openapi')) {
$this->generateOpenApi();
return;
}
}
public function generateOpenApi()
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
// https://github.com/OAI/OpenAPI-Specification/releases
$process = Process::run([
'/var/www/html/vendor/bin/openapi',
'app',
'-o',
'openapi.yaml',
'--version',
'3.1.0',
]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
// Convert YAML to JSON
$yaml = file_get_contents('openapi.yaml');
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
file_put_contents('openapi.json', $json);
echo "Converted OpenAPI YAML to JSON.\n";
}
public function init()

View file

@ -1,13 +1,14 @@
<?php
namespace App\Console\Commands;
namespace App\Console\Commands\Generate;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
class OpenApi extends Command
{
protected $signature = 'openapi';
protected $signature = 'generate:openapi';
protected $description = 'Generate OpenApi file.';
@ -17,7 +18,7 @@ public function handle()
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',
@ -29,5 +30,10 @@ public function handle()
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
$yaml = file_get_contents('openapi.yaml');
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
file_put_contents('openapi.json', $json);
echo "Converted OpenAPI YAML to JSON.\n";
}
}

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

@ -39,7 +39,13 @@ public function handle()
}
$this->info('Updating root password...');
try {
User::find(0)->update(['password' => Hash::make($password)]);
$user = User::find(0);
if (! $user) {
$this->error('Root user not found.');
return;
}
$user->update(['password' => Hash::make($password)]);
$this->info('Root password updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root password.');

View file

@ -9,6 +9,7 @@
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\RegenerateSslCertJob;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
use App\Jobs\ServerStorageCheckJob;
@ -50,6 +51,7 @@ protected function schedule(Schedule $schedule): void
}
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
$this->scheduleInstance->command('cleanup:redis')->everyTenMinutes();
if (isDev()) {
// Instance Jobs
@ -83,6 +85,8 @@ protected function schedule(Schedule $schedule): void
$this->checkScheduledBackups();
$this->checkScheduledTasks();
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
$this->scheduleInstance->command('cleanup:database --yes')->daily();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}

View file

@ -12,21 +12,22 @@ class ApplicationStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -12,21 +12,22 @@ class BackupCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -12,21 +12,22 @@ class CloudflareTunnelConfigured implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -7,27 +7,27 @@
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth;
class DatabaseProxyStopped implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = Auth::user()?->currentTeam()?->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -13,28 +13,24 @@ class DatabaseStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId = null;
public int|string|null $userId = null;
public function __construct($userId = null)
{
if (is_null($userId)) {
$userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): ?array
{
if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];
if (is_null($this->userId)) {
return [];
}
return null;
return [
new PrivateChannel("user.{$this->userId}"),
];
}
}

View file

@ -12,18 +12,22 @@ class FileStorageChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -12,21 +12,22 @@ class ProxyStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -12,21 +12,22 @@ class ScheduledTaskDone implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

View file

@ -13,27 +13,24 @@ class ServiceStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?string $userId = null;
public int|string|null $userId = null;
public function __construct($userId = null)
{
if (is_null($userId)) {
$userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): ?array
{
if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];
if (is_null($this->userId)) {
return [];
}
return null;
return [
new PrivateChannel("user.{$this->userId}"),
];
}
}

View file

@ -12,15 +12,21 @@ class TestEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public ?int $teamId = null;
public function __construct()
{
$this->teamId = auth()->user()->currentTeam()->id;
if (auth()->check() && auth()->user()->currentTeam()) {
$this->teamId = auth()->user()->currentTeam()->id;
}
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];

233
app/Helpers/SslHelper.php Normal file
View file

@ -0,0 +1,233 @@
<?php
namespace App\Helpers;
use App\Models\Server;
use App\Models\SslCertificate;
use Carbon\CarbonImmutable;
class SslHelper
{
private const DEFAULT_ORGANIZATION_NAME = 'Coolify';
private const DEFAULT_COUNTRY_NAME = 'XX';
private const DEFAULT_STATE_NAME = 'Default';
public static function generateSslCertificate(
string $commonName,
array $subjectAlternativeNames = [],
?string $resourceType = null,
?int $resourceId = null,
?int $serverId = null,
int $validityDays = 365,
?string $caCert = null,
?string $caKey = null,
bool $isCaCertificate = false,
?string $configurationDir = null,
?string $mountPath = null,
bool $isPemKeyFileRequired = false,
): SslCertificate {
$organizationName = self::DEFAULT_ORGANIZATION_NAME;
$countryName = self::DEFAULT_COUNTRY_NAME;
$stateName = self::DEFAULT_STATE_NAME;
try {
$privateKey = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_EC,
'curve_name' => 'secp521r1',
]);
if ($privateKey === false) {
throw new \RuntimeException('Failed to generate private key: '.openssl_error_string());
}
if (! openssl_pkey_export($privateKey, $privateKeyStr)) {
throw new \RuntimeException('Failed to export private key: '.openssl_error_string());
}
if (! is_null($serverId) && ! $isCaCertificate) {
$server = Server::find($serverId);
if ($server) {
$ip = $server->getIp;
if ($ip) {
$type = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)
? 'IP'
: 'DNS';
$subjectAlternativeNames = array_unique(
array_merge($subjectAlternativeNames, ["$type:$ip"])
);
}
}
}
$basicConstraints = $isCaCertificate ? 'critical, CA:TRUE, pathlen:0' : 'critical, CA:FALSE';
$keyUsage = $isCaCertificate ? 'critical, keyCertSign, cRLSign' : 'critical, digitalSignature, keyAgreement';
$subjectAltNameSection = '';
$extendedKeyUsageSection = '';
if (! $isCaCertificate) {
$extendedKeyUsageSection = "\nextendedKeyUsage = serverAuth, clientAuth";
$subjectAlternativeNames = array_values(
array_unique(
array_merge(["DNS:$commonName"], $subjectAlternativeNames)
)
);
$formattedSubjectAltNames = array_map(
function ($index, $san) {
[$type, $value] = explode(':', $san, 2);
return "{$type}.".($index + 1)." = $value";
},
array_keys($subjectAlternativeNames),
$subjectAlternativeNames
);
$subjectAltNameSection = "subjectAltName = @subject_alt_names\n\n[ subject_alt_names ]\n"
.implode("\n", $formattedSubjectAltNames);
}
$config = <<<CONF
[ req ]
prompt = no
distinguished_name = distinguished_name
req_extensions = req_ext
[ distinguished_name ]
CN = $commonName
[ req_ext ]
basicConstraints = $basicConstraints
keyUsage = $keyUsage
{$extendedKeyUsageSection}
[ v3_req ]
basicConstraints = $basicConstraints
keyUsage = $keyUsage
{$extendedKeyUsageSection}
subjectKeyIdentifier = hash
{$subjectAltNameSection}
CONF;
$tempConfig = tmpfile();
fwrite($tempConfig, $config);
$tempConfigPath = stream_get_meta_data($tempConfig)['uri'];
$csr = openssl_csr_new([
'commonName' => $commonName,
'organizationName' => $organizationName,
'countryName' => $countryName,
'stateOrProvinceName' => $stateName,
], $privateKey, [
'digest_alg' => 'sha512',
'config' => $tempConfigPath,
'req_extensions' => 'req_ext',
]);
if ($csr === false) {
throw new \RuntimeException('Failed to generate CSR: '.openssl_error_string());
}
$certificate = openssl_csr_sign(
$csr,
$caCert ?? null,
$caKey ?? $privateKey,
$validityDays,
[
'digest_alg' => 'sha512',
'config' => $tempConfigPath,
'x509_extensions' => 'v3_req',
],
random_int(1, PHP_INT_MAX)
);
if ($certificate === false) {
throw new \RuntimeException('Failed to sign certificate: '.openssl_error_string());
}
if (! openssl_x509_export($certificate, $certificateStr)) {
throw new \RuntimeException('Failed to export certificate: '.openssl_error_string());
}
SslCertificate::query()
->where('resource_type', $resourceType)
->where('resource_id', $resourceId)
->where('server_id', $serverId)
->delete();
$sslCertificate = SslCertificate::create([
'ssl_certificate' => $certificateStr,
'ssl_private_key' => $privateKeyStr,
'resource_type' => $resourceType,
'resource_id' => $resourceId,
'server_id' => $serverId,
'configuration_dir' => $configurationDir,
'mount_path' => $mountPath,
'valid_until' => CarbonImmutable::now()->addDays($validityDays),
'is_ca_certificate' => $isCaCertificate,
'common_name' => $commonName,
'subject_alternative_names' => $subjectAlternativeNames,
]);
if ($configurationDir && $mountPath && $resourceType && $resourceId) {
$model = app($resourceType)->find($resourceId);
$model->fileStorages()
->where('resource_type', $model->getMorphClass())
->where('resource_id', $model->id)
->get()
->filter(function ($storage) use ($mountPath) {
return in_array($storage->mount_path, [
$mountPath.'/server.crt',
$mountPath.'/server.key',
$mountPath.'/server.pem',
]);
})
->each(function ($storage) {
$storage->delete();
});
if ($isPemKeyFileRequired) {
$model->fileStorages()->create([
'fs_path' => $configurationDir.'/ssl/server.pem',
'mount_path' => $mountPath.'/server.pem',
'content' => $certificateStr."\n".$privateKeyStr,
'is_directory' => false,
'chmod' => '600',
'resource_type' => $resourceType,
'resource_id' => $resourceId,
]);
} else {
$model->fileStorages()->create([
'fs_path' => $configurationDir.'/ssl/server.crt',
'mount_path' => $mountPath.'/server.crt',
'content' => $certificateStr,
'is_directory' => false,
'chmod' => '644',
'resource_type' => $resourceType,
'resource_id' => $resourceId,
]);
$model->fileStorages()->create([
'fs_path' => $configurationDir.'/ssl/server.key',
'mount_path' => $mountPath.'/server.key',
'content' => $privateKeyStr,
'is_directory' => false,
'chmod' => '600',
'resource_type' => $resourceType,
'resource_id' => $resourceId,
]);
}
}
return $sslCertificate;
} catch (\Throwable $e) {
throw new \RuntimeException('SSL Certificate generation failed: '.$e->getMessage(), 0, $e);
} finally {
fclose($tempConfig);
}
}
}

View file

@ -18,6 +18,7 @@
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
@ -44,6 +45,7 @@ private function removeSensitiveData($application)
'private_key_id',
'value',
'real_value',
'http_basic_auth_password',
]);
}
@ -182,6 +184,9 @@ public function applications(Request $request)
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_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'],
],
)
),
@ -298,6 +303,9 @@ public function create_public_application(Request $request)
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_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'],
],
)
),
@ -414,6 +422,9 @@ public function create_private_gh_app_application(Request $request)
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_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'],
],
)
),
@ -514,6 +525,9 @@ public function create_private_deploy_key_application(Request $request)
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_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'],
],
)
),
@ -611,6 +625,9 @@ public function create_dockerfile_application(Request $request)
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_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'],
],
)
),
@ -710,7 +727,6 @@ public function create_dockercompose_application(Request $request)
private function create_application(Request $request, $type)
{
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@ -720,6 +736,8 @@ private function create_application(Request $request, $type)
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|nullable',
@ -728,6 +746,9 @@ private function create_application(Request $request, $type)
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'is_http_basic_auth_enabled' => 'boolean',
'http_basic_auth_username' => 'string|nullable',
'http_basic_auth_password' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@ -811,6 +832,11 @@ private function create_application(Request $request, $type)
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
];
// ports_exposes is not required for dockercompose
if ($request->build_pack === 'dockercompose') {
$validationRules['ports_exposes'] = 'string';
$request->offsetSet('ports_exposes', '80');
}
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@ -822,10 +848,6 @@ private function create_application(Request $request, $type)
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@ -848,7 +870,13 @@ private function create_application(Request $request, $type)
if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson;
}
$repository_url_parsed = Url::fromString($request->git_repository);
$git_host = $repository_url_parsed->getHost();
if ($git_host === 'github.com') {
$application->source_type = GithubApp::class;
$application->source_id = GithubApp::find(0)->id;
}
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
$application->fqdn = $fqdn;
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
@ -872,12 +900,17 @@ private function create_application(Request $request, $type)
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);
@ -924,10 +957,31 @@ private function create_application(Request $request, $type)
if (! $githubApp) {
return response()->json(['message' => 'Github App not found.'], 404);
}
$token = generateGithubInstallationToken($githubApp);
if (! $token) {
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
}
$repositories = collect();
$page = 1;
$repositories = loadRepositoryByPage($githubApp, $token, $page);
if ($repositories['total_count'] > 0) {
while (count($repositories['repositories']) < $repositories['total_count']) {
$page++;
$repositories = loadRepositoryByPage($githubApp, $token, $page);
}
}
$gitRepository = $request->git_repository;
if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) {
$gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', '');
}
$gitRepositoryFound = collect($repositories['repositories'])->firstWhere('full_name', $gitRepository);
if (! $gitRepositoryFound) {
return response()->json(['message' => 'Repository not found.'], 404);
}
$repository_project_id = data_get($gitRepositoryFound, 'id');
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
@ -958,6 +1012,8 @@ private function create_application(Request $request, $type)
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
$application->repository_project_id = $repository_project_id;
$application->save();
$application->refresh();
if (isset($useBuildServer)) {
@ -973,12 +1029,17 @@ private function create_application(Request $request, $type)
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);
@ -1070,12 +1131,17 @@ private function create_application(Request $request, $type)
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);
@ -1159,12 +1225,17 @@ private function create_application(Request $request, $type)
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([
@ -1223,12 +1294,17 @@ private function create_application(Request $request, $type)
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([
@ -1302,7 +1378,6 @@ private function create_application(Request $request, $type)
$service->destination_type = $destination->getMorphClass();
$service->save();
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
if ($instantDeploy) {
StartService::dispatch($service);
@ -1580,6 +1655,18 @@ public function delete_by_uuid(Request $request)
['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,
@ -1691,25 +1778,19 @@ public function update_by_uuid(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($request->collect()->count() == 0) {
return response()->json([
'message' => 'Invalid request.',
], 400);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'message' => 'Application not found',
], 404);
}
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration'];
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password'];
$validationRules = [
'name' => 'string|max:255',
@ -1722,6 +1803,9 @@ public function update_by_uuid(Request $request)
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
'custom_nginx_configuration' => 'string|nullable',
'is_http_basic_auth_enabled' => 'boolean|nullable',
'http_basic_auth_username' => 'string',
'http_basic_auth_password' => 'string',
];
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
@ -1777,6 +1861,29 @@ public function update_by_uuid(Request $request)
'errors' => $errors,
], 422);
}
if ($request->has('is_http_basic_auth_enabled') && $request->is_http_basic_auth_enabled === true) {
if (blank($application->http_basic_auth_username) || blank($application->http_basic_auth_password)) {
$validationErrors = [];
if (blank($request->http_basic_auth_username)) {
$validationErrors['http_basic_auth_username'] = 'The http_basic_auth_username is required.';
}
if (blank($request->http_basic_auth_password)) {
$validationErrors['http_basic_auth_password'] = 'The http_basic_auth_password is required.';
}
if (count($validationErrors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validationErrors,
], 422);
}
}
}
if ($request->has('is_http_basic_auth_enabled') && $application->is_container_label_readonly_enabled === false) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
$domains = $request->domains;
$requestHasDomains = $request->has('domains');
if ($requestHasDomains && $server->isProxyShouldRun()) {
@ -1854,11 +1961,16 @@ public function update_by_uuid(Request $request)
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([
@ -2490,10 +2602,6 @@ public function create_env(Request $request)
])->setStatusCode(201);
}
}
return response()->json([
'message' => 'Something went wrong.',
], 500);
}
#[OA\Delete(
@ -2675,13 +2783,21 @@ public function action_deploy(Request $request)
$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(
[
@ -2836,12 +2952,17 @@ public function action_restart(Request $request)
$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(
[
@ -2851,130 +2972,130 @@ public function action_restart(Request $request)
);
}
#[OA\Post(
summary: 'Execute Command',
description: "Execute a command on the application's current container.",
path: '/applications/{uuid}/execute',
operationId: 'execute-command-application',
security: [
['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(
required: true,
description: 'Command to execute.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
],
),
),
),
responses: [
new OA\Response(
response: 200,
description: "Execute a command on the application's current container.",
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Command executed.'],
'response' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function execute_command_by_uuid(Request $request)
{
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
$allowedFields = ['command'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'command' => 'string|required',
]);
// #[OA\Post(
// summary: 'Execute Command',
// description: "Execute a command on the application's current container.",
// path: '/applications/{uuid}/execute',
// operationId: 'execute-command-application',
// security: [
// ['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(
// required: true,
// description: 'Command to execute.',
// content: new OA\MediaType(
// mediaType: 'application/json',
// schema: new OA\Schema(
// type: 'object',
// properties: [
// 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
// ],
// ),
// ),
// ),
// responses: [
// new OA\Response(
// response: 200,
// description: "Execute a command on the application's current container.",
// content: [
// new OA\MediaType(
// mediaType: 'application/json',
// schema: new OA\Schema(
// type: 'object',
// properties: [
// 'message' => ['type' => 'string', 'example' => 'Command executed.'],
// 'response' => ['type' => 'string'],
// ]
// )
// ),
// ]
// ),
// new OA\Response(
// response: 401,
// ref: '#/components/responses/401',
// ),
// new OA\Response(
// response: 400,
// ref: '#/components/responses/400',
// ),
// new OA\Response(
// response: 404,
// ref: '#/components/responses/404',
// ),
// ]
// )]
// public function execute_command_by_uuid(Request $request)
// {
// // TODO: Need to review this from security perspective, to not allow arbitrary command execution
// $allowedFields = ['command'];
// $teamId = getTeamIdFromToken();
// if (is_null($teamId)) {
// return invalidTokenResponse();
// }
// $uuid = $request->route('uuid');
// if (! $uuid) {
// return response()->json(['message' => 'UUID is required.'], 400);
// }
// $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
// if (! $application) {
// return response()->json(['message' => 'Application not found.'], 404);
// }
// $return = validateIncomingRequest($request);
// if ($return instanceof \Illuminate\Http\JsonResponse) {
// return $return;
// }
// $validator = customApiValidator($request->all(), [
// 'command' => 'string|required',
// ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
// $extraFields = array_diff(array_keys($request->all()), $allowedFields);
// if ($validator->fails() || ! empty($extraFields)) {
// $errors = $validator->errors();
// if (! empty($extraFields)) {
// foreach ($extraFields as $field) {
// $errors->add($field, 'This field is not allowed.');
// }
// }
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => $errors,
// ], 422);
// }
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
$status = getContainerStatus($application->destination->server, $container['Names']);
// $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
// $status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
// if ($status !== 'running') {
// return response()->json([
// 'message' => 'Application is not running.',
// ], 400);
// }
$commands = collect([
executeInDocker($container['Names'], $request->command),
]);
// $commands = collect([
// executeInDocker($container['Names'], $request->command),
// ]);
$res = instant_remote_process(command: $commands, server: $application->destination->server);
// $res = instant_remote_process(command: $commands, server: $application->destination->server);
return response()->json([
'message' => 'Command executed.',
'response' => $res,
]);
}
// return response()->json([
// 'message' => 'Command executed.',
// 'response' => $res,
// ]);
// }
private function validateDataApplications(Request $request, Server $server)
{

View file

@ -5,8 +5,10 @@
use App\Actions\Database\StartDatabase;
use App\Actions\Service\StartService;
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;
@ -131,7 +133,7 @@ public function deployment_by_uuid(Request $request)
#[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: [
@ -142,6 +144,7 @@ public function deployment_by_uuid(Request $request)
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')),
],
responses: [
@ -184,26 +187,32 @@ public function deployment_by_uuid(Request $request)
public function deploy(Request $request)
{
$teamId = getTeamIdFromToken();
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if (is_null($teamId)) {
return invalidTokenResponse();
}
$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);
}
if (is_null($teamId)) {
return invalidTokenResponse();
if ($tags && $pr) {
return response()->json(['message' => 'You can only use tag or pr, not both.'], 400);
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
return $this->by_uuids($uuids, $teamId, $force, $pr);
}
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
@ -216,7 +225,7 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false)
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force, $pr);
if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
@ -281,7 +290,7 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
return response()->json(['message' => 'No resources found with this tag.'], 404);
}
public function deploy_resource($resource, bool $force = false): array
public function deploy_resource($resource, bool $force = false, int $pr = 0): array
{
$message = null;
$deployment_uuid = null;
@ -289,16 +298,21 @@ public function deploy_resource($resource, bool $force = false): array
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
switch ($resource?->getMorphClass()) {
case \App\Models\Application::class:
case Application::class:
$deployment_uuid = new Cuid2;
queue_application_deployment(
$result = queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
pull_request_id: $pr,
);
$message = "Application {$resource->name} deployment queued.";
if ($result['status'] === 'skipped') {
$message = $result['message'];
} else {
$message = "Application {$resource->name} deployment queued.";
}
break;
case \App\Models\Service::class:
case Service::class:
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
break;
@ -314,4 +328,102 @@ public function deploy_resource($resource, bool $force = false): array
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
#[OA\Get(
summary: 'List application deployments',
description: 'List application deployments by using the app uuid',
path: '/deployments/applications/{uuid}',
operationId: 'list-deployments-by-app-uuid',
security: [
['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,
description: 'List application deployments by using the app uuid.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Application'),
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function get_application_deployments(Request $request)
{
$request->validate([
'skip' => ['nullable', 'integer', 'min:0'],
'take' => ['nullable', 'integer', 'min:1'],
]);
$app_uuid = $request->route('uuid', null);
$skip = $request->get('skip', 0);
$take = $request->get('take', 10);
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$servers = Server::whereTeamId($teamId)->get();
if (is_null($app_uuid)) {
return response()->json(['message' => 'Application uuid is required'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $app_uuid)->first();
if (is_null($application)) {
return response()->json(['message' => 'Application not found'], 404);
}
$deployments = $application->deployments($skip, $take);
return response()->json($deployments);
}
}

View file

@ -267,6 +267,18 @@ public function create_project(Request $request)
['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

@ -368,6 +368,20 @@ public function update_key(Request $request)
response: 404,
description: 'Private Key not found.',
),
new OA\Response(
response: 422,
description: 'Private Key is in use and cannot be deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Private Key is in use and cannot be deleted.'],
]
)
),
]),
]
)]
public function delete_key(Request $request)
@ -384,6 +398,14 @@ public function delete_key(Request $request)
if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404);
}
if ($key->isInUse()) {
return response()->json([
'message' => 'Private Key is in use and cannot be deleted.',
'details' => 'This private key is currently being used by servers, applications, or Git integrations.',
], 422);
}
$key->forceDelete();
return response()->json([

View file

@ -809,6 +809,6 @@ public function validate_server(Request $request)
}
ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.']);
return response()->json(['message' => 'Validation started.'], 201);
}
}

View file

@ -13,6 +13,7 @@
use App\Models\Service;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Symfony\Component\Yaml\Yaml;
class ServicesController extends Controller
{
@ -88,8 +89,8 @@ public function services(Request $request)
}
#[OA\Post(
summary: 'Create',
description: 'Create a one-click service',
summary: 'Create service',
description: 'Create a one-click / custom service',
path: '/services',
operationId: 'create-service',
security: [
@ -102,7 +103,7 @@ public function services(Request $request)
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'type'],
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'type' => [
'description' => 'The one-click service type',
@ -204,6 +205,7 @@ public function services(Request $request)
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
],
),
),
@ -211,7 +213,7 @@ public function services(Request $request)
responses: [
new OA\Response(
response: 201,
description: 'Create a service.',
description: 'Service created successfully.',
content: [
new OA\MediaType(
mediaType: 'application/json',
@ -237,7 +239,7 @@ public function services(Request $request)
)]
public function create_service(Request $request)
{
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy'];
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@ -249,12 +251,13 @@ public function create_service(Request $request)
return $return;
}
$validator = customApiValidator($request->all(), [
'type' => 'string|required',
'type' => 'string|required_without:docker_compose_raw',
'docker_compose_raw' => 'string|required_without:type',
'project_uuid' => 'string|required',
'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'destination_uuid' => 'string|nullable',
'name' => 'string|max:255',
'description' => 'string|nullable',
'instant_deploy' => 'boolean',
@ -372,12 +375,19 @@ public function create_service(Request $request)
]);
}
return response()->json(['message' => 'Service not found.'], 404);
} else {
return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
}
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
} elseif (filled($request->docker_compose_raw)) {
return response()->json(['message' => 'Invalid service type.'], 400);
$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 {
return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400);
}
}
#[OA\Get(
@ -511,6 +521,220 @@ public function delete_by_uuid(Request $request)
]);
}
#[OA\Patch(
summary: 'Update',
description: 'Update service by UUID.',
path: '/services/{uuid}',
operationId: 'update-service-by-uuid',
security: [
['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,
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
properties: [
'name' => ['type' => 'string', 'description' => 'The service name.'],
'description' => ['type' => 'string', 'description' => 'The service description.'],
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'],
'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
],
)
),
]
),
responses: [
new OA\Response(
response: 200,
description: 'Service updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function update_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$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);
}
private function upsert_service(Request $request, Service $service, string $teamId)
{
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network'];
$validator = customApiValidator($request->all(), [
'project_uuid' => 'string|required',
'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'name' => 'string|max:255',
'description' => 'string|nullable',
'instant_deploy' => 'boolean',
'connect_to_docker_network' => 'boolean',
'docker_compose_raw' => 'string|required',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) {
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$destinations = $server->destinations();
if ($destinations->count() == 0) {
return response()->json(['message' => 'Server has no destinations.'], 400);
}
if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
}
$destination = $destinations->first();
if (! isBase64Encoded($request->docker_compose_raw)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerCompose = base64_decode($request->docker_compose_raw);
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$connectToDockerNetwork = $request->connect_to_docker_network ?? false;
$service->name = $request->name ?? null;
$service->description = $request->description ?? null;
$service->docker_compose_raw = $dockerComposeRaw;
$service->environment_id = $environment->id;
$service->server_id = $server->id;
$service->destination_id = $destination->id;
$service->destination_type = $destination->getMorphClass();
$service->connect_to_docker_network = $connectToDockerNetwork;
$service->save();
$service->parse();
if ($instantDeploy) {
StartService::dispatch($service);
}
$domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) {
if (count(explode(':', $domain)) > 2) {
return str($domain)->beforeLast(':')->value();
}
return $domain;
})->values();
return [
'uuid' => $service->uuid,
'domains' => $domains,
];
}
#[OA\Get(
summary: 'List Envs',
description: 'List all envs by service UUID.',

View file

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

View file

@ -116,19 +116,27 @@ public function manual(Request $request)
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
queue_application_deployment(
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
$return_payloads->push([
'status' => 'success',
'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
if ($result['status'] === 'skipped') {
$return_payloads->push([
'application' => $application->name,
'status' => 'skipped',
'message' => $result['message'],
]);
} else {
$return_payloads->push([
'status' => 'success',
'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
}
} else {
$paths = str($application->watch_paths)->explode("\n");
$return_payloads->push([
@ -152,7 +160,7 @@ public function manual(Request $request)
}
}
if ($x_gitea_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
@ -175,7 +183,7 @@ public function manual(Request $request)
]);
}
}
queue_application_deployment(
$result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
@ -184,11 +192,19 @@ public function manual(Request $request)
is_webhook: true,
git_type: 'gitea'
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
if ($result['status'] === 'skipped') {
$return_payloads->push([
'application' => $application->name,
'status' => 'skipped',
'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
}
} else {
$return_payloads->push([
'application' => $application->name,

View file

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

View file

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

View file

@ -27,7 +27,6 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use RuntimeException;
@ -329,13 +328,8 @@ public function handle(): void
} else {
$this->write_deployment_configurations();
}
$this->execute_remote_command(
[
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
'hidden' => true,
'ignore_errors' => true,
]
);
$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'));
}
@ -904,100 +898,12 @@ private function save_environment_variables()
$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;
@ -1022,6 +928,32 @@ private function save_environment_variables()
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;
@ -1211,7 +1143,7 @@ private function health_check()
if ($this->container_name) {
$counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
if ($this->full_healthcheck_url) {
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
}
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
@ -1366,13 +1298,7 @@ private function prepare_builder_image()
}
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->execute_remote_command(
[
'command' => "docker rm -f {$this->deployment_uuid}",
'ignore_errors' => true,
'hidden' => true,
]
);
$this->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command(
[
$runCommand,
@ -1404,6 +1330,9 @@ private function deploy_to_additional_destinations()
}
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?!");
@ -1447,6 +1376,17 @@ private function set_coolify_variables()
private function check_git_if_build_needed()
{
if (is_object($this->source) && $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) {
@ -1636,20 +1576,128 @@ private function generate_nixpacks_env_variables()
$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);
}
}
}
}
}
}
@ -1674,25 +1722,6 @@ private function generate_compose_file()
$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 {
@ -1718,8 +1747,11 @@ private function generate_compose_file()
'save' => 'dockerfile_from_repo',
'ignore_errors' => true,
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
$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' => [
@ -1730,9 +1762,10 @@ private function generate_compose_file()
'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,
@ -2028,7 +2061,11 @@ private function build_image()
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
if ($this->application->settings->is_spa) {
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
}
}
} else {
if ($this->application->build_pack === 'nixpacks') {
@ -2095,7 +2132,11 @@ private function build_image()
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration);
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
if ($this->application->settings->is_spa) {
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
}
}
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
@ -2204,43 +2245,16 @@ private function build_image()
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
{
try {
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
break;
}
usleep(100000);
}
$isRunning = $this->execute_remote_command(
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
) === 'true';
if ($isRunning) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
} catch (\Exception $error) {
$this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
} catch (Exception $error) {
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}
$this->remove_container($containerName);
}
private function remove_container(string $containerName)
{
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
private function stop_running_container(bool $force = false)
@ -2412,20 +2426,23 @@ private function run_post_deployment_command()
private function next(string $status)
{
queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status
if (
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
) {
$this->application_deployment_queue->update([
'status' => $status,
]);
// Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value ||
$this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
return;
}
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
$this->application_deployment_queue->update([
'status' => $status,
]);
if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();

View file

@ -20,7 +20,7 @@ public function __construct(public Server $server) {}
public function handle(): void
{
try {
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) {

View file

@ -17,6 +17,8 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 60;
public function __construct() {}
public function middleware(): array

View file

@ -390,7 +390,7 @@ private function backup_standalone_postgresql(string $database): void
$commands[] = 'mkdir -p '.$this->backup_dir;
$backupCommand = 'docker exec';
if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
$backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\"";
}
if ($this->backup->dump_all) {
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
@ -484,6 +484,11 @@ private function upload_to_s3(): void
$fullImageName = $this->getFullImageName();
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup->uuid}"], $this->server, false);
if (filled($containerExists)) {
instant_remote_process(["docker rm -f backup-of-{$this->backup->uuid}"], $this->server, false);
}
if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;

View file

@ -42,10 +42,8 @@ public function __construct(
public function handle()
{
try {
$persistentStorages = collect();
switch ($this->resource->type()) {
case 'application':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopApplication::run($this->resource, previewDeployments: true);
break;
case 'standalone-postgresql':
@ -56,44 +54,52 @@ public function handle()
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource, true);
break;
case 'service':
StopService::run($this->resource, true);
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
return;
}
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
$this->resource?->delete_volumes($persistentStorages);
}
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
$this->resource->deleteConfigurations();
}
if ($this->deleteVolumes) {
$this->resource->deleteVolumes();
$this->resource->persistentStorages()->delete();
}
$this->resource->fileStorages()->delete();
$isDatabase = $this->resource instanceof StandalonePostgresql
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
|| $this->resource instanceof StandaloneMysql
|| $this->resource instanceof StandaloneMariadb
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::dispatch($server, true);
}
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
|| $this->resource instanceof StandaloneMysql
|| $this->resource instanceof StandaloneMariadb
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
if ($this->deleteConnectedNetworks && ! $isDatabase) {
$this->resource?->delete_connected_networks($this->resource->uuid);
if ($isDatabase) {
$this->resource->sslCertificates()->delete();
$this->resource->scheduledBackups()->delete();
$this->resource->tags()->detach();
}
$this->resource->environment_variables()->delete();
if ($this->deleteConnectedNetworks && $this->resource->type() === 'application') {
$this->resource->deleteConnectedNetworks();
}
} catch (\Throwable $e) {
throw $e;
} finally {
$this->resource->forceDelete();
if ($this->dockerCleanup) {
CleanupDocker::dispatch($server, true);
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if ($server) {
CleanupDocker::dispatch($server, true);
}
}
Artisan::queue('cleanup:stucked-resources');
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Jobs;
use App\Helpers\SSLHelper;
use App\Models\SslCertificate;
use App\Models\Team;
use App\Notifications\SslExpirationNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class RegenerateSslCertJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = 60;
public function __construct(
protected ?Team $team = null,
protected ?int $server_id = null,
protected bool $force_regeneration = false,
) {}
public function handle()
{
$query = SslCertificate::query();
if ($this->server_id) {
$query->where('server_id', $this->server_id);
}
if (! $this->force_regeneration) {
$query->where('valid_until', '<=', now()->addDays(14));
}
$query->where('is_ca_certificate', false);
$regenerated = collect();
$query->cursor()->each(function ($certificate) use ($regenerated) {
try {
$caCert = SslCertificate::where('server_id', $certificate->server_id)
->where('is_ca_certificate', true)
->first();
if (! $caCert) {
Log::error("No CA certificate found for server_id: {$certificate->server_id}");
return;
}
SSLHelper::generateSslCertificate(
commonName: $certificate->common_name,
subjectAlternativeNames: $certificate->subject_alternative_names,
resourceType: $certificate->resource_type,
resourceId: $certificate->resource_id,
serverId: $certificate->server_id,
configurationDir: $certificate->configuration_dir,
mountPath: $certificate->mount_path,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
);
$regenerated->push($certificate);
} catch (\Exception $e) {
Log::error('Failed to regenerate SSL certificate: '.$e->getMessage());
}
});
if ($regenerated->isNotEmpty()) {
$this->team?->notify(new SslExpirationNotification($regenerated));
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Jobs;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct(public Server $server) {}
public function handle()
{
try {
StopProxy::run($this->server);
$this->server->proxy->force_stop = false;
$this->server->save();
StartProxy::run($this->server, force: true);
CheckProxy::run($this->server, true);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View file

@ -7,6 +7,7 @@
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use App\Services\ConfigurationRepository;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@ -266,7 +267,7 @@ public function installServer()
public function validateServer()
{
try {
config()->set('constants.ssh.mux_enabled', false);
$this->disableSshMux();
// EC2 does not have `uptime` command, lol
instant_remote_process(['ls /'], $this->createdServer, true);
@ -376,6 +377,12 @@ private function createNewPrivateKey()
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
}
private function disableSshMux(): void
{
$configRepository = app(ConfigurationRepository::class);
$configRepository->disableSshMux();
}
public function render()
{
return view('livewire.boarding.index')->layout('layouts.boarding');

View file

@ -51,7 +51,7 @@ public function loadDeployments()
public function navigateToProject($projectUuid)
{
return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), true);
return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), navigate: false);
}
public function render()

View file

@ -56,6 +56,9 @@ class Discord extends Component
#[Validate(['boolean'])]
public bool $serverUnreachableDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $discordPingEnabled = true;
public function mount()
{
try {
@ -87,6 +90,8 @@ public function syncData(bool $toModel = false)
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
$this->settings->discord_ping_enabled = $this->discordPingEnabled;
$this->settings->save();
refreshSession();
} else {
@ -105,12 +110,30 @@ public function syncData(bool $toModel = false)
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
$this->discordPingEnabled = $this->settings->discord_ping_enabled;
}
}
public function instantSaveDiscordPingEnabled()
{
try {
$original = $this->discordPingEnabled;
$this->validate([
'discordPingEnabled' => 'required',
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->discordPingEnabled = $original;
return handleError($e, $this);
}
}
public function instantSaveDiscordEnabled()
{
try {
$original = $this->discordEnabled;
$this->validate([
'discordWebhookUrl' => 'required',
], [
@ -118,7 +141,7 @@ public function instantSaveDiscordEnabled()
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->discordEnabled = false;
$this->discordEnabled = $original;
return handleError($e, $this);
}

View file

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

View file

@ -70,6 +70,7 @@ public function resetPassword()
$this->current_password = '';
$this->new_password = '';
$this->new_password_confirmation = '';
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View file

@ -22,6 +22,7 @@ class Configuration extends Component
public function mount()
{
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
@ -39,6 +40,9 @@ public function mount()
$this->project = $project;
$this->environment = $environment;
$this->application = $application;
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
}
}
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',
@ -86,10 +87,14 @@ class General extends Component
'application.post_deployment_command_container' => 'nullable',
'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_spa' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
'application.settings.is_preserve_repository_enabled' => 'boolean|required',
'application.is_http_basic_auth_enabled' => 'boolean|required',
'application.http_basic_auth_username' => 'string|nullable',
'application.http_basic_auth_password' => 'string|nullable',
'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required',
];
@ -120,10 +125,12 @@ 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',
'application.settings.is_static' => 'Is static',
'application.settings.is_spa' => 'Is SPA',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
@ -171,6 +178,12 @@ public function mount()
public function instantSave()
{
if ($this->application->settings->isDirty('is_spa')) {
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
}
if ($this->application->isDirty('is_http_basic_auth_enabled')) {
$this->application->save();
}
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
@ -190,6 +203,7 @@ public function instantSave()
if ($this->application->settings->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false);
}
}
public function loadComposeFile($isInit = false)
@ -287,9 +301,9 @@ public function getWildcardDomain()
}
}
public function generateNginxConfiguration()
public function generateNginxConfiguration($type = 'static')
{
$this->application->custom_nginx_configuration = defaultNginxConfiguration();
$this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
$this->application->save();
$this->dispatch('success', 'Nginx configuration generated.');
}
@ -369,6 +383,9 @@ public function submit($showToaster = true)
if ($this->application->isDirty('redirect')) {
$this->setRedirect();
}
if ($this->application->isDirty('dockerfile')) {
$this->application->parseHealthcheckFromDockerfile($this->application->dockerfile);
}
$this->checkFqdns();
@ -446,7 +463,6 @@ public function downloadConfig()
{
$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,18 +84,23 @@ public function deploy(bool $force_rebuild = false)
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'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: true);
], navigate: false);
}
protected function setDeploymentUuid()
@ -126,18 +131,23 @@ public function restart()
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'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: true);
], navigate: false);
}
public function render()

View file

@ -5,10 +5,7 @@
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@ -159,13 +156,18 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu
'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'],
@ -188,13 +190,12 @@ public function stop(int $pull_request_id)
{
try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
$this->stopContainers($containers, $server);
}
GetContainersStatus::run($server);
@ -210,13 +211,12 @@ public function delete(int $pull_request_id)
{
try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
$this->stopContainers($containers, $server);
}
ApplicationPreview::where('application_id', $this->application->id)
@ -232,48 +232,14 @@ public function delete(int $pull_request_id)
}
}
private function stopContainers(array $containers, $server, int $timeout)
private function stopContainers(array $containers, $server, int $timeout = 30)
{
$processes = [];
foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = Carbon::now()->getTimestamp();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function removeContainer(string $containerName, $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
private function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
$this->removeContainer($containerName, $server);
instant_remote_process(command: [
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
}
}
}

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 @@ private function getPrivateKeys()
});
}
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 @@ public function submit()
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

@ -4,8 +4,11 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
@ -50,12 +53,19 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
#[Validate(['nullable', 'boolean'])]
public bool $enable_ssl = false;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
];
}
@ -64,6 +74,12 @@ public function mount()
try {
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@ -82,6 +98,7 @@ public function syncData(bool $toModel = false)
$this->database->public_port = $this->publicPort;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
@ -96,6 +113,7 @@ public function syncData(bool $toModel = false)
$this->publicPort = $this->database->public_port;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
@ -174,4 +192,61 @@ public function submit()
}
}
}
public function instantSaveSSL()
{
try {
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)
->where('is_ca_certificate', true)
->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
SslHelper::generateSslCertificate(
commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
}

View file

@ -31,8 +31,8 @@ public function activityFinished()
$this->database->update([
'started_at' => now(),
]);
$this->dispatch('refresh');
$this->check_status();
if (is_null($this->database->config_hash) || $this->database->isConfigurationChanged()) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
@ -44,7 +44,7 @@ public function activityFinished()
public function check_status($showNotification = false)
{
if ($this->database->destination->server->isFunctional()) {
GetContainersStatus::dispatch($this->database->destination->server);
GetContainersStatus::run($this->database->destination->server);
}
if ($showNotification) {
@ -63,6 +63,7 @@ public function stop()
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
$this->dispatch('refresh');
}
public function restart()

View file

@ -4,8 +4,11 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
@ -53,12 +56,20 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
#[Validate(['boolean'])]
public bool $enable_ssl = false;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
@ -67,6 +78,12 @@ public function mount()
try {
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@ -86,6 +103,7 @@ public function syncData(bool $toModel = false)
$this->database->public_port = $this->publicPort;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
@ -101,6 +119,7 @@ public function syncData(bool $toModel = false)
$this->publicPort = $this->database->public_port;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
@ -179,4 +198,48 @@ public function submit()
}
}
}
public function instantSaveSSL()
{
try {
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
->where('is_ca_certificate', true)
->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
}

View file

@ -4,9 +4,13 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMariadb;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@ -21,6 +25,18 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@ -35,6 +51,7 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
];
protected $validationAttributes = [
@ -50,6 +67,7 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Options',
'database.enable_ssl' => 'Enable SSL',
];
public function mount()
@ -57,6 +75,12 @@ public function mount()
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@ -127,6 +151,48 @@ public function instantSave()
}
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View file

@ -4,9 +4,13 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@ -21,6 +25,18 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@ -34,6 +50,8 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
];
protected $validationAttributes = [
@ -48,6 +66,8 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
'database.enable_ssl' => 'Enable SSL',
'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@ -55,6 +75,12 @@ public function mount()
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@ -128,6 +154,53 @@ public function instantSave()
}
}
public function updatedDatabaseSslMode()
{
$this->instantSaveSSL();
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View file

@ -4,9 +4,13 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMysql;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@ -21,6 +25,18 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@ -35,6 +51,8 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
];
protected $validationAttributes = [
@ -50,6 +68,8 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
'database.enable_ssl' => 'Enable SSL',
'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@ -57,6 +77,12 @@ public function mount()
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@ -127,6 +153,53 @@ public function instantSave()
}
}
public function updatedDatabaseSslMode()
{
$this->instantSaveSSL();
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View file

@ -4,9 +4,13 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandalonePostgresql;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@ -23,10 +27,15 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
'refresh',
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
'save_init_script',
'delete_init_script',
];
@ -48,6 +57,8 @@ public function getListeners()
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
];
protected $validationAttributes = [
@ -65,6 +76,8 @@ public function getListeners()
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
'database.enable_ssl' => 'Enable SSL',
'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@ -72,6 +85,12 @@ public function mount()
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@ -91,6 +110,55 @@ public function instantSaveAdvanced()
}
}
public function updatedDatabaseSslMode()
{
$this->instantSaveSSL();
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
@ -143,7 +211,7 @@ public function save_init_script($script)
$delete_command = "rm -f $old_file_path";
try {
instant_remote_process([$delete_command], $this->server);
} catch (\Exception $e) {
} catch (Exception $e) {
$this->dispatch('error', 'Failed to remove old init script from server: '.$e->getMessage());
return;
@ -184,7 +252,7 @@ public function delete_init_script($script)
$command = "rm -f $file_path";
try {
instant_remote_process([$command], $this->server);
} catch (\Exception $e) {
} catch (Exception $e) {
$this->dispatch('error', 'Failed to remove init script from server: '.$e->getMessage());
return;
@ -201,16 +269,11 @@ public function delete_init_script($script)
$this->database->init_scripts = $updatedScripts;
$this->database->save();
$this->refresh();
$this->dispatch('refresh')->self();
$this->dispatch('success', 'Init script deleted from the database and the server.');
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function save_new_init_script()
{
$this->validate([

View file

@ -4,25 +4,24 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
protected $listeners = [
'envsUpdated' => 'refresh',
'refresh',
];
public Server $server;
public StandaloneRedis $database;
public string $redis_username;
public string $redis_password;
public ?string $redis_password;
public string $redis_version;
@ -30,6 +29,19 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'envsUpdated' => 'refresh',
'refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@ -42,6 +54,7 @@ class General extends Component
'database.custom_docker_run_options' => 'nullable',
'redis_username' => 'required',
'redis_password' => 'required',
'database.enable_ssl' => 'boolean',
];
protected $validationAttributes = [
@ -55,12 +68,18 @@ class General extends Component
'database.custom_docker_run_options' => 'Custom Docker Options',
'redis_username' => 'Redis Username',
'redis_password' => 'Redis Password',
'database.enable_ssl' => 'Enable SSL',
];
public function mount()
{
$this->server = data_get($this->database, 'destination.server');
$this->refreshView();
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@ -136,6 +155,48 @@ public function instantSave()
}
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View file

@ -19,6 +19,8 @@ class ScheduledBackups extends Component
public $s3s;
public string $custom_type = 'mysql';
protected $listeners = ['refreshScheduledBackups'];
protected $queryString = ['selectedBackupId'];
@ -49,6 +51,14 @@ public function setSelectedBackup($backupId, $force = false)
}
}
public function setCustomType()
{
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups();
}
public function delete($scheduled_backup_id): void
{
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
@ -62,5 +72,6 @@ public function refreshScheduledBackups(?int $id = null): void
if ($id) {
$this->setSelectedBackup($id);
}
$this->dispatch('refreshScheduledBackups');
}
}

View file

@ -35,6 +35,6 @@ public function navigateToProject($projectUuid)
{
$project = collect($this->projects)->firstWhere('uuid', $projectUuid);
return $this->redirect($project->navigateTo(), true);
return $this->redirect($project->navigateTo(), navigate: false);
}
}

View file

@ -7,7 +7,6 @@
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Support\Str;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@ -66,7 +65,6 @@ public function submit()
$destination_class = $destination->getMorphClass();
$service = Service::create([
'name' => 'service'.Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
@ -85,8 +83,6 @@ public function submit()
'resourceable_type' => $service->getMorphClass(),
]);
}
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
return redirect()->route('project.service.configuration', [

View file

@ -106,11 +106,15 @@ public function loadRepositories($github_app_id)
$this->selected_github_app_id = $github_app_id;
$this->github_app = GithubApp::where('id', $github_app_id)->first();
$this->token = generateGithubInstallationToken($this->github_app);
$this->loadRepositoryByPage();
$repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
$this->total_repositories_count = $repositories['total_count'];
$this->repositories = $this->repositories->concat(collect($repositories['repositories']));
if ($this->repositories->count() < $this->total_repositories_count) {
while ($this->repositories->count() < $this->total_repositories_count) {
$this->page++;
$this->loadRepositoryByPage();
$repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
$this->total_repositories_count = $repositories['total_count'];
$this->repositories = $this->repositories->concat(collect($repositories['repositories']));
}
}
$this->repositories = $this->repositories->sortBy('name');
@ -120,21 +124,6 @@ public function loadRepositories($github_app_id)
$this->current_step = 'repository';
}
protected function loadRepositoryByPage()
{
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
return $this->dispatch('error', $json['message']);
}
if ($json['total_count'] === 0) {
return;
}
$this->total_repositories_count = $json['total_count'];
$this->repositories = $this->repositories->concat(collect($json['repositories']));
}
public function loadBranches()
{
$this->selected_repository_owner = $this->repositories->where('id', $this->selected_repository_id)->first()['owner']['login'];

View file

@ -74,7 +74,7 @@ public function submit()
'fqdn' => $fqdn,
]);
$application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true);
$application->parseHealthcheckFromDockerfile(dockerfile: $this->dockerfile, isInit: true);
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,

View file

@ -73,7 +73,6 @@ public function mount()
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service_payload = [
'name' => "$oneClickServiceName-".str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,

View file

@ -31,8 +31,9 @@ public function getListeners()
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
'check_status',
'refreshStatus' => '$refresh',
'check_status',
'refreshServices',
];
}
@ -63,6 +64,13 @@ public function mount()
$this->databases = $this->service->databases->sort();
}
public function refreshServices()
{
$this->service->refresh();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function restartApplication($id)
{
try {

View file

@ -4,7 +4,11 @@
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\InstanceSettings;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Database extends Component
@ -15,6 +19,8 @@ class Database extends Component
public $fileStorages;
public $parameters;
protected $listeners = ['refreshFileStorages'];
protected $rules = [
@ -34,12 +40,33 @@ public function render()
public function mount()
{
$this->parameters = get_route_parameters();
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages();
}
public function delete($password)
{
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
}
try {
$this->database->delete();
$this->dispatch('success', 'Database deleted.');
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSaveExclude()
{
$this->submit();
@ -57,6 +84,42 @@ public function instantSaveLogDrain()
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
public function convertToApplication()
{
try {
$service = $this->database->service;
$serviceDatabase = $this->database;
// Check if application with same name already exists
if ($service->applications()->where('name', $serviceDatabase->name)->exists()) {
throw new \Exception('An application with this name already exists.');
}
// Create new parameters removing database_uuid
$redirectParams = collect($this->parameters)
->except('database_uuid')
->all();
DB::transaction(function () use ($service, $serviceDatabase) {
$service->applications()->create([
'name' => $serviceDatabase->name,
'human_name' => $serviceDatabase->human_name,
'description' => $serviceDatabase->description,
'exclude_from_status' => $serviceDatabase->exclude_from_status,
'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled,
'image' => $serviceDatabase->image,
'service_id' => $service->id,
'is_migrated' => true,
]);
$serviceDatabase->delete();
});
return redirect()->route('project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
if ($this->database->is_public && ! $this->database->public_port) {

View file

@ -43,12 +43,11 @@ public function submit()
updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
! $warning && $this->dispatch('success', 'Service saved.');
}
$this->application->service->parse();
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
$this->dispatch('refreshStatus');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {

View file

@ -49,7 +49,6 @@ public function mount()
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
$this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@ -68,6 +67,18 @@ public function convertToDirectory()
}
}
public function loadStorageOnServer()
{
try {
$this->fileStorage->loadStorageOnServer();
$this->dispatch('success', 'File storage loaded from server.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refreshStorages');
}
}
public function convertToFile()
{
try {

View file

@ -24,7 +24,7 @@ class Index extends Component
public $s3s;
protected $listeners = ['generateDockerCompose'];
protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh'];
public function mount()
{

View file

@ -5,6 +5,7 @@
use App\Models\InstanceSettings;
use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
@ -23,7 +24,7 @@ class ServiceApplicationView extends Component
'application.human_name' => 'nullable',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.image' => 'required',
'application.image' => 'string|nullable',
'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean',
@ -73,6 +74,40 @@ public function mount()
$this->parameters = get_route_parameters();
}
public function convertToDatabase()
{
try {
$service = $this->application->service;
$serviceApplication = $this->application;
// Check if database with same name already exists
if ($service->databases()->where('name', $serviceApplication->name)->exists()) {
throw new \Exception('A database with this name already exists.');
}
$redirectParams = collect($this->parameters)
->except('database_uuid')
->all();
DB::transaction(function () use ($service, $serviceApplication) {
$service->databases()->create([
'name' => $serviceApplication->name,
'human_name' => $serviceApplication->human_name,
'description' => $serviceApplication->description,
'exclude_from_status' => $serviceApplication->exclude_from_status,
'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled,
'image' => $serviceApplication->image,
'service_id' => $service->id,
'is_migrated' => true,
]);
$serviceApplication->delete();
});
return redirect()->route('project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View file

@ -82,6 +82,7 @@ public function submit($notify = true)
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->dispatch('refreshEnvs');
$this->dispatch('refreshServices');
$notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);

View file

@ -79,7 +79,7 @@ public function redeploy(int $network_id, int $server_id)
$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 @@ public function redeploy(int $network_id, int $server_id)
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'),

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