diff --git a/.env.production b/.env.production
index 96833c253..fe3c8370e 100644
--- a/.env.production
+++ b/.env.production
@@ -14,3 +14,5 @@ PUSHER_APP_SECRET=
ROOT_USERNAME=
ROOT_USER_EMAIL=
ROOT_USER_PASSWORD=
+
+REGISTRY_URL=ghcr.io
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa6a28264..a2d370ce6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,337 @@ All notable changes to this project will be documented in this file.
### 🚀 Features
+- *(api)* Update OpenAPI spec for services (#5448)
+
+### 🐛 Bug Fixes
+
+- *(api)* Used ssh keys can be deleted
+- *(email)* Transactional emails not sending
+
+### 📚 Documentation
+
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Bump version to 406
+
+## [4.0.0-beta.404] - 2025-04-03
+
+### 🚀 Features
+
+- *(lang)* Added Azerbaijani language updated turkish language. (#5497)
+- *(lang)* Added Portuguese from Brazil language (#5500)
+- *(lang)* Add Indonesian language translations (#5513)
+
+### 🐛 Bug Fixes
+
+- *(docs)* Comment out execute for now
+- *(installation)* Mount the docker config
+- *(installation)* Path to config file for docker login
+- *(service)* Add health check to Bugsink service (#5512)
+- *(email)* Emails are not sent in multiple cases
+- *(deployments)* Use graceful shutdown instead of `rm`
+- *(docs)* Contribute service url (#5517)
+- *(proxy)* Proxy restart does not work on domain
+- *(ui)* Only show copy button on https
+- *(database)* Custom config for MongoDB (#5471)
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Remove unused code in Bugsink service
+- *(versions)* Update version to 404
+- *(versions)* Bump version to 403 (#5520)
+- *(versions)* Bump version to 404
+
+## [4.0.0-beta.402] - 2025-04-01
+
+### 🚀 Features
+
+- *(deployments)* Add list application deployments api route
+- *(deploy)* Add pull request ID parameter to deploy endpoint
+- *(api)* Add pull request ID parameter to applications endpoint
+- *(api)* Add endpoints for retrieving application logs and deployments
+- *(lang)* Added Norwegian language (#5280)
+- *(dep)* Bump all dependencies
+
+### 🐛 Bug Fixes
+
+- Only get apps for the current team
+- *(DeployController)* Cast 'pr' query parameter to integer
+- *(deploy)* Validate team ID before deployment
+- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424)
+- *(ui)* Instance Backup settings
+
+### 🚜 Refactor
+
+- *(dev)* Remove OpenAPI generation functionality
+- *(migration)* Enhance local file volumes migration with logging
+
+### ⚙️ Miscellaneous Tasks
+
+- *(service)* Update minecraft service ENVs
+- *(service)* Add more vars to infisical.yaml (#5418)
+- *(service)* Add google variables to plausible.yaml (#5429)
+- *(service)* Update authentik.yaml versions (#5373)
+- *(core)* Remove redocs
+- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
+
+## [4.0.0-beta.401] - 2025-03-28
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+## [4.0.0-beta.400] - 2025-03-27
+
+### 🚀 Features
+
+- *(database)* Disable MongoDB SSL by default in migration
+- *(database)* Add CA certificate generation for database servers
+- *(application)* Add SPA configuration and update Nginx generation logic
+
+### 🐛 Bug Fixes
+
+- *(file-storage)* Double save on compose volumes
+- *(parser)* Add logging support for applications in services
+
+### 🚜 Refactor
+
+- *(proxy)* Improve port availability checks with multiple methods
+- *(database)* Update MongoDB SSL configuration for improved security
+- *(database)* Enhance SSL configuration handling for various databases
+- *(notifications)* Update Telegram button URL for staging environment
+- *(models)* Remove unnecessary cloud check in isEnabled method
+- *(database)* Streamline event listeners in Redis General component
+- *(database)* Remove redundant database status display in MongoDB view
+- *(database)* Update import statements for Auth in database components
+- *(database)* Require PEM key file for SSL certificate regeneration
+- *(database)* Change MySQL daemon command to MariaDB daemon
+- *(nightly)* Update version numbers and enhance upgrade script
+- *(versions)* Update version numbers for coolify and nightly
+- *(email)* Validate team membership for email recipients
+- *(shared)* Simplify deployment status check logic
+- *(shared)* Add logging for running deployment jobs
+- *(shared)* Enhance job status check to include 'reserved'
+- *(email)* Improve error handling by passing context to handleError
+- *(email)* Streamline email sending logic and improve configuration handling
+- *(email)* Remove unnecessary whitespace in email sending logic
+- *(email)* Allow custom email recipients in email sending logic
+- *(email)* Enhance sender information formatting in email logic
+- *(proxy)* Remove redundant stop call in restart method
+- *(file-storage)* Add loadStorageOnServer method for improved error handling
+- *(docker)* Parse and sanitize YAML compose file before encoding
+- *(file-storage)* Improve layout and structure of input fields
+- *(email)* Update label for test email recipient input
+- *(database-backup)* Remove existing Docker container before backup upload
+- *(database)* Improve decryption and deduplication of local file volumes
+- *(database)* Remove debug output from volume update process
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Update version numbers for coolify and nightly
+
+### ◀️ Revert
+
+- Encrypting mount and fs_path
+
+## [4.0.0-beta.399] - 2025-03-25
+
+### 🚀 Features
+
+- *(service)* Neon
+- *(migration)* Add `ssl_certificates` table and model
+- *(migration)* Add ssl setting to `standalone_postgresqls` table
+- *(ui)* Add ssl settings to Postgres ui
+- *(db)* Add ssl mode to Postgres URLs
+- *(db)* Setup ssl during Postgres start
+- *(migration)* Encrypt local file volumes content and paths
+- *(ssl)* Ssl generation helper
+- *(ssl)* Migrate to `ECC`certificates using `secp521r1`
+- *(ssl)* Improve SSL helper
+- *(ssl)* Add a Coolify CA Certificate to all servers
+- *(seeder)* Call CA SSL seeder in prod and dev
+- *(ssl)* Add Coolify CA Certificate when adding a new server
+- *(installer)* Create CA folder during installation
+- *(ssl)* Improve SSL helper
+- *(ssl)* Use new improved helper for SSL generation
+- *(ui)* Add CA cert UI
+- *(ui)* New copy button component
+- *(ui)* Use new copy button component everywhere
+- *(ui)* Improve server advanced view
+- *(migration)* Add CN and alternative names to DB
+- *(databases)* Add CA SSL crt location to Postgres URLs
+- *(ssl)* Improve ssl generation
+- *(ssl)* Regenerate SSL certs job
+- *(ssl)* Regenerate certificate and valid until UI
+- *(ssl)* Regenerate CA cert and all other certs logic
+- *(ssl)* Add full MySQL SSL Support
+- *(ssl)* Add full MariaDB SSL support
+- *(ssl)* Add `openssl.conf` to configure SSL extension properly
+- *(ssl)* Improve SSL generation and security a lot
+- *(ssl)* Check for SSL renewal twice daily
+- *(ssl)* Add SSL relationships to all DBs
+- Add full SSL support to MongoDB
+- *(ssl)* Fix some issues and improve ssl generation helper
+- *(ssl)* Ability to create `.pem` certs and add `clientAuth` to `extendedKeyUsage`
+- *(ssl)* New modes for MongoDB and get `caCert` and `mountPath` correctly
+- *(ssl)* Full SSL support for Redis
+- New mode implementation for MongoDB
+- *(ssl)* Improve Redis and remove modes
+- Full SSL support for DrangonflyDB
+- SSL notification
+- *(github-source)* Enhance GitHub App configuration with manual and private key support
+- *(ui)* Improve GitHub repository selection and styling
+- *(database)* Implement two-step confirmation for database deletion
+- *(assets)* Add new SVG logo for Coolify
+- *(install)* Enhance Docker address pool configuration and validation
+- *(install)* Improve Docker address pool management and service restart logic
+- *(install)* Add missing env variable to install script
+- *(LocalFileVolume)* Add binary file detection and update UI logic
+- *(templates)* Change glance for v0.7
+- *(templates)* Add Freescout service template
+- *(service)* Add Evolution API template
+- *(service)* Add evolution-api and neon-ws-proxy templates
+- *(svg)* Add coolify and evolution-api SVG logos
+- *(api)* Add api to create custom services
+- *(api)* Separate create and one-click routes
+- *(api)* Update Services api routes and handlers
+- *(api)* Unify service creation endpoint and enhance validation
+- *(notifications)* Add discord ping functionality and settings
+- *(user)* Implement session deletion on password reset
+- *(github)* Enhance repository loading and validation in applications
+
+### 🐛 Bug Fixes
+
+- *(api)* Docker compose based apps creationg through api
+- *(database)* Improve database type detection for Supabase Postgres images
+- *(ssl)* Permission of ssl crt and key inside the container
+- *(ui)* Make sure file mounts do not showing the encrypted values
+- *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert
+- *(ui)* Select component should not always uses title case
+- *(db)* SSL certificates table and model
+- *(migration)* Ssl certificates table
+- *(databases)* Fix database name users new `uuid` instead of DB one
+- *(database)* Fix volume and file mounts and naming
+- *(migration)* Store subjectAlternativeNames as a json array in the db
+- *(ssl)* Make sure the subjectAlternativeNames are unique and stored correctly
+- *(ui)* Certificate expiration data is null before starting the DB
+- *(deletion)* Fix DB deletion
+- *(ssl)* Improve SSL cert file mounts
+- *(ssl)* Always create ca crt on disk even if it is already there
+- *(ssl)* Use mountPath parameter not a hardcoded path
+- *(ssl)* Use 1 instead of on for mysql
+- *(ssl)* Do not remove SSL directory
+- *(ssl)* Wrong ssl cert is loaded to the server and UI error when regenerating SSL
+- *(ssl)* Make sure when regenerating the CA cert it is not overwritten with a server cert
+- *(ssl)* Regenerating certs for a specific DB
+- *(ssl)* Fix MariaDB and MySQL need CA cert
+- *(ssl)* Add mount path to DB to fix regeneration of certs
+- *(ssl)* Fix SSL regeneration to sign with CA cert and use mount path
+- *(ssl)* Get caCert correctly
+- *(ssl)* Remove caCert even if it is a folder by accident
+- *(ssl)* Ger caCert and `mountPath` correctly
+- *(ui)* Only show Regenerate SSL Certificates button when there is a cert
+- *(ssl)* Server id
+- *(ssl)* When regenerating SSL certs the cert is not singed with the new CN
+- *(ssl)* Adjust ca paths for MySQL
+- *(ssl)* Remove mode selection for MariaDB as it is not supported
+- *(ssl)* Permission issue with MariDB cert and key and paths
+- *(ssl)* Rename Redis mode to verify-ca as it is not verify-full
+- *(ui)* Remove unused mode for MongoDB
+- *(ssl)* KeyDB port and caCert args are missing
+- *(ui)* Enable SSL is not working correctly for KeyDB
+- *(ssl)* Add `--tls` arg to DrangflyDB
+- *(notification)* Always send SSL notifications
+- *(database)* Change default value of enable_ssl to false for multiple tables
+- *(ui)* Correct grammatical error in 404 page
+- *(seeder)* Update GitHub app name in GithubAppSeeder
+- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration
+- *(domain)* Dispatch refreshStatus event after successful domain update
+- *(database)* Correct container name generation for service databases
+- *(database)* Limit container name length for database proxy
+- *(database)* Handle unsupported database types in StartDatabaseProxy
+- *(database)* Simplify container name generation in StartDatabaseProxy
+- *(install)* Handle potential errors in Docker address pool configuration
+- *(backups)* Retention settings
+- *(redis)* Set default redis_username for new instances
+- *(core)* Improve instantSave logic and error handling
+- *(general)* Correct link to framework specific documentation
+- *(core)* Redirect healthcheck route for dockercompose applications
+- *(api)* Use name from request payload
+- *(issue#4746)* Do not use setGitImportSettings inside of generateGitLsRemoteCommands
+- Correct some spellings
+- *(service)* Replace deprecated credentials env variables on keycloak service
+- *(keycloak)* Update keycloak image version to 26.1
+- *(console)* Handle missing root user in password reset command
+- *(ssl)* Handle missing CA certificate in SSL regeneration job
+- *(copy-button)* Ensure text is safely passed to clipboard
+
+### 💼 Other
+
+- Bump Coolify to 4.0.0-beta.400
+- *(migration)* Add SSL fields to database tables
+- SSL Support for KeyDB
+
+### 🚜 Refactor
+
+- *(ui)* Unhide log toggle in application settings
+- *(nginx)* Streamline default Nginx configuration and improve error handling
+- *(install)* Clean up install script and enhance Docker installation logic
+- *(ScheduledTask)* Clean up code formatting and remove unused import
+- *(app)* Remove unused MagicBar component and related code
+- *(database)* Streamline SSL configuration handling across database types
+- *(application)* Streamline healthcheck parsing from Dockerfile
+- *(notifications)* Standardize getRecipients method signatures
+- *(configuration)* Centralize configuration management in ConfigurationRepository
+- *(docker)* Update image references to use centralized registry URL
+- *(env)* Add centralized registry URL to environment configuration
+- *(storage)* Simplify file storage iteration in Blade template
+- *(models)* Add is_directory attribute to LocalFileVolume model
+- *(modal)* Add ignoreWire attribute to modal-confirmation component
+- *(invite-link)* Adjust layout for better responsiveness in form
+- *(invite-link)* Enhance form layout for improved responsiveness
+- *(network)* Enhance docker network creation with ipv6 fallback
+- *(network)* Check for existing coolify network before creation
+- *(database)* Enhance encryption process for local file volumes
+
+### 📚 Documentation
+
+- Update changelog
+- Update changelog
+- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility
+- Update changelog
+
+### ⚙️ Miscellaneous Tasks
+
+- *(migration)* Remove unused columns
+- *(ssl)* Improve code in ssl helper
+- *(migration)* Ssl cert and key should not be nullable
+- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts
+- Rename ca crt folder to ssl
+- *(ui)* Improve valid until handling
+- Improve code quality suggested by code rabbit
+- *(supabase)* Update Supabase service template and Postgres image version
+- *(versions)* Update version numbers for coolify and nightly
+
+## [4.0.0-beta.398] - 2025-03-01
+
+### 🚀 Features
+
- *(billing)* Add Stripe past due subscription status tracking
- *(ui)* Add past due subscription warning banner
@@ -6223,6 +6554,40 @@ All notable changes to this project will be documented in this file.
### 🚀 Features
+- Use tags in update
+- New update process (#115)
+- VaultWarden service
+- Www <-> non-www redirection for apps
+- Www <-> non-www redirection
+- Follow logs
+- Generate www & non-www SSL certs
+- Basic password reset form
+- Scan for lock files and set right commands
+- Public port range (WIP)
+- Ports range
+- Random subdomain for demo
+- Random domain for services
+- Astro buildpack
+- 11ty buildpack
+- Registration page
+- Languagetool service
+- Send version with update request
+- Service secrets
+- Webhooks inititate all applications with the correct branch
+- Check ssl for new apps/services first
+- Autodeploy pause
+- Install pnpm into docker image if pnpm lock file is used
+- Add PHP modules
+- Use compose instead of normal docker cmd
+- Be able to redeploy PRs
+- Add n8n.io service
+- Add update kuma service
+- Ghost service
+- Initial python support
+- Add loading on register button
+- *(dev)* Allow windows users to use pnpm dev
+- MeiliSearch service
+- Add abilitry to paste env files
- Wordpress on-demand SFTP
- Finalize on-demand sftp for wp
- PHP Composer support
@@ -6231,9 +6596,817 @@ All notable changes to this project will be documented in this file.
- Able to change service version/tag
- Basic white labeled version
- Able to modify database passwords
+- Add persistent storage for services
+- Multiply dockerfile locations for docker buildpack
+- Testing fluentd logging driver
+- Fluentbit investigation
+- Initial deno support
+- Deno DB migration
+- Show exited containers on UI & better UX
+- Query container state periodically
+- Install svelte-18n and init setup
+- Umami service
+- Coolify auto-updater
+- Autoupdater
+- Select base image for buildpacks
+- Hasura as a service
+- Gzip compression
+- Laravel buildpack is working!
+- Laravel
+- Fider service
+- Database and services logs
+- DNS check settings for SSL generation
+- Cancel builds!
+- Basic server usage on dashboard
+- Show usage trends
+- Usage on dashboard
+- Custom script path for Plausible
+- WP could have custom db
+- Python image selection
+- PageLoader
+- Database + service usage
+- Ability to change deployment type for nextjs
+- Ability to change deployment type for nuxtjs
+- Gitpod ready code(almost)
+- Add Docker buildpack exposed port setting
+- Custom port for git instances
+- Gitpod integration
+- Init moodle and separate stuffs to shared package
+- Moodle init
+- Remote docker engine init
+- Working on remote docker engine
+- Rde
+- Remote docker engine
+- Ipv4 and ipv6
+- Contributors
+- Add arch to database
+- Stop preview deployment
+- Persistent storage for all services
+- Cleanup clickhouse db
+- Init heroku buildpacks
+- Databases on ARM
+- Mongodb arm support
+- New dashboard
+- Appwrite service
+- Heroku deployments
+- Deploy bots (no domains)
+- Custom dns servers
+- Import public repos (wip)
+- Public repo deployment
+- Force rebuild + env.PORT for port + public repo build
+- Add GlitchTip service
+- Searxng service
+- *(ui)* Rework home UI and with responsive design
+- New service - weblate
+- Restart application
+- Show elapsed time on running builds
+- Github allow fual branches
+- Gitlab dual branch
+- Taiga
+- *(routes)* Rework ui from login and register page
+- Add traefik acme json to coolify container
+- Database secrets
+- New servers view
+- Add queue reset button
+- Previewapplications init
+- PreviewApplications finalized
+- Fluentbit
+- Show remote servers
+- *(layout)* Added drawer when user is in mobile
+- Re-apply ui improves
+- *(ui)* Improve header of pages
+- *(styles)* Make header css component
+- *(routes)* Improve ui for apps, databases and services logs
+- Add migration button to appwrite
+- Custom certificate
+- Ssl cert on traefik config
+- Refresh resource status on dashboard
+- Ssl certificate sets custom ssl for applications
+- System-wide github apps
+- Cleanup unconfigured applications
+- Cleanup unconfigured services and databases
+- Docker compose support
+- Docker compose
+- Docker compose
+- Monitoring by container
+- Initial support for specific git commit
+- Add default to latest commit and support for gitlab
+- Redirect catch-all rule
+- Rollback coolify
+- Only show expose if no proxy conf defined in template
+- Custom/private docker registries
+- Use registry for building
+- Docker registries working
+- Custom docker compose file location in repo
+- Save doNotTrackData to db
+- Add default sentry
+- Do not track in settings
+- System wide git out of beta
+- Custom previewseparator
+- Sentry frontend
+- Able to host static/php sites on arm
+- Save application data before deploying
+- SimpleDockerfile deployment
+- Able to push image to docker registry
+- Revert to remote image
+- *(api)* Name label
+- Add Openblocks icon
+- Adding icon for whoogle
+- *(ui)* Add libretranslate service icon
+- Handle invite_only plausible analytics
+- Init h2c (http2/grpc) support
+- Http + h2c paralel
+- Github raw icon url
+- Remove svg support
+- Add host path to any container
+- Able to control multiplexing
+- Add runRemoteCommandSync
+- Github repo with deployment key
+- Add persistent volumes
+- Debuggable executeNow commands
+- Add private gh repos
+- Delete gh app
+- Installation/update github apps
+- Auto-deploy
+- Deploy key based deployments
+- Resource limits
+- Long running queue with 1 hour of timeout
+- Add arm build to dev
+- Disk cleanup threshold by server
+- Notify user of disk cleanup init
+- Pricing plans ans subs
+- Add s3 storages
+- Init postgresql database
+- Add backup notifications
+- Dockerfile build pack
+- Cloud
+- Force password reset + waitlist
+- Send internal notification to discord
+- Monitor server connection
+- Invite by email from waitlist
+- Rolling update
+- Add resend as transactional emails
+- Send request in cloud
+- Add discord notifications
+- Public database
+- Telegram topics separation
+- Developer view for env variables
+- Cache team settings
+- Generate public key from private keys
+- Able to invite more people at once
+- Trial
+- Dynamic trial period
+- Ssh-agent instead of filesystem based ssh keys
+- New container status checks
+- Generate ssh key
+- Sentry add email for better support
+- Healthcheck for apps
+- Add cloudflare tunnel support
+- Services
+- Image tag for services
+- Container logs
+- Reset root password
+- Attach Coolify defined networks to services
+- Delete resource command
+- Multiselect removable resources
+- Disable service, required version
+- Basedir / monorepo initial support
+- Init version of any git deployment
+- Deploy private repo with ssh key
+- Add email verification for cloud
+- Able to deploy docker images
+- Add dockerfile location
+- Proxy logs on the ui
+- Add custom redis conf
+- Use docker login credentials from server
+- Able to customize docker labels on applications
+- Show if config is not applied
+- Standalone mongodb
+- Cloning project
+- Api tokens + deploy webhook
+- Start all kinds of things
+- Simple search functionality
+- Mysql, mariadb
+- Lock environment variables
+- Download local backups
+- Improve deployment time by a lot
+- Deployment logs fullscreen
+- Service database backups
+- Make service databases public
+- Log drain (wip)
+- Enable/disable log drain by service
+- Log drainer container check
+- Add docker engine support install script to rhel based systems
+- Save timestamp configuration for logs
+- Custom log drain endpoints
+- Auto-restart tcp proxies for databases
+- Execute command in container
+- Autoupdate env during seed
+- Disable autoupdate
+- Randomly sleep between executions
+- Pull latest images for services
+- Custom docker compose commands
+- Add environment description + able to change name
+- Raw docker compose deployments
+- Add www-non-www redirects to traefik
+- Import backups
+- Search between resources
+- Move resources between projects / environments
+- Clone any resource
+- Shared environments
+- Concurrent builds / server
+- Able to deploy multiple resources with webhook
+- Add PR comments
+- Dashboard live deployment view
+- Added manual webhook support for bitbucket
+- Add initial support for custom docker run commands
+- Cleanup unreachable servers
+- Tags and tag deploy webhooks
+- Clone to env
+- Multi deployments
+- Cleanup queue
+- Magic for traefik redirectregex in services
+- Revalidate server
+- Disable gzip compression on service applications
+- Save github app permission locally
+- Minversion for services
+- Able to add dynamic configurations from proxy dashboard
+- Custom server limit
+- Delay container/server jobs
+- Add static ipv4 ipv6 support
+- Server disabled by overflow
+- Preview deployment logs
+- Collect webhooks during maintenance
+- Logs and execute commands with several servers
+- Domains api endpoint
+- Resources api endpoint
+- Team api endpoint
+- Add deployment details to deploy endpoint
+- Add deployments api
+- Experimental caddy support
+- Dynamic configuration for caddy
+- Reset password
+- Show resources on source page
+- Able to run scheduler/horizon programatically
+- Change page width
+- Watch paths
+- Able to make rsa/ed ssh keys
+- *(application)* Update submodules after git checkout
+- Add amazon linux 2023
+- Upload large backups
+- Edit domains easier for compose
+- Able to delete configuration from server
+- Configuration checker for all resources
+- Allow tab in textarea
+- Dynamic mux time
+- Literal env variables
+- Lazy load stuffs + tell user if compose based deployments have missing envs
+- Can edit file/dir volumes from ui in compose based apps
+- Upgrade Appwrite service template to 1.5
+- Upgrade Appwrite service template to 1.5
+- Add db name to backup notifications
+- Initial datalist
+- Update service contribution docs URL
+- The final pricing plan, pay-as-you-go
+- Add container name to network aliases in ApplicationDeploymentJob
+- Add lazy loading for images in General.php and improve Docker Compose file handling in Application.php
+- Experimental sentinel
+- Start Sentinel on servers.
+- Pull new sentinel image and restart container
+- Init metrics
+- Add AdminRemoveUser command to remove users from the database
+- Adding new COOLIFY_ variables
+- Save commit message and better view on deployments
+- Toggle label escaping mechanism
+- Shows the latest deployment commit + message on status
+- New manual update process + remove next_channel
+- Add lastDeploymentInfo and lastDeploymentLink props to breadcrumbs and status components
+- Sort envs alphabetically and creation date
+- Improve sorting of environment variables in the All component
+- Update healthcheck test in StartMongodb action
+- Add pull_request_id filter to get_last_successful_deployment method in Application model
+- Add hc logs to healthchecks
+- Add SerpAPI as a Github Sponsor
+- Admin view for deleting users
+- Scheduled task failed notification
+- If the time seems too long it remains at 0s
+- Improve Docker Engine start logic in ServerStatusJob
+- If proxy stopped manually, it won't start back again
+- Exclude_from_hc magic
+- Gitea manual webhooks
+- Add container logs in case the container does not start healthy
+- Handle incomplete expired subscriptions in Stripe webhook
+- Add more persistent storage types
+- Add PHP memory limit environment variable to docker-compose.prod.yml
+- Add manual update option to UpdateCoolify handle method
+- Add port configuration for Vaultwarden service
+- Able to change database passwords on the UI. It won't sync to the database.
+- Able to add several domains to compose based previews
+- Add bounty program link to bug report template
+- Add titles
+- Db proxy logs
+- Easily redirect between www-and-non-www domains
+- Add logos for new sponsors
+- Add homepage template
+- Update homepage.yaml with environment variables and volumes
+- Spanish translation
+- Cancelling a deployment will check if new could be started.
+- Add supaguide logo to donations section
+- Nixpacks now could reach local dbs internally
+- Add Tigris logo to other/logos directory
+- COOLIFY_CONTAINER_NAME predefined variable
+- Charts
+- Sentinel + charts
+- Container metrics
+- Add high priority queue
+- Add metrics warning for servers without Sentinel enabled
+- Add blacksmith logo to donations section
+- Preselect server and destination if only one found
+- More api endpoints
+- Add API endpoint to update application by UUID
+- Update statusnook logo filename in compose template
+- Local fonts
+- More API endpoints
+- Bulk env update api endpoint
+- Update server settings metrics history days to 7
+- New app API endpoint
+- Private gh deployments through api
+- Lots of api endpoints
+- Api api api api api api
+- Rename CloudCleanupSubs to CloudCleanupSubscriptions
+- Early fraud warning webhook
+- Improve internal notification message for early fraud warning webhook
+- Add schema for uuid property in app update response
+- Cleanup unused docker networks from proxy
+- Compose parser v2
+- Display time interval for rollback images
+- Add security and storage access key env to twenty template
+- Add new logo for Latitude
+- Enable legacy model binding in Livewire configuration
+- Improve error handling in loadComposeFile method
+- Add readonly labels
+- Preserve git repository
+- Force cleanup server
+- Create/delete project endpoints
+- Add patch request to projects
+- Add server api endpoints
+- Add branddev logo to README.md
+- Update API endpoint summaries
+- Update Caddy button label in proxy.blade.php
+- Check custom internal name through server's applications.
+- New server check job
+- Delete team in cloud without subscription
+- Coolify init should cleanup stuck networks in proxy
+- Add manual update check functionality to settings page
+- Update auto update and update check frequencies in settings
+- Update Upgrade component to check for latest version of Coolify
+- Improve homepage service template
+- Support map fields in Directus
+- Labels by proxy type
+- Able to generate only the required labels for resources
+- Preserve git repository with advanced file storages
+- Added Windmill template
+- Added Budibase template
+- Add shm-size for custom docker commands
+- Add custom docker container options to all databases
+- Able to select different postgres database
+- Add new logos for jobscollider and hostinger
+- Order scheduled task executions
+- Add Code Server environment variables to Service model
+- Add coolify build env variables to building phase
+- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid
+- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid
+- Update server_settings table to force docker cleanup
+- Update Docker Compose file with DB_URL environment variable
+- Refactor shared.php to improve environment variable handling
+- Expose project description in API response
+- Add elixir finetunes to the deployment job
+- Make coolify full width by default
+- Fully functional terminal for command center
+- Custom terminal host
+- Add buddy logo
+- Add nullable constraint to 'fingerprint' column in private_keys table
+- *(api)* Add an endpoint to execute a command
+- *(api)* Add endpoint to execute a command
+- Add ContainerStatusTypes enum for managing container status
+- Allow specify use_build_server when creating/updating an application
+- Add support for `use_build_server` in API endpoints for creating/updating applications
+- Add Mixpost template
+- Update resource deletion job to allow configurable options through API
+- Add query parameters for deleting configurations, volumes, docker cleanup, and connected networks
+- Add command to check application deployment queue
+- Support Hetzner S3
+- Handle HTTPS domain in ConfigureCloudflareTunnels
+- Backup all databases for mysql,mariadb,postgresql
+- Restart service without pulling the latest image
+- Add strapi template
+- Add it-tools service template and logo
+- Add homarr service tamplate and logo
+- Add Argilla service configuration to Service model
+- Add Invoice Ninja service configuration to Service model
+- Project search on frontend
+- Add ollama service with open webui and logo
+- Update setType method to use slug value for type
+- Refactor setType method to use slug value for type
+- Refactor setType method to use slug value for type
+- Add Supertokens template
+- Add easyappointments service template
+- Add dozzle template
+- Adds forgejo service with runners
+- Add Mautic 4 and 5 to service templates
+- Add keycloak template
+- Add onedev template
+- Improve search functionality in project selection
+- Add customHelper to stack-form
+- Add cloudbeaver template
+- Add ntfy template
+- Add qbittorrent template
+- Add Homebox template
+- Add owncloud service and logo
+- Add immich service
+- Auto generate url
+- Refactored to work with coolify auto env vars
+- Affine service template and logo
+- Add LibreTranslate template
+- Open version in a new tab
+- Add Transmission template
+- Add transmission healhcheck
+- Add zipline template
+- Dify template
+- Required envs
+- Add EdgeDB
+- Show warning if people would like to use sslip with https
+- Add is shared to env variables
+- Variabel sync and support shared vars
+- Add notification settings to server_disk_usage
+- Add coder service tamplate and logo
+- Debug mode for sentinel
+- Add jitsi template
+- Add --gpu support for custom docker command
+- Add Firefox template
+- Add template for Wiki.js
+- Add upgrade logs to /data/coolify/source
+- Custom nginx configuration for static deployments + fix 404 redirects in nginx conf
+- Check local horizon scheduler deployments
+- Add internal api docs to /docs/api with auth
+- Add proxy type change to create/update apis
+- Add MacOS template
+- Add Windows template
+- *(service)* :sparkles: add mealie
+- Add hex magic env var
+- Add deploy-only token permission
+- Able to deploy without cache on every commit
+- Update private key nam with new slug as well
+- Allow disabling default redirect, set status to 503
+- Add TLS configuration for default redirect in Server model
+- Slack notifications
+- Introduce root permission
+- Able to download schedule task logs
+- Migrate old email notification settings from the teams table
+- Migrate old discord notification settings from the teams table
+- Migrate old telegram notification settings from the teams table
+- Add slack notifications to a new table
+- Enable success messages again
+- Use new notification stuff inside team model
+- Some more notification settings and better defaults
+- New email notification settings
+- New shared function name `is_transactional_emails_enabled()`
+- New shared notifications functions
+- Email Notification Settings Model
+- Telegram notification settings Model
+- Discord notification settings Model
+- Slack notification settings Model
+- New Discord notification UI
+- New Slack notification UI
+- New telegram UI
+- Use new notification event names
+- Always sent notifications
+- Scheduled task success notification
+- Notification trait
+- Get discord Webhook form new table
+- Get Slack Webhook form new table
+- Use new table or instance settings for email
+- Use new place for settings and topic IDs for telegram
+- Encrypt instance email settings
+- Use encryption in instance settings model
+- Scheduled task success and failure notifications
+- Add docker cleanup success and failure notification settings columns
+- UI for docker cleanup success and failure notification
+- Docker cleanup email views
+- Docker cleanup success and failure notification files
+- Scheduled task success email
+- Send new docker cleanup notifications
+- :passport_control: integrate Authentik authentication with Coolify
+- *(notification)* Add Pushover
+- Add seeder command and configuration for database seeding
+- Add new password magic env with symbols
+- Add documenso service
+- New ServerReachabilityChanged event
+- Use new ServerReachabilityChanged event instead of isDirty
+- Add infomaniak oauth
+- Add server disk usage check frequency
+- Add environment_uuid support and update API documentation
+- Add service/resource/project labels
+- Add coolify.environment label
+- Add database subtype
+- Migrate to new encryption options
+- New encryption options
+- Able to import full db backups for pg/mysql/mariadb
+- Restore backup from server file
+- Docker volume data cloning
+- Move volume data cloning to a Job
+- Volume cloning for ResourceOperations
+- Remote server volume cloning
+- Add horizon server details to queue
+- Enhance horizon:manage command with worker restart check
+- Add is_coolify_host to the server api responses
+- DB migration for Backup retention
+- UI for backup retention settings
+- New global s3 and local backup deletion function
+- Use new backup deletion functions
+- Add calibre-web service
+- Add actual-budget service
+- Add rallly service
+- Template for Gotenberg, a Docker-powered stateless API for PDF files
+- Enhance import command options with additional guidance and improved checkbox label
+- Purify for better sanitization
+- Move docker cleanup to its own tab
+- DB and Model for docker cleanup executions
+- DockerCleanupExecutions relationship
+- DockerCleanupDone event
+- Get command and output for logs from CleanupDocker
+- New sidebar menu and order
+- Docker cleanup executions UI
+- Add execution log to dockerCleanupJob
+- Improve deployment UI
+- Root user envs and seeding
+- Email, username and password validation when they are set via envs
+- Improved error handling and log output
+- Add root user configuration variables to production environment
+- Add log file check message in upgrade script for better troubleshooting
+- Add root user details to install script
+- *(core)* Wip version of coolify.json
+- *(core)* Add SOURCE_COMMIT variable to build environment in ApplicationDeploymentJob
+- *(service)* Update affine.yaml with AI environment variables (#4918)
+- *(service)* Add new service Flipt (#4875)
+- *(docs)* Update tech stack
+- *(terminal)* Show terminal unavailable if the container does not have a shell on the global terminal UI
+- *(ui)* Improve deployment UI
+- *(template)* Add Open Web UI
+- *(templates)* Add Open Web UI service template
+- *(ui)* Update GitHub source creation advanced section label
+- *(core)* Add dynamic label reset for application settings
+- *(ui)* Conditionally enable advanced application settings based on label readonly status
+- *(env)* Added COOLIFY_RESOURCE_UUID environment variable
+- *(vite)* Add Cloudflare async script and style tag attributes
+- *(meta)* Add comprehensive SEO and social media meta tags
+- *(core)* Add name to default proxy configuration
+- Add application api route
+- Container logs
+- Remove ansi color from log
+- Add lines query parameter
+- *(changelog)* Add git cliff for automatic changelog generation
+- *(workflows)* Improve changelog generation and workflows
+- *(ui)* Add periodic status checking for services
+- *(deployment)* Ensure private key is stored in filesystem before deployment
+- *(slack)* Show message title in notification previews (#5063)
+- *(i18n)* Add Arabic translations (#4991)
+- *(i18n)* Add French translations (#4992)
+- *(services)* Update `service-templates.json`
+- *(ui)* Add top padding to pricing plans view
+- *(core)* Add error logging and cron parsing to docker/server schedules
+- *(core)* Prevent using servers with existing resources as build servers
+- *(ui)* Add textarea switching option in service compose editor
+- *(ui)* Add wire:key to two-step confirmation settings
+- *(database)* Add index to scheduled task executions for improved query performance
+- *(database)* Add index to scheduled database backup executions
+- *(billing)* Add Stripe past due subscription status tracking
+- *(ui)* Add past due subscription warning banner
+- *(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
+- *(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
+- *(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
+- *(proxy)* Enhance proxy handling and port conflict detection
+- *(lang)* Added Azerbaijani language updated turkish language. (#5497)
+- *(lang)* Added Portuguese from Brazil language (#5500)
+- *(lang)* Add Indonesian language translations (#5513)
+- *(api)* Update OpenAPI spec for services (#5448)
### 🐛 Bug Fixes
+- Secrets join
+- ENV variables set differently
+- Capture non-error as error
+- Only delete id.rsa in case of it exists
+- Status is not available yet
+- Docker Engine bug related to live-restore and IPs
+- Version
+- PreventDefault on a button, thats all
+- Haproxy check should not throw error
+- Delete all build files
+- Cleanup images
+- More error handling in proxy configuration + cleanups
+- Local static assets
+- Check sentry
+- Typo
+- Package.json
+- Build secrets should be visible in runtime
+- New secret should have default values
+- Validate secrets
+- Truncate git clone errors
+- Branch used does not throw error
+- Typo
+- Error handling
+- Stopping service without proxy
+- Coolify proxy start
+- Window error in SSR
+- GitHub sync PR's
+- Load more button
+- Small fixes
+- Typo
+- Error with follow logs
+- IsDomainConfigured
+- TransactionIds
+- Coolify image cleanup
+- Cleanup every 10 mins
+- Cleanup images
+- Add no user redis to uri
+- Secure cookie disabled by default
+- Buggy svelte-kit-cookie-session
+- Login issues
+- SSL app off
+- Local docker host
+- Typo
+- Lets encrypt
+- Remove SSL with stop
+- SSL off for services
+- Grr
+- Running state css
+- Minor fixes
+- Remove force SSL when doing let's encrypt request
+- GhToken in session now
+- Random port for certbot
+- Follow icon
+- Plausible volume fixed
+- Database connection strings
+- Gitlab webhooks fixed
+- If DNS not found, do not redirect
+- Github token
+- Move tokens from session to cookie/store
+- Email is lowercased in login
+- Lowercase email everywhere
+- Use normal docker-compose in dev
+- Random network name for demo
+- Settings fqdn grr
+- Revert default network
+- Http for demo, oops
+- Docker scanner
+- Improvement on image pulls
+- Coolify image pulls
+- Remove wrong/stuck proxy configurations
+- Always use a buildpack
+- Add icons for eleventy + astro
+- Fix proxy every 10 secs
+- Do not remove coolify proxy
+- Update version
+- Be sure .env exists
+- Missing fqdn for services
+- Default npm command
+- Add coolify-image label for build images
+- Cleanup old images, > 3 days
+- Better proxy check
+- Ssl + sslrenew
+- Null proxyhash on restart
+- Reconfigure proxy on restart
+- Update process
+- Reload proxy on ssl cert
+- Volume name
+- Update process
+- Check when a container is running
+- Reload haproxy if new cert is added
+- Cleanup coolify images
+- Application state in UI
+- Do not error if proxy is not running
+- Personal Gitlab repos
+- Autodeploy true by default for GH repos
+- No cookie found
+- Missing session data
+- No error if GitSource is missing
+- No webhook secret found?
+- Basedir for dockerfiles
+- Better queue system + more support on monorepos
+- Remove build logs in case of app removed
+- Cleanup old builds
+- Only cleanup same app
+- Add nginx + htaccess files
+- Skip ssl cert in case of error
+- Volumes
+- Cleanup only 2 hours+ old images
+- Ghost logo size
+- Ghost icon, remove console.log
+- List ghost services
+- Reload window on settings saved
+- Persistent storage on webhooks
+- Add license
+- Space in repo names
+- Gitlab repo url
+- No need to dashify anymore
+- Registration enabled/disabled
+- Add PROTO headers
+- Haproxy errors
+- Build variables
+- Use NodeJS for sveltekit for now
+- Ignore coolify proxy error for now
+- Python no wsgi
+- If user not found
+- Rename envs to secrets
+- Infinite loop on www domains
+- No need to paste clear text env for previews
+- Build log fix attempt #1
+- Small UI fix on logs
+- Lets await!
+- Async progress
+- Remove console.log
+- Build log
+- UI
+- Gitlab & Github urls
+- Secrets build/runtime coudl be changed after save
+- Default configuration
+- *(php)* If .htaccess file found use apache
+- Add default webhook domain for n8n
+- Add git lfs while deploying
+- Try to update build status several times
+- Update stucked builds
+- Update stucked builds on startup
+- Revert seed
+- Lame fixing
+- Remove asyncUntil
- Add openssl to image
- Permission issues
- On-demand sFTP for wp
@@ -6255,9 +7428,2259 @@ All notable changes to this project will be documented in this file.
- Html/apiUrls cannot end with /
- Typo
- Missing buildpack
+- Enable https for Ghost
+- Postgres root passwor shown and set
+- Able to change postgres user password from ui
+- DB Connecting string generator
+- Missing install repositories GitHub
+- Return own and other sources better
+- Show config missing on sources
+- Remove unnecessary save button haha
+- Update dockerfile
+- Haproxy build stuffs
+- Proxy
+- Types
+- Invitations
+- Timeout values
+- Cleanup images older than a day
+- Meilisearch service
+- Load all branches, not just the first 30
+- ProjectID for Github
+- DNS check before creating SSL cert
+- Try catch me
+- Restart policy for resources
+- No permission on first registration
+- Reverting postgres password for now
+- Destinations to HAProxy
+- Register should happen if coolify proxy cannot be started
+- GitLab typo
+- Remove system wide pw reset
+- Postgres root pw is pw field
+- Teams view
+- Improved tcp proxy monitoring for databases/ftp
+- Add HTTP proxy checks
+- Loading of new destinations
+- Better performance for cleanup images
+- Remove proxy container in case of dependent container is down
+- Restart local docker coolify proxy in case of something happens to it
+- Id of service container
+- Switch from bitnami/redis to normal redis
+- Use redis-alpine
+- Wordpress extra config
+- Stop sFTP connection on wp stop
+- Change user's id in sftp wp instance
+- Use arm based certbot on arm
+- Buildlog line number is not string
+- Application logs paginated
+- Switch to stream on applications logs
+- Scroll to top for logs
+- Pull new images for services all the time it's started.
+- White-labeled custom logo
+- Application logs
+- Deno configurations
+- Text on deno buildpack
+- Correct branch shown in build logs
+- Vscode permission fix
+- I18n
+- Locales
+- Application logs is not reversed and queried better
+- Do not activate i18n for now
+- GitHub token cleanup on team switch
+- No logs found
+- Code cleanups
+- Reactivate posgtres password
+- Contribution guide
+- Simplify list services
+- Contribution
+- Contribution guide
+- Contribution guide
+- Packagemanager finder
+- Unami svg size
+- Team switching moved to IAM menu
+- Always use IP address for webhooks
+- Remove unnecessary test endpoint
+- UI
+- Migration
+- Fider envs
+- Checking low disk space
+- Build image
+- Update autoupdate env variable
+- Renew certificates
+- Webhook build images
+- Missing node versions
+- ExposedPorts
+- Logos for dbs
+- Do not run SSL renew in development
+- Check domain for coolify before saving
+- Remove debug info
+- Cancel jobs
+- Cancel old builds in database
+- Better DNS check to prevent errors
+- Check DNS in prod only
+- DNS check
+- Disable sentry for now
+- Cancel
+- Sentry
+- No image for Docker buildpack
+- Default packagemanager
+- Server usage only shown for root team
+- Expose ports for services
+- UI
+- Navbar UI
+- UI
+- UI
+- Remove RC python
+- UI
+- UI
+- UI
+- Default Python package
+- WP custom db
+- UI
+- Gastby buildpack
+- Service checks
+- Remove console.log
+- Traefik
+- Remove debug things
+- WIP Traefik
+- Proxy for http
+- PR deployments view
+- Minio urls + domain checks
+- Remove gh token on git source changes
+- Do not fetch app state in case of missconfiguration
+- Demo instance save domain instantly
+- Instant save on demo instance
+- New source canceled view
+- Lint errors in database services
+- Otherfqdns
+- Host key verification
+- Ftp connection
+- GitHub fixes
+- TrustProxy
+- Force restart proxy
+- Only restart coolify proxy in case of version prior to 2.9.2
+- Force restart proxy on seeding
+- Add GIT ENV variable for submodules
+- Recurisve clone instead of submodule
+- Versions
+- Only reconfigure coolify proxy if its missconfigured
+- Demo version forms
+- Typo
+- Revert gh and gl cloning
+- Proxy stop missing argument
+- Fider changed an env variable name
+- Pnpm command
+- Plausible custom script
+- Plausible script and middlewares
+- Remove console log
+- Remove comments
+- Traefik middleware
+- Persistent nocodb
+- Nocodb persistency
+- Host and reload for uvicorn
+- Remove package-lock
+- Be able to change database + service versions
+- Lock file
+- Seeding
+- Forgot that the version bump changed 😅
+- New destination can be created
+- Include post
+- New destinations
+- Domain check
+- Domain check
+- TrustProxy for Fastify
+- Hostname issue
+- GitLab pagination load data
+- Service domain checker
+- Wp missing ftp solution
+- Ftp WP issues
+- Ftp?!
+- Gitpod updates
+- Gitpod
+- Gitpod
+- Wordpress FTP permission issues
+- GitLab search fields
+- GitHub App button
+- GitLab loop on misconfigured source
+- Gitpod
+- Cleanup less often and can do it manually
+- Admin password reset should not timeout
+- Message for double branches
+- Turn off autodeploy if double branch is configured
+- More types for API
+- More types
+- Do not rebuild in case image exists and sha not changed
+- Gitpod urls
+- Remove new service start process
+- Remove shared dir, deployment does not work
+- Gitlab custom url
+- Location url for services and apps
+- Settings from api
+- Selectable destinations
+- Gitpod hardcodes
+- Typo
+- Typo
+- Expose port checker
+- States and exposed ports
+- CleanupStorage
+- Remote traefik webhook
+- Remote engine ip address
+- RemoteipAddress
+- Explanation for remote engine url
+- Tcp proxy
+- Lol
+- Webhook
+- Dns check for rde
+- Gitpod
+- Revert last commit
+- Dns check
+- Dns checker
+- Webhook
+- Df and more debug
+- Webhooks
+- Load previews async
+- Destination icon
+- Pr webhook
+- Cache image
+- No ssh key found
+- Prisma migration + update of docker and stuffs
+- Ui
+- Ui
+- Only 1 ssh-agent is needed
+- Reuse ssh connection
+- Ssh tunnel
+- Dns checking
+- Fider BASE_URL set correctly
+- Rde local ports
+- Empty remote destinations could be removed
+- Tips
+- Lowercase issues fider
+- Tooltip colors
+- Update clickhouse configuration
+- Cleanup command
+- Enterprise Github instance endpoint
+- Follow/cancel buttons
+- Only remove coolify managed containers
+- White-labeled env
+- Schema
+- Coolify-network on verification
+- Cleanup stucked prisma-engines
+- Toast
+- Secrets
+- Cleanup prisma engine if there is more than 1
+- !isARM to isARM
+- Enterprise GH link
+- Empty buildpack icons
+- Debounce dashboard status requests
+- Decryption errors
+- Postgresql on ARM
+- Make it public button
+- Loading indicator
+- Replace docker compose with docker-compose on CSB
+- Dashboard ui
+- Create coolify-infra, if it does not exists
+- Gitpod conf and heroku buildpacks
+- Appwrite
+- Autoimport + readme
+- Services import
+- Heroku icon
+- Heroku icon
+- Dns button ui
+- Bot deployments
+- Bots
+- AutoUpdater & cleanupStorage jobs
+- Revert docker compose version to 2.6.1
+- Trim secrets
+- Restart containers on-failure instead of always
+- Show that Ghost values could be changed
+- Bots without exposed ports
+- Missing commas
+- ExposedPort is just optional
+- Port checker
+- Cancel build after 5 seconds
+- ExposedPort checker
+- Batch secret =
+- Dashboard for non-root users
+- Stream build logs
+- Show build log start/end
+- Ui buttons
+- Clear queue on cancelling jobs
+- Cancelling jobs
+- Dashboard for admins
+- Never stop deplyo queue
+- Build queue system
+- High cpu usage
+- Worker
+- Better worker system
+- Secrets decryption
+- UI thinkgs
+- Delete team while it is active
+- Team switching
+- Queue cleanup
+- Decrypt secrets
+- Cleanup build cache as well
+- Pr deployments + remove public gits
+- Copy all files during install process
+- Typo
+- Process
+- White labeled icon on navbar
+- Whitelabeled icon
+- Next/nuxt deployment type
+- Again
+- Pr deployment
+- CompareVersions
+- Include
+- Include
+- Gitlab apps
+- Oh god Prisma
+- Glitchtip things
+- Loading state on start
+- Ui
+- Submodule
+- Gitlab webhooks
+- UI + refactor
+- Exposedport on save
+- Appwrite letsencrypt
+- Traefik appwrite
+- Traefik
+- Finally works! :)
+- Rename components + remove PR/MR deployment from public repos
+- Settings missing id
+- Explainer component
+- Database name on logs view
+- Taiga
+- Ssh pid agent name
+- Repository link trim
+- Fqdn or expose port required
+- Service deploymentEnabled
+- Expose port is not required
+- Remote verification
+- Dockerfile
+- Debug api logging + gh actions
+- Workdir
+- Move restart button to settings
+- Gitlab webhook
+- Use ip address instead of window location
+- Use ip instead of window location host
+- Service state update
+- Add initial DNS servers
+- Revert last change with domain check
+- Service volume generation
+- Minio default env variables
+- Add php 8.1/8.2
+- Edgedb ui
+- Edgedb stuff
+- Edgedb
+- Pr previews
+- DnsServer formatting
+- Settings for service
+- Change to execa from utils
+- Save search input
+- Ispublic status on databases
+- Port checkers
+- Ui variables
+- Glitchtip env to pyhton boolean
+- Autoupdater
+- Show restarting apps
+- Show restarting application & logs
+- Remove unnecessary gitlab group name
+- Secrets for PR
+- Volumes for services
+- Build secrets for apps
+- Delete resource use window location
+- Changing umami image URL to get latest version
+- Gitlab importer for public repos
+- Show error logs
+- Umami init sql
+- Plausible analytics actions
+- Login
+- Dev url
+- UpdateMany build logs
+- Fallback to db logs
+- Fluentbit configuration
+- Coolify update
+- Fluentbit and logs
+- Canceling build
+- Logging
+- Load more
+- Build logs
+- Versions of appwrite
+- Appwrite?!
+- Get building status
+- Await
+- Await #2
+- Update PR building status
+- Appwrite default version 1.0
+- Undead endpoint does not require JWT
+- *(routes)* Improve design of application page
+- *(routes)* Improve design of git sources page
+- *(routes)* Ui from destinations page
+- *(routes)* Ui from databases page
+- *(routes)* Ui from databases page
+- *(routes)* Ui from databases page
+- *(routes)* Ui from services page
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* Ui from settings page
+- *(routes)* Duplicates classes in services page
+- *(routes)* Searchbar ui
+- Github conflicts
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- *(routes)* More ui tweaks
+- Ui with headers
+- *(routes)* Header of settings page in databases
+- *(routes)* Ui from secrets table
+- Ui
+- Tooltip
+- Dropdown
+- Ssl certificate distribution
+- Db migration
+- Multiplex ssh connections
+- Able to search with id
+- Not found redirect
+- Settings db requests
+- Error during saving logs
+- Consider base directory in heroku bp
+- Basedirectory should be empty if null
+- Allow basedirectory for heroku
+- Stream logs for heroku bp
+- Debug log for bp
+- Scp without host verification & cert copy
+- Base directory & docker bp
+- Laravel php chooser
+- Multiplex ssh and ssl copy
+- Seed new preview secret types
+- Error notification
+- Empty preview value
+- Error notification
+- Seed
+- Service logs
+- Appwrite function network is not the default
+- Logs in docker bp
+- Able to delete apps in unconfigured state
+- Disable development low disk space
+- Only log things to console in dev mode
+- Do not get status of more than 10 resources defined by category
+- BaseDirectory
+- Dashboard statuses
+- Default buildImage and baseBuildImage
+- Initial deploy status
+- Show logs better
+- Do not start tcp proxy without main container
+- Cleanup stucked tcp proxies
+- Default 0 pending invitations
+- Handle forked repositories
+- Typo
+- Pr branches
+- Fork pr previews
+- Remove unnecessary things
+- Meilisearch data dir
+- Verify and configure remote docker engines
+- Add buildkit features
+- Nope if you are not logged in
+- Do not use npx
+- Pure docker based development
+- Do not show nope as ip address for dbs
+- Add git sha to build args
+- Smart search for new services
+- Logs for not running containers
+- Update docker binaries
+- Gh release
+- Dev container
+- Gitlab auth and compose reload
+- Check compose domains in general
+- Port required if fqdn is set
+- Appwrite v1 missing containers
+- Dockerfile
+- Pull does not work remotely on huge compose file
+- Single container logs and usage with compose
+- Secret errors
+- Service logs
+- Heroku bp
+- Expose port is readonly on the wrong condition
+- Toast
+- Traefik proxy q 10s
+- App logs view
+- Tooltip
+- Toast, rde, webhooks
+- Pathprefix
+- Load public repos
+- Webhook simplified
+- Remote webhooks
+- Previews wbh
+- Webhooks
+- Websecure redirect
+- Wb for previews
+- Pr stopps main deployment
+- Preview wbh
+- Wh catchall for all
+- Remove old minio proxies
+- Template files
+- Compose icon
+- Templates
+- Confirm restart service
+- Template
+- Templates
+- Templates
+- Plausible analytics things
+- Appwrite webhook
+- Coolify instance proxy
+- Migrate template
+- Preview webhooks
+- Simplify webhooks
+- Remove ghost-mariadb from the list
+- More simplified webhooks
+- Umami + ghost issues
+- Remove contribution docs
+- Umami template
+- Compose webhooks fixed
+- Variable replacements
+- Doc links
+- For rollback
+- N8n and weblate icon
+- Expose ports for services
+- Wp + mysql on arm
+- Show rollback button loading
+- No tags error
+- Update on mobile
+- Dashboard error
+- GetTemplates
+- Docker compose persistent volumes
+- Application persistent storage things
+- Volume names for undefined volume names in compose
+- Empty secrets on UI
+- Ports for services
+- Default icon for new services
+- IsBot issue
+- Local dev api/ws urls
+- Wrong template/type
+- Gitea icon is svg
+- Gh actions
+- Gh actions
+- Replace $$generate vars
+- Webhook traefik
+- Exposed ports
+- Wrong icons on dashboard
+- Escape % in secrets
+- Move debug log settings to build logs
+- Storage for compose bp + debug on
+- Hasura admin secret
+- Logs
+- Mounts
+- Load logs after build failed
+- Accept logged and not logged user in /base
+- Remote haproxy password/etc
+- Remove hardcoded sentry dsn
+- Nope in database strings
+- 0 destinations redirect after creation
+- Seed
+- Sentry dsn update
+- Dnt
+- Ui
+- Only visible with publicrepo
+- Migrations
+- Prevent webhook errors to be logged
+- Login error
+- Remove beta from systemwide git
+- Git checkout
+- Remove sentry before migration
+- Webhook previewseparator
+- Apache on arm
+- Update PR/MRs with new previewSeparator
+- Static for arm
+- Failed builds should not push images
+- Turn off autodeploy for simpledockerfiles
+- Security hole
+- Rde
+- Delete resource on dashboard
+- Wrong port in case of docker compose
+- Public db icon on dashboard
+- Cleanup
+- Build commands
+- Migration file
+- Adding missing appwrite volume
+- Appwrite tmp volume
+- Do not replace secret
+- Root user for dbs on arm
+- Escape secrets
+- Escape env vars
+- Envs
+- Docker buildpack env
+- Secrets with newline
+- Secrets
+- Add default node_env variable
+- Add default node_env variable
+- Secrets
+- Secrets
+- Gh actions
+- Duplicate env variables
+- Cleanupstorage
+- Remove unused imports
+- Parsing secrets
+- Read-only permission
+- Read-only iam
+- $ sign in secrets
+- Custom gitlab git user
+- Add documentation link again
+- Remove prefetches
+- Doc link
+- Temporary disable dns check with dns servers
+- Local images for reverting
+- Secrets
+- Compose file location
+- Docker log sequence
+- Delete apps with previews
+- Do not cleanup compose applications as unconfigured
+- Build env variables with docker compose
+- Public gh repo reload compose
+- Build args docker compose
+- Grpc
+- Secrets
+- Www redirect
+- Cleanup function
+- Cleanup stucked containers
+- Deletion + cleanupStuckedContainers
+- Stucked containers
+- CleanupStuckedContainers
+- CleanupStuckedContainers
+- Typos in docs
+- Url
+- Network in compose files
+- Escape new line chars in wp custom configs
+- Applications cannot be deleted
+- Arm servics
+- Base directory not found
+- Cannot delete resource when you are not on root team
+- Empty port in docker compose
+- Set PACK_VERSION to 0.27.0
+- PublishDirectory
+- Host volumes
+- Replace . & .. & $PWD with ~
+- Handle log format volumes
+- Nestjs buildpack
+- Show ip address as host in public dbs
+- Revert from dockerhub if ghcr.io does not exists
+- Logo of CCCareers
+- Typo
+- Ssh
+- Nullable name on deploy_keys
+- Enviroments
+- Remove dd - oops
+- Add inprogress activity
+- Application view
+- Only set status in case the last command block is finished
+- Poll activity
+- Small typo
+- Show activity on load
+- Deployment should fail on error
+- Tests
+- Version
+- Status not needed
+- No project redirect
+- Gh actions
+- Set status
+- Seeders
+- Do not modify localhost
+- Deployment_uuid -> type_uuid
+- Read env from config, bc of cache
+- Private key change view
+- New destination
+- Do not update next channel all the time
+- Cancel deployment button
+- Public repo limit shown + branch should be preselected.
+- Better status on ui for apps
+- Arm coolify version
+- Formatting
+- Gh actions
+- Show github app secrets
+- Do not force next version updates
+- Debug log button
+- Deployment key based works
+- Deployment cancel/debug buttons
+- Upgrade button
+- Changing static build changes port
+- Overwrite default nginx configuration
+- Do not overlap docker image names
+- Oops
+- Found image name
+- Name length
+- Semicolons encoding by traefik
+- Base_dir wip & outputs
+- Cleanup docker images
+- Nginx try_files
+- Master is the default, not main
+- No ms in rate limit resets
+- Loading after button text
+- Default value
+- Localhost is usable
+- Update docker-compose prod
+- Cloud/checkoutid/lms
+- Type of license code
+- More verbose error
+- Version lol
+- Update prod compose
+- Version
+- Remove buggregator from dev
+- Able to change localhost's private key
+- Readonly input box
+- Notifications
+- Licensing
+- Subscription link
+- Migrate db schema for smtp + discord
+- Text field
+- Null fqdn notifications
+- Remove old modal
+- Proxy stop/start ui
+- Proxy UI
+- Empty description
+- Input and textarea
+- Postgres_username name to not name, lol
+- DatabaseBackupJob.php
+- No storage
+- Backup now button
+- Ui + subscription
+- Self-hosted
+- Make coolify-db backups unique dir
+- Limits & server creation page
+- Fqdn on apps
+- DockerCleanupjob
+- Validation
+- Webhook endpoint in cloud and no system wide gh app
+- Subscriptions
+- Password confirmation
+- Proxy start job
+- Dockerimage jobs are not overlapping
+- Sentry bug
+- Button loading animation
+- Form address
+- Show hosted email service, just disable for non pro subs
+- Add navbar for source + keys
+- Add docker network to build process
+- Overlapping apps
+- Do not show system wide git on cloud
+- Lowercase image names
+- Typo
+- SaveModel email settings
+- Bug
+- Db backup job
+- Sentry 4459819517
+- Sentry 4451028626
+- Ui
+- Retry notifications
+- Instance email settings
+- Ui
+- Test email on for admins or custom smtp
+- Coolify already exists should not throw error
+- Delete database related things when delete database
+- Remove -q from docker compose
+- Errors in views
+- Only send internal notifcations to enabled channels
+- Recovery code
+- Email sending error
+- Sentry 4469575117
+- Old docker version error
+- Errors
+- Proxy check, reduce jobs, etc
+- Queue after commit
+- Remove nixpkgarchive
+- Remove nixpkgarchive from ui
+- Webhooks should not run if server is not functional
+- Server is functional check
+- Confirm email before sending
+- Help should send cc on email
+- Sub type
+- Show help modal everywhere
+- Forgot password
+- Disable dockerfile based healtcheck for now
+- Add timeout for ssh commands
+- Prevent weird ui bug for validateServer
+- Lowercase email in forgot password
+- Lower case email on waitlist
+- Encrypt jobs
+- ProcessWithEnv()->run
+- Plus boarding step about Coolify
+- SaveConfigurationSync
+- Help uri
+- Sub for root
+- Redirect on server not found
+- Ip check
+- Uniqueips
+- Simply reply to help messages
+- Help
+- Rate limit
+- Collect billing address
+- Invitation
+- Smtp view
+- Ssh-agent revert
+- Restarting container state on ui
+- Generate new key
+- Missing upgrade js
+- Team error
+- 4.0.0-beta.37
+- Localhost
+- Proxy start (if not proxy defined, use Traefik)
+- Do not remove localhost in boarding
+- Allow non ip address (DNS)
+- InstallDocker id not found
+- Boarding
+- Errors
+- Proxy container status
+- Proxy configuration saving
+- Convert startProxy to action
+- Stop/start UI on apps and dbs
+- Improve localhost boarding process
+- Try to use old docker-compose
+- Boarding again
+- Send internal notifications of email errors
+- Add github app change on new app view
+- Delete environment variables on app/db delete
+- Save proxy configuration
+- Add proxy to network with periodic check
+- Proxy connections
+- Delete persistent storages on resource deletion
+- Prevent overwrite already existing env variables in services
+- Mappings
+- Sentry issue 4478125289
+- Make sure proxy path created
+- StartProxy
+- Server validation with cf tunnels
+- Only show traefik dashboard if its available
+- Services
+- Database schema
+- Report livewire errors
+- Links with path
+- Add traefik labels no matter if traefik is selected or not
+- Add expose port for containers
+- Also check docker socks permission on validation
+- Applications with port mappins do a normal update (not rolling update)
+- Put back build pack chooser
+- Proxy configuration + starter
+- Show real storage name on services
+- New service template layout
+- Containerstatusjob
+- Aaaaaaaaaaaaaaaaa
+- Services view
+- Services
+- Manually create network for services
+- Disable early updates
+- Sslip for localhost
+- ContainerStatusJob
+- Cannot delete env with available services
+- Sync command
+- Install script drops an error
+- Prevent sync version (it needs an option)
+- Instance fqdn setting
+- Sentry 4510197209
+- Sentry 4504136641
+- Sentry 4502634789
+- Next helper image
+- Service templates
+- Sync:bunny
+- Update process if server has been renamed
+- Reporting handler
+- Localhost privatekey update
+- Remove private key in case you removed a github app
+- Only show manually added private keys on server view
+- Show source on all type of applications
+- Docker cleanup should be a job by server
+- File/dir based volumes are now read from the server
+- Respect server fqdn
+- If public repository does not have a main branch
+- Preselect branc on private repos
+- Deploykey branch
+- Backups are now working again
+- Not found base_branch in git webhooks
+- Coolify db backup
+- Preview deployments name, status etc
+- Services should have destination as well
+- Dockerfile expose is not overwritten
+- If app settings is not saved to db
+- Do not show subscription cancelled noti
+- Show real volume names
+- Only parse expose in dockerfiles if ports_exposes is empty
+- Add uuid to volume names
+- New volumes for services should have - instead of _
+- Always pull helper image in dev
+- Only show last 1000 lines
+- Service status
+- If waitlist is disabled, redirect to register
+- Add destination to new services
+- Predefined content for files
+- Move /data to ./_data in dev
+- UI
+- Show all storages in one place for services
+- Ui
+- Add _data to vite ignore
+- Only use _ in volume names for services
+- Volume names in services
+- Volume names
+- Service logs visible if the whole service stack is not running
+- Ui
+- Compose magic
+- Compose parser updated
+- Dev compose files
+- Traefik labels for multiport deployments
+- Visible version number
+- Remove SERVICE_ from deployable compose
+- Delete event to deleting
+- Move dev data to volumes to prevent permission issues
+- Traefik labelling in case of several http and https domain added
+- PR deployments use the first fqdn as base
+- Email notifications subscription fixed
+- Services - do not remove unnecessary things for now
+- Decrease max horizon processes to get lower memory usage
+- Test emails only available for user owned smtp/resend
+- Ui for self-hosted email settings
+- Set smtp notifications on by default
+- Select branch on other git
+- Private repository
+- Contribution guide
+- Public repository names
+- *(create)* Flex wrap on server & network selection
+- Better unreachable/revived server statuses
+- Able to set base dir for Dockerfile build pack
+- Server validation process
+- Fqdn could be null
+- Small
+- Server unreachable count
+- Do not reset unreachable count
+- Contact docs
+- Check connection
+- Server saving
+- No env goto envs from dashboard
+- Goto
+- Tcp proxy for dbs
+- Database backups
+- Only send email if transactional email set
+- Backupfailed notification is forced
+- Use port exposed for reverse proxy
+- Contact link
+- Use only ip addresses for servers
+- Deleted team and it is the current one
+- Add new team button
+- Transactional email link
+- Dashboard goto link
+- Only require registry image in case of dockerimage bp
+- Instant save build pack change
+- Public git
+- Cannot remove localhost
+- Check localhost connection
+- Send unreachable/revived notifications
+- Boarding + verification
+- Make sure proxy wont start in NONE mode
+- Service check status 10 sec
+- IsCloud in production seeder
+- Make sure to use IP address
+- Dockerfile location feature
+- Server ip could be hostname in self-hosted
+- Urls should be password fields
+- No backup for redis
+- Show database logs in case of its not healthy and running
+- Proxy check for ports, do not kill anything listening on port 80/443
+- Traefik dashboard ip
+- Db labels
+- Docker cleanup jobs
+- Timeout for instant remote processes
+- Dev containerjobs
+- Backup database one-by-one.
+- Turn off static deployment if you switch buildpacks
+- Docker hub URL
+- Redis URL generated
+- Build image before starting dockerfile buildpacks
+- Service status check is a bit better
+- Generate fqdn if you deleted a service app, but it requires fqdn
+- Cancel any deployments + queue next
+- Add internal domain names during build process
+- Noindex meta tag
+- Show docker build logs
+- Only include config.json if its exists and a file
+- Always start proxy if not NONE is selected
+- Proxy start process
+- Setup:dev script & contribution guide
+- Do not show configuration changed if config_hash is null
+- Add config_hash if its null (old deployments)
+- Label generation
+- Labels
+- Email channel no recepients
+- Limit horizon processes to 2 by default
+- Add custom port as ssh option to deploy_key based commands
+- Remove custom port from git repo url
+- ContainerStatus job
+- Service docs links
+- Add PGUSER to prevent HC warning
+- Preselect s3 storage if available
+- Port exposes change, shoud regenerate label
+- Boarding
+- Clone to with the same environment name
+- Cleanup stucked resources on start
+- Do not allow to delete env if a resource is defined
+- Service template generator + appwrite
+- Mongodb backup
+- Make sure coolfiy network exists on install
+- Syncbunny command
+- Encrypt mongodb password
+- Mongodb healtcheck command
+- Rate limit for api + add mariadb + mysql
+- Server settings guarded
+- Space in build args
+- Lock SERVICE_FQDN envs
+- If user is invited, that means its email is verified
+- Force password reset on invited accounts
+- Add ssh options to git ls-remote
+- Git ls-remote
+- Remove coolify labels from ui
+- Missing environment variables prevewi on service
+- Invoice.paid should sleep for 5 seconds
+- Local dev repo
+- Deployments ui
+- Dockerfile build pack fix
+- Set labels on generate domain
+- Network service parse
+- Notification url in containerstatusjob
+- Gh webhook response 200 to installation_repositories
+- Delete destination
+- No id found
+- Missing $mailMessage
+- Set default from/sender names
+- No environments
+- Telegram text
+- Private key not found error
+- UI
+- Resourcesdelete command
+- Port number should be int
+- Separate delete with validation of server
+- Add nixpacks info
+- Remove filter
+- Container logs are now followable in full-screen and sorted by timestamp
+- Ui for labels
+- Ui
+- Deletions
+- Build_image not found
+- Github source view
+- Github source view
+- Dockercleanupjob should be released back
+- Ui
+- Local ip address
+- Revert workdir to basedir
+- Container status jobs for old pr deployments
+- Service updates
+- *(fider template)* Use the correct docs url
+- Fqdn for minio
+- Generate service fields
+- Mariadb backups
+- When to pull image
+- Do not allow to enter local ip addresses
+- Reset password
+- Only report nonruntime errors
+- Handle different label formats in services
+- Server adding process
+- Show defined resources in server tab, so you will know what you need to delete before you can delete the server.
+- Lots of regarding git + docker compose deployments
+- Pull request build variables
+- Double default password length
+- Do not remove deployment in case compose based failed
+- No container servers
+- Sentry issue
+- Dockercompose save ./ volumes under /data/coolify
+- Server view for link()
+- Default value do not overwrite existing env value
+- Use official install script with rancher (one will work for sure)
+- Add cf tunnel to boarding server view
+- Prevent autorefresh of proxy status
+- Missing docker image thing
+- Add hc for soketi
+- Deploy the right compose file
+- Bind volumes for compose bp
+- Use hc port 80 in case of static build
+- Switching to static build
+- Container selection
+- Service navbar using new realtime events
+- Do not create duplicated networks
+- Live event
+- Service start + event
+- Service deletion job
+- Double ws connection
+- Boarding view
+- Do not send telegram noti on intent payment failed
+- Database ui is realtime based
+- Live mode for github webhooks
+- Ui
+- Realtime connection popup could be disabled
+- Realtime check
+- Add new destination
+- Proxy logs
+- Db status check
+- Pusher host
+- Add ipv6
+- Realtime connection?!
+- Websocket
+- Better handling of errors with install script
+- Install script parse version
+- Only allow to modify in .env file if AUTOUPDATE is set
+- Is autoupdate not null
+- Run init command after production seeder
+- Init
+- Comma in traefik custom labels
+- Ignore if dynamic config could not be set
+- Service env variable ovewritten if it has a default value
+- Labelling
+- Non-ascii chars in labels
+- Labels
+- Init script echos
+- Update Coolify script
+- Null notify
+- Check queued deployments as well
+- Copy invitation
+- Password reset / invitation link requests
+- Add catch all route
+- Revert random container job delay
+- Backup executions view
+- Only check server status in container status job
+- Improve server status check times
+- Handle other types of generated values
+- Server checking status
+- Ui for adding new destination
+- Reset domains on compose file change
+- Domains for compose bp
+- No action in webhooks
+- Add debug output to gitlab webhooks
+- Do not push dockerimage
+- Add alpha to swarm
+- Server not found
+- Do not autovalidate server on mount
+- Server update schedule
+- Swarm support ui
+- Server ready
+- Get swarm service logs
+- Docker compose apps env rewritten
+- Storage error on dbs
+- Why?!
+- Stay tuned
+- Cpu limit to float from int
+- Add source commit to final envs
+- Routing, switch back to old one
+- Deploy instead of restart in case swarm is used
+- Button title
+- Restore falsely deleted coolify-db-backup
+- Sub
+- Wrong env variable parsing
+- Deploy key + docker compose
+- Horizon
+- Duplicate compose variable
+- Set deployment failed if new container is not healthy
+- Nixpacks cache
+- Only add restart policy if its empty (compose)
+- Nixpacks buildpack
+- File storage save
+- Database env variables
+- Healthy status
+- Show framework based notification in build logs
+- Traefik labels
+- Use ip for sslip in dev if remote server is used
+- Service labels without ports (unknown ports)
+- Sort and rename (unique part) of labels
+- Settings menu
+- Remove traefik debug in dev mode
+- Php pgsql to 8.2
+- Static buildpack should set port 80
+- Update navbar on build_pack change
+- Do not include thegameplan.json into build image
+- Submit error on postgresql
+- Email verification / forgot password
+- Escape build envs properly for nixpacks + docker build
+- Undead endpoint
+- Upload limit on ui
+- Save cmd output propely (merge)
+- Load profile on remote commands
+- Load profile and set envs on remote cmd
+- Restart should not update config hash
+- Preview deployments with nixpacks
+- Cleanup docker stuffs before upgrading
+- Service deletion command
+- Cpuset limits was determined in a way that apps only used 1 CPU max, ehh, sorry.
+- Service stack view
+- Change proxy view
+- Checkbox click
+- Git pull command for deploy key based previews
+- Server status job
+- Service deletion bug!
+- Links
+- Redis custom conf
+- Sentry error
+- Restrict concurrent deployments per server
+- Queue
+- Change env variable length
+- Bitbucket manual deployments
+- Webhooks for multiple apps
+- Unhealthy deployments should be failed
+- Add env variables for wordpress template without database
+- Service deletion function
+- Service deletion fix
+- Dns validation + duplicated fqdns
+- Validate server navbar upated
+- Regenerate labels on application clone
+- Service deletion
+- Not able to use other shared envs
+- Sentry fix
+- Sentry
+- Sentry error
+- Sentry
+- Sentry error
+- Create dynamic directory
+- Migrate to new modal
+- Duplicate domain check
+- Tags
+- Wrap tags and avoid horizontal overflow
+- Stripe webhooks
+- Feedback from self-hosted envs to discord
+- New menu on navbar
+- Make sure resources are deleted in async mode
+- Go to prod env from dashboard if there is no other envs defined
+- User proper image_tag, if set
+- New menu ui
+- Lock logdrain configuration when one of them are enabled
+- Add docker compose check during server validation
+- Get service stack as uuid, not name
+- Menu
+- Flex wrap deployment previews
+- Boolean docker options
+- Only add 'networks' key if 'network_mode' is absent
+- Cleanup scheduled tasks
+- Padding left on input boxes
+- Use ls / command instead ls
+- Do not add the same server twice
+- Only show redeployment required if status is not exited
+- Add openbsd ssh server check
+- Resources
+- Empty build variables
+- *(server)* Revalidate server button not showing in server's page
+- Fluent bit ident level
+- Submodule cloning
+- Database status
+- Permission change updates from webhook
+- Server validation
+- Connections being stuck and not processed until proxy restarts
+- Use latest image if nothing is specified
+- No coolify.yaml found
+- Server validation
+- Statuses
+- Unknown image of service until it is uploaded
+- Subscription / plan switch, etc
+- Firefly service
+- Force enable/disable server in case ultimate package quantity decreases
+- Server disabled
+- Custom dockerfile location always checked
+- Import to mysql and mariadb
+- Resource tab not loading if server is not reachable
+- Load unmanaged async
+- Do not show n/a networsk
+- Service container status updates
+- Public prs should not be commented
+- Pull request deployments + build servers
+- Env value generation
+- Sentry error
+- Service status updated
+- Should note delete personal teams
+- Make sure to show some buttons
+- Sort repositories by name
+- Deploy api messages
+- Fqdn null in case docker compose bp
+- Reload caddy issue
+- /realtime endpoint
+- Proxy switch
+- Service ports for services + caddy
+- Failed deployments should send failed email/notification
+- Consider custom healthchecks in dockerfile
+- Create initial files async
+- Docker compose validation
+- Duplicate dockerfile
+- Multiline env variables
+- Server stopped, service page not reachable
+- Empty get logs number of lines
+- Only escape envs after v239+
+- 0 in env value
+- Consistent container name
+- Custom ip address should turn off rolling update
+- Multiline input
+- Raw compose deployment
+- Dashboard view if no project found
+- Volumes for prs
+- Shared env variable parsing
+- Compose env has SERVICE, but not defined for Coolify
+- Public service database
+- Make sure service db proxy restarted
+- Restart service db proxies
+- Two factor
+- Ui for tags
+- Update resources view
+- Realtime connection check
+- Multline env in dev mode
+- Scheduled backup for other service databases (supabase)
+- PR deployments should not be distributed to 2 servers
+- Name/from address required for resend
+- Autoupdater
+- Async service loads
+- Disabled inputs are not trucated
+- Duplicated generated fqdns are now working
+- Uis
+- Ui for cftunnels
+- Search services
+- Trial users subscription page
+- Async public key loading
+- Unfunctional server should see resources
+- Warning if you use multiple domains for a service
+- New github app creation
+- Always rebuild Dockerfile / dockerimage buildpacks
+- Do not rebuild dockerfile based apps twice
+- Make sure if envs are changed, rebuild is needed
+- Members cannot manage subscriptions
+- IsMember
+- Storage layout
+- How to update docker-compose, environment variables and fqdns
+- Git submodule update
+- Unintended left padding on sidebar
+- Hashed random delimeter in ssh commands + make sure to remove the delimeter from the command
+- Service config hash update
+- Redeploy if image not found in restart only mode
+- Check each required binaries one-by-one
+- Helper image only pulled if required, not every 10 mins
+- Make sure that confs when checking if it is changed sorted
+- Respect .env file (for default values)
+- Remove temporary cloudflared config
+- Remove lazy loading until bug figured out
+- Rollback feature
+- Base64 encode .env
+- $ in labels escaped
+- .env saved to deployment server, not to build server
+- Do no able to delete gh app without deleting resources
+- 500 error on edge case
+- Able to select server when creating new destination
+- N8n template
+- Refresh public ips on start
+- Move s3 storages to separate view
+- Mongo db backup
+- Backups
+- Autoupdate
+- Respect start period and chekc interval for hc
+- Parse HEALTHCHECK from dockerfile
+- Make s3 name and endpoint required
+- Able to update source path for predefined volumes
+- Get logs with non-root user
+- Mongo 4.0 db backup
+- Formbricks image origin
+- Add port even if traefik is used
+- Typo in tags.blade.php
+- Install.sh error
+- Env file
+- Comment out internal notification in email_verify method
+- Confirmation for custom labels
+- Change permissions on newly created dirs
+- Color for resource operation server and project name
+- Only show realtime error on non-cloud instances
+- Only allow push and mr gitlab events
+- Improve scheduled task adding/removing
+- Docker compose dependencies for pr previews
+- Properly populating dependencies
+- Use commit hash on webhooks
+- Commit message length
+- Hc from localhost to 127.0.0.1
+- Use rc in hc
+- Telegram group chat notifications
+- PR deployments have good predefined envs
+- Optimize new resource creation
+- Show it docker compose has syntax errors
+- Wrong time during a failed deployment
+- Removal of the failed deployment condition, addition of since started instead of finished time
+- Use local versions + service templates and query them every 10 minutes
+- Check proxy functionality before removing unnecessary coolify.yaml file and checking Docker Engine
+- Show first 20 users only in admin view
+- Add subpath for services
+- Ghost subdir
+- Do not pull templates in dev
+- Templates
+- Update error message for invalid token to mention invalid signature
+- Disable containerStopped job for now
+- Disable unreachable/revived notifications for now
+- JSON_UNESCAPED_UNICODE
+- Add wget to nixpacks builds
+- Pre and post deployment commands
+- Bitbucket commits link
+- Better way to add curl/wget to nixpacks
+- Root team able to download backups
+- Build server should not have a proxy
+- Improve build server functionalities
+- Sentry issue
+- Sentry
+- Sentry error + livewire downgrade
+- Sentry
+- Sentry
+- Sentry error
+- Sentry
+- Force load services from cdn on reload list
+- Do not allow service storage mount point modifications
+- Volume adding
+- Sync upgrade process
+- Publish horizon
+- Add missing team model
+- Test new upgrade process?
+- Throw exception
+- Build server dirs not created on main server
+- Compose load with non-root user
+- Able to redeploy dockerfile based apps without cache
+- Compose previews does have env variables
+- Fine-tune cdn pulls
+- Spamming :D
+- Parse docker version better
+- Compose issues
+- SERVICE_FQDN has source port in it
+- Logto service
+- Allow invitations via email
+- Sort by defined order + fixed typo
+- Only ignore volumes with driver_opts
+- Check env in args for compose based apps
+- Custom docker compose commands, add project dir if needed
+- Autoupdate process
+- Backup executions view
+- Handle previously defined compose previews
+- Sort backup executions
+- Supabase service, newest versions
+- Set default name for Docker volumes if it is null
+- Multiline variable should be literal + should be multiline in bash with \
+- Gitlab merge request should close PR
+- Multiline build args
+- Setup script doesnt link to the correct source code file
+- Install.sh do not reinstall packages on arch
+- Just restart
+- Stripprefix middleware correctly labeled to http
+- Bitbucket link
+- Compose generator
+- Do no truncate repositories wtih domain (git) in it
+- In services should edit compose file for volumes and envs
+- Handle laravel deployment better
+- Db proxy status shown better in the UI
+- Show commit message on webhooks + prs
+- Metrics parsing
+- Charts
+- Application custom labels reset after saving
+- Static build with new nixpacks build process
+- Make server charts one livewire component with one interval selector
+- You can now add env variable from ui to services
+- Update compose environment with UI defined variables
+- Refresh deployable compose without reload
+- Remove cloud stripe notifications
+- App deployment should be in high queue
+- Remove zoom from modals
+- Get envs before sortby
+- MB is % lol
+- Projects with 0 envs
+- Run user commands on high prio queue
+- Load js locally
+- Remove lemon + paddle things
+- Run container commands on high priority
+- Image logo
+- Remove both option for api endpoints. it just makes things complicated
+- Cleanup subs in cloud
+- Show keydbs/dragonflies/clickhouses
+- Only run cloud clean on cloud + remove root team
+- Force cleanup on busy servers
+- Check domain on new app via api
+- Custom container name will be the container name, not just internal network name
+- Api updates
+- Yaml everywhere
+- Add newline character to private key before saving
+- Add validation for webhook endpoint selection
+- Database input validators
+- Remove own app from domain checks
+- Return data of app update
+- Do not overwrite hardcoded variables if they rely on another variable
+- Remove networks when deleting a docker compose based app
+- Api
+- Always set project name during app deployments
+- Remove volumes as well
+- Gitea pr previews
+- Prevent instance fqdn persisting to other servers dynamic proxy configs
+- Better volume cleanups
+- Cleanup parameter
+- Update redirect URL in unauthenticated exception handler
+- Respect top-level configs and secrets
+- Service status changed event
+- Disable sentinel until a few bugs are fixed
+- Service domains and envs are properly updated
+- *(reactive-resume)* New healthcheck command for MinIO
+- *(MinIO)* New command healthcheck
+- Update minio hc in services
+- Add validation for missing docker compose file
+- Typo in is_literal helper
+- Env is_literal helper text typo
+- Update docker compose pull command with --policy always
+- Plane service template
+- Vikunja
+- Docmost template
+- Drupal
+- Improve github source creation
+- Tag deployments
+- New docker compose parsing
+- Handle / in preselecting branches
+- Handle custom_internal_name check in ApplicationDeploymentJob.php
+- If git limit reached, ignore it and continue with a default selection
+- Backup downloads
+- Missing input for api endpoint
+- Volume detection (dir or file) is fixed
+- Supabase
+- Create file storage even if content is empty
+- Preview deployments should be stopped properly via gh webhook
+- Deleting application should delete preview deployments
+- Plane service images
+- Fix issue with deployment start command in ApplicationDeploymentJob
+- Directory will be created by default for compose host mounts
+- Restart proxy does not work + status indicator on the UI
+- Uuid in api docs type
+- Raw compose deployment .env not found
+- Api -> application patch endpoint
+- Remove pull always when uploading backup to s3
+- Handle array env vars
+- Link in task failed job notifications
+- Random generated uuid will be full length (not 7 characters)
+- Gitlab service
+- Gitlab logo
+- Bitbucket repository url
+- By default volumes that we cannot determine if they are directories or files are treated as directories
+- Domain update on services on the UI
+- Update SERVICE_FQDN/URL env variables when you change the domain
+- Several shared environment variables in one value, parsed correctly
+- Members of root team should not see instance admin stuff
+- Parse docker composer
+- Service env parsing
+- Service env variables
+- Activity type invalid
+- Update env on ui
+- Only append docker network if service/app is running
+- Remove lazy load from scheduled tasks
+- Plausible template
+- Service_url should not have a trailing slash
+- If usagebefore cannot be determined, cleanup docker with force
+- Async remote command
+- Only run logdrain if necessary
+- Remove network if it is only connected to coolify proxy itself
+- Dir mounts should have proper dirs
+- File storages (dir/file mount) handled properly
+- Do not use port exposes on docker compose buildpacks
+- Minecraft server template fixed
+- Graceful shutdown
+- Stop resources gracefully
+- Handle null and empty disk usage in DockerCleanupJob
+- Show latest version on manual update view
+- Empty string content should be saved as a file
+- Update Traefik labels on init
+- Add missing middleware for server check job
+- Scheduledbackup not found
+- Manual update process
+- Timezone not updated when systemd is missing
+- If volumes + file mounts are defined, should merge them together in the compose file
+- All mongo v4 backups should use the different backup command
+- Database custom environment variables
+- Connect compose apps to the right predefined network
+- Docker compose destination network
+- Server status when there are multiple servers
+- Sync fqdn change on the UI
+- Pr build names in case custom name is used
+- Application patch request instant_deploy
+- Canceling deployment on build server
+- Backup of password protected postgresql database
+- Docker cleanup job
+- Storages with preserved git repository
+- Parser parser parser
+- New parser only in dev
+- Parser parser
+- Numberoflines should be number
+- Docker cleanup job
+- Fix directory and file mount headings in file-storage.blade.php
+- Preview fqdn generation
+- Revert a few lines
+- Service ui sync bug
+- Setup script doesn't work on rhel based images with some curl variant already installed
+- Let's wait for healthy container during installation and wait an extra 20 seconds (for migrations)
+- Infra files
+- Log drain only for Applications
+- Copy large compose files through scp (not ssh)
+- Check if array is associative or not
+- Openapi endpoint urls
+- Convert environment variables to one format in shared.php
+- Logical volumes could be overwritten with new path
+- Env variable in value parsed
+- Pull coolify image only when the app needs to be updated
+- Wrong executions order
+- Handle project not found error in environment_details API endpoint
+- Deployment running for - without "ago"
+- Update helper image pulling logic to only pull if the version is newer
+- Parser
+- Plunk NEXT_PUBLIC_API_URI
+- Reenable overlapping servercheckjob
+- Appwrite template + parser
+- Don't add `networks` key if `network_mode` is used
+- Remove debug statement in shared.php
+- Scp through cloudflare
+- Delete older versions of the helper image other than the latest one
+- Update remoteProcess.php to handle null values in logItem properties
+- Disable mux_enabled during server validation
+- Move mc command to coolify image from helper
+- Keydb. add `:` delimiter for connection string
+- Cloudflare tunnel with new multiplexing feature
+- Keep-alive ws connections
+- Add build.sh to debug logs
+- Update Coolify installer
+- Terminal
+- Generate https for minio
+- Install script
+- Handle WebSocket connection close in terminal.blade.php
+- Able to open terminal to any containers
+- Refactor run-command
+- If you exit a container manually, it should close the underlying tty as well
+- Move terminal to separate view on services
+- Only update helper image in DB
+- Generated fqdn for SERVICE_FQDN_APP_3000 magic envs
+- Proxy status
+- Coolify-db should not be in the managed resources
+- Store original root key in the original location
+- Logto service
+- Cloudflared service
+- Migrations
+- Cloudflare tunnel configuration, ui, etc
+- Parser
+- Exited services statuses
+- Make sure to reload window if app status changes
+- Deploy key based deployments
+- Proxy fixes
+- Proxy
+- *(templates)* Filebrowser FQDN env variable
+- Handle edge case when build variables and env variables are in different format
+- Compose based terminal
+- Filebrowser template
+- Edit is_build_server_enabled upon creating application on other application type
+- Save settings after assigning value
+- In dev mode do not ask confirmation on delete
+- Mixpost
+- Handle deletion of 'hello' in confirmation modal for dev environment
+- Remove autofocuses
+- Ipv6 scp should use -6 flag
+- Cleanup stucked applicationdeploymentqueue
+- Realtime watch in development mode
+- Able to select root permission easier
+- Able to support more database dynamically from Coolify's UI
+- Strapi template
+- Bitcoin core template
+- Api useBuildServer
+- Service application view
+- Add new supported database images
+- Parse proxy config and check the set ports usage
+- Update FQDN
+- Scheduled backup for services view
+- Parser, espacing container labels
+- Reset description and subject fields after submitting feedback
+- Tag mass redeployments
+- Service env orders, application env orders
+- Proxy conf in dev
+- One-click services
+- Use local service-templates in dev
+- New services
+- Remove not used extra host
+- Chatwoot service
+- Directus
+- Database descriptions
+- Update services
+- Soketi
+- Select server view
+- Update mattermost image tag and add default port
+- Remove env, change timezone
+- Postgres healthcheck
+- Azimutt template - still not working haha
+- New parser with SERVICE_URL_ envs
+- Improve service template readability
+- Update password variables in Service model
+- Scheduled database server
+- Select server view
+- Signup
+- Application domains should be http and https only
+- Validate and sanitize application domains
+- Sanitize and validate application domains
+- Use correct env variable for invoice ninja password
+- Make sure caddy is not removed by cleanup
+- Libretranslate
+- Do not allow to change number of lines when streaming logs
+- Plunk
+- No manual timezones
+- Helper push
+- Format
+- Add port metadata and Coolify magic to generate the domain
+- Sentinel
+- Metrics
+- Generate sentinel url
+- Only enable Sentinel for new servers
+- Is_static through API
+- Allow setting standalone redis variables via ENVs (team variables...)
+- Check for username separately form password
+- Encrypt all existing redis passwords
+- Pull helper image on helper_version change
+- Redis database user and password
+- Able to update ipv4 / ipv6 instance settings
+- Metrics for dbs
+- Sentinel start fixed
+- Validate sentinel custom URL when enabling sentinel
+- Should be able to reset labels in read-only mode with manual click
+- No sentinel for swarm yet
+- Charts ui
+- Volume
+- Sentinel config changes restarts sentinel
+- Disable sentinel for now
+- Disable Sentinel temporarily
+- Disable Sentinel temporarily for non-dev environments
+- Access team's github apps only
+- Admins should now invite owner
+- Add experimental flag
+- GenerateSentinelUrl method
+- NumberOfLines could be null
+- Login / register view
+- Restart sentinel once a day
+- Changing private key manually won't trigger a notification
+- Grammar for helper
+- Fix my own grammar
+- Add telescope only in dev mode
+- New way to update container statuses
+- Only run server storage every 10 mins if sentinel is not active
+- Cloud admin view
+- Queries in kernel.php
+- Lower case emails only
+- Change emails to lowercase on init
+- Do not error on update email
+- Always authenticate with lowercase emails
+- Dashboard refactor
+- Add min/max length to input/texarea
+- Remove livewire legacy from help view
+- Remove unnecessary endpoints (magic)
+- Transactional email livewire
+- Destinations livewire refactor
+- Refactor destination/docker view
+- Logdrains validation
+- Reworded
+- Use Auth(), add new db proxy stop event refactor clickhouse view
+- Add user/pw to db view
+- Sort servers by name
+- Keydb view
+- Refactor tags view / remove obsolete one
+- Send discord/telegram notifications on high job queue
+- Server view refresh on validation
+- ShowBoarding
+- Show docker installation logs & ubuntu 24.10 notification
+- Do not overlap servercheckjob
+- Server limit check
+- Server validation
+- Clear route / view
+- Only skip docker installation on 24.10 if its not installed
+- For --gpus device support
+- Db/service start should be on high queue
+- Do not stop sentinel on Coolify restart
+- Run resourceCheck after new serviceCheckJob
+- Mongodb in dev
+- Better invitation errors
+- Loading indicator for db proxies
+- Do not execute gh workflow on template changes
+- Only use sentry in cloud
+- Update packagejson of coolify-realtime + add lock file
+- Update last online with old function
+- Seeder should not start sentinel
+- Start sentinel on seeder
+- Notifications ui
+- Disable wire:navigate
+- Confirmation Settings css for light mode
+- Server wildcard
+- Saving resend api key
+- Wildcard domain save
+- Disable cloudflare tunnel on "localhost"
+- Define separate volumes for mattermost service template
+- Github app name is too long
+- ServerTimezone update
+- Trigger.dev db host & sslmode=disable
+- Manual update should be executed only once + better UX
+- Upgrade.sh
+- Missing privateKey
+- Show proper error message on invalid Git source
+- Convert HTTP to SSH source when using deploy key on GitHub
+- Cloud + stripe related
+- Terminal view loading in async
+- Cool 500 error (thanks hugodos)
+- Update schema in code decorator
+- Openapi docs
+- Add tests for git url converts
+- Minio / logto url generation
+- Admin view
+- Min docker version 26
+- Pull latest service-templates.json on init
+- Workflow files for coolify build
+- Autocompletes
+- Timezone settings validation
+- Invalid tz should not prevent other jobs to be executed
+- Testing-host should be built locally
+- Poll with modal issue
+- Terminal opening issue
+- If service img not found, use github as a source
+- Fallback to local coolify.png
+- Gather private ips
+- Cf tunnel menu should be visible when server is not validated
+- Deployment optimizations
+- Init script + optimize laravel
+- Default docker engine version + fix install script
+- Pull helper image on init
+- SPA static site default nginx conf
+- Modal-input
+- Modal (+ add) on dynamic config was not opening, removed x-cloak
+- AUTOUPDATE + checkbox opacity
+- Improve helper text for metrics input fields
+- Refine helper text for metrics input fields
+- If mux conn fails, still use it without mux + save priv key with better logic
+- Migration
+- Always validate ssh key
+- Make sure important jobs/actions are running on high prio queue
+- Do not send internal notification for backups and status jobs
+- Validateconnection
+- View issue
+- Heading
+- Remove mux cleanup
+- Db backup for services
+- Version should come from constants + fix stripe webhook error reporting
+- Undefined variable
+- Remove version.php as everything is coming from constants.php
+- Sentry error
+- Websocket connections autoreconnect
+- Sentry error
+- Sentry
+- Empty server API response
+- Incorrect server API patch response
+- Missing `uuid` parameter on server API patch
+- Missing `settings` property on servers API
+- Move servers API `delete_unused_*` properties
+- Servers API returning `port` as a string -> integer
+- Only return server uuid on server update
+- Service generate includes yml files as well (haha)
+- ServercheckJob should run every 5 minutes on cloud
+- New resource icons
+- Search should be more visible on scroll on new resource
+- Logdrain settings
+- Ui
+- Email should be retried with backoff
+- Alpine in body layout
+- Application view loading
+- Postiz service
+- Only able to select the right keys
+- Test email should not be required
+- A few inputs
+- Api endpoint
+- Resolve undefined searchInput reference in Alpine.js component
+- URL and sync new app name
+- Typos and naming
+- Client and webhook secret disappear after sync
+- Missing `mysql_password` API property
+- Incorrect MongoDB init API property
+- Old git versions does not have --cone implemented properly
+- Don't allow editing traefik config
+- Restart proxy
+- Dev mode
+- Ui
+- Display actual values for disk space checks in installer script
+- Proxy change behaviour
+- Add warning color
+- Import NotificationSlack correctly
+- Add middleware to new abilities, better ux for selecting permissions, etc.
+- Root + read:sensive could read senstive data with a middlewarew
+- Always have download logs button on scheduled tasks
+- Missing css
+- Development image
+- Dockerignore
+- DB migration error
+- Drop all unused smtp columns
+- Backward compatibility
+- Email notification channel enabled function
+- Instance email settins
+- Make sure resend is false if SMTP is true and vice versa
+- Email Notification saving
+- Slack and discord url now uses text filed because encryption makes the url very long
+- Notification trait
+- Encryption fixes
+- Docker cleanup email template
+- Add missing deployment notifications to telegram
+- New docker cleanup settings are now saved to the DB correctly
+- Ui + migrations
+- Docker cleanup email notifications
+- General notifications does not go through email channel
+- Test notifications to only send it to the right channel
+- Remove resale_license from db as well
+- Nexus service
+- Fileflows volume names
+- --cone
+- Provider error
+- Database migration
+- Seeder
+- Migration call
+- Slack helper
+- Telegram helper
+- Discord helper
+- Telegram topic IDs
+- Make pushover settings more clear
+- Typo in pushover user key
+- Use Livewire refresh method and lock properties
+- Create pushover settings for existing teams
+- Update token permission check from 'write' to 'root'
+- Pushover
+- Oauth seeder
+- Correct heading display for OAuth settings in settings-oauth.blade.php
+- Adjust spacing in login form for improved layout
+- Services env values should be sensitive
+- Documenso
+- Dolibarr
+- Typo
+- Update OauthSettingSeeder to handle new provider definitions and ensure authentik is recreated if missing
+- Improve OauthSettingSeeder to correctly delete non-existent providers and ensure proper handling of provider definitions
+- Encrypt resend API key in instance settings
+- Resend api key is already a text column
+- Monaco editor light and dark mode switching
+- Service status indicator + oauth saving
+- Socialite for azure and authentik
+- Saving oauth
+- Fallback for copy button
+- Copy the right text
+- Maybe fallback is now working
+- Only show copy button on secure context
+- Render html on error page correctly
+- Invalid API response on missing project
+- Applications API response code + schema
+- Applications API writing to unavailable models
+- If an init script is renamed the old version is still on the server
+- Oauthseeder
+- Compose loading seq
+- Resource clone name + volume name generation
+- Update Dockerfile entrypoint path to /etc/entrypoint.d
+- Debug mode
+- Unreachable notifications
+- Remove duplicated ServerCheckJob call
+- Few fixes and use new ServerReachabilityChanged event
+- Use serverStatus not just status
+- Oauth seeder
+- Service ui structure
+- Check port 8080 and fallback to 80
+- Refactor database view
+- Always use docker cleanup frequency
+- Advanced server UI
+- Html css
+- Fix domain being override when update application
+- Use nixpacks predefined build variables, but still could update the default values from Coolify
+- Use local monaco-editor instead of Cloudflare
+- N8n timezone
+- Smtp encryption
+- Bind() to 0.0.0.0:80 failed
+- Oauth seeder
+- Unreachable notifications
+- Instance settings migration
+- Only encrypt instance email settings if there are any
+- Error message
+- Update healthcheck and port configurations to use port 8080
+- Compose envs
+- Scheduled tasks and backups are executed by server timezone.
+- Show backup timezone on the UI
+- Disappearing UI after livewire event received
+- Add default vector db for anythingllm
+- We need XSRF-TOKEN for terminal
+- Prevent default link behavior for resource and settings actions in dashboard
+- Increase default php memory limit
+- Show if only build servers are added to your team
+- Update Livewire button click method to use camelCase
+- Local dropzonejs
+- Import backups due to js stuff should not be navigated
+- Install inetutils on Arch Linux
+- Use ip in place of hostname from inetutils in arch
+- Update import command to append file redirection for database restoration
+- Ui bug on pw confirmation
+- Exclude system and computed fields from model replication
+- Service cloning on a separate server
+- Application cloning
+- `Undefined variable $fs_path` for databases
+- Service and database cloning and label generation
+- Labels and URL generation when cloning
+- Clone naming for different database data volumes
+- Implement all the cloneMe changes for ResourceOperations as well
+- Volume and fileStorages cloning
+- View text and helpers
+- Teable
+- Trigger with external db
+- Set `EXPERIMENTAL_FEATURES` to false for labelstudio
+- Monaco editor disabled state
+- Edge case where executions could be null
+- Create destination properly
+- Getcontainer status should timeout after 30s
+- Enable response for temporary unavailability in sentinel push endpoint
+- Use timeout in cleanup resources
+- Add timeout to sentinel process checks for improved reliability
+- Horizon job checker
+- Update response message for sentinel push route
+- Add own servers on cloud
+- Application deployment
+- Service update statsu
+- If $SERVICE found in the service specific configuration, then search for it in the db
+- Instance wide GitHub apps are not available on other teams then the source team
+- Function calls
+- UI
+- Deletion of single backup
+- Backup job deletion - delete all backups from s3 and local
+- Use new removeOldBackups function
+- Retention functions and folder deletion for local backups
+- Storage retention setting
+- Db without s3 should still backup
+- Wording
+- `Undefined variable $service` when creating a new service
+- Nodebb service
+- Calibre-web service
+- Rallly and actualbudget service
+- Removed container_name
+- Added healthcheck for gotenberg template
+- Gotenberg
+- *(template)* Gotenberg healthcheck, use /health instead of /version
+- Use wire:navigate on sidebar
+- Use wire:navigate on dashboard
+- Use wire:navigate on projects page
+- More wire:navigate
+- Even more wire:navigate
+- Service navigation
+- Logs icons everywhere + terminal
+- Redis DB should use the new resourceable columns
+- Joomla service
+- Add back letters to prod password requirement
+- Check System and GitHub time and throw and error if it is over 50s out of sync
+- Error message and server time getting
+- Error rendering
+- Render html correctly now
+- Indent
+- Potential fix for permissions update
+- Expiration time claim ('exp') must be a numeric value
+- Sanitize html error messages
+- Production password rule and cleanup code
+- Use json as it is just better than string for huge amount of logs
+- Use `wire:navigate` on server sidebar
+- Use finished_at for the end time instead of created_at
+- Cancelled deployments should not show end and duration time
+- Redirect to server index instead of show on error in Advanced and DockerCleanup components
+- Disable registration after creating the root user
+- RootUserSeeder
+- Regex username validation
+- Add spacing around echo outputs
+- Success message
+- Silent return if envs are empty or not set.
+- Create the private key before the server in the prod seeder
+- Update ProductionSeeder to check for private key instead of server's private key
+- *(ui)* Missing underline for docs link in the Swarm section (#4860)
+- *(service)* Change chatwoot service postgres image from `postgres:12` to `pgvector/pgvector:pg12`
+- Docker image parser
+- Add public key attribute to privatekey model
+- Correct service update logic in Docker Compose parser
+- Update CDN URL in install script to point to nightly version
+- *(service)* Add healthcheck to Cloudflared service (#4859)
+- Remove wire:navigate from import backups
+- *(ui)* Backups link should not redirected to general
+- Envs with special chars during build
+- *(db)* `finished_at` timestamps are not set for existing deployments
+- Load service templates on cloud
+- *(email)* Transactional email sending
+- *(ui)* Add missing save button for new Docker Cleanup page
+- *(ui)* Show preview deployment environment variables
+- *(ui)* Show error on terminal if container has no shell (bash/sh)
+- *(parser)* Resource URL should only be parsed if there is one
+- *(core)* Compose parsing for apps
+- *(redis)* Update environment variable keys from standalone_redis_id to resourceable_id
+- *(routes)* Local API docs not available on domain or IP
+- *(routes)* Local API docs not available on domain or IP
+- *(core)* Update application_id references to resourable_id and resourable_type for Nixpacks configuration
+- *(core)* Correct spelling of 'resourable' to 'resourceable' in Nixpacks configuration for ApplicationDeploymentJob
+- *(ui)* Traefik dashboard url not working
+- *(ui)* Proxy status badge flashing during navigation
+- *(core)* Update environment variable generation logic in ApplicationDeploymentJob to handle different build packs
+- *(env)* Shared variables can not be updated
+- *(ui)* Metrics stuck in loading state
+- *(ui)* Use `wire:navigate` to navigate to the server settings page
+- *(service)* Plunk API & health check endpoint (#4925)
+- *(service)* Infinite loading and lag with invoiceninja service (#4876)
+- *(service)* Invoiceninja service
+- *(workflows)* `Waiting for changes` label should also be considered and improved messages
+- *(workflows)* Remove tags only if the PR has been merged into the main branch
+- *(terminal)* Terminal shows that it is not available, even though it is
+- *(labels)* Docker labels do not generated correctly
+- *(helper)* Downgrade Nixpacks to v1.29.0
+- *(labels)* Generate labels when they are empty not when they are already generated
+- *(storage)* Hetzner storage buckets not working
+- *(ui)* Update database control UI to check server functionality before displaying actions
+- *(ui)* Typo in upgrade message
+- *(ui)* Cloudflare tunnel configuration should be an info, not a warning
+- *(s3)* DigitalOcean storage buckets do not work
+- *(ui)* Correct typo in container label helper text
+- Disable certain parts if readonly label is turned off
+- Cleanup old scheduled_task_executions
+- Validate cron expression in Scheduled Task update
+- *(core)* Check cron expression on save
+- *(database)* Detect more postgres database image types
+- *(templates)* Update service templates
+- Remove quotes in COOLIFY_CONTAINER_NAME
+- *(templates)* Update Trigger.dev service templates with v3 configuration
+- *(database)* Adjust MongoDB restore command and import view styling
+- *(core)* Improve public repository URL parsing for branch and base directory
+- *(core)* Increase HTTP/2 max concurrent streams to 250 (default)
+- *(ui)* Update docker compose file helper text to clarify repository modification
+- *(ui)* Skip SERVICE_FQDN and SERVICE_URL variables during update
+- *(core)* Stopping database is not disabling db proxy
+- *(core)* Remove --remove-orphans flag from proxy startup command to prevent other proxy deletions (db)
+- *(api)* Domain check when updating domain
+- *(ui)* Always redirect to dashboard after team switch
+- *(backup)* Escape special characters in database backup commands
+- *(core)* Improve deployment failure Slack notification formatting
+- *(core)* Update Slack notification formatting to use bold correctly
+- *(core)* Enhance Slack deployment success notification formatting
+- *(ui)* Simplify service templates loading logic
+- *(ui)* Align title and add button vertically in various views
+- Handle pullrequest:updated for reliable preview deployments
+- *(ui)* Fix typo on team page (#5105)
+- Cal.com documentation link give 404 (#5070)
+- *(slack)* Notification settings URL in `HighDiskUsage` message (#5071)
+- *(ui)* Correct typo in Storage delete dialog (#5061)
+- *(lang)* Add missing italian translations (#5057)
+- *(service)* Improve duplicati.yaml (#4971)
+- *(service)* Links in homepage service (#5002)
+- *(service)* Added SMTP credentials to getoutline yaml template file (#5011)
+- *(service)* Added `KEY` Variable to Beszel Template (#5021)
+- *(cloudflare-tunnels)* Dead links to docs (#5104)
+- System-wide GitHub apps (#5114)
+- Pull latest image from registry when using build server
+- *(deployment)* Improve server selection for deployment cancellation
+- *(deployment)* Improve log line rendering and formatting
+- *(s3-storage)* Optimize team admin notification query
+- *(core)* Improve connection testing with dynamic disk configuration for s3 backups
+- *(core)* Update service status refresh event handling
+- *(ui)* Adjust polling intervals for database and service status checks
+- *(service)* Update Fider service template healthcheck command
+- *(core)* Improve server selection error handling in Docker component
+- *(core)* Add server functionality check before dispatching container status
+- *(ui)* Disable sticky scroll in Monaco editor
+- *(ui)* Add literal and multiline env support to services.
+- *(services)* Owncloud docs link
+- *(template)* Remove db-migration step from `infisical.yaml` (#5209)
+- *(service)* Penpot (#5047)
+- *(core)* Production dockerfile
+- *(ui)* Update storage configuration guidance link
+- *(ui)* Set default SMTP encryption to starttls
+- *(notifications)* Correct environment URL path in application notifications
+- *(config)* Update default PostgreSQL host to coolify-db instead of postgres
+- *(docker)* Improve Docker compose file validation process
+- *(ui)* Restrict service retrieval to current team
+- *(core)* Only validate custom compose files
+- *(mail)* Set default mailer to array when not specified
+- *(ui)* Correct redirect routes after task deletion
+- *(core)* Adding a new server should not try to make the default docker network
+- *(core)* Clean up unnecessary files during application image build
+- *(core)* Improve label generation and merging for applications and services
+- *(billing)* Handle 'past_due' subscription status in Stripe processing
+- *(revert)* Label parsing
+- *(helpers)* Initialize command variable in parseCommandFromMagicEnvVariable
+- *(billing)* Restrict Stripe subscription status update to 'active' only
+- *(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
+- *(file-storage)* Double save on compose volumes
+- *(parser)* Add logging support for applications in services
+- 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)
+- *(database)* Custom config for MongoDB (#5471)
+- *(ui)* Instance Backup settings
+- *(docs)* Comment out execute for now
+- *(installation)* Mount the docker config
+- *(installation)* Path to config file for docker login
+- *(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
+- *(api)* Used ssh keys can be deleted
+- *(email)* Transactional emails not sending
### 💼 Other
+- Only allow cleanup in production
+- Make copy/password visible
+- Dns check
+- Remote docker engine
+- Colorful states
+- Application start
+- Colors on svelte-select
+- Improvements
- Fix
- Better layout for root team
- Fix
@@ -6276,569 +9699,1273 @@ All notable changes to this project will be documented in this file.
- Fix
- Fixes
- Fixes
+- Show extraconfig if wp is running
+- Umami service
+- Base image selector
+- Laravel
+- Appwrite
+- Testing WS
+- Traefik?!
+- Traefik
+- Traefik
+- Traefik migration
+- Traefik
+- Traefik
+- Traefik
+- Notifications and application usage
+- *(fix)* Traefik
+- Css
+- Error message https://github.com/coollabsio/coolify/issues/502
+- Changes
+- Settings
+- For removing app
+- Local ssh port
+- Redesign a lot
+- Fixes
+- Loading indicator for plausible buttons
+- Fix
+- Fider
+- Typing
+- Fixes here and there
+- Dashboard fine-tunes
+- Fine-tune
+- Fixes
+- Fix
+- Dashbord fixes
+- Fixes
+- Fixes
+- Route to the correct path when creating destination from db config
+- Fixes
+- Change tooltips and info boxes
+- Added rc release
+- Database_branches
+- Login page
+- Fix login/register page
+- Update devcontainer
+- Add debug log
+- Fix initial loading icon bg
+- Fix loading start/stop db/services
+- Dashboard updates and a lot more
+- Dashboard updates
+- Fix tooltip
+- Fix button
+- Fix follow button
+- Arm should be on next all the time
+- Fix plausible
+- Fix cleanup button
+- Fix buttons
+- Responsive!
+- Fixes
+- Fix git icon
+- Dropdown as infobox
+- Small logs on mobile
+- Improvements
+- Fix destination view
+- Settings view
+- More UI improvements
+- Fixes
+- Fixes
+- Fix
+- Fixes
+- Beta features
+- Fix button
+- Service fixes
+- Fix basedirectory meaning
+- Resource button fix
+- Main resource search
+- Dev logs
+- Loading button
+- Fix gitlab importer view
+- Small fix
+- Beta flag
+- Hasura console notification
+- Fix
+- Fix
+- Fixes
+- Inprogress version of iam
+- Fix indicato
+- Iam & settings update
+- Send 200 for ping and installation wh
+- Settings icon
+- Docker-compose support
+- Docker compose
+- Remove worker jobs
+- One less worker thread
+- New resource label
+- Secrets on apps
+- Fix
+- Fixes
+- Reload compose loading
+- Pocketbase release
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Conditional on environment
+- Add missing variables
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Trpc
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Extract process handling from async job.
+- Persisting data
+- Scheduled backups
+- Boarding
+- Backup existing database
+- User should know that the public key
+- Services are not availble yet
+- Show registered users on waitlist page
+- Nixpacksarchive
+- Add Plausible analytics
+- Global env variables
+- Fix
+- Trial emails
+- Server check instead of app check
+- Show trial instead of sub
+- Server lost connection
+- Services
+- Services
+- Services
+- Ui for services
+- Services
+- Services
+- Services
+- Fixes
+- Fix typo
+- Fixed z-index for version link.
+- Add source button
+- Fixed z-index for magicbar
+- A bit better error
+- More visible feedback button
+- Update help modal
+- Help
+- Marketing emails
+- Fix previews to preview
+- Uptime kume hc updated
+- Switch back to /data (volume errors)
+- Notifications
+- Add shared email option to everyone
+- Dockerimage
+- Updated dashboard
+- Fix
+- Fix
+- Coolify proxy access logs exposed in dev
+- Able to select environment on new resource
+- Delete server
+- Redis
+- Wordpress
+- Add helper to service domains
+- PAT by team
+- Generate services
+- Mongodb backup
+- Mongodb backup
+- Updates
+- Fix subs
+- New deployment jobs
+- Compose based apps
+- Swarm
+- Swarm
+- Swarm
+- Swarm
+- Disable trial
+- Meilisearch
+- Broadcast
+- 🌮
+- Env vars
+- Migrate to livewire 3
+- Fix for comma in labels
+- Add image name to service stack + better options visibility
+- Swarm
+- Swarm
+- Send notification email if payment
+- New modal component
+- Specific about newrelic logdrains
+- Updates
+- Change + icon to hamburger.
+- Redesign
+- Redesign
+- Run cleanup every day
+- Fix
+- Fix log outputs
+- Automatic cloudflare tunnels
+- Backup executions
+- Light buttons
+- Multiple server view
+- New pricing
+- Fix allowTab logic
+- Use 2 space instead of tab
+- Non-root user for remote servers
+- Non-root
+- Update resource operations view
+- Fix tag view
+- Fix a few boxes here and there
+- Responsive here and there
+- Rocketchat
+- New services based git apps
+- Unnecessary notification
+- Update process
+- Glances service
+- Glances
+- Able to update application
+- Add basedir + compose file in new compose based apps
+- Formbricks template add required CRON_SECRET
+- Add required CRON_SECRET to Formbricks template
+- Service env parsing
+- Actually update timezone on the server
+- Cron jobs are executed based on the server timezone
+- Server timezone seeder
+- Recent backups UI
+- Use apt-get instead of apt
+- Typo
+- Only pull helper image if the version is newer than the one
+- Plunk svg
+- Pull helper image if not available otherwise s3 backup upload fails
+- Set a default server timezone
+- Implement SSH Multiplexing
+- Enabel mux
+- Cleanup stale multiplexing connections
+- Remote servers with port and user
+- Do not change localhost server name on revalidation
+- Release.md file
+- SSH Multiplexing on docker desktop on Windows
+- Remove labels and assignees on issue close
+- Make sure this action is also triggered on PR issue close
+- Volumes on development environment
+- Clean new volume name for dev volumes
+- Persist DBs, services and so on stored in data/coolify
+- Add SSH Key fingerprint to DB
+- Add a fingerprint to every private key on save, create...
+- Make sure invalid private keys can not be added
+- Encrypt private SSH keys in the DB
+- Add is_sftp and is_server_ssh_key coloums
+- New ssh key file name on disk
+- Store all keys on disk by default
+- Populate SSH key folder
+- Populate SSH keys in dev
+- Use new function names and logic everywhere
+- Create a Multiplexing Helper
+- SSH multiplexing
+- Remove unused code form multiplexing
+- SSH Key cleanup job
+- Private key with ID 2 on dev
+- Move more functions to the PrivateKey Model
+- Add ssh key fingerprint and generate one for existing keys
+- ID issues on dev seeders
+- Server ID 0
+- Make sure in use private keys are not deleted
+- Do not delete SSH Key from disk during server validation error
+- UI bug, do not write ssh key to disk in server dialog
+- SSH Multiplexing for Jobs
+- SSH algorhytm text
+- Few multiplexing things
+- Clear mux directory
+- Multiplexing do not write file manually
+- Integrate tow step process in the modal component WIP
+- Ability to hide labels
+- DB start, stop confirm
+- Del init script
+- General confirm
+- Preview deployments and typos
+- Service confirmation
+- Confirm file storage
+- Stop service confirm
+- DB image cleanup
+- Confirm ressource operation
+- Environment variabel deletion
+- Confirm scheduled tasks
+- Confirm API token
+- Confirm private key
+- Confirm server deletion
+- Confirm server settings
+- Proxy stop and restart confirmation
+- GH app deletion confirmation
+- Redeploy all confirmation
+- User deletion confirmation
+- Team deletion confirmation
+- Backup job confirmation
+- Delete volume confirmation
+- More conformations and fixes
+- Delete unused private keys button
+- Ray error because port is not uncommented
+- #3322 deploy DB alterations before updating
+- Css issue with advanced settings and remove cf tunnel in onboarding
+- New cf tunnel install flow
+- Made help text more clear
+- Cloudflare tunnel
+- Make helper text more clean to use a FQDN and not an URL
+- Manual cleanup button and unused volumes and network deletion
+- Force helper image removal
+- Use the new confirmation flow
+- Typo
+- Typo in install script
+- If API is disabeled do not show API token creation stuff
+- Disable API by default
+- Add debug bar
+- Remove memlock as it caused problems for some users
+- Server storage check
+- Show backup button on supported db service stacks
+- Update helper version
+- Outline
+- Directus
+- Supertokens
+- Supertokens json
+- Rabbitmq
+- Easyappointments
+- Soketi
+- Dozzle
+- Windmill
+- Coolify.json
+- Keycloak
+- Other DB options for freshrss
+- Nextcloud MariaDB and MySQL versions
+- Add peppermint
+- Loggy
+- Add UI for redis password and username
+- Wireguard-easy template
+- Https://github.com/coollabsio/coolify/issues/4186
+- Separate resources by type in projects view
+- Improve s3 add view
+- Caddy docker labels do not honor "strip prefix" option
+- Test rename GitHub app
+- Checkmate service and fix prowlar slogan (too long)
+- Arrrrr
+- Dep
+- Docker dep
+- Trigger.dev templates - wrong key length issue
+- Trigger.dev template - missing ports and wrong env usage
+- Trigger.dev template - fixed otel config
+- Trigger.dev template - fixed otel config
+- Trigger.dev template - fixed port config
+- Bump all dependencies (#5216)
+- Bump Coolify to 4.0.0-beta.398
+- Bump Coolify to 4.0.0-beta.400
+- *(migration)* Add SSL fields to database tables
+- SSL Support for KeyDB
+
+### 🚜 Refactor
+
+- Code
+- Env variable generator
+- Service logs are now on one page
+- Application status changed realtime
+- Custom labels
+- Clone project
+- Compose file and install script
+- Add SCHEDULER environment variable to StartSentinel.php
+- Update edit-domain form in project service view
+- Add Huly services to compose file
+- Remove redundant heading in backup settings page
+- Add isBuildServer method to Server model
+- Update docker network creation in ApplicationDeploymentJob
+- Update destination.blade.php to add group class for better styling
+- Applicationdeploymentjob
+- Improve code structure in ApplicationDeploymentJob.php
+- Remove unnecessary debug statement in ApplicationDeploymentJob.php
+- Remove unnecessary debug statements and improve code structure in RunRemoteProcess.php and ApplicationDeploymentJob.php
+- Remove unnecessary logging statements from UpdateCoolify
+- Update storage form inputs in show.blade.php
+- Improve Docker Compose parsing for services
+- Remove unnecessary port appending in updateCompose function
+- Remove unnecessary form class in profile index.blade.php
+- Update form layout in invite-link.blade.php
+- Add log entry when starting new application deployment
+- Improve Docker Compose parsing for services
+- Update Docker Compose parsing for services
+- Update slogan in shlink.yaml
+- Improve display of deployment time in index.blade.php
+- Remove commented out code for clearing Ray logs
+- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview
+- Append utm_source parameter to documentation URL
+- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview
+- Update deployment previews heading to "Deployments"
+- Remove unused variables and improve code readability
+- Initialize null properties in Github Change component
+- Improve pre and post deployment command inputs
+- Improve handling of Docker volumes in parseDockerComposeFile function
+- Replaces duplications in code with a single function
+- Update text color for stderr output in deployment show view
+- Update text color for stderr output in deployment show view
+- Remove debug code for saving environment variables
+- Update Docker build commands for better performance and flexibility
+- Update image sizes and add new logos to README.md
+- Update README.md with new logos and fix styling
+- Update shared.php to use correct key for retrieving sentinel version
+- Update container name assignment in Application model
+- Remove commented code for docker container removal
+- Update Application model to include getDomainsByUuid method
+- Update Project/Show component to sort environments by created_at
+- Update profile index view to display 2FA QR code in a centered container
+- Update dashboard.blade.php to use project's default environment for redirection
+- Update gitCommitLink method to handle null values in source.html_url
+- Update docker-compose generation to use multi-line literal block
+- Update Service model's saveComposeConfigs method
+- Add default environment to Service model's saveComposeConfigs method
+- Improve handling of default environment in Service model's saveComposeConfigs method
+- Remove commented out code in Service model's saveComposeConfigs method
+- Update stack-form.blade.php to include wire:target attribute for submit button
+- Update code to use str() instead of Str::of() for string manipulation
+- Improve formatting and readability of source.blade.php
+- Add is_build_time property to nixpacks_php_fallback_path and nixpacks_php_root_dir
+- Simplify code for retrieving subscription in Stripe webhook
+- Add force parameter to StartProxy handle method
+- Comment out unused code for network cleanup
+- Reset default labels when docker_compose_domains is modified
+- Webhooks view
+- Tags view
+- Only get instanceSettings once from db
+- Update Dockerfile to set CI environment variable to true
+- Remove unnecessary code in AppServiceProvider.php
+- Update Livewire configuration views
+- Update Webhooks.php to use nullable type for webhook URLs
+- Add lazy loading to tags in Livewire configuration view
+- Update metrics.blade.php to improve alert message clarity
+- Update version numbers to 4.0.0-beta.312
+- Update version numbers to 4.0.0-beta.314
+- Remove unused code and fix storage form layout
+- Update Docker Compose build command to include --pull flag
+- Update DockerCleanupJob to handle nullable usageBefore property
+- Server status job and docker cleanup job
+- Update DockerCleanupJob to use server settings for force cleanup
+- Update DockerCleanupJob to use server settings for force cleanup
+- Disable health check for Rust applications during deployment
+- Update CleanupDatabase.php to adjust keep_days based on environment
+- Adjust keep_days in CleanupDatabase.php based on environment
+- Remove commented out code for cleaning up networks in CleanupDocker.php
+- Update livewire polling interval in heading.blade.php
+- Remove unused code for checking server status in Heading.php
+- Simplify log drain installation in ServerCheckJob
+- Remove unnecessary debug statement in ServerCheckJob
+- Simplify log drain installation and stop log drain if necessary
+- Cleanup unnecessary dynamic proxy configuration in Init command
+- Remove unnecessary debug statement in ApplicationDeploymentJob
+- Update timeout for graceful_shutdown_container in ApplicationDeploymentJob
+- Remove unused code and optimize CheckForUpdatesJob
+- Update ProxyTypes enum values to use TRAEFIK instead of TRAEFIK_V2
+- Update Traefik labels on init and cleanup unnecessary dynamic proxy configuration
+- Update StandalonePostgresql database initialization and backup handling
+- Update cron expressions and add helper text for scheduled tasks
+- Update Server model getContainers method to use collect() for containers and containerReplicates
+- Import ProxyTypes enum and use TRAEFIK instead of TRAEFIK_V2
+- Update event listeners in Show components
+- Refresh application to get latest database changes
+- Update RabbitMQ configuration to use environment variable for port
+- Remove debug statement in parseDockerComposeFile function
+- ParseServiceVolumes
+- Update OpenApi command to generate documentation
+- Remove unnecessary server status check in destination view
+- Remove unnecessary admin user email and password in budibase.yaml
+- Improve saving of custom internal name in Advanced.php
+- Add conditional check for volumes in generate_compose_file()
+- Improve storage mount forms in add.blade.php
+- Load environment variables based on resource type in sortEnvironmentVariables()
+- Remove unnecessary network cleanup in Init.php
+- Remove unnecessary environment variable checks in parseDockerComposeFile()
+- Add null check for docker_compose_raw in parseCompose()
+- Update dockerComposeParser to use YAML data from $yaml instead of $compose
+- Convert service variables to key-value pairs in parseDockerComposeFile function
+- Update database service name from mariadb to mysql
+- Remove unnecessary code in DatabaseBackupJob and BackupExecutions
+- Update Docker Compose parsing function to convert service variables to key-value pairs
+- Update Docker Compose parsing function to convert service variables to key-value pairs
+- Remove unused server timezone seeder and related code
+- Remove unused server timezone seeder and related code
+- Remove unused PullCoolifyImageJob from schedule
+- Update parse method in Advanced, All, ApplicationPreview, General, and ApplicationDeploymentJob classes
+- Remove commented out code for getIptables() in Dashboard.php
+- Update .env file path in install.sh script
+- Update SELF_HOSTED environment variable in docker-compose.prod.yml
+- Remove unnecessary code for creating coolify network in upgrade.sh
+- Update environment variable handling in StartClickhouse.php and ApplicationDeploymentJob.php
+- Improve handling of COOLIFY_URL in shared.php
+- Update build_args property type in ApplicationDeploymentJob
+- Update background color of sponsor section in README.md
+- Update Docker Compose location handling in PublicGitRepository
+- Upgrade process of Coolify
+- Improve handling of server timezones in scheduled backups and tasks
+- Improve handling of server timezones in scheduled backups and tasks
+- Improve handling of server timezones in scheduled backups and tasks
+- Update cleanup schedule to run daily at midnight
+- Skip returning volume if driver type is cifs or nfs
+- Improve environment variable handling in shared.php
+- Improve handling of environment variable merging in upgrade script
+- Remove unnecessary code in ExecuteContainerCommand.php
+- Improve Docker network connection command in StartService.php
+- Terminal / run command
+- Add authorization check in ExecuteContainerCommand mount method
+- Remove unnecessary code in Terminal.php
+- Remove unnecessary code in Terminal.blade.php
+- Update WebSocket connection initialization in terminal.blade.php
+- Remove unnecessary console.log statements in terminal.blade.php
+- Update Docker cleanup label in Heading.php and Navbar.php
+- Remove commented out code in Navbar.php
+- Remove CleanupSshKeysJob from schedule in Kernel.php
+- Update getAJoke function to exclude offensive jokes
+- Update getAJoke function to use HTTPS for API request
+- Update CleanupHelperContainersJob to use more efficient Docker command
+- Update PrivateKey model to improve code readability and maintainability
+- Remove unnecessary code in PrivateKey model
+- Update PrivateKey model to use ownedByCurrentTeam() scope for cleanupUnusedKeys()
+- Update install.sh script to check if coolify-db volume exists before generating SSH key
+- Update ServerSeeder and PopulateSshKeysDirectorySeeder
+- Improve attribute sanitization in Server model
+- Update confirmation button text for deletion actions
+- Remove unnecessary code in shared.php file
+- Update environment variables for services in compose files
+- Update select.blade.php to improve trademarks policy display
+- Update select.blade.php to improve trademarks policy display
+- Fix typo in subscription URLs
+- Add Postiz service to compose file (disabled for now)
+- Update shared.php to include predefined ports for services
+- Simplify SSH key synchronization logic
+- Remove unused code in DatabaseBackupStatusJob and PopulateSshKeysDirectorySeeder
+- Remove commented out code and improve environment variable handling in newParser function
+- Improve label positioning in input and checkbox components
+- Group and sort fields in StackForm by service name and password status
+- Improve layout and add checkbox for task enablement in scheduled task form
+- Update checkbox component to support full width option
+- Update confirmation label in danger.blade.php template
+- Fix typo in execute-container-command.blade.php
+- Update OS_TYPE for Asahi Linux in install.sh script
+- Add localhost as Server if it doesn't exist and not in cloud environment
+- Add localhost as Server if it doesn't exist and not in cloud environment
+- Update ProductionSeeder to fix issue with coolify_key assignment
+- Improve modal confirmation titles and button labels
+- Update install.sh script to remove redirection of upgrade output to /dev/null
+- Fix modal input closeOutside prop in configuration.blade.php
+- Add support for IPv6 addresses in sslip function
+- Update environment variable name for uptime-kuma service
+- Improve start proxy script to handle existing containers gracefully
+- Update delete server confirmation modal buttons
+- Remove unnecessary code
+- Update search input placeholder in resource index view
+- Remove deployment queue when deleting an application
+- Improve SSH command generation in Terminal.php and terminal-server.js
+- Fix indentation in modal-confirmation.blade.php
+- Improve parsing of commands for sudo in parseCommandsByLineForSudo
+- Improve popup component styling and button behavior
+- Encode delimiter in SshMultiplexingHelper
+- Remove inactivity timer in terminal-server.js
+- Improve socket reconnection interval in terminal.js
+- Remove unnecessary watch command from soketi service entrypoint
+- Update Traefik configuration for improved security and logging
+- Improve proxy configuration and code consistency in Server model
+- Rename name method to sanitizedName in BaseModel for clarity
+- Improve migration command and enhance application model with global scope and status checks
+- Unify notification icon
+- Remove unused Azure and Authentik service configurations from services.php
+- Change email column types in instance_settings migration from string to text
+- Change OauthSetting creation to updateOrCreate for better handling of existing records
+- Rename `coolify.environment` to `coolify.environmentName`
+- Rename parameter in DatabaseBackupJob for clarity
+- Improve checkbox component accessibility and styling
+- Remove unused tags method from ApplicationDeploymentJob
+- Improve deployment status check in isAnyDeploymentInprogress function
+- Extend HorizonServiceProvider from HorizonApplicationServiceProvider
+- Streamline job status retrieval and clean up repository interface
+- Enhance ApplicationDeploymentJob and HorizonServiceProvider for improved job handling
+- Remove commented-out unsubscribe route from API
+- Update redirect calls to use a consistent navigation method in deployment functions
+- AppServiceProvider
+- Github.php
+- Improve data formatting and UI
+- Comment out RootUserSeeder call in ProductionSeeder for clarity
+- Streamline ProductionSeeder by removing debug logs and unnecessary checks, while ensuring essential seeding operations remain intact
+- Remove debug echo statements from Init command to clean up output and improve readability
+- *(workflows)* Replace jq with PHP script for version retrieval in workflows
+- *(s3)* Improve S3 bucket endpoint formatting
+- *(vite)* Improve environment variable handling in Vite configuration
+- *(ui)* Simplify GitHub App registration UI and layout
+- Simplify service start and restart workflows
+- Use pull flag on docker compose up
+- *(ui)* Simplify file storage modal confirmations
+- *(notifications)* Improve transactional email settings handling
+- *(scheduled-tasks)* Improve scheduled task creation and management
+- *(billing)* Enhance Stripe subscription status handling and notifications
+- *(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
+- *(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
+- *(dev)* Remove OpenAPI generation functionality
+- *(migration)* Enhance local file volumes migration with logging
+- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks
### 📚 Documentation
- Contribution guide
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.3.3] - 2022-04-05
-
-### 🐛 Bug Fixes
-
-- Add git lfs while deploying
-- Try to update build status several times
-- Update stucked builds
-- Update stucked builds on startup
-- Revert seed
-- Lame fixing
-- Remove asyncUntil
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.3.2] - 2022-04-04
-
-### 🐛 Bug Fixes
-
-- *(php)* If .htaccess file found use apache
-- Add default webhook domain for n8n
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.3.1] - 2022-04-04
-
-### 🐛 Bug Fixes
-
-- Secrets build/runtime coudl be changed after save
-- Default configuration
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.3.0] - 2022-04-04
-
-### 🚀 Features
-
-- Initial python support
-- Add loading on register button
-- *(dev)* Allow windows users to use pnpm dev
-- MeiliSearch service
-- Add abilitry to paste env files
-
-### 🐛 Bug Fixes
-
-- Ignore coolify proxy error for now
-- Python no wsgi
-- If user not found
-- Rename envs to secrets
-- Infinite loop on www domains
-- No need to paste clear text env for previews
-- Build log fix attempt #1
-- Small UI fix on logs
-- Lets await!
-- Async progress
-- Remove console.log
-- Build log
-- UI
-- Gitlab & Github urls
-
-### 💼 Other
-
-- Improvements
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-- Version++
-- Lock file + fix packages
-
-## [2.2.7] - 2022-04-01
-
-### 🐛 Bug Fixes
-
-- Haproxy errors
-- Build variables
-- Use NodeJS for sveltekit for now
-
-## [2.2.6] - 2022-03-31
-
-### 🐛 Bug Fixes
-
-- Add PROTO headers
-
-## [2.2.5] - 2022-03-31
-
-### 🐛 Bug Fixes
-
-- Registration enabled/disabled
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.2.4] - 2022-03-31
-
-### 🐛 Bug Fixes
-
-- Gitlab repo url
-- No need to dashify anymore
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.2.3] - 2022-03-31
-
-### 🐛 Bug Fixes
-
-- List ghost services
-- Reload window on settings saved
-- Persistent storage on webhooks
-- Add license
-- Space in repo names
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-- Version++
-- Version++
-- Fixed typo on New Git Source view
-
-## [2.2.0] - 2022-03-27
-
-### 🚀 Features
-
-- Add n8n.io service
-- Add update kuma service
-- Ghost service
-
-### 🐛 Bug Fixes
-
-- Ghost logo size
-- Ghost icon, remove console.log
-
-### 💼 Other
-
-- Colors on svelte-select
-
-### ⚙️ Miscellaneous Tasks
-
-- Version ++
-
-## [2.1.1] - 2022-03-25
-
-### 🐛 Bug Fixes
-
-- Cleanup only 2 hours+ old images
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.1.0] - 2022-03-23
-
-### 🚀 Features
-
-- Use compose instead of normal docker cmd
-- Be able to redeploy PRs
-
-### 🐛 Bug Fixes
-
-- Skip ssl cert in case of error
-- Volumes
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.31] - 2022-03-20
-
-### 🚀 Features
-
-- Add PHP modules
-
-### 🐛 Bug Fixes
-
-- Cleanup old builds
-- Only cleanup same app
-- Add nginx + htaccess files
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.30] - 2022-03-19
-
-### 🐛 Bug Fixes
-
-- No cookie found
-- Missing session data
-- No error if GitSource is missing
-- No webhook secret found?
-- Basedir for dockerfiles
-- Better queue system + more support on monorepos
-- Remove build logs in case of app removed
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.29] - 2022-03-11
-
-### 🚀 Features
-
-- Webhooks inititate all applications with the correct branch
-- Check ssl for new apps/services first
-- Autodeploy pause
-- Install pnpm into docker image if pnpm lock file is used
-
-### 🐛 Bug Fixes
-
-- Personal Gitlab repos
-- Autodeploy true by default for GH repos
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.28] - 2022-03-04
-
-### 🚀 Features
-
-- Service secrets
-
-### 🐛 Bug Fixes
-
-- Do not error if proxy is not running
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.27] - 2022-03-02
-
-### 🚀 Features
-
-- Send version with update request
-
-### 🐛 Bug Fixes
-
-- Check when a container is running
-- Reload haproxy if new cert is added
-- Cleanup coolify images
-- Application state in UI
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.26] - 2022-03-02
-
-### 🐛 Bug Fixes
-
-- Update process
-
-## [2.0.25] - 2022-03-02
-
-### 🚀 Features
-
-- Languagetool service
-
-### 🐛 Bug Fixes
-
-- Reload proxy on ssl cert
-- Volume name
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.24] - 2022-03-02
-
-### 🐛 Bug Fixes
-
-- Better proxy check
-- Ssl + sslrenew
-- Null proxyhash on restart
-- Reconfigure proxy on restart
-- Update process
-
-## [2.0.23] - 2022-02-28
-
-### 🐛 Bug Fixes
-
-- Be sure .env exists
-- Missing fqdn for services
-- Default npm command
-- Add coolify-image label for build images
-- Cleanup old images, > 3 days
-
-### 💼 Other
-
-- Colorful states
-- Application start
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.22] - 2022-02-27
-
-### 🐛 Bug Fixes
-
-- Coolify image pulls
-- Remove wrong/stuck proxy configurations
-- Always use a buildpack
-- Add icons for eleventy + astro
-- Fix proxy every 10 secs
-- Do not remove coolify proxy
-- Update version
-
-### 💼 Other
-
-- Remote docker engine
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.21] - 2022-02-24
-
-### 🚀 Features
-
-- Random subdomain for demo
-- Random domain for services
-- Astro buildpack
-- 11ty buildpack
-- Registration page
-
-### 🐛 Bug Fixes
-
-- Http for demo, oops
-- Docker scanner
-- Improvement on image pulls
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.20] - 2022-02-23
-
-### 🐛 Bug Fixes
-
-- Revert default network
-
-### 💼 Other
-
-- Dns check
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.19] - 2022-02-23
-
-### 🐛 Bug Fixes
-
-- Random network name for demo
-- Settings fqdn grr
-
-## [2.0.18] - 2022-02-22
-
-### 🚀 Features
-
-- Ports range
-
-### 🐛 Bug Fixes
-
-- Email is lowercased in login
-- Lowercase email everywhere
-- Use normal docker-compose in dev
-
-### 💼 Other
-
-- Make copy/password visible
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.17] - 2022-02-21
-
-### 🐛 Bug Fixes
-
-- Move tokens from session to cookie/store
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.14] - 2022-02-18
-
-### 🚀 Features
-
-- Basic password reset form
-- Scan for lock files and set right commands
-- Public port range (WIP)
-
-### 🐛 Bug Fixes
-
-- SSL app off
-- Local docker host
-- Typo
-- Lets encrypt
-- Remove SSL with stop
-- SSL off for services
-- Grr
-- Running state css
-- Minor fixes
-- Remove force SSL when doing let's encrypt request
-- GhToken in session now
-- Random port for certbot
-- Follow icon
-- Plausible volume fixed
-- Database connection strings
-- Gitlab webhooks fixed
-- If DNS not found, do not redirect
-- Github token
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-- Version ++
-
-## [2.0.13] - 2022-02-17
-
-### 🐛 Bug Fixes
-
-- Login issues
-
-## [2.0.11] - 2022-02-15
-
-### 🚀 Features
-
-- Follow logs
-- Generate www & non-www SSL certs
-
-### 🐛 Bug Fixes
-
-- Window error in SSR
-- GitHub sync PR's
-- Load more button
-- Small fixes
-- Typo
-- Error with follow logs
-- IsDomainConfigured
-- TransactionIds
-- Coolify image cleanup
-- Cleanup every 10 mins
-- Cleanup images
-- Add no user redis to uri
-- Secure cookie disabled by default
-- Buggy svelte-kit-cookie-session
-
-### 💼 Other
-
-- Only allow cleanup in production
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-- Version++
-
-## [2.0.10] - 2022-02-15
-
-### 🐛 Bug Fixes
-
-- Typo
-- Error handling
-- Stopping service without proxy
-- Coolify proxy start
-
-### ⚙️ Miscellaneous Tasks
-
-- Version++
-
-## [2.0.8] - 2022-02-14
-
-### 🐛 Bug Fixes
-
-- Validate secrets
-- Truncate git clone errors
-- Branch used does not throw error
-
-## [2.0.7] - 2022-02-13
-
-### 🚀 Features
-
-- Www <-> non-www redirection for apps
-- Www <-> non-www redirection
-
-### 🐛 Bug Fixes
-
-- Package.json
-- Build secrets should be visible in runtime
-- New secret should have default values
-
-## [2.0.5] - 2022-02-11
-
-### 🚀 Features
-
-- VaultWarden service
-
-### 🐛 Bug Fixes
-
-- PreventDefault on a button, thats all
-- Haproxy check should not throw error
-- Delete all build files
-- Cleanup images
-- More error handling in proxy configuration + cleanups
-- Local static assets
-- Check sentry
-- Typo
-
-### ⚙️ Miscellaneous Tasks
-
-- Version
-- Version
-
-## [2.0.4] - 2022-02-11
-
-### 🚀 Features
-
-- Use tags in update
-- New update process (#115)
-
-### 🐛 Bug Fixes
-
-- Docker Engine bug related to live-restore and IPs
-- Version
-
-## [2.0.3] - 2022-02-10
-
-### 🐛 Bug Fixes
-
-- Capture non-error as error
-- Only delete id.rsa in case of it exists
-- Status is not available yet
+- How to add new services
+- Update
+- Update
+- Update Plunk documentation link in compose/plunk.yaml
+- Update link to deploy api docs
+- Add TECH_STACK.md (#4883)
+- *(services)* Reword nitropage url and slogan
+- *(readme)* Add Convex to special sponsors section
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+- Update changelog
+
+### 🎨 Styling
+
+- Linting
+
+### 🧪 Testing
+
+- Native binary target
+- Dockerfile
+- Remove prisma
+- More tests
+- Setup database for upcoming tests
### ⚙️ Miscellaneous Tasks
- Version bump
+- Version
+- Version
+- Version++
+- Version++
+- Version++
+- Version++
+- Version ++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version ++
+- Version++
+- Version++
+- Version++
+- Fixed typo on New Git Source view
+- Version++
+- Version++
+- Version++
+- Version++
+- Lock file + fix packages
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Update packages
+- Version++
+- Update build scripts
+- Update build packages
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Add .pnpm-store in .gitignore
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Minor changes
+- Minor changes
+- Minor changes
+- Whoops
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Update staging release
+- Version++
+- Version++
+- Add jda icon for lavalink service
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Version++
+- Update version to 4.0.0-beta.275
+- Update DNS server validation helper text
+- Dark mode should be the default
+- Improve menu item styling and spacing in service configuration and index views
+- Improve menu item styling and spacing in service configuration and index views
+- Improve menu item styling and spacing in project index and show views
+- Remove docker compose versions
+- Add Listmonk service template and logo
+- Refactor GetContainersStatus.php for improved readability and maintainability
+- Refactor ApplicationDeploymentJob.php for improved readability and maintainability
+- Add metrics and logs directories to installation script
+- Update sentinel version to 0.0.2 in versions.json
+- Update permissions on metrics and logs directories
+- Comment out server sentinel check in ServerStatusJob
+- Update version numbers to 4.0.0-beta.278
+- Update hover behavior and cursor style in scheduled task executions view
+- Refactor scheduled task view to improve code readability and maintainability
+- Skip scheduled tasks if application or service is not running
+- Remove debug logging statements in Kernel.php
+- Handle invalid cron strings in Kernel.php
+- Refactor Service.php to handle missing admin user in extraFields() method
+- Update twenty CRM template with environment variables and dependencies
+- Refactor applications.php to remove unused imports and improve code readability
+- Refactor deployment index.blade.php for improved readability and rollback handling
+- Refactor GitHub app selection UI in project creation form
+- Update ServerLimitCheckJob.php to handle missing serverLimit value
+- Remove unnecessary code for saving commit message
+- Update DOCKER_VERSION to 26.0 in install.sh script
+- Update Docker and Docker Compose versions in Dockerfiles
+- Update version numbers to 4.0.0-beta.279
+- Limit commit message length to 50 characters in ApplicationDeploymentJob
+- Update version to 4.0.0-beta.283
+- Change pre and post deployment command length in applications table
+- Refactor container name logic in GetContainersStatus.php and ForcePasswordReset.php
+- Remove unnecessary content from Docker Compose file
+- Update Sentry release version to 4.0.0-beta.287
+- Add Thompson Edolo as a sponsor
+- Add null checks for team in Stripe webhook
+- Update Sentry release version to 4.0.0-beta.288
+- Update for version 289
+- Fix formatting issue in deployment index.blade.php file
+- Remove unnecessary wire:navigate attribute in breadcrumbs.blade.php
+- Rename docker dirs
+- Update laravel/socialite to version v5.14.0 and livewire/livewire to version 3.4.9
+- Update modal styles for better user experience
+- Update deployment index.blade.php script for better performance
+- Update version numbers to 4.0.0-beta.290
+- Update version numbers to 4.0.0-beta.291
+- Update version numbers to 4.0.0-beta.292
+- Update version numbers to 4.0.0-beta.293
+- Add upgrade guide link to upgrade.blade.php
+- Improve upgrade.blade.php with clearer instructions and formatting
+- Update version numbers to 4.0.0-beta.294
+- Add Lightspeed.run as a sponsor
+- Update Dockerfile to install vim
+- Update Dockerfile with latest versions of Docker, Docker Compose, Docker Buildx, Pack, and Nixpacks
+- Update version numbers to 4.0.0-beta.295
+- Update supported OS list with almalinux
+- Update install.sh to support PopOS
+- Update install.sh script to version 1.3.2 and handle Linux Mint as Ubuntu
+- Update page title in resource index view
+- Update logo file path in logto.yaml
+- Update logo file path in logto.yaml
+- Remove commented out code for docker container removal
+- Add isAnyDeploymentInprogress function to check if any deployments are in progress
+- Add ApplicationDeploymentJob and pint.json
+- Update version numbers to 4.0.0-beta.298
+- Switch to database sessions from redis
+- Update dependencies and remove unused code
+- Update tailwindcss and vue versions in package.json
+- Update service template URL in constants.php
+- Update sentinel version to 0.0.8
+- Update chart styling and loading text
+- Update sentinel version to 0.0.9
+- Update Spanish translation for failed authentication messages
+- Add portuguese traslation
+- Add Turkish translations
+- Add Vietnamese translate
+- Add Treive logo to donations section
+- Update README.md with latest release version badge
+- Update latest release version badge in README.md
+- Update version to 4.0.0-beta.299
+- Move server delete component to the bottom of the page
+- Update version to 4.0.0-beta.301
+- Update version to 4.0.0-beta.302
+- Update version to 4.0.0-beta.303
+- Update version to 4.0.0-beta.305
+- Update version to 4.0.0-beta.306
+- Add log1x/laravel-webfonts package
+- Update version to 4.0.0-beta.307
+- Refactor ServerStatusJob constructor formatting
+- Update Monaco Editor for Docker Compose and Proxy Configuration
+- More details
+- Refactor shared.php helper functions
+- Update Plausible docker compose template to Plausible 2.1.0
+- Update Plausible docker compose template to Plausible 2.1.0
+- Update livewire/livewire dependency to version 3.4.9
+- Refactor checkIfDomainIsAlreadyUsed function
+- Update storage.blade.php view for livewire project service
+- Update version to 4.0.0-beta.310
+- Update composer dependencies
+- Add new logo for Latitude
+- Bump version to 4.0.0-beta.311
+- Update version to 4.0.0-beta.315
+- Update version to 4.0.0-beta.316
+- Update bug report template
+- Update repository form with simplified URL input field
+- Update width of container in general.blade.php
+- Update checkbox labels in general.blade.php
+- Update general page of apps
+- Handle JSON parsing errors in format_docker_command_output_to_json
+- Update Traefik image version to v2.11
+- Update version to 4.0.0-beta.317
+- Update version to 4.0.0-beta.318
+- Update helper message with link to documentation
+- Disable health check by default
+- Remove commented out code for sending internal notification
+- Update APP_BASE_URL to use SERVICE_FQDN_PLANE
+- Update resource-limits.blade.php with improved input field helpers
+- Update version numbers to 4.0.0-beta.319
+- Remove commented out code for docker image pruning
+- Collect/create/update volumes in parseDockerComposeFile function
+- Update version to 4.0.0-beta.320
+- Add pull_request image builds to GH actions
+- Add comment explaining the purpose of disconnecting the network in cleanup_unused_network_from_coolify_proxy()
+- Update formbricks template
+- Update registration view to display a notice for first user that it will be an admin
+- Update server form to use password input for IP Address/Domain field
+- Update navbar to include service status check
+- Update navbar and configuration to improve service status check functionality
+- Update workflows to include PR build and merge manifest steps
+- Update UpdateCoolifyJob timeout to 10 minutes
+- Update UpdateCoolifyJob to dispatch CheckForUpdatesJob synchronously
+- Update version to 4.0.0-beta.321
+- Update version to 4.0.0-beta.322
+- Update version to 4.0.0-beta.323
+- Update version to 4.0.0-beta.324
+- New compose parser with tests
+- Update version to 1.3.4 in install.sh and 1.0.6 in upgrade.sh
+- Update memory limit to 64MB in horizon configuration
+- Update php packages
+- Update axios npm dependency to version 1.7.5
+- Update Coolify version to 4.0.0-beta.324 and fix file paths in upgrade script
+- Update Coolify version to 4.0.0-beta.324
+- Update Coolify version to 4.0.0-beta.325
+- Update Coolify version to 4.0.0-beta.326
+- Add cd command to change directory before removing .env file
+- Update Coolify version to 4.0.0-beta.327
+- Update Coolify version to 4.0.0-beta.328
+- Update sponsor links in README.md
+- Update version.json to versions.json in GitHub workflow
+- Cleanup stucked resources and scheduled backups
+- Update GitHub workflow to use versions.json instead of version.json
+- Update GitHub workflow to use versions.json instead of version.json
+- Update GitHub workflow to use versions.json instead of version.json
+- Update GitHub workflow to use jq container for version extraction
+- Update GitHub workflow to use jq container for version extraction
+- Update UI for displaying no executions found in scheduled task list
+- Update UI for displaying deployment status in deployment list
+- Update UI for displaying deployment status in deployment list
+- Ignore unnecessary files in production build workflow
+- Update server form layout and settings
+- Update Dockerfile with latest versions of PACK and NIXPACKS
+- Update coolify-helper.yml to get version from versions.json
+- Disable Ray by default
+- Enable Ray by default and update Dockerfile with latest versions of PACK and NIXPACKS
+- Update Ray configuration and Dockerfile
+- Add middleware for updating environment variables by UUID in `api.php` routes
+- Expose port 3000 in browserless.yaml template
+- Update Ray configuration and Dockerfile
+- Update coolify version to 4.0.0-beta.331
+- Update versions.json and sentry.php to 4.0.0-beta.332
+- Update version to 4.0.0-beta.332
+- Update DATABASE_URL in plunk.yaml to use plunk database
+- Add coolify.managed=true label to Docker image builds
+- Update docker image pruning command to exclude managed images
+- Update docker cleanup schedule to run daily at midnight
+- Update versions.json to version 1.0.1
+- Update coolify-helper.yml to include "next" branch in push trigger
+- Set timeout for ServerCheckJob to 60 seconds
+- Update appwrite.yaml to include OpenSSL key variable assignment
+- Update version numbers to 4.0.0-beta.333
+- Copy .env file to .env-{DATE} if it exists
+- Update .env file with new values
+- Update server check job middleware to use server ID instead of UUID
+- Add reminder to backup .env file before running install script again
+- Copy .env file to backup location during installation script
+- Add reminder to backup .env file during installation script
+- Update permissions in pr-build.yml and version numbers
+- Add minio/mc command to Dockerfile
+- Remove itsgoingd/clockwork from require-dev in composer.json
+- Update 'key' value of gitlab in Service.php to use environment variable
+- Update release version to 4.0.0-beta.335
+- Update constants.ssh.mux_enabled in remoteProcess.php
+- Update listeners and proxy settings in server form and new server components
+- Remove unnecessary null check for proxy_type in generate_default_proxy_configuration
+- Remove unnecessary SSH command execution time logging
+- Update release version to 4.0.0-beta.336
+- Update coolify environment variable assignment with double quotes
+- Update shared.php to fix issues with source and network variables
+- Update terminal styling for better readability
+- Update button text for container connection form
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Remove unused entrypoint script and update volume mapping
+- Update .env file and docker-compose configuration
+- Update APP_NAME environment variable in docker-compose.prod.yml
+- Update WebSocket URL in terminal.blade.php
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Update Dockerfile and workflow for Coolify Realtime (v4)
+- Rename Command Center to Terminal in code and views
+- Update branch restriction for push event in coolify-helper.yml
+- Update terminal button text and layout in application heading view
+- Refactor terminal component and select form layout
+- Update coolify nightly version to 4.0.0-beta.335
+- Update helper version to 1.0.1
+- Fix syntax error in versions.json
+- Update version numbers to 4.0.0-beta.337
+- Update Coolify installer and scripts to include a function for fetching programming jokes
+- Update docker network connection command in ApplicationDeploymentJob.php
+- Add validation to prevent selecting 'default' server or container in RunCommand.php
+- Update versions.json to reflect latest version of realtime container
+- Update soketi image to version 1.0.1
+- Nightly - Update soketi image to version 1.0.1 and versions.json to reflect latest version of realtime container
+- Update version numbers to 4.0.0-beta.339
+- Update version numbers to 4.0.0-beta.340
+- Update version numbers to 4.0.0-beta.341
+- Update version numbers to 4.0.0-beta.342
+- Update remove-labels-and-assignees-on-close.yml
+- Add SSH key for localhost in ProductionSeeder
+- Update SSH key generation in install.sh script
+- Update ProductionSeeder to call OauthSettingSeeder and PopulateSshKeysDirectorySeeder
+- Update install.sh to support Asahi Linux
+- Update install.sh version to 1.6
+- Remove unused middleware and uniqueId method in DockerCleanupJob
+- Refactor DockerCleanupJob to remove unused middleware and uniqueId method
+- Remove unused migration file for populating SSH keys and clearing mux directory
+- Add modified files to the commit
+- Refactor pre-commit hook to improve performance and readability
+- Update CONTRIBUTING.md with troubleshooting note about database migrations
+- Refactor pre-commit hook to improve performance and readability
+- Update cleanup command to use Redis instead of queue
+- Update Docker commands to start proxy
+- Update version numbers to 4.0.0-beta.343
+- Update version numbers to 4.0.0-beta.344
+- Update version numbers to 4.0.0-beta.345
+- Update version numbers to 4.0.0-beta.346
+- Add autocomplete attribute to input fields
+- Refactor API Tokens component to use isApiEnabled flag
+- Update versions.json file
+- Remove unused .env.development.example file
+- Update API Tokens view to include link to Settings menu
+- Update web.php to cast server port as integer
+- Update backup deletion labels to use language files
+- Update database startup heading title
+- Update database startup heading title
+- Custom vite envs
+- Update version numbers to 4.0.0-beta.348
+- Refactor code to improve SSH key handling and storage
+- Update Mailpit logo to use SVG format
+- Fix docs link in running state
+- Update Coolify Realtime workflow to only trigger on the main branch
+- Refactor instanceSettings() function to improve code readability
+- Update Coolify Realtime image to version 1.0.2
+- Remove unnecessary code in DatabaseBackupJob.php
+- Add "Not Usable" indicator for storage items
+- Refactor instanceSettings() function and improve code readability
+- Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350
+- Update version numbers to 4.0.0-beta.350 in configuration files
+- Update command signature and description for cleanup application deployment queue
+- Add missing import for Attribute class in ApplicationDeploymentQueue model
+- Update modal input in server form to prevent closing on outside click
+- Remove unnecessary command from SshMultiplexingHelper
+- Remove commented out code for uploading to S3 in DatabaseBackupJob
+- Update soketi service image to version 1.0.3
+- Update version to 4.0.0-beta.352
+- Refactor DatabaseBackupJob to handle missing team
+- Update version to 4.0.0-beta.353
+- Update service application view
+- Update version to 4.0.0-beta.354
+- Remove debug statement in Service model
+- Remove commented code in Server model
+- Fix application deployment queue filter logic
+- Refactor modal-confirmation component
+- Update it-tools service template and port configuration
+- Update homarr service template and remove unnecessary code
+- Update homarr service template and remove unnecessary code
+- Update version to 4.0.0-beta.355
+- Update version to 4.0.0-beta.356
+- Remove commented code for shared variable type validation
+- Update MariaDB image to version 11 and fix service environment variable orders
+- Update anythingllm.yaml volumes configuration
+- Update proxy configuration paths for Caddy and Nginx in dev
+- Update password form submission in modal-confirmation component
+- Update project query to order by name in uppercase
+- Update project query to order by name in lowercase
+- Update select.blade.php with improved search functionality
+- Add Nitropage service template and logo
+- Bump coolify-helper version to 1.0.2
+- Refactor loadServices2 method and remove unused code
+- Update version to 4.0.0-beta.357
+- Update service names and volumes in windmill.yaml
+- Update version to 4.0.0-beta.358
+- Ignore .ignition.json files in Docker and Git
+- Add mattermost logo as svg
+- Add mattermost svg to compose
+- Update version to 4.0.0-beta.357
+- Fix form submission and keydown event handling in modal-confirmation.blade.php
+- Update version numbers to 4.0.0-beta.359 in configuration files
+- Disable adding default environment variables in shared.php
+- Update laravel/horizon dependency to version 5.29.1
+- Update service extra fields to use dynamic keys
+- Update livewire/livewire dependency to version 3.4.9
+- Add transmission template desc
+- Update transmission docs link
+- Update version numbers to 4.0.0-beta.360 in configuration files
+- Update AWS environment variable names in unsend.yaml
+- Update AWS environment variable names in unsend.yaml
+- Update livewire/livewire dependency to version 3.4.9
+- Update version to 4.0.0-beta.361
+- Update Docker build and push actions to v6
+- Update Docker build and push actions to v6
+- Update Docker build and push actions to v6
+- Sync coolify-helper to dockerhub as well
+- Push realtime to dockerhub
+- Sync coolify-realtime to dockerhub
+- Rename workflows
+- Rename development to staging build
+- Sync coolify-testing-host to dockerhbu
+- Sync coolify prod image to dockerhub as well
+- Update Docker version to 26.0
+- Update project resource index page
+- Update project service configuration view
+- Edit www helper
+- Update dep
+- Regenerate openapi spec
+- Composer dep bump
+- Dep bump
+- Upgrade cloudflared and minio
+- Remove comments and improve DB column naming
+- Remove unused seeder
+- Remove unused waitlist stuff
+- Remove wired.php (not used anymore)
+- Remove unused resale license job
+- Remove commented out internal notification
+- Remove more waitlist stuff
+- Remove commented out notification
+- Remove more waitlist stuff
+- Remove unused code
+- Fix typo
+- Remove comment out code
+- Some reordering
+- Remove resale license reference
+- Remove functions from shared.php
+- Public settings for email notification
+- Remove waitlist redirect
+- Remove log
+- Use new notification trait
+- Remove unused route
+- Remove unused email component
+- Comment status changes as it is disabled for now
+- Bump dep
+- Reorder navbar
+- Rename topicID to threadId like in the telegram API response
+- Update PHP configuration to set memory limit using environment variable
+- Regenerate API spec, removing notification fields
+- Remove ray debugging
+- Version ++
+- Improve Penpot healthchecks
+- Switch up readonly lables to make more sense
+- Remove unused computed fields
+- Use the new job dispatch
+- Disable volume data cloning for now
+- Improve code
+- Lowcoder service naming
+- Use new functions
+- Improve error styling
+- Css
+- More css as it still looks like shit
+- Final css touches
+- Ajust time to 50s (tests done)
+- Remove debug log, finally found it
+- Remove more logging
+- Remove limit on commit message
+- Remove dayjs
+- Remove unused code and fix import
+- *(dep)* Bump nixpacks version
+- *(dep)* Version++
+- *(dep)* Bump helper version to 1.0.5
+- *(docker)* Add blank line for readability in Dockerfile
+- *(versions)* Update coolify versions to v4.0.0-beta.388
+- *(versions)* Update coolify versions to v4.0.0-beta.389 and add helper version retrieval script
+- *(versions)* Update coolify versions to v4.0.0-beta.389
+- *(core)* EnvironmentVariable Model now extends BaseModel to remove duplicated code
+- *(versions)* Update coolify versions to v4.0.0-beta.3909
+- *(version)* Bump Coolify version to 4.0.0-beta.391
+- *(config)* Increase default PHP memory limit to 256M
+- Add openapi response
+- *(workflows)* Make naming more clear and remove unused code
+- Bump Coolify version to 4.0.0-beta.392/393
+- *(ci)* Update changelog generation workflow to target 'next' branch
+- *(ci)* Update changelog generation workflow to target main branch
+- Rollback Coolify version to 4.0.0-beta.392
+- Bump Coolify version to 4.0.0-beta.393
+- Bump Coolify version to 4.0.0-beta.394
+- Bump Coolify version to 4.0.0-beta.395
+- Bump Coolify version to 4.0.0-beta.396
+- *(services)* Update zipline to use new Database env var. (#5210)
+- *(service)* Upgrade authentik service
+- *(service)* Remove unused env from zipline
+- Bump helper and realtime version
+- *(migration)* Remove unused columns
+- *(ssl)* Improve code in ssl helper
+- *(migration)* Ssl cert and key should not be nullable
+- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts
+- Rename ca crt folder to ssl
+- *(ui)* Improve valid until handling
+- Improve code quality suggested by code rabbit
+- *(supabase)* Update Supabase service template and Postgres image version
+- *(versions)* Update version numbers for coolify and nightly
+- *(versions)* Update version numbers for coolify and nightly
+- *(service)* Update minecraft service ENVs
+- *(service)* Add more vars to infisical.yaml (#5418)
+- *(service)* Add google variables to plausible.yaml (#5429)
+- *(service)* Update authentik.yaml versions (#5373)
+- *(core)* Remove redocs
+- *(versions)* Bump version to 403 (#5520)
+- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404
+- *(service)* Remove unused code in Bugsink service
+- *(versions)* Update version to 404
+- *(versions)* Bump version to 404
+- *(versions)* Bump version to 406
+- *(versions)* Bump version to 407
-## [2.0.2] - 2022-02-10
+### ◀️ Revert
-### 🐛 Bug Fixes
-
-- Secrets join
-- ENV variables set differently
+- Show usage everytime
+- Revert: revert
+- Wip
+- Variable parsing
+- Hc return code check
+- Instancesettings
+- Pull policy
+- Advanced dropdown
+- Databasebackup
+- Remove Cloudflare async tag attributes
+- Encrypting mount and fs_path
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dba3676cf..1ba4d1876 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -136,6 +136,7 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
- 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 @@ After completing these steps, you'll have a fresh development setup.
### 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)
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 4f9f45b7c..38ad99d2e 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -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 @@ class StartDragonfly
],
],
];
+
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 @@ class StartDragonfly
$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 = [];
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 6c733d318..59bcd4123 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -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 @@ class StartKeydb
],
],
];
+
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 @@ class StartKeydb
$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 @@ class StartKeydb
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;
+ }
}
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 299b07385..13dba4b43 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -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 @@ class StartMariadb
$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 @@ class StartMariadb
],
],
];
+
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 @@ class StartMariadb
$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');
}
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 89d35ca7b..a42f03eb5 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -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 @@ class StartMongodb
$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,119 @@ class StartMongodb
],
],
];
+
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'];
+
+ $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 +257,9 @@ class StartMongodb
$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');
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 73db1512a..5d5611e07 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -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 @@ class StartMysql
$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 @@ class StartMysql
],
],
];
+
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 @@ class StartMysql
$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');
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 035849340..a40eac17b 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -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 @@ class StartPostgresql
$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 @@ class StartPostgresql
],
],
];
+
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 @@ class StartPostgresql
$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');
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 1beebd134..68a1f3fe3 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -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 @@ class StartRedis
$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 @@ class StartRedis
],
],
];
+
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 @@ class StartRedis
$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 @@ class StartRedis
$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;
}
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index e4cea7cee..de4eaa31f 100644
--- a/app/Actions/Database/StopDatabase.php
+++ b/app/Actions/Database/StopDatabase.php
@@ -26,7 +26,7 @@ class StopDatabase
}
$this->stopContainer($database, $database->uuid, 300);
- if (! $isDeleteOperation) {
+ if ($isDeleteOperation) {
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php
index d3727a52c..158996c90 100644
--- a/app/Actions/Fortify/ResetUserPassword.php
+++ b/app/Actions/Fortify/ResetUserPassword.php
@@ -24,5 +24,6 @@ class ResetUserPassword implements ResetsUserPasswords
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
+ $user->deleteAllSessions();
}
}
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 6c8dd5234..5a2562073 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -27,13 +27,9 @@ class CheckProxy
return false;
}
$proxyType = $server->proxyType();
- if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
+ if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
return false;
}
- ['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 @@ class CheckProxy
return false;
}
}
+
+ // Determine proxy container name based on environment
+ $proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
+
if ($server->isSwarm()) {
- $status = getContainerStatus($server, 'coolify-proxy_traefik');
+ $status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
@@ -51,7 +51,7 @@ class CheckProxy
return true;
} else {
- $status = getContainerStatus($server, 'coolify-proxy');
+ $status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
@@ -65,9 +65,18 @@ class CheckProxy
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. You must stop the process using this port.
Docs: https://coolify.io/docs Discord: https://coolify.io/discord");
+ } else {
+ return false;
+ }
+ }
+ }
try {
if ($server->proxyType() !== ProxyTypes::NONE->value) {
$proxyCompose = CheckConfiguration::run($server);
@@ -94,18 +103,148 @@ class CheckProxy
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. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord");
- } 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;
+ }
+ }
}
diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php
new file mode 100644
index 000000000..a5dcc6cf4
--- /dev/null
+++ b/app/Actions/Proxy/StopProxy.php
@@ -0,0 +1,56 @@
+isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
+ $timeout = 30;
+
+ $process = $this->stopContainer($containerName, $timeout);
+
+ $startTime = Carbon::now()->getTimestamp();
+ while ($process->running()) {
+ if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName, $server);
+ break;
+ }
+ usleep(100000);
+ }
+
+ $this->removeContainer($containerName, $server);
+ } catch (\Throwable $e) {
+ return handleError($e);
+ } finally {
+ $server->proxy->force_stop = $forceStop;
+ $server->proxy->status = 'exited';
+ $server->save();
+ }
+ }
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function forceStopContainer(string $containerName, Server $server)
+ {
+ instant_remote_process(["docker kill $containerName"], $server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName, Server $server)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
+ }
+}
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index cbcb20368..5410b1cbd 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -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 @@ class InstallDocker
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
}
+
+ 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": {
diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php
index 587ac4a8d..2785505c0 100644
--- a/app/Actions/Server/StartSentinel.php
+++ b/app/Actions/Server/StartSentinel.php
@@ -25,7 +25,7 @@ class StartSentinel
$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.');
}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index be9b4062c..9a6cc140b 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -52,7 +52,8 @@ class UpdateCoolify
{
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',
diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php
index 95b08b437..e16dd5616 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -23,7 +23,7 @@ class StopService
$containersToStop = $service->getContainersToStop();
$service->stopContainers($containersToStop, $server);
- if (! $isDeleteOperation) {
+ if ($isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 257de0a92..a4cfde6f8 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -5,12 +5,10 @@ namespace App\Console\Commands;
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 @@ class Dev extends Command
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()
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
index 6cbcb310c..3cef85477 100644
--- a/app/Console/Commands/OpenApi.php
+++ b/app/Console/Commands/OpenApi.php
@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;
+use Symfony\Component\Yaml\Yaml;
class OpenApi extends Command
{
@@ -29,5 +30,10 @@ class OpenApi extends Command
$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";
}
}
diff --git a/app/Console/Commands/RootResetPassword.php b/app/Console/Commands/RootResetPassword.php
index f36c11a4f..436363d06 100644
--- a/app/Console/Commands/RootResetPassword.php
+++ b/app/Console/Commands/RootResetPassword.php
@@ -39,7 +39,13 @@ class RootResetPassword extends Command
}
$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.');
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 122d72c39..a6f24aaad 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -9,6 +9,7 @@ use App\Jobs\CleanupInstanceStuffsJob;
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;
@@ -83,6 +84,8 @@ class Kernel extends ConsoleKernel
$this->checkScheduledBackups();
$this->checkScheduledTasks();
+ $this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
+
$this->scheduleInstance->command('cleanup:database --yes')->daily();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
diff --git a/app/Helpers/SslHelper.php b/app/Helpers/SslHelper.php
new file mode 100644
index 000000000..6397c330d
--- /dev/null
+++ b/app/Helpers/SslHelper.php
@@ -0,0 +1,233 @@
+ 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 = << $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);
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index aef19af23..45968b6c6 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -932,10 +932,31 @@ class ApplicationsController extends Controller
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);
@@ -966,6 +987,8 @@ class ApplicationsController extends Controller
$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)) {
@@ -1310,7 +1333,6 @@ class ApplicationsController extends Controller
$service->destination_type = $destination->getMorphClass();
$service->save();
- $service->name = "service-$service->uuid";
$service->parse(isNew: true);
if ($instantDeploy) {
StartService::dispatch($service);
@@ -2859,198 +2881,198 @@ class ApplicationsController extends Controller
);
}
- #[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)
- {
- $teamId = getTeamIdFromToken();
+ private function validateDataApplications(Request $request, Server $server)
+ {
+ $teamId = getTeamIdFromToken();
- // Validate ports_mappings
- if ($request->has('ports_mappings')) {
- $ports = [];
- foreach (explode(',', $request->ports_mappings) as $portMapping) {
- $port = explode(':', $portMapping);
- if (in_array($port[0], $ports)) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => [
- 'ports_mappings' => 'The first number before : should be unique between mappings.',
- ],
- ], 422);
- }
- $ports[] = $port[0];
- }
- }
- // Validate custom_labels
- if ($request->has('custom_labels')) {
- if (! isBase64Encoded($request->custom_labels)) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => [
- 'custom_labels' => 'The custom_labels should be base64 encoded.',
- ],
- ], 422);
- }
- $customLabels = base64_decode($request->custom_labels);
- if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => [
- 'custom_labels' => 'The custom_labels should be base64 encoded.',
- ],
- ], 422);
- }
- }
- if ($request->has('domains') && $server->isProxyShouldRun()) {
- $uuid = $request->uuid;
- $fqdn = $request->domains;
- $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
- $fqdn = str($fqdn)->replaceStart(',', '')->trim();
- $errors = [];
- $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
- if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
- $errors[] = 'Invalid domain: '.$domain;
- }
+ // Validate ports_mappings
+ if ($request->has('ports_mappings')) {
+ $ports = [];
+ foreach (explode(',', $request->ports_mappings) as $portMapping) {
+ $port = explode(':', $portMapping);
+ if (in_array($port[0], $ports)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'ports_mappings' => 'The first number before : should be unique between mappings.',
+ ],
+ ], 422);
+ }
+ $ports[] = $port[0];
+ }
+ }
+ // Validate custom_labels
+ if ($request->has('custom_labels')) {
+ if (! isBase64Encoded($request->custom_labels)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_labels' => 'The custom_labels should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customLabels = base64_decode($request->custom_labels);
+ if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_labels' => 'The custom_labels should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
+ if ($request->has('domains') && $server->isProxyShouldRun()) {
+ $uuid = $request->uuid;
+ $fqdn = $request->domains;
+ $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
+ $fqdn = str($fqdn)->replaceStart(',', '')->trim();
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
- return str($domain)->trim()->lower();
- });
- if (count($errors) > 0) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => $errors,
- ], 422);
- }
- if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => [
- 'domains' => 'One of the domain is already used.',
- ],
- ], 422);
- }
- }
- }
+ return str($domain)->trim()->lower();
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
+ }
+ }
+ }
}
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 73b452f86..424c2cc76 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
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\Tag;
@@ -142,6 +143,7 @@ class DeployController extends Controller
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 +186,32 @@ class DeployController extends Controller
public function deploy(Request $request)
{
$teamId = getTeamIdFromToken();
+
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
+ $pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0;
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 +224,7 @@ class DeployController extends Controller
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 +289,7 @@ class DeployController extends Controller
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;
@@ -295,6 +303,7 @@ class DeployController extends Controller
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
+ pull_request_id: $pr,
);
$message = "Application {$resource->name} deployment queued.";
break;
@@ -314,4 +323,68 @@ class DeployController extends Controller
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'],
+ 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);
+ }
}
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index fdd46b100..55a6cd9f4 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -368,6 +368,20 @@ class SecurityController extends Controller
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 @@ class SecurityController extends Controller
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([
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 03d9d209c..027bd5c1c 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -13,6 +13,7 @@ use App\Models\Server;
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 @@ class ServicesController extends Controller
}
#[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 @@ class ServicesController extends Controller
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 @@ class ServicesController extends Controller
'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 @@ class ServicesController extends Controller
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 @@ class ServicesController extends Controller
)]
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 @@ class ServicesController extends Controller
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,16 @@ class ServicesController extends Controller
]);
}
- 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);
+
+ 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 +518,206 @@ class ServicesController extends Controller
]);
}
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update service by UUID.',
+ path: '/services/{uuid}',
+ operationId: 'update-service-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ 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);
+
+ 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.',
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 93b43ea07..9afcbe371 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -329,13 +329,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} 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("Starting graceful shutdown container: {$this->deployment_uuid}");
+ $this->graceful_shutdown_container($this->deployment_uuid);
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
@@ -1211,7 +1206,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
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 +1361,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
$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->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
+ $this->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command(
[
$runCommand,
@@ -1718,8 +1708,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'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'));
}
$docker_compose = [
'services' => [
@@ -2029,7 +2018,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
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') {
@@ -2096,7 +2089,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
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}";
diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php
index 0e1fcb4d7..c82a27ce9 100644
--- a/app/Jobs/CleanupHelperContainersJob.php
+++ b/app/Jobs/CleanupHelperContainersJob.php
@@ -20,7 +20,7 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
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) {
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 6070ad16a..3276711c5 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -484,6 +484,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$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;
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index 8b9228e5f..9fd46db77 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -66,12 +66,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
- $this->resource?->delete_volumes($persistentStorages);
+ $this->resource->delete_volumes($persistentStorages);
+ $this->resource->persistentStorages()->delete();
}
- if ($this->deleteConfigurations) {
- $this->resource?->delete_configurations();
- }
-
$isDatabase = $this->resource instanceof StandalonePostgresql
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
@@ -80,6 +77,18 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
+
+ if ($this->deleteConfigurations) {
+ $this->resource->delete_configurations(); // rename to FileStorages
+ $this->resource->fileStorages()->delete();
+ }
+ if ($isDatabase) {
+ $this->resource->sslCertificates()->delete();
+ $this->resource->scheduledBackups()->delete();
+ $this->resource->environment_variables()->delete();
+ $this->resource->tags()->detach();
+ }
+
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::dispatch($server, true);
diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php
new file mode 100644
index 000000000..cf598c75c
--- /dev/null
+++ b/app/Jobs/RegenerateSslCertJob.php
@@ -0,0 +1,78 @@
+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));
+ }
+ }
+}
diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php
new file mode 100644
index 000000000..7fc716f70
--- /dev/null
+++ b/app/Jobs/RestartProxyJob.php
@@ -0,0 +1,46 @@
+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);
+ }
+ }
+}
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 15eabfec5..430470fa0 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -7,6 +7,7 @@ use App\Models\PrivateKey;
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 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
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 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
['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');
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index 57007813e..9489eb128 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -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 @@ class Discord extends Component
$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 @@ class Discord extends Component
$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 @@ class Discord extends Component
]);
$this->saveModel();
} catch (\Throwable $e) {
- $this->discordEnabled = false;
+ $this->discordEnabled = $original;
return handleError($e, $this);
}
diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php
index 53314cd5c..788802353 100644
--- a/app/Livewire/Profile/Index.php
+++ b/app/Livewire/Profile/Index.php
@@ -70,6 +70,7 @@ class Index extends Component
$this->current_password = '';
$this->new_password = '';
$this->new_password_confirmation = '';
+ $this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index 56e0caf75..267ca72ad 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -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 @@ class Configuration extends Component
$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()
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index ec7ea6381..bfe0f8387 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -87,6 +87,7 @@ 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',
@@ -126,6 +127,7 @@ class General extends Component
'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',
@@ -173,6 +175,9 @@ class General extends Component
public function instantSave()
{
+ if ($this->application->settings->isDirty('is_spa')) {
+ $this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
+ }
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
@@ -192,6 +197,7 @@ class General extends Component
if ($this->application->settings->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false);
}
+
}
public function loadComposeFile($isInit = false)
@@ -289,9 +295,9 @@ class General extends Component
}
}
- 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.');
}
@@ -371,6 +377,9 @@ class General extends Component
if ($this->application->isDirty('redirect')) {
$this->setRedirect();
}
+ if ($this->application->isDirty('dockerfile')) {
+ $this->application->parseHealthcheckFromDockerfile($this->application->dockerfile);
+ }
$this->checkFqdns();
@@ -448,7 +457,6 @@ class General extends Component
{
$config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json');
- dd($config);
return response()->streamDownload(function () use ($config) {
echo $config;
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index ea6cd46b0..0fffbef31 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Dragonfly;
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 @@ class General extends Component
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 @@ class General extends Component
$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 @@ class General extends Component
$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 @@ class General extends Component
}
}
}
+
+ 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);
+ }
+ }
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index c3b57b9f4..9ddb1909c 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -31,8 +31,8 @@ class Heading extends Component
$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 @@ class Heading extends Component
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 @@ class Heading extends Component
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
+ $this->dispatch('refresh');
}
public function restart()
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index e768495eb..cfc22aedc 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Keydb;
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 @@ class General extends Component
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 @@ class General extends Component
$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 @@ class General extends Component
$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 @@ class General extends Component
}
}
}
+
+ 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);
+ }
+ }
}
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index c9d473223..174f907c8 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mariadb;
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 @@ class General extends Component
$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 @@ class General extends Component
}
}
+ 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();
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index e19895dae..2ac6e43b7 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mongodb;
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 @@ class General extends Component
$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 @@ class General extends Component
}
}
+ 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();
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 7d5270ddf..ea0ea4691 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mysql;
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 @@ class General extends Component
$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 @@ class General extends Component
}
}
+ 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();
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index 88dd5c1a8..4162f47b5 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Postgresql;
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 @@ 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-ca,verify-full',
];
protected $validationAttributes = [
@@ -65,6 +76,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()
@@ -72,6 +85,12 @@ class General extends Component
$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,53 @@ class General extends Component
}
}
+ 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 instantSave()
{
try {
@@ -143,7 +209,7 @@ class General extends Component
$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 +250,7 @@ class General extends Component
$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 +267,11 @@ class General extends Component
$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([
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index 05babeaec..f03f1256d 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -4,25 +4,24 @@ namespace App\Livewire\Project\Database\Redis;
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 @@ class General extends Component
}
}
+ 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();
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 2f51094d1..3d47ffae5 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -7,7 +7,6 @@ use App\Models\Project;
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 @@ class DockerCompose extends Component
$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 @@ class DockerCompose extends Component
'resourceable_type' => $service->getMorphClass(),
]);
}
- $service->name = "service-$service->uuid";
-
$service->parse(isNew: true);
return redirect()->route('project.service.configuration', [
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 4a81d841f..b1b0aef15 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -106,11 +106,15 @@ class GithubPrivateRepository extends Component
$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 @@ class GithubPrivateRepository extends Component
$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'];
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index c3ed6039a..ebc9878dc 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -74,7 +74,7 @@ CMD ["nginx", "-g", "daemon off;"]
'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,
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 0faf0b8da..e7cff4f29 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -73,7 +73,6 @@ class Create extends Component
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,
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 4d070bc0c..5b88c15eb 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -49,7 +49,6 @@ class FileStorage extends Component
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
- $this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -68,6 +67,18 @@ class FileStorage extends Component
}
}
+ 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 {
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 35e585c82..57952ddb3 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
+use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class All extends Component
{
+ use EnvironmentVariableProtection;
+
public $resource;
public string $resourceClass;
@@ -138,17 +141,57 @@ class All extends Component
private function handleBulkSubmit()
{
$variables = parseEnvFormatToArray($this->variables);
+ $changesMade = false;
+ $errorOccurred = false;
- $this->deleteRemovedVariables(false, $variables);
- $this->updateOrCreateVariables(false, $variables);
+ // Try to delete removed variables
+ $deletedCount = $this->deleteRemovedVariables(false, $variables);
+ if ($deletedCount > 0) {
+ $changesMade = true;
+ } elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
+ // If we tried to delete but couldn't (due to Docker Compose), mark as error
+ $errorOccurred = true;
+ }
+
+ // Update or create variables
+ $updatedCount = $this->updateOrCreateVariables(false, $variables);
+ if ($updatedCount > 0) {
+ $changesMade = true;
+ }
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
- $this->deleteRemovedVariables(true, $previewVariables);
- $this->updateOrCreateVariables(true, $previewVariables);
+
+ // Try to delete removed preview variables
+ $deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
+ if ($deletedPreviewCount > 0) {
+ $changesMade = true;
+ } elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
+ // If we tried to delete but couldn't (due to Docker Compose), mark as error
+ $errorOccurred = true;
+ }
+
+ // Update or create preview variables
+ $updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables);
+ if ($updatedPreviewCount > 0) {
+ $changesMade = true;
+ }
}
- $this->dispatch('success', 'Environment variables updated.');
+ // Debug information
+ \Log::info('Environment variables update status', [
+ 'deletedCount' => $deletedCount,
+ 'updatedCount' => $updatedCount,
+ 'deletedPreviewCount' => $deletedPreviewCount ?? 0,
+ 'updatedPreviewCount' => $updatedPreviewCount ?? 0,
+ 'changesMade' => $changesMade,
+ 'errorOccurred' => $errorOccurred,
+ ]);
+
+ // Only show success message if changes were actually made and no errors occurred
+ if ($changesMade && ! $errorOccurred) {
+ $this->dispatch('success', 'Environment variables updated.');
+ }
}
private function handleSingleSubmit($data)
@@ -184,11 +227,46 @@ class All extends Component
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
+
+ // Get all environment variables that will be deleted
+ $variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
+
+ // If there are no variables to delete, return 0
+ if ($variablesToDelete->isEmpty()) {
+ return 0;
+ }
+
+ // Check for system variables that shouldn't be deleted
+ foreach ($variablesToDelete as $envVar) {
+ if ($this->isProtectedEnvironmentVariable($envVar->key)) {
+ $this->dispatch('error', "Cannot delete system environment variable '{$envVar->key}'.");
+
+ return 0;
+ }
+ }
+
+ // Check if any of these variables are used in Docker Compose
+ if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') {
+ foreach ($variablesToDelete as $envVar) {
+ [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose);
+
+ if ($isUsed) {
+ $this->dispatch('error', "Cannot delete environment variable '{$envVar->key}'
Please remove it from the Docker Compose file first.");
+
+ return 0;
+ }
+ }
+ }
+
+ // If we get here, no variables are used in Docker Compose, so we can delete them
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
+
+ return $variablesToDelete->count();
}
private function updateOrCreateVariables($isPreview, $variables)
{
+ $count = 0;
foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
continue;
@@ -198,8 +276,12 @@ class All extends Component
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
- $found->value = $value;
- $found->save();
+ // Only count as a change if the value actually changed
+ if ($found->value !== $value) {
+ $found->value = $value;
+ $found->save();
+ $count++;
+ }
}
} else {
$environment = new EnvironmentVariable;
@@ -212,8 +294,11 @@ class All extends Component
$environment->resourceable_type = $this->resource->getMorphClass();
$environment->save();
+ $count++;
}
}
+
+ return $count;
}
public function refreshEnvs()
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index 3a7d0faa5..d58151abf 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
+use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class Show extends Component
{
+ use EnvironmentVariableProtection;
+
public $parameters;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
@@ -40,6 +43,8 @@ class Show extends Component
public bool $is_really_required = false;
+ public bool $is_redis_credential = false;
+
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -65,7 +70,9 @@ class Show extends Component
}
$this->parameters = get_route_parameters();
$this->checkEnvs();
-
+ if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
+ $this->is_redis_credential = true;
+ }
}
public function refresh()
@@ -171,6 +178,24 @@ class Show extends Component
public function delete()
{
try {
+ // Check if the variable is protected
+ if ($this->isProtectedEnvironmentVariable($this->env->key)) {
+ $this->dispatch('error', "Cannot delete system environment variable '{$this->env->key}'.");
+
+ return;
+ }
+
+ // Check if the variable is used in Docker Compose
+ if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) {
+ [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose);
+
+ if ($isUsed) {
+ $this->dispatch('error', "Cannot delete environment variable '{$this->env->key}'
Please remove it from the Docker Compose file first.");
+
+ return;
+ }
+ }
+
$this->env->delete();
$this->dispatch('environmentVariableDeleted');
$this->dispatch('success', 'Environment variable deleted successfully.');
diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php
index b269c916f..b2b8b1518 100644
--- a/app/Livewire/Server/Advanced.php
+++ b/app/Livewire/Server/Advanced.php
@@ -2,7 +2,11 @@
namespace App\Livewire\Server;
+use App\Helpers\SslHelper;
+use App\Jobs\RegenerateSslCertJob;
use App\Models\Server;
+use App\Models\SslCertificate;
+use Carbon\Carbon;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -10,6 +14,14 @@ class Advanced extends Component
{
public Server $server;
+ public ?SslCertificate $caCertificate = null;
+
+ public $showCertificate = false;
+
+ public $certificateContent = '';
+
+ public ?Carbon $certificateValidUntil = null;
+
public array $parameters = [];
#[Validate(['string'])]
@@ -30,11 +42,99 @@ class Advanced extends Component
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->parameters = get_route_parameters();
$this->syncData();
+ $this->loadCaCertificate();
} catch (\Throwable) {
return redirect()->route('server.index');
}
}
+ public function loadCaCertificate()
+ {
+ $this->caCertificate = SslCertificate::where('server_id', $this->server->id)->where('is_ca_certificate', true)->first();
+
+ if ($this->caCertificate) {
+ $this->certificateContent = $this->caCertificate->ssl_certificate;
+ $this->certificateValidUntil = $this->caCertificate->valid_until;
+ }
+ }
+
+ public function toggleCertificate()
+ {
+ $this->showCertificate = ! $this->showCertificate;
+ }
+
+ public function saveCaCertificate()
+ {
+ try {
+ if (! $this->certificateContent) {
+ throw new \Exception('Certificate content cannot be empty.');
+ }
+
+ if (! openssl_x509_read($this->certificateContent)) {
+ throw new \Exception('Invalid certificate format.');
+ }
+
+ if ($this->caCertificate) {
+ $this->caCertificate->ssl_certificate = $this->certificateContent;
+ $this->caCertificate->save();
+
+ $this->loadCaCertificate();
+
+ $this->writeCertificateToServer();
+
+ dispatch(new RegenerateSslCertJob(
+ server_id: $this->server->id,
+ force_regeneration: true
+ ));
+ }
+ $this->dispatch('success', 'CA Certificate saved successfully.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function regenerateCaCertificate()
+ {
+ try {
+ SslHelper::generateSslCertificate(
+ commonName: 'Coolify CA Certificate',
+ serverId: $this->server->id,
+ isCaCertificate: true,
+ validityDays: 10 * 365
+ );
+
+ $this->loadCaCertificate();
+
+ $this->writeCertificateToServer();
+
+ dispatch(new RegenerateSslCertJob(
+ server_id: $this->server->id,
+ force_regeneration: true
+ ));
+
+ $this->loadCaCertificate();
+ $this->dispatch('success', 'CA Certificate regenerated successfully.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ private function writeCertificateToServer()
+ {
+ $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 '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
+ "chmod 644 $caCertPath/coolify-ca.crt",
+ ]);
+
+ remote_process($commands, $this->server);
+ }
+
public function syncData(bool $toModel = false)
{
if ($toModel) {
diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php
index f823ff3d4..48eede4e5 100644
--- a/app/Livewire/Server/Proxy/Deploy.php
+++ b/app/Livewire/Server/Proxy/Deploy.php
@@ -4,11 +4,10 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
+use App\Actions\Proxy\StopProxy;
use App\Events\ProxyStatusChanged;
+use App\Jobs\RestartProxyJob;
use App\Models\Server;
-use Carbon\Carbon;
-use Illuminate\Process\InvokedProcess;
-use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -65,7 +64,7 @@ class Deploy extends Component
public function restart()
{
try {
- $this->stop();
+ RestartProxyJob::dispatch($this->server);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -98,43 +97,10 @@ class Deploy extends Component
public function stop(bool $forceStop = true)
{
try {
- $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
- $timeout = 30;
-
- $process = $this->stopContainer($containerName, $timeout);
-
- $startTime = Carbon::now()->getTimestamp();
- while ($process->running()) {
- if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
- $this->forceStopContainer($containerName);
- break;
- }
- usleep(100000);
- }
-
- $this->removeContainer($containerName);
+ StopProxy::run($this->server, $forceStop);
+ $this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
- } finally {
- $this->server->proxy->force_stop = $forceStop;
- $this->server->proxy->status = 'exited';
- $this->server->save();
- $this->dispatch('proxyStatusUpdated');
}
}
-
- private function stopContainer(string $containerName, int $timeout): InvokedProcess
- {
- return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
- }
-
- private function forceStopContainer(string $containerName)
- {
- instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
- }
-
- private function removeContainer(string $containerName)
- {
- instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
- }
}
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
index 1b0599ffe..bb5ed0aa8 100644
--- a/app/Livewire/SettingsBackup.php
+++ b/app/Livewire/SettingsBackup.php
@@ -15,6 +15,8 @@ class SettingsBackup extends Component
{
public InstanceSettings $settings;
+ public Server $server;
+
public ?StandalonePostgresql $database = null;
public ScheduledDatabaseBackup|null|array $backup = [];
@@ -46,6 +48,7 @@ class SettingsBackup extends Component
return redirect()->route('dashboard');
} else {
$settings = instanceSettings();
+ $this->server = Server::findOrFail(0);
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
@@ -60,6 +63,10 @@ class SettingsBackup extends Component
$this->database->save();
}
$this->backup = $this->database->scheduledBackups->first();
+ if ($this->backup && ! $this->server->isFunctional()) {
+ $this->backup->enabled = false;
+ $this->backup->save();
+ }
$this->executions = $this->backup->executions;
}
$this->settings = $settings;
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 4205594a5..b2394d7b0 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -4,7 +4,7 @@ namespace App\Livewire;
use App\Models\InstanceSettings;
use App\Models\Team;
-use App\Notifications\Test;
+use App\Notifications\TransactionalEmails\Test;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -225,7 +225,7 @@ class SettingsEmail extends Component
'test-email:'.$this->team->id,
$perMinute = 0,
function () {
- $this->team?->notify(new Test($this->testEmailAddress, 'email'));
+ $this->team?->notify(new Test($this->testEmailAddress));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
@@ -235,7 +235,7 @@ class SettingsEmail extends Component
throw new \Exception('Too many messages sent!');
}
} catch (\Throwable $e) {
- return handleError($e);
+ return handleError($e, $this);
}
}
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index e8ad4c1b4..d2c2d97a7 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -1068,7 +1068,6 @@ class Application extends BaseModel
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$base_command = "{$base_command} {$customRepository}";
- $base_command = $this->setGitImportSettings($deployment_uuid, $base_command, public: true);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
@@ -1511,6 +1510,7 @@ class Application extends BaseModel
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
{
+ $dockerfile = str($dockerfile)->trim()->explode("\n");
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
$healthcheckCommand = null;
$lines = $dockerfile->toArray();
@@ -1530,27 +1530,24 @@ class Application extends BaseModel
}
}
if (str($healthcheckCommand)->isNotEmpty()) {
- $interval = str($healthcheckCommand)->match('/--interval=(\d+)/');
- $timeout = str($healthcheckCommand)->match('/--timeout=(\d+)/');
- $start_period = str($healthcheckCommand)->match('/--start-period=(\d+)/');
- $start_interval = str($healthcheckCommand)->match('/--start-interval=(\d+)/');
+ $interval = str($healthcheckCommand)->match('/--interval=([0-9]+[a-zµ]*)/');
+ $timeout = str($healthcheckCommand)->match('/--timeout=([0-9]+[a-zµ]*)/');
+ $start_period = str($healthcheckCommand)->match('/--start-period=([0-9]+[a-zµ]*)/');
$retries = str($healthcheckCommand)->match('/--retries=(\d+)/');
+
if ($interval->isNotEmpty()) {
- $this->health_check_interval = $interval->toInteger();
+ $this->health_check_interval = parseDockerfileInterval($interval);
}
if ($timeout->isNotEmpty()) {
- $this->health_check_timeout = $timeout->toInteger();
+ $this->health_check_timeout = parseDockerfileInterval($timeout);
}
if ($start_period->isNotEmpty()) {
- $this->health_check_start_period = $start_period->toInteger();
+ $this->health_check_start_period = parseDockerfileInterval($start_period);
}
- // if ($start_interval) {
- // $this->health_check_start_interval = $start_interval->value();
- // }
if ($retries->isNotEmpty()) {
$this->health_check_retries = $retries->toInteger();
}
- if ($interval || $timeout || $start_period || $start_interval || $retries) {
+ if ($interval || $timeout || $start_period || $retries) {
$this->custom_healthcheck_found = true;
$this->save();
}
diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php
index 619393ddc..1ba16ccd8 100644
--- a/app/Models/DiscordNotificationSettings.php
+++ b/app/Models/DiscordNotificationSettings.php
@@ -28,6 +28,7 @@ class DiscordNotificationSettings extends Model
'server_disk_usage_discord_notifications',
'server_reachable_discord_notifications',
'server_unreachable_discord_notifications',
+ 'discord_ping_enabled',
];
protected $casts = [
@@ -45,6 +46,7 @@ class DiscordNotificationSettings extends Model
'server_disk_usage_discord_notifications' => 'boolean',
'server_reachable_discord_notifications' => 'boolean',
'server_unreachable_discord_notifications' => 'boolean',
+ 'discord_ping_enabled' => 'boolean',
];
public function team()
@@ -56,4 +58,9 @@ class DiscordNotificationSettings extends Model
{
return $this->discord_enabled;
}
+
+ public function isPingEnabled()
+ {
+ return $this->discord_ping_enabled;
+ }
}
diff --git a/app/Models/EmailNotificationSettings.php b/app/Models/EmailNotificationSettings.php
index ae118986f..445987619 100644
--- a/app/Models/EmailNotificationSettings.php
+++ b/app/Models/EmailNotificationSettings.php
@@ -70,10 +70,6 @@ class EmailNotificationSettings extends Model
public function isEnabled()
{
- if (isCloud()) {
- return true;
- }
-
return $this->smtp_enabled || $this->resend_enabled || $this->use_instance_email_settings;
}
}
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 5b89bb401..ac95bb8a9 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -3,16 +3,12 @@
namespace App\Models;
use App\Jobs\PullHelperImageJob;
-use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Notifications\Notifiable;
use Spatie\Url\Url;
-class InstanceSettings extends Model implements SendsEmail
+class InstanceSettings extends Model
{
- use Notifiable;
-
protected $guarded = [];
protected $casts = [
@@ -92,15 +88,15 @@ class InstanceSettings extends Model implements SendsEmail
return InstanceSettings::findOrFail(0);
}
- public function getRecipients($notification)
- {
- $recipients = data_get($notification, 'emails', null);
- if (is_null($recipients) || $recipients === '') {
- return [];
- }
+ // public function getRecipients($notification)
+ // {
+ // $recipients = data_get($notification, 'emails', null);
+ // if (is_null($recipients) || $recipients === '') {
+ // return [];
+ // }
- return explode(',', $recipients);
- }
+ // return explode(',', $recipients);
+ // }
public function getTitleDisplayName(): string
{
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index d96f7125e..c56cd7694 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -8,6 +8,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
class LocalFileVolume extends BaseModel
{
+ protected $casts = [
+ // 'fs_path' => 'encrypted',
+ // 'mount_path' => 'encrypted',
+ 'content' => 'encrypted',
+ 'is_directory' => 'boolean',
+ ];
+
use HasFactory;
protected $guarded = [];
@@ -169,4 +176,19 @@ class LocalFileVolume extends BaseModel
return instant_remote_process($commands, $server);
}
+
+ // Accessor for convenient access
+ protected function plainMountPath(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->mount_path,
+ set: fn ($value) => $this->mount_path = $value
+ );
+ }
+
+ // Scope for searching
+ public function scopeWherePlainMountPath($query, $path)
+ {
+ return $query->get()->where('plain_mount_path', $path);
+ }
}
diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php
index 68e476365..b5dfd9663 100644
--- a/app/Models/LocalPersistentVolume.php
+++ b/app/Models/LocalPersistentVolume.php
@@ -24,11 +24,6 @@ class LocalPersistentVolume extends Model
return $this->morphTo('resource');
}
- public function standalone_postgresql()
- {
- return $this->morphTo('resource');
- }
-
protected function name(): Attribute
{
return Attribute::make(
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 828500c40..60d0da3ed 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -7,9 +7,12 @@ use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
use App\Events\ServerReachabilityChanged;
+use App\Helpers\SslHelper;
use App\Jobs\CheckAndStartSentinelJob;
+use App\Jobs\RegenerateSslCertJob;
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
+use App\Services\ConfigurationRepository;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -484,7 +487,7 @@ $schema://$host {
$base_path = config('constants.coolify.base_config_path');
$proxyType = $this->proxyType();
$proxy_path = "$base_path/proxy";
- // TODO: should use /traefik for already exisiting configurations?
+ // TODO: should use /traefik for already existing configurations?
// Should move everything except /caddy and /nginx to /traefik
// The code needs to be modified as well, so maybe it does not worth it
if ($proxyType === ProxyTypes::TRAEFIK->value) {
@@ -543,7 +546,7 @@ $schema://$host {
$this->settings->save();
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
- Storage::disk('ssh-mux')->delete($this->muxFilename());
+ $this->disableSshMux();
}
public function sentinelHeartbeat(bool $isReset = false)
@@ -922,7 +925,7 @@ $schema://$host {
public function isFunctional()
{
- $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
+ $isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && $this->ip !== '1.2.3.4';
if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename());
@@ -1103,7 +1106,7 @@ $schema://$host {
public function validateConnection(bool $justCheckingNewKey = false)
{
- config()->set('constants.ssh.mux_enabled', false);
+ $this->disableSshMux();
if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
@@ -1330,4 +1333,47 @@ $schema://$host {
$this->databases()->count() == 0 &&
$this->services()->count() == 0;
}
+
+ private function disableSshMux(): void
+ {
+ $configRepository = app(ConfigurationRepository::class);
+ $configRepository->disableSshMux();
+ }
+
+ public function generateCaCertificate()
+ {
+ try {
+ ray('Generating CA certificate for server', $this->id);
+ SslHelper::generateSslCertificate(
+ commonName: 'Coolify CA Certificate',
+ serverId: $this->id,
+ isCaCertificate: true,
+ validityDays: 10 * 365
+ );
+ $caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first();
+ ray('CA certificate generated', $caCertificate);
+ if ($caCertificate) {
+ $certificateContent = $caCertificate->ssl_certificate;
+ $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 '{$certificateContent}' > $caCertPath/coolify-ca.crt",
+ "chmod 644 $caCertPath/coolify-ca.crt",
+ ]);
+
+ instant_remote_process($commands, $this, false);
+
+ dispatch(new RegenerateSslCertJob(
+ server_id: $this->id,
+ force_regeneration: true
+ ));
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 25e6b92ea..23ddb5923 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -50,6 +50,11 @@ class Service extends BaseModel
protected static function booted()
{
+ static::creating(function ($service) {
+ if (blank($service->name)) {
+ $service->name = 'service-'.(new Cuid2);
+ }
+ });
static::created(function ($service) {
$service->compose_parsing_version = self::$parserVersion;
$service->save();
diff --git a/app/Models/SslCertificate.php b/app/Models/SslCertificate.php
new file mode 100644
index 000000000..eb2175d44
--- /dev/null
+++ b/app/Models/SslCertificate.php
@@ -0,0 +1,49 @@
+ 'encrypted',
+ 'ssl_private_key' => 'encrypted',
+ 'subject_alternative_names' => 'array',
+ 'valid_until' => 'datetime',
+ ];
+
+ public function application()
+ {
+ return $this->morphTo('resource');
+ }
+
+ public function service()
+ {
+ return $this->morphTo('resource');
+ }
+
+ public function database()
+ {
+ return $this->morphTo('resource');
+ }
+
+ public function server()
+ {
+ return $this->belongsTo(Server::class);
+ }
+}
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 60198115d..bc1f9b4b3 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -163,6 +163,11 @@ class StandaloneClickhouse extends BaseModel
return data_get($this, 'environment.project');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -218,7 +223,12 @@ class StandaloneClickhouse extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
+ get: function () {
+ $encodedUser = rawurlencode($this->clickhouse_admin_user);
+ $encodedPass = rawurlencode($this->clickhouse_admin_password);
+
+ return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$this->clickhouse_db}";
+ },
);
}
@@ -227,7 +237,10 @@ class StandaloneClickhouse extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
+ $encodedUser = rawurlencode($this->clickhouse_admin_user);
+ $encodedPass = rawurlencode($this->clickhouse_admin_password);
+
+ return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
}
return null;
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 3c1127d8d..a14c5e378 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -168,6 +168,11 @@ class StandaloneDragonfly extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -218,7 +223,18 @@ class StandaloneDragonfly extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $port = $this->enable_ssl ? 6380 : 6379;
+ $encodedPass = rawurlencode($this->dragonfly_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
+ }
);
}
@@ -227,7 +243,15 @@ class StandaloneDragonfly extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $encodedPass = rawurlencode($this->dragonfly_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
return null;
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index ebf1c22e9..2d3aea755 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -168,6 +168,11 @@ class StandaloneKeydb extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -218,7 +223,18 @@ class StandaloneKeydb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $port = $this->enable_ssl ? 6380 : 6379;
+ $encodedPass = rawurlencode($this->keydb_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
+ }
);
}
@@ -227,7 +243,15 @@ class StandaloneKeydb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $encodedPass = rawurlencode($this->keydb_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
return null;
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 004ead4d9..7549ace3e 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -218,7 +218,12 @@ class StandaloneMariadb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
+ get: function () {
+ $encodedUser = rawurlencode($this->mariadb_user);
+ $encodedPass = rawurlencode($this->mariadb_password);
+
+ return "mysql://{$encodedUser}:{$encodedPass}@{$this->uuid}:3306/{$this->mariadb_database}";
+ },
);
}
@@ -227,7 +232,10 @@ class StandaloneMariadb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
+ $encodedUser = rawurlencode($this->mariadb_user);
+ $encodedPass = rawurlencode($this->mariadb_password);
+
+ return "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
}
return null;
@@ -271,6 +279,11 @@ class StandaloneMariadb extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index aba0f6123..3092216bd 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -177,6 +177,11 @@ class StandaloneMongodb extends BaseModel
return data_get($this, 'is_log_drain_enabled', false);
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -238,7 +243,19 @@ class StandaloneMongodb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true",
+ get: function () {
+ $encodedUser = rawurlencode($this->mongo_initdb_root_username);
+ $encodedPass = rawurlencode($this->mongo_initdb_root_password);
+ $url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->uuid}:27017/?directConnection=true";
+ if ($this->enable_ssl) {
+ $url .= '&tls=true&tlsCAFile=/etc/mongo/certs/ca.pem';
+ if (in_array($this->ssl_mode, ['verify-full'])) {
+ $url .= '&tlsCertificateKeyFile=/etc/mongo/certs/server.pem';
+ }
+ }
+
+ return $url;
+ },
);
}
@@ -247,7 +264,17 @@ class StandaloneMongodb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
+ $encodedUser = rawurlencode($this->mongo_initdb_root_username);
+ $encodedPass = rawurlencode($this->mongo_initdb_root_password);
+ $url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
+ if ($this->enable_ssl) {
+ $url .= '&tls=true&tlsCAFile=/etc/mongo/certs/ca.pem';
+ if (in_array($this->ssl_mode, ['verify-full'])) {
+ $url .= '&tlsCertificateKeyFile=/etc/mongo/certs/server.pem';
+ }
+ }
+
+ return $url;
}
return null;
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 9ae0fdcae..dbb5b1ae6 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -169,6 +169,11 @@ class StandaloneMysql extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -219,7 +224,19 @@ class StandaloneMysql extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}",
+ get: function () {
+ $encodedUser = rawurlencode($this->mysql_user);
+ $encodedPass = rawurlencode($this->mysql_password);
+ $url = "mysql://{$encodedUser}:{$encodedPass}@{$this->uuid}:3306/{$this->mysql_database}";
+ if ($this->enable_ssl) {
+ $url .= "?ssl-mode={$this->ssl_mode}";
+ if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
+ $url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt';
+ }
+ }
+
+ return $url;
+ },
);
}
@@ -228,7 +245,17 @@ class StandaloneMysql extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
+ $encodedUser = rawurlencode($this->mysql_user);
+ $encodedPass = rawurlencode($this->mysql_password);
+ $url = "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
+ if ($this->enable_ssl) {
+ $url .= "?ssl-mode={$this->ssl_mode}";
+ if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
+ $url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt';
+ }
+ }
+
+ return $url;
}
return null;
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index dd92ae7c9..a74d567a0 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -219,7 +219,19 @@ class StandalonePostgresql extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}",
+ get: function () {
+ $encodedUser = rawurlencode($this->postgres_user);
+ $encodedPass = rawurlencode($this->postgres_password);
+ $url = "postgres://{$encodedUser}:{$encodedPass}@{$this->uuid}:5432/{$this->postgres_db}";
+ if ($this->enable_ssl) {
+ $url .= "?sslmode={$this->ssl_mode}";
+ if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
+ $url .= '&sslrootcert=/etc/ssl/certs/coolify-ca.crt';
+ }
+ }
+
+ return $url;
+ },
);
}
@@ -228,7 +240,17 @@ class StandalonePostgresql extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
+ $encodedUser = rawurlencode($this->postgres_user);
+ $encodedPass = rawurlencode($this->postgres_password);
+ $url = "postgres://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
+ if ($this->enable_ssl) {
+ $url .= "?sslmode={$this->ssl_mode}";
+ if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
+ $url .= '&sslrootcert=/etc/ssl/certs/coolify-ca.crt';
+ }
+ }
+
+ return $url;
}
return null;
@@ -241,11 +263,21 @@ class StandalonePostgresql extends BaseModel
return $this->belongsTo(Environment::class);
}
+ public function persistentStorages()
+ {
+ return $this->morphMany(LocalPersistentVolume::class, 'resource');
+ }
+
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function destination()
{
return $this->morphTo();
@@ -256,16 +288,17 @@ class StandalonePostgresql extends BaseModel
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
- public function persistentStorages()
- {
- return $this->morphMany(LocalPersistentVolume::class, 'resource');
- }
-
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+ public function environment_variables()
+ {
+ return $this->morphMany(EnvironmentVariable::class, 'resourceable')
+ ->orderBy('key', 'asc');
+ }
+
public function isBackupSolutionAvailable()
{
return true;
@@ -314,10 +347,4 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray();
}
-
- public function environment_variables()
- {
- return $this->morphMany(EnvironmentVariable::class, 'resourceable')
- ->orderBy('key', 'asc');
- }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 6037364fe..fccbb24a5 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -170,6 +170,11 @@ class StandaloneRedis extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -222,9 +227,17 @@ class StandaloneRedis extends BaseModel
return new Attribute(
get: function () {
$redis_version = $this->getRedisVersion();
- $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+ $username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
+ $encodedPass = rawurlencode($this->redis_password);
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $port = $this->enable_ssl ? 6380 : 6379;
+ $url = "{$scheme}://{$username_part}{$encodedPass}@{$this->uuid}:{$port}/0";
- return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
);
}
@@ -235,9 +248,16 @@ class StandaloneRedis extends BaseModel
get: function () {
if ($this->is_public && $this->public_port) {
$redis_version = $this->getRedisVersion();
- $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+ $username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
+ $encodedPass = rawurlencode($this->redis_password);
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $url = "{$scheme}://{$username_part}{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
- return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
return null;
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 6796b22ad..d36f8c1ab 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -163,14 +163,17 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
];
}
- public function getRecipients($notification)
+ public function getRecipients(): array
{
- $recipients = data_get($notification, 'emails', null);
- if (is_null($recipients)) {
- return $this->members()->pluck('email')->toArray();
+ $recipients = $this->members()->pluck('email')->toArray();
+ $validatedEmails = array_filter($recipients, function ($email) {
+ return filter_var($email, FILTER_VALIDATE_EMAIL);
+ });
+ if (is_null($validatedEmails)) {
+ return [];
}
- return explode(',', $recipients);
+ return array_values($validatedEmails);
}
public function isAnyNotificationEnabled()
diff --git a/app/Models/User.php b/app/Models/User.php
index 7c23631c3..f9515ad09 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
+use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -37,7 +38,7 @@ use OpenApi\Attributes as OA;
)]
class User extends Authenticatable implements SendsEmail
{
- use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
+ use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $guarded = [];
@@ -57,6 +58,7 @@ class User extends Authenticatable implements SendsEmail
protected static function boot()
{
parent::boot();
+
static::created(function (User $user) {
$team = [
'name' => $user->name."'s Team",
@@ -114,9 +116,9 @@ class User extends Authenticatable implements SendsEmail
return $this->belongsToMany(Team::class)->withPivot('role');
}
- public function getRecipients($notification)
+ public function getRecipients(): array
{
- return $this->email;
+ return [$this->email];
}
public function sendVerificationEmail()
diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php
index 362006d8e..b4ba9bf8c 100644
--- a/app/Notifications/Channels/DiscordChannel.php
+++ b/app/Notifications/Channels/DiscordChannel.php
@@ -20,6 +20,10 @@ class DiscordChannel
return;
}
+ if (! $discordSettings->discord_ping_enabled) {
+ $message->isCritical = false;
+ }
+
SendMessageToDiscordJob::dispatch($message, $discordSettings->discord_webhook_url);
}
}
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 215fae4ea..8a9a95107 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -2,89 +2,69 @@
namespace App\Notifications\Channels;
-use Exception;
-use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Facades\Mail;
+use Resend;
class EmailChannel
{
+ public function __construct() {}
+
public function send(SendsEmail $notifiable, Notification $notification): void
{
- try {
- $this->bootConfigs($notifiable);
- $recipients = $notifiable->getRecipients($notification);
- if (count($recipients) === 0) {
- throw new Exception('No email recipients found');
- }
-
- $mailMessage = $notification->toMail($notifiable);
- Mail::send(
- [],
- [],
- fn (Message $message) => $message
- ->to($recipients)
- ->subject($mailMessage->subject)
- ->html((string) $mailMessage->render())
- );
- } catch (Exception $e) {
- $error = $e->getMessage();
- if ($error === 'No email settings found.') {
- throw $e;
- }
- $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
- if (isset($recipients)) {
- $message .= implode(', ', $recipients);
- }
- if (isset($mailMessage)) {
- $message .= " with subject: {$mailMessage->subject}";
- }
- send_internal_notification($message);
- throw $e;
+ $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings;
+ $isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false);
+ $customEmails = data_get($notification, 'emails', null);
+ if ($useInstanceEmailSettings || $isTransactionalEmail) {
+ $settings = instanceSettings();
+ } else {
+ $settings = $notifiable->emailNotificationSettings;
}
- }
-
- private function bootConfigs($notifiable): void
- {
- $emailSettings = $notifiable->emailNotificationSettings;
-
- if ($emailSettings->use_instance_email_settings) {
- $type = set_transanctional_email_settings();
- if (blank($type)) {
- throw new Exception('No email settings found.');
- }
-
- return;
+ $isResendEnabled = $settings->resend_enabled;
+ $isSmtpEnabled = $settings->smtp_enabled;
+ if ($customEmails) {
+ $recipients = [$customEmails];
+ } else {
+ $recipients = $notifiable->getRecipients();
}
+ $mailMessage = $notification->toMail($notifiable);
- config()->set('mail.from.address', $emailSettings->smtp_from_address ?? 'test@example.com');
- config()->set('mail.from.name', $emailSettings->smtp_from_name ?? 'Test');
-
- if ($emailSettings->resend_enabled) {
- config()->set('mail.default', 'resend');
- config()->set('resend.api_key', $emailSettings->resend_api_key);
- }
-
- if ($emailSettings->smtp_enabled) {
- $encryption = match (strtolower($emailSettings->smtp_encryption)) {
+ if ($isResendEnabled) {
+ $resend = Resend::client($settings->resend_api_key);
+ $from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>";
+ $resend->emails->send([
+ 'from' => $from,
+ 'to' => $recipients,
+ 'subject' => $mailMessage->subject,
+ 'html' => (string) $mailMessage->render(),
+ ]);
+ } elseif ($isSmtpEnabled) {
+ $encryption = match (strtolower($settings->smtp_encryption)) {
'starttls' => null,
'tls' => 'tls',
'none' => null,
default => null,
};
- config()->set('mail.default', 'smtp');
- config()->set('mail.mailers.smtp', [
- 'transport' => 'smtp',
- 'host' => $emailSettings->smtp_host,
- 'port' => $emailSettings->smtp_port,
- 'encryption' => $encryption,
- 'username' => $emailSettings->smtp_username,
- 'password' => $emailSettings->smtp_password,
- 'timeout' => $emailSettings->smtp_timeout,
- 'local_domain' => null,
- 'auto_tls' => $emailSettings->smtp_encryption === 'none' ? '0' : '', // If encryption is "none", it will not try to upgrade to TLS via StartTLS to make sure it is unencrypted.
- ]);
+ $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
+ $settings->smtp_host,
+ $settings->smtp_port,
+ $encryption
+ );
+ $transport->setUsername($settings->smtp_username ?? '');
+ $transport->setPassword($settings->smtp_password ?? '');
+
+ $mailer = new \Symfony\Component\Mailer\Mailer($transport);
+
+ $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost';
+ $fromName = $settings->smtp_from_name ?? 'System';
+ $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName);
+ $email = (new \Symfony\Component\Mime\Email)
+ ->from($from)
+ ->to(...$recipients)
+ ->subject($mailMessage->subject)
+ ->html((string) $mailMessage->render());
+
+ $mailer->send($email);
}
}
}
diff --git a/app/Notifications/Channels/SendsEmail.php b/app/Notifications/Channels/SendsEmail.php
index 3adc6d0a2..7039a3066 100644
--- a/app/Notifications/Channels/SendsEmail.php
+++ b/app/Notifications/Channels/SendsEmail.php
@@ -4,5 +4,5 @@ namespace App\Notifications\Channels;
interface SendsEmail
{
- public function getRecipients($notification);
+ public function getRecipients(): array;
}
diff --git a/app/Notifications/Notification.php b/app/Notifications/Notification.php
new file mode 100644
index 000000000..d37716a8b
--- /dev/null
+++ b/app/Notifications/Notification.php
@@ -0,0 +1,22 @@
+onQueue('high');
+ $this->resources = collect($resources);
+
+ // Collect URLs for each resource
+ $this->resources->each(function ($resource) {
+ if (data_get($resource, 'environment.project.uuid')) {
+ $routeName = match ($resource->type()) {
+ 'application' => 'project.application.configuration',
+ 'database' => 'project.database.configuration',
+ 'service' => 'project.service.configuration',
+ default => null
+ };
+
+ if ($routeName) {
+ $route = route($routeName, [
+ 'project_uuid' => data_get($resource, 'environment.project.uuid'),
+ 'environment_uuid' => data_get($resource, 'environment.uuid'),
+ $resource->type().'_uuid' => data_get($resource, 'uuid'),
+ ]);
+
+ $settings = instanceSettings();
+ if (data_get($settings, 'fqdn')) {
+ $url = Url::fromString($route);
+ $url = $url->withPort(null);
+ $fqdn = data_get($settings, 'fqdn');
+ $fqdn = str_replace(['http://', 'https://'], '', $fqdn);
+ $url = $url->withHost($fqdn);
+
+ $this->urls[$resource->name] = $url->__toString();
+ } else {
+ $this->urls[$resource->name] = $route;
+ }
+ }
+ }
+ });
+ }
+
+ public function via(object $notifiable): array
+ {
+ return $notifiable->getEnabledChannels('ssl_certificate_renewal');
+ }
+
+ public function toMail(): MailMessage
+ {
+ $mail = new MailMessage;
+ $mail->subject('Coolify: [Action Required] SSL Certificates Renewed - Manual Redeployment Needed');
+ $mail->view('emails.ssl-certificate-renewed', [
+ 'resources' => $this->resources,
+ 'urls' => $this->urls,
+ ]);
+
+ return $mail;
+ }
+
+ public function toDiscord(): DiscordMessage
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+
+ $message = new DiscordMessage(
+ title: '🔒 SSL Certificates Renewed',
+ description: "SSL certificates have been renewed for: {$resourceNames}.\n\n**Action Required:** These resources need to be redeployed manually.",
+ color: DiscordMessage::warningColor(),
+ );
+
+ foreach ($this->urls as $name => $url) {
+ $message->addField($name, "[View Resource]({$url})");
+ }
+
+ return $message;
+ }
+
+ public function toTelegram(): array
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+ $message = "Coolify: SSL certificates have been renewed for: {$resourceNames}.\n\nAction Required: These resources need to be redeployed manually for the new SSL certificates to take effect.";
+
+ $buttons = [];
+ foreach ($this->urls as $name => $url) {
+ $buttons[] = [
+ 'text' => "View {$name}",
+ 'url' => $url,
+ ];
+ }
+
+ return [
+ 'message' => $message,
+ 'buttons' => $buttons,
+ ];
+ }
+
+ public function toPushover(): PushoverMessage
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+ $message = "SSL certificates have been renewed for: {$resourceNames}
https://github.com/coollabsio/coolify-examples main branch-ı seçiləcək https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch-ı seçiləcək. https://gitea.com/sedlav/expressjs.git main branch-ı seçiləcək. https://gitlab.com/andrasbacsai/nodejs-example.git main branch-ı seçiləcək.",
+ "service.stop": "Bu xidmət dayandırılacaq.",
+ "resource.docker_cleanup": "Docker təmizlənməsini işə salın (istifadə olunmayan şəkillər və builder keşini silin).",
+ "resource.non_persistent": "Bütün qeyri-daimi məlumatlar silinəcək.",
+ "resource.delete_volumes": "Bu resursla əlaqəli bütün həcm məlumatları tamamilə silinəcək.",
+ "resource.delete_connected_networks": "Bu resursla əlaqəli bütün əvvəlcədən təyin olunmamış şəbəkələr tamamilə silinəcək.",
+ "resource.delete_configurations": "Serverdən bütün konfiqurasiya faylları tamamilə silinəcək.",
+ "database.delete_backups_locally": "Bütün ehtiyat nüsxələr lokal yaddaşdan tamamilə silinəcək.",
+ "warning.sslipdomain": "Konfiqurasiya yadda saxlanıldı, lakin sslip domeni ilə https TÖVSİYƏ EDİLMİR, çünki Let's Encrypt serverləri bu ümumi domenlə məhdudlaşdırılır (SSL sertifikatının təsdiqlənməsi uğursuz olacaq).
Əvəzində öz domeninizdən istifadə edin."
+}
diff --git a/lang/id.json b/lang/id.json
new file mode 100644
index 000000000..d35556402
--- /dev/null
+++ b/lang/id.json
@@ -0,0 +1,40 @@
+{
+ "auth.login": "Masuk",
+ "auth.login.authentik": "Masuk dengan Authentik",
+ "auth.login.azure": "Masuk dengan Microsoft",
+ "auth.login.bitbucket": "Masuk dengan Bitbucket",
+ "auth.login.github": "Masuk dengan GitHub",
+ "auth.login.gitlab": "Masuk dengan Gitlab",
+ "auth.login.google": "Masuk dengan Google",
+ "auth.login.infomaniak": "Masuk dengan Infomaniak",
+ "auth.already_registered": "Sudah terdaftar?",
+ "auth.confirm_password": "Konfirmasi kata sandi",
+ "auth.forgot_password": "Lupa kata sandi",
+ "auth.forgot_password_send_email": "Kirim email reset kata sandi",
+ "auth.register_now": "Daftar",
+ "auth.logout": "Keluar",
+ "auth.register": "Daftar",
+ "auth.registration_disabled": "Pendaftaran dinonaktifkan. Harap hubungi administrator.",
+ "auth.reset_password": "Reset kata sandi",
+ "auth.failed": "Kredensial ini tidak cocok dengan catatan kami.",
+ "auth.failed.callback": "Gagal memproses callback dari penyedia login.",
+ "auth.failed.password": "Kata sandi yang diberikan salah.",
+ "auth.failed.email": "Kami tidak dapat menemukan pengguna dengan alamat e-mail tersebut.",
+ "auth.throttle": "Terlalu banyak percobaan login. Silakan coba lagi dalam :seconds detik.",
+ "input.name": "Nama",
+ "input.email": "Email",
+ "input.password": "Kata sandi",
+ "input.password.again": "Kata sandi lagi",
+ "input.code": "Kode sekali pakai",
+ "input.recovery_code": "Kode pemulihan",
+ "button.save": "Simpan",
+ "repository.url": "Contoh Untuk repositori Publik, gunakan https://.... Untuk repositori Privat, gunakan git@....
https://github.com/coollabsio/coolify-examples cabang main akan dipilih https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify cabang nodejs-fastify akan dipilih. https://gitea.com/sedlav/expressjs.git cabang main akan dipilih. https://gitlab.com/andrasbacsai/nodejs-example.git cabang main akan dipilih.",
+ "service.stop": "Layanan ini akan dihentikan.",
+ "resource.docker_cleanup": "Jalankan Pembersihan Docker (hapus gambar yang tidak digunakan dan cache builder).",
+ "resource.non_persistent": "Semua data non-persisten akan dihapus.",
+ "resource.delete_volumes": "Hapus permanen semua volume yang terkait dengan sumber daya ini.",
+ "resource.delete_connected_networks": "Hapus permanen semua jaringan non-predefined yang terkait dengan sumber daya ini.",
+ "resource.delete_configurations": "Hapus permanen semua file konfigurasi dari server.",
+ "database.delete_backups_locally": "Semua backup akan dihapus permanen dari penyimpanan lokal.",
+ "warning.sslipdomain": "Konfigurasi Anda disimpan, tetapi domain sslip dengan https TIDAK direkomendasikan, karena server Let's Encrypt dengan domain publik ini dibatasi (validasi sertifikat SSL akan gagal).
Gunakan domain Anda sendiri sebagai gantinya."
+}
diff --git a/lang/no.json b/lang/no.json
new file mode 100644
index 000000000..29d5af124
--- /dev/null
+++ b/lang/no.json
@@ -0,0 +1,40 @@
+{
+ "auth.login": "Logg inn",
+ "auth.login.authentik": "Logg inn med Authentik",
+ "auth.login.azure": "Logg inn med Microsoft",
+ "auth.login.bitbucket": "Logg inn med Bitbucket",
+ "auth.login.github": "Logg inn med GitHub",
+ "auth.login.gitlab": "Logg inn med Gitlab",
+ "auth.login.google": "Logg inn med Google",
+ "auth.login.infomaniak": "Logg inn med Infomaniak",
+ "auth.already_registered": "Allerede registrert?",
+ "auth.confirm_password": "Bekreft passord",
+ "auth.forgot_password": "Glemt passord",
+ "auth.forgot_password_send_email": "Send e-post for tilbakestilling av passord",
+ "auth.register_now": "Registrer deg",
+ "auth.logout": "Logg ut",
+ "auth.register": "Registrer",
+ "auth.registration_disabled": "Registrering er deaktivert. Vennligst kontakt administrator.",
+ "auth.reset_password": "Tilbakestill passord",
+ "auth.failed": "Disse legitimasjonene samsvarer ikke med våre registre.",
+ "auth.failed.callback": "Klarte ikke å behandle tilbakekall fra innloggingsleverandør.",
+ "auth.failed.password": "Det oppgitte passordet er feil.",
+ "auth.failed.email": "Vi finner ingen bruker med den e-postadressen.",
+ "auth.throttle": "For mange innloggingsforsøk. Vennligst prøv igjen om :seconds sekunder.",
+ "input.name": "Navn",
+ "input.email": "E-post",
+ "input.password": "Passord",
+ "input.password.again": "Passord igjen",
+ "input.code": "Engangskode",
+ "input.recovery_code": "Gjenopprettingskode",
+ "button.save": "Lagre",
+ "repository.url": "Eksempler For offentlige repositorier, bruk https://.... For private repositorier, bruk git@....
https://github.com/coollabsio/coolify-examples main gren vil bli valgt https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify gren vil bli valgt. https://gitea.com/sedlav/expressjs.git main gren vil bli valgt. https://gitlab.com/andrasbacsai/nodejs-example.git main gren vil bli valgt.",
+ "service.stop": "Denne tjenesten vil bli stoppet.",
+ "resource.docker_cleanup": "Kjør Docker-opprydding (fjern ubrukte bilder og byggebuffer).",
+ "resource.non_persistent": "Alle ikke-persistente data vil bli slettet.",
+ "resource.delete_volumes": "Slett alle volumer tilknyttet denne ressursen permanent.",
+ "resource.delete_connected_networks": "Slett alle ikke-forhåndsdefinerte nettverk tilknyttet denne ressursen permanent.",
+ "resource.delete_configurations": "Slett alle konfigurasjonsfiler fra serveren permanent.",
+ "database.delete_backups_locally": "Alle sikkerhetskopier vil bli slettet permanent fra lokal lagring.",
+ "warning.sslipdomain": "Konfigurasjonen din er lagret, men sslip-domene med https er IKKE anbefalt, fordi Let's Encrypt-servere med dette offentlige domenet er hastighetsbegrenset (SSL-sertifikatvalidering vil mislykkes).
Bruk ditt eget domene i stedet."
+}
diff --git a/lang/pt-br.json b/lang/pt-br.json
new file mode 100644
index 000000000..2e793890f
--- /dev/null
+++ b/lang/pt-br.json
@@ -0,0 +1,40 @@
+{
+ "auth.login": "Entrar",
+ "auth.login.authentik": "Entrar com Authentik",
+ "auth.login.azure": "Entrar com Microsoft",
+ "auth.login.bitbucket": "Entrar com Bitbucket",
+ "auth.login.github": "Entrar com GitHub",
+ "auth.login.gitlab": "Entrar com Gitlab",
+ "auth.login.google": "Entrar com Google",
+ "auth.login.infomaniak": "Entrar com Infomaniak",
+ "auth.already_registered": "Já tem uma conta?",
+ "auth.confirm_password": "Confirmar senha",
+ "auth.forgot_password": "Esqueceu a senha",
+ "auth.forgot_password_send_email": "Enviar e-mail para redefinir senha",
+ "auth.register_now": "Cadastre-se",
+ "auth.logout": "Sair",
+ "auth.register": "Cadastrar",
+ "auth.registration_disabled": "O registro está desativado. Por favor, contate o administrador.",
+ "auth.reset_password": "Redefinir senha",
+ "auth.failed": "Essas credenciais não correspondem aos nossos registros.",
+ "auth.failed.callback": "Falha ao processar o callback do provedor de login.",
+ "auth.failed.password": "A senha fornecida está incorreta.",
+ "auth.failed.email": "Não encontramos nenhum usuário com esse endereço de e-mail.",
+ "auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.",
+ "input.name": "Nome",
+ "input.email": "E-mail",
+ "input.password": "Senha",
+ "input.password.again": "Senha novamente",
+ "input.code": "Código de uso único",
+ "input.recovery_code": "Código de recuperação",
+ "button.save": "Salvar",
+ "repository.url": "Exemplos Para repositórios públicos, use https://.... Para repositórios privados, use git@....
https://github.com/coollabsio/coolify-examples main branch será selecionado https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch será selecionado. https://gitea.com/sedlav/expressjs.git main branch será selecionado. https://gitlab.com/andrasbacsai/nodejs-example.git main branch será selecionado.",
+ "service.stop": "Este serviço será parado.",
+ "resource.docker_cleanup": "Executar limpeza do Docker (remover imagens não utilizadas e cache de build).",
+ "resource.non_persistent": "Todos os dados não persistentes serão excluídos.",
+ "resource.delete_volumes": "Excluir permanentemente todos os volumes associados a este recurso.",
+ "resource.delete_connected_networks": "Excluir permanentemente todas as redes não predefinidas associadas a este recurso.",
+ "resource.delete_configurations": "Excluir permanentemente todos os arquivos de configuração do servidor.",
+ "database.delete_backups_locally": "Todos os backups serão excluídos permanentemente do armazenamento local.",
+ "warning.sslipdomain": "Sua configuração foi salva, mas o domínio sslip com https NÃO é recomendado, porque os servidores do Let's Encrypt com este domínio público têm limitação de taxa (a validação do certificado SSL falhará).
Use seu próprio domínio em vez disso."
+}
diff --git a/lang/tr.json b/lang/tr.json
index 3cbcee409..663c756f9 100644
--- a/lang/tr.json
+++ b/lang/tr.json
@@ -27,5 +27,13 @@
"input.code": "Tek Kullanımlık Kod",
"input.recovery_code": "Kurtarma Kodu",
"button.save": "Kaydet",
- "repository.url": "Örnekler Halka açık depolar için https://... kullanın. Özel depolar için git@... kullanın.
https://github.com/coollabsio/coolify-examples main dalı seçilecek https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek. https://gitea.com/sedlav/expressjs.git main dalı seçilecek. https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek."
+ "repository.url": "Örnekler Halka açık depolar için https://... kullanın. Özel depolar için git@... kullanın.
https://github.com/coollabsio/coolify-examples main dalı seçilecek https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek. https://gitea.com/sedlav/expressjs.git main dalı seçilecek. https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek.",
+ "service.stop": "Bu servis durdurulacak.",
+ "resource.docker_cleanup": "Docker temizliği çalıştır (kullanılmayan imajları ve oluşturucu önbelleğini kaldır).",
+ "resource.non_persistent": "Tüm kalıcı olmayan veriler silinecek.",
+ "resource.delete_volumes": "Bu kaynakla ilişkili tüm hacimler kalıcı olarak silinecek.",
+ "resource.delete_connected_networks": "Bu kaynakla ilişkili önceden tanımlanmamış tüm ağlar kalıcı olarak silinecek.",
+ "resource.delete_configurations": "Sunucudaki tüm yapılandırma dosyaları kalıcı olarak silinecek.",
+ "database.delete_backups_locally": "Tüm yedekler yerel depolamadan kalıcı olarak silinecek.",
+ "warning.sslipdomain": "Yapılandırmanız kaydedildi, ancak sslip domain ile https ÖNERİLMEZ, çünkü Let's Encrypt sunucuları bu genel domain ile sınırlandırılmıştır (SSL sertifikası doğrulaması başarısız olur).