diff --git a/CHANGELOG.md b/CHANGELOG.md index 3447b223b..aefabfd29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,78 @@ # Changelog All notable changes to this project will be documented in this file. -## [unreleased] +## [4.0.0-beta.434] - 2025-10-03 + +### 🚀 Features + +- *(deployments)* Enhance Docker build argument handling for multiline variables +- *(deployments)* Add log copying functionality to clipboard in dev +- *(deployments)* Generate SERVICE_NAME environment variables from Docker Compose services + +### 🐛 Bug Fixes + +- *(deployments)* Enhance builder container management and environment variable handling + +### 📚 Documentation + +- Update changelog +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Update version numbers for Coolify releases +- *(versions)* Bump Coolify stable version to 4.0.0-beta.434 + +## [4.0.0-beta.433] - 2025-10-01 + +### 🚀 Features + +- *(user-deletion)* Implement file locking to prevent concurrent user deletions and enhance error handling +- *(ui)* Enhance resource operations interface with dynamic selection for cloning and moving resources +- *(global-search)* Integrate projects and environments into global search functionality +- *(storage)* Consolidate storage management into a single component with enhanced UI +- *(deployments)* Add support for Coolify variables in Dockerfile + +### 🐛 Bug Fixes + +- *(workflows)* Update CLAUDE API key reference in GitHub Actions workflow +- *(ui)* Update docker registry image helper text for clarity +- *(ui)* Correct HTML structure and improve clarity in Docker cleanup options +- *(workflows)* Update CLAUDE API key reference in GitHub Actions workflow +- *(api)* Correct OpenAPI schema annotations for array items +- *(ui)* Improve queued deployment status readability in dark mode +- *(git)* Handle additional repository URL cases for 'tangled' and improve branch assignment logic +- *(git)* Enhance error handling for missing branch information during deployment +- *(git)* Trim whitespace from repository, branch, and commit SHA fields +- *(deployments)* Order deployments by ID for consistent retrieval + +### 💼 Other + +- *(storage)* Enhance file storage management with new properties and UI improvements +- *(core)* Update projects property type and enhance UI styling +- *(components)* Adjust SVG icon sizes for consistency across applications and services +- *(components)* Auto-focus first input in modal on open +- *(styles)* Enhance focus styles for buttons and links +- *(components)* Enhance close button accessibility in modal + +### 🚜 Refactor + +- *(global-search)* Change event listener to window level for global search modal +- *(dashboard)* Remove deployment loading logic and introduce DeploymentsIndicator component for better UI management +- *(dashboard)* Replace project navigation method with direct link in UI +- *(global-search)* Improve event handling and cleanup in global search component + +### 📚 Documentation + +- Update changelog +- Update changelog +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Update coolify version to 4.0.0-beta.433 and nightly version to 4.0.0-beta.434 in configuration files + +## [4.0.0-beta.432] - 2025-09-29 ### 🚀 Features @@ -188,6 +259,7 @@ ## [4.0.0-beta.427] - 2025-09-15 ### 🚀 Features +- Add Ente Photos service template - *(command)* Add option to sync GitHub releases to BunnyCDN and refactor sync logic - *(ui)* Display current version in settings dropdown and update UI accordingly - *(settings)* Add option to restrict PR deployments to repository members and contributors diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index ce9e723d4..065d7f767 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1512,9 +1512,32 @@ private function create_application(Request $request, $type) if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } - if (! $request->docker_registry_image_tag) { - $request->offsetSet('docker_registry_image_tag', 'latest'); + // Process docker image name and tag for SHA256 digests + $dockerImageName = $request->docker_registry_image_name; + $dockerImageTag = $request->docker_registry_image_tag; + + // Strip 'sha256:' prefix if user provided it in the tag + if ($dockerImageTag) { + $dockerImageTag = preg_replace('/^sha256:/i', '', trim($dockerImageTag)); } + + // Remove @sha256 from image name if user added it + if ($dockerImageName) { + $dockerImageName = preg_replace('/@sha256$/i', '', trim($dockerImageName)); + } + + // Check if tag is a valid SHA256 hash (64 hex characters) + $isSha256Hash = $dockerImageTag && preg_match('/^[a-f0-9]{64}$/i', $dockerImageTag); + + // Append @sha256 to image name if using digest and not already present + if ($isSha256Hash && ! str_ends_with($dockerImageName, '@sha256')) { + $dockerImageName .= '@sha256'; + } + + // Set processed values back to request + $request->offsetSet('docker_registry_image_name', $dockerImageName); + $request->offsetSet('docker_registry_image_tag', $dockerImageTag ?: 'latest'); + $application = new Application; removeUnnecessaryFieldsFromRequest($request); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 04b11b9b4..2a3f20f37 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -116,16 +116,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private $env_args; - private $environment_variables; - private $env_nixpacks_args; private $docker_compose; private $docker_compose_base64; - private ?string $env_filename = null; - private ?string $nixpacks_plan = null; private Collection $nixpacks_plan_json; @@ -503,7 +499,12 @@ private function deploy_dockerimage_buildpack() } else { $this->dockerImageTag = $this->application->docker_registry_image_tag; } - $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); + + // Check if this is an image hash deployment + $isImageHash = str($this->dockerImageTag)->startsWith('sha256-'); + $displayName = $isImageHash ? "{$this->dockerImage}@sha256:".str($this->dockerImageTag)->after('sha256-') : "{$this->dockerImage}:{$this->dockerImageTag}"; + + $this->application_deployment_queue->addLogEntry("Starting deployment of {$displayName} to {$this->server->name}."); $this->generate_image_names(); $this->prepare_builder_image(); $this->generate_compose_file(); @@ -571,7 +572,6 @@ private function deploy_docker_compose_buildpack() if ($this->application->settings->is_raw_compose_deployment_enabled) { $this->application->oldRawParser(); $yaml = $composeFile = $this->application->docker_compose_raw; - $this->generate_runtime_environment_variables(); // For raw compose, we cannot automatically add secrets configuration // User must define it manually in their docker-compose file @@ -580,16 +580,14 @@ private function deploy_docker_compose_buildpack() } } else { $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); - $this->generate_runtime_environment_variables(); - if (filled($this->env_filename)) { - $services = collect(data_get($composeFile, 'services', [])); - $services = $services->map(function ($service, $name) { - $service['env_file'] = [$this->env_filename]; + // Always add .env file to services + $services = collect(data_get($composeFile, 'services', [])); + $services = $services->map(function ($service, $name) { + $service['env_file'] = ['.env']; - return $service; - }); - $composeFile['services'] = $services->toArray(); - } + return $service; + }); + $composeFile['services'] = $services->toArray(); if (empty($composeFile)) { $this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.'); $this->fail('Failed to parse docker-compose file.'); @@ -615,6 +613,9 @@ private function deploy_docker_compose_buildpack() // Build new container to limit downtime. $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); + // Save build-time .env file BEFORE the build + $this->save_buildtime_environment_variables(); + if ($this->docker_compose_custom_build_command) { // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported $build_command = $this->docker_compose_custom_build_command; @@ -630,9 +631,8 @@ private function deploy_docker_compose_buildpack() if ($this->dockerBuildkitSupported) { $command = "DOCKER_BUILDKIT=1 {$command}"; } - if (filled($this->env_filename)) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; - } + // Always use .env file + $command .= " --env-file {$this->workdir}/.env"; if ($this->force_rebuild) { $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache"; } else { @@ -652,6 +652,10 @@ private function deploy_docker_compose_buildpack() ); } + // Save runtime environment variables AFTER the build + // This overwrites the build-time .env with ALL variables (build-time + runtime) + $this->save_runtime_environment_variables(); + $this->stop_running_container(force: true); $this->application_deployment_queue->addLogEntry('Starting new application.'); $networkId = $this->application->uuid; @@ -685,9 +689,8 @@ private function deploy_docker_compose_buildpack() $this->docker_compose_location = '/docker-compose.yaml'; $command = "{$this->coolify_variables} docker compose"; - if (filled($this->env_filename)) { - $command .= " --env-file {$server_workdir}/{$this->env_filename}"; - } + // Always use .env file + $command .= " --env-file {$server_workdir}/.env"; $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( ['command' => $command, 'hidden' => true], @@ -702,9 +705,8 @@ private function deploy_docker_compose_buildpack() } else { $command = "{$this->coolify_variables} docker compose"; if ($this->preserveRepository) { - if (filled($this->env_filename)) { - $command .= " --env-file {$server_workdir}/{$this->env_filename}"; - } + // Always use .env file + $command .= " --env-file {$server_workdir}/.env"; $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->write_deployment_configurations(); @@ -712,9 +714,8 @@ private function deploy_docker_compose_buildpack() ['command' => $command, 'hidden' => true], ); } else { - if (filled($this->env_filename)) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; - } + // Always use .env file + $command .= " --env-file {$this->workdir}/.env"; $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], @@ -748,9 +749,18 @@ private function deploy_dockerfile_buildpack() } $this->cleanup_git(); $this->generate_compose_file(); + + // Save build-time .env file BEFORE the build + $this->save_buildtime_environment_variables(); + $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); + + // Save runtime environment variables AFTER the build + // This overwrites the build-time .env with ALL variables (build-time + runtime) + $this->save_runtime_environment_variables(); + $this->push_to_docker_registry(); $this->rolling_update(); } @@ -774,11 +784,15 @@ private function deploy_nixpacks_buildpack() $this->cleanup_git(); $this->generate_nixpacks_confs(); $this->generate_compose_file(); + + // Save build-time .env file BEFORE the build for Nixpacks + $this->save_buildtime_environment_variables(); + $this->generate_build_env_variables(); $this->build_image(); // For Nixpacks, save runtime environment variables AFTER the build - // to prevent them from being accessible during the build process + // This overwrites the build-time .env with ALL variables (build-time + runtime) $this->save_runtime_environment_variables(); $this->push_to_docker_registry(); $this->rolling_update(); @@ -802,7 +816,16 @@ private function deploy_static_buildpack() $this->clone_repository(); $this->cleanup_git(); $this->generate_compose_file(); + + // Save build-time .env file BEFORE the build + $this->save_buildtime_environment_variables(); + $this->build_static_image(); + + // Save runtime environment variables AFTER the build + // This overwrites the build-time .env with ALL variables (build-time + runtime) + $this->save_runtime_environment_variables(); + $this->push_to_docker_registry(); $this->rolling_update(); } @@ -934,7 +957,13 @@ private function generate_image_names() $this->production_image_name = "{$this->application->uuid}:latest"; } } elseif ($this->application->build_pack === 'dockerimage') { - $this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}"; + // Check if this is an image hash deployment + if (str($this->dockerImageTag)->startsWith('sha256-')) { + $hash = str($this->dockerImageTag)->after('sha256-'); + $this->production_image_name = "{$this->dockerImage}@sha256:{$hash}"; + } else { + $this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}"; + } } elseif ($this->pull_request_id !== 0) { if ($this->application->docker_registry_image_name) { $this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"; @@ -1049,8 +1078,6 @@ private function generate_runtime_environment_variables() $envs->push($key.'='.$item); }); if ($this->pull_request_id === 0) { - $this->env_filename = '.env'; - // Generate SERVICE_ variables first for dockercompose if ($this->build_pack === 'dockercompose') { $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect([]); @@ -1109,8 +1136,6 @@ private function generate_runtime_environment_variables() $envs->push('HOST=0.0.0.0'); } } else { - $this->env_filename = '.env'; - // Generate SERVICE_ variables first for dockercompose preview if ($this->build_pack === 'dockercompose') { $domains = collect(json_decode(data_get($this->preview, 'docker_compose_domains'))) ?? collect([]); @@ -1165,99 +1190,250 @@ private function generate_runtime_environment_variables() $envs->push('HOST=0.0.0.0'); } } - if ($envs->isEmpty()) { - if ($this->env_filename) { - if ($this->use_build_server) { - $this->server = $this->original_server; - $this->execute_remote_command( - [ - 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); - $this->server = $this->build_server; - $this->execute_remote_command( - [ - 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); - } else { - $this->execute_remote_command( - [ - 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); - } - } - $this->env_filename = null; - } else { - // For Nixpacks builds, we save the .env file AFTER the build to prevent - // runtime-only variables from being accessible during the build process - if ($this->application->build_pack !== 'nixpacks' || $this->skip_build) { - $envs_base64 = base64_encode($envs->implode("\n")); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), - ], - ); - if ($this->use_build_server) { - $this->server = $this->original_server; - $this->execute_remote_command( - [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", - ] - ); - $this->server = $this->build_server; - } else { - $this->execute_remote_command( - [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", - ] - ); - } - } - } - $this->environment_variables = $envs; + // Return the generated environment variables instead of storing them globally + return $envs; } private function save_runtime_environment_variables() { - // This method saves the .env file with runtime variables - // It should be called AFTER the build for Nixpacks to prevent runtime-only variables - // from being accessible during the build process + // This method saves the .env file with ALL runtime variables + // For builds, it should be called AFTER the build to include runtime-only variables - if ($this->environment_variables && $this->environment_variables->isNotEmpty() && $this->env_filename) { - $envs_base64 = base64_encode($this->environment_variables->implode("\n")); + // Generate runtime environment variables locally + $environment_variables = $this->generate_runtime_environment_variables(); - // Write .env file to workdir (for container runtime) + // Handle empty environment variables + if ($environment_variables->isEmpty()) { + // For Docker Compose, we need to create an empty .env file + // because we always reference it in the compose file + if ($this->build_pack === 'dockercompose') { + $this->application_deployment_queue->addLogEntry('Creating empty .env file (no environment variables defined).'); + + // Create empty .env file + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "touch $this->workdir/.env"), + ] + ); + + // Also create in configuration directory + if ($this->use_build_server) { + $this->server = $this->original_server; + $this->execute_remote_command( + [ + "touch $this->configuration_dir/.env", + ] + ); + $this->server = $this->build_server; + } else { + $this->execute_remote_command( + [ + "touch $this->configuration_dir/.env", + ] + ); + } + } else { + // For non-Docker Compose deployments, clean up any existing .env files + if ($this->use_build_server) { + $this->server = $this->original_server; + $this->execute_remote_command( + [ + 'command' => "rm -f $this->configuration_dir/.env", + 'hidden' => true, + 'ignore_errors' => true, + ] + ); + $this->server = $this->build_server; + $this->execute_remote_command( + [ + 'command' => "rm -f $this->configuration_dir/.env", + 'hidden' => true, + 'ignore_errors' => true, + ] + ); + } else { + $this->execute_remote_command( + [ + 'command' => "rm -f $this->configuration_dir/.env", + 'hidden' => true, + 'ignore_errors' => true, + ] + ); + } + } + + return; + } + + // Write the environment variables to file + $envs_base64 = base64_encode($environment_variables->implode("\n")); + + // Write .env file to workdir (for container runtime) + $this->application_deployment_queue->addLogEntry('Creating .env file with runtime variables for build phase.', hidden: true); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/.env > /dev/null"), + ], + [ + executeInDocker($this->deployment_uuid, "cat $this->workdir/.env"), + 'hidden' => true, + + ] + ); + + // Write .env file to configuration directory + if ($this->use_build_server) { + $this->server = $this->original_server; $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/.env > /dev/null", + ] + ); + $this->server = $this->build_server; + } else { + $this->execute_remote_command( + [ + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/.env > /dev/null", + ] + ); + } + } + + private function generate_buildtime_environment_variables() + { + $envs = collect([]); + $coolify_envs = $this->generate_coolify_env_variables(); + + // Add COOLIFY variables + $coolify_envs->each(function ($item, $key) use ($envs) { + $envs->push($key.'='.$item); + }); + + // Add SERVICE_NAME variables for Docker Compose builds + if ($this->build_pack === 'dockercompose') { + if ($this->pull_request_id === 0) { + // Generate SERVICE_NAME for dockercompose services from processed compose + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $dockerCompose = Yaml::parse($this->application->docker_compose_raw); + } else { + $dockerCompose = Yaml::parse($this->application->docker_compose); + } + $services = data_get($dockerCompose, 'services', []); + foreach ($services as $serviceName => $_) { + $envs->push('SERVICE_NAME_'.str($serviceName)->upper().'='.$serviceName); + } + + // Generate SERVICE_FQDN & SERVICE_URL for non-PR deployments + $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect([]); + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); + } + } + } else { + // Generate SERVICE_NAME for preview deployments + $rawDockerCompose = Yaml::parse($this->application->docker_compose_raw); + $rawServices = data_get($rawDockerCompose, 'services', []); + foreach ($rawServices as $rawServiceName => $_) { + $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id)); + } + + // Generate SERVICE_FQDN & SERVICE_URL for preview deployments with PR-specific domains + $domains = collect(json_decode(data_get($this->preview, 'docker_compose_domains'))) ?? collect([]); + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); + } + } + } + } + + // Add build-time user variables only + if ($this->pull_request_id === 0) { + $sorted_environment_variables = $this->application->environment_variables() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) // ONLY build-time variables + ->orderBy($this->application->settings->is_env_sorting_enabled ? 'key' : 'id') + ->get(); + + // For Docker Compose, filter out SERVICE_FQDN and SERVICE_URL as we generate these + if ($this->build_pack === 'dockercompose') { + $sorted_environment_variables = $sorted_environment_variables->filter(function ($env) { + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + }); + } + + foreach ($sorted_environment_variables as $env) { + $envs->push($env->key.'='.$env->real_value); + } + } else { + $sorted_environment_variables = $this->application->environment_variables_preview() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) // ONLY build-time variables + ->orderBy($this->application->settings->is_env_sorting_enabled ? 'key' : 'id') + ->get(); + + // For Docker Compose, filter out SERVICE_FQDN and SERVICE_URL as we generate these with PR-specific values + if ($this->build_pack === 'dockercompose') { + $sorted_environment_variables = $sorted_environment_variables->filter(function ($env) { + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + }); + } + + foreach ($sorted_environment_variables as $env) { + $envs->push($env->key.'='.$env->real_value); + } + } + + // Return the generated environment variables + return $envs; + } + + private function save_buildtime_environment_variables() + { + // Generate build-time environment variables locally + $environment_variables = $this->generate_buildtime_environment_variables(); + + // Save .env file for build phase + if ($environment_variables->isNotEmpty()) { + $envs_base64 = base64_encode($environment_variables->implode("\n")); + + $this->application_deployment_queue->addLogEntry('Creating .env file with build-time variables for build phase.', hidden: true); + + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/.env > /dev/null"), + ], + [ + executeInDocker($this->deployment_uuid, "cat $this->workdir/.env"), + 'hidden' => true, ], ); + } elseif ($this->build_pack === 'dockercompose') { + // For Docker Compose, create an empty .env file even if there are no build-time variables + // This ensures the file exists when referenced in docker-compose commands + $this->application_deployment_queue->addLogEntry('Creating empty .env file for build phase (no build-time variables defined).', hidden: true); - // Write .env file to configuration directory - if ($this->use_build_server) { - $this->server = $this->original_server; - $this->execute_remote_command( - [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", - ] - ); - $this->server = $this->build_server; - } else { - $this->execute_remote_command( - [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", - ] - ); - } + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "touch $this->workdir/.env"), + ] + ); } } @@ -1472,15 +1648,18 @@ private function deploy_pull_request() $this->generate_nixpacks_confs(); } $this->generate_compose_file(); + + // Save build-time .env file BEFORE the build + $this->save_buildtime_environment_variables(); + $this->generate_build_env_variables(); if ($this->application->build_pack === 'dockerfile') { $this->add_build_env_variables_to_dockerfile(); } $this->build_image(); - // For Nixpacks, save runtime environment variables AFTER the build - if ($this->application->build_pack === 'nixpacks') { - $this->save_runtime_environment_variables(); - } + + // This overwrites the build-time .env with ALL variables (build-time + runtime) + $this->save_runtime_environment_variables(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -1526,7 +1705,6 @@ private function prepare_builder_image() $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); $env_flags = $this->generate_docker_env_flags_for_secrets(); - if ($this->use_build_server) { if ($this->dockerConfigFileExists === 'NOK') { throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.'); @@ -1553,6 +1731,18 @@ private function prepare_builder_image() $this->run_pre_deployment_command(); } + private function restart_builder_container_with_actual_commit() + { + // Stop and remove the current helper container + $this->graceful_shutdown_container($this->deployment_uuid); + + // Clear cached env_args to force regeneration with actual SOURCE_COMMIT value + $this->env_args = null; + + // Restart the helper container with updated environment variables (including actual SOURCE_COMMIT) + $this->prepare_builder_image(); + } + private function deploy_to_additional_destinations() { if ($this->application->additional_networks->count() === 0) { @@ -1621,6 +1811,8 @@ private function set_coolify_variables() if (isset($this->application->git_branch)) { $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} "; } + $this->coolify_variables .= "COOLIFY_RESOURCE_UUID={$this->application->uuid} "; + $this->coolify_variables .= "COOLIFY_CONTAINER_NAME={$this->container_name} "; } private function check_git_if_build_needed() @@ -1688,6 +1880,12 @@ private function check_git_if_build_needed() $this->application_deployment_queue->save(); } $this->set_coolify_variables(); + + // Restart helper container with actual SOURCE_COMMIT value + if ($this->application->settings->use_build_secrets && $this->commit !== 'HEAD') { + $this->application_deployment_queue->addLogEntry('Restarting helper container with actual SOURCE_COMMIT value.'); + $this->restart_builder_container_with_actual_commit(); + } } private function clone_repository() @@ -1935,11 +2133,14 @@ private function generate_env_variables() { $this->env_args = collect([]); $this->env_args->put('SOURCE_COMMIT', $this->commit); + $coolify_envs = $this->generate_coolify_env_variables(); + $coolify_envs->each(function ($value, $key) { + $this->env_args->put($key, $value); + }); // For build process, include only environment variables where is_buildtime = true if ($this->pull_request_id === 0) { - // Get environment variables that are marked as available during build $envs = $this->application->environment_variables() ->where('key', 'not like', 'NIXPACKS_%') ->where('is_buildtime', true) @@ -1948,24 +2149,9 @@ private function generate_env_variables() foreach ($envs as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); - if (str($env->real_value)->startsWith('$')) { - $variable_key = str($env->real_value)->after('$'); - if ($variable_key->startsWith('COOLIFY_')) { - $variable = $coolify_envs->get($variable_key->value()); - if (filled($variable)) { - $this->env_args->prepend($variable, $variable_key->value()); - } - } else { - $variable = $this->application->environment_variables()->where('key', $variable_key)->first(); - if ($variable) { - $this->env_args->prepend($variable->real_value, $env->key); - } - } - } } } } else { - // Get preview environment variables that are marked as available during build $envs = $this->application->environment_variables_preview() ->where('key', 'not like', 'NIXPACKS_%') ->where('is_buildtime', true) @@ -1974,20 +2160,6 @@ private function generate_env_variables() foreach ($envs as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); - if (str($env->real_value)->startsWith('$')) { - $variable_key = str($env->real_value)->after('$'); - if ($variable_key->startsWith('COOLIFY_')) { - $variable = $coolify_envs->get($variable_key->value()); - if (filled($variable)) { - $this->env_args->prepend($variable, $variable_key->value()); - } - } else { - $variable = $this->application->environment_variables_preview()->where('key', $variable_key)->first(); - if ($variable) { - $this->env_args->prepend($variable->real_value, $env->key); - } - } - } } } } @@ -2001,7 +2173,6 @@ private function generate_compose_file() $persistent_storages = $this->generate_local_persistent_volumes(); $persistent_file_volumes = $this->application->fileStorages()->get(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); - $this->generate_runtime_environment_variables(); if (data_get($this->application, 'custom_labels')) { $this->application->parseContainerLabels(); $labels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels))); @@ -2070,9 +2241,8 @@ private function generate_compose_file() ], ], ]; - if (filled($this->env_filename)) { - $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; - } + // Always use .env file + $docker_compose['services'][$this->container_name]['env_file'] = ['.env']; $docker_compose['services'][$this->container_name]['healthcheck'] = [ 'test' => [ 'CMD-SHELL', @@ -2364,13 +2534,12 @@ private function build_image() // Coolify variables are already included in the secrets from generate_build_env_variables // build_secrets is already a string at this point } else { - // Traditional build args approach - $this->environment_variables->filter(function ($key, $value) { - return str($key)->startsWith('COOLIFY_'); - })->each(function ($key, $value) { + // Traditional build args approach - generate COOLIFY_ variables locally + // Generate COOLIFY_ variables locally for build args + $coolify_envs = $this->generate_coolify_env_variables(); + $coolify_envs->each(function ($value, $key) { $this->build_args->push("--build-arg '{$key}'"); }); - $this->build_args = $this->build_args instanceof \Illuminate\Support\Collection ? $this->build_args->implode(' ') : (string) $this->build_args; @@ -2610,6 +2779,8 @@ private function build_image() } else { // Dockerfile buildpack if ($this->dockerBuildkitSupported) { + // Modify the Dockerfile to use build secrets + $this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}"); // Use BuildKit with secrets $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; if ($this->force_rebuild) { @@ -2764,7 +2935,6 @@ private function generate_build_env_variables() $this->generate_env_variables(); $variables = collect([])->merge($this->env_args); } - // Analyze build variables for potential issues if ($variables->isNotEmpty()) { $this->analyzeBuildTimeVariables($variables); @@ -2809,9 +2979,13 @@ private function generate_docker_env_flags_for_secrets() return ''; } - $variables = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get(); + // Generate env variables if not already done + // This populates $this->env_args with both user-defined and COOLIFY_* variables + if (! $this->env_args || $this->env_args->isEmpty()) { + $this->generate_env_variables(); + } + + $variables = $this->env_args; if ($variables->isEmpty()) { return ''; @@ -2819,12 +2993,19 @@ private function generate_docker_env_flags_for_secrets() $secrets_hash = $this->generate_secrets_hash($variables); + // Get database env vars to check for multiline flag + $env_vars = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('is_buildtime', true)->get() + : $this->application->environment_variables_preview()->where('is_buildtime', true)->get(); + // Map to simple array format for the helper function - $vars_array = $variables->map(function ($env) { + $vars_array = $variables->map(function ($value, $key) use ($env_vars) { + $env = $env_vars->firstWhere('key', $key); + return [ - 'key' => $env->key, - 'value' => $env->real_value, - 'is_multiline' => $env->is_multiline, + 'key' => $key, + 'value' => $value, + 'is_multiline' => $env ? $env->is_multiline : false, ]; }); @@ -2888,9 +3069,9 @@ private function add_build_env_variables_to_dockerfile() executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile', + 'ignore_errors' => true, ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - if ($this->pull_request_id === 0) { // Only add environment variables that are available during build $envs = $this->application->environment_variables() @@ -2947,10 +3128,17 @@ private function add_build_env_variables_to_dockerfile() } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), - 'hidden' => true, - ]); + $this->application_deployment_queue->addLogEntry('Final Dockerfile:', type: 'info'); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), + 'hidden' => true, + 'ignore_errors' => true, + ]); } } @@ -2975,16 +3163,19 @@ private function modify_dockerfile_for_secrets($dockerfile_path) $dockerfile->prepend('# syntax=docker/dockerfile:1'); } - // Get environment variables for secrets - $variables = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get(); + // Generate env variables if not already done + // This populates $this->env_args with both user-defined and COOLIFY_* variables + if (! $this->env_args || $this->env_args->isEmpty()) { + $this->generate_env_variables(); + } + + $variables = $this->env_args; if ($variables->isEmpty()) { return; } // Generate mount strings for all secrets - $mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' '); + $mountStrings = $variables->map(fn ($value, $key) => "--mount=type=secret,id={$key},env={$key}")->implode(' '); // Add mount for the secrets hash to ensure cache invalidation $mountStrings .= ' --mount=type=secret,id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH'; @@ -3012,8 +3203,6 @@ private function modify_dockerfile_for_secrets($dockerfile_path) executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$dockerfile_path} > /dev/null"), 'hidden' => true, ]); - - $this->application_deployment_queue->addLogEntry('Modified Dockerfile to use build secrets.'); } } @@ -3023,15 +3212,13 @@ private function modify_dockerfiles_for_compose($composeFile) return; } - $variables = $this->pull_request_id === 0 - ? $this->application->environment_variables() - ->where('key', 'not like', 'NIXPACKS_%') - ->where('is_buildtime', true) - ->get() - : $this->application->environment_variables_preview() - ->where('key', 'not like', 'NIXPACKS_%') - ->where('is_buildtime', true) - ->get(); + // Generate env variables if not already done + // This populates $this->env_args with both user-defined and COOLIFY_* variables + if (! $this->env_args || $this->env_args->isEmpty()) { + $this->generate_env_variables(); + } + + $variables = $this->env_args; if ($variables->isEmpty()) { $this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.'); @@ -3105,11 +3292,10 @@ private function modify_dockerfiles_for_compose($composeFile) $isMultiStage = count($fromIndices) > 1; $argsToAdd = collect([]); - foreach ($variables as $env) { - $argsToAdd->push("ARG {$env->key}"); + foreach ($variables as $key => $value) { + $argsToAdd->push("ARG {$key}"); } - ray($argsToAdd); if ($argsToAdd->isEmpty()) { $this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add."); @@ -3177,19 +3363,22 @@ private function modify_dockerfiles_for_compose($composeFile) private function add_build_secrets_to_compose($composeFile) { - // Get environment variables for secrets - $variables = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Generate env variables if not already done + // This populates $this->env_args with both user-defined and COOLIFY_* variables + if (! $this->env_args || $this->env_args->isEmpty()) { + $this->generate_env_variables(); + } + + $variables = $this->env_args; if ($variables->isEmpty()) { return $composeFile; } $secrets = []; - foreach ($variables as $env) { - $secrets[$env->key] = [ - 'environment' => $env->key, + foreach ($variables as $key => $value) { + $secrets[$key] = [ + 'environment' => $key, ]; } @@ -3204,9 +3393,9 @@ private function add_build_secrets_to_compose($composeFile) if (! isset($service['build']['secrets'])) { $service['build']['secrets'] = []; } - foreach ($variables as $env) { - if (! in_array($env->key, $service['build']['secrets'])) { - $service['build']['secrets'][] = $env->key; + foreach ($variables as $key => $value) { + if (! in_array($key, $service['build']['secrets'])) { + $service['build']['secrets'][] = $key; } } } @@ -3325,7 +3514,6 @@ private function next(string $status) queue_next_deployment($this->application); if ($status === ApplicationDeploymentStatus::FINISHED->value) { - ray($this->application->team()->id); event(new ApplicationConfigurationChanged($this->application->team()->id)); if (! $this->only_this_server) { diff --git a/app/Livewire/DeploymentsIndicator.php b/app/Livewire/DeploymentsIndicator.php index 0293ad6c6..ac9cfd1c2 100644 --- a/app/Livewire/DeploymentsIndicator.php +++ b/app/Livewire/DeploymentsIndicator.php @@ -16,7 +16,8 @@ public function deployments() { $servers = Server::ownedByCurrentTeam()->get(); - return ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued']) + return ApplicationDeploymentQueue::with(['application.environment.project']) + ->whereIn('status', ['in_progress', 'queued']) ->whereIn('server_id', $servers->pluck('id')) ->orderBy('id') ->get([ diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index dccd1e499..ebdc014ae 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -50,6 +50,28 @@ public function force_start() } } + public function copyLogsToClipboard(): string + { + $logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); + + if (! $logs) { + return ''; + } + + $markdown = "# Deployment Logs\n\n"; + $markdown .= "```\n"; + + foreach ($logs as $log) { + if (isset($log['output'])) { + $markdown .= $log['output']."\n"; + } + } + + $markdown .= "```\n"; + + return $markdown; + } + public function cancel() { $deployment_uuid = $this->application_deployment_queue->deployment_uuid; diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index dbb223de2..e105c956a 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -12,7 +12,11 @@ class DockerImage extends Component { - public string $dockerImage = ''; + public string $imageName = ''; + + public string $imageTag = ''; + + public string $imageSha256 = ''; public array $parameters; @@ -26,12 +30,41 @@ public function mount() public function submit() { + // Strip 'sha256:' prefix if user pasted it + if ($this->imageSha256) { + $this->imageSha256 = preg_replace('/^sha256:/i', '', trim($this->imageSha256)); + } + + // Remove @sha256 from image name if user added it + if ($this->imageName) { + $this->imageName = preg_replace('/@sha256$/i', '', trim($this->imageName)); + } + $this->validate([ - 'dockerImage' => 'required', + 'imageName' => ['required', 'string'], + 'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'], + 'imageSha256' => ['nullable', 'string', 'regex:/^[a-f0-9]{64}$/i'], ]); + // Validate that either tag or sha256 is provided, but not both + if ($this->imageTag && $this->imageSha256) { + $this->addError('imageTag', 'Provide either a tag or SHA256 digest, not both.'); + $this->addError('imageSha256', 'Provide either a tag or SHA256 digest, not both.'); + + return; + } + + // Build the full Docker image string + if ($this->imageSha256) { + $dockerImage = $this->imageName.'@sha256:'.$this->imageSha256; + } elseif ($this->imageTag) { + $dockerImage = $this->imageName.':'.$this->imageTag; + } else { + $dockerImage = $this->imageName.':latest'; + } + $parser = new DockerImageParser; - $parser->parse($this->dockerImage); + $parser->parse($dockerImage); $destination_uuid = $this->query['destination']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); @@ -45,6 +78,16 @@ public function submit() $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); + + // Determine the image tag based on whether it's a hash or regular tag + $imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag(); + + // Append @sha256 to image name if using digest and not already present + $imageName = $parser->getFullImageNameWithoutTag(); + if ($parser->isImageHash() && ! str_ends_with($imageName, '@sha256')) { + $imageName .= '@sha256'; + } + $application = Application::create([ 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, @@ -52,7 +95,7 @@ public function submit() 'git_branch' => 'main', 'build_pack' => 'dockerimage', 'ports_exposes' => 80, - 'docker_registry_image_name' => $parser->getFullImageNameWithoutTag(), + 'docker_registry_image_name' => $imageName, 'docker_registry_image_tag' => $parser->getTag(), 'environment_id' => $environment->id, 'destination_id' => $destination->id, diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2933a8cca..7f0caaba3 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -34,6 +34,8 @@ class FileStorage extends Component public bool $permanently_delete = true; + public bool $isReadOnly = false; + protected $rules = [ 'fileStorage.is_directory' => 'required', 'fileStorage.fs_path' => 'required', @@ -52,6 +54,8 @@ public function mount() $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; } + + $this->isReadOnly = $this->fileStorage->isReadOnlyVolume(); } public function convertToDirectory() diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 3928ee1d4..4f57cbfa6 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -37,6 +37,11 @@ class Show extends Component 'host_path' => 'host', ]; + public function mount() + { + $this->isReadOnly = $this->storage->isReadOnlyVolume(); + } + public function submit() { $this->authorize('update', $this->resource); diff --git a/app/Models/Application.php b/app/Models/Application.php index 4f1796790..914d9948d 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -739,9 +739,9 @@ public function environment_variables() return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->where('is_preview', false) ->orderByRaw(" - CASE - WHEN LOWER(key) LIKE 'service_%' THEN 1 - WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + CASE + WHEN is_required = true THEN 1 + WHEN LOWER(key) LIKE 'service_%' THEN 2 ELSE 3 END, LOWER(key) ASC @@ -767,9 +767,9 @@ public function environment_variables_preview() return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->where('is_preview', true) ->orderByRaw(" - CASE - WHEN LOWER(key) LIKE 'service_%' THEN 1 - WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + CASE + WHEN is_required = true THEN 1 + WHEN LOWER(key) LIKE 'service_%' THEN 2 ELSE 3 END, LOWER(key) ASC diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 8df6877ab..4e8eee10f 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -41,11 +41,9 @@ class ApplicationDeploymentQueue extends Model { protected $guarded = []; - public function application(): Attribute + public function application() { - return Attribute::make( - get: fn () => Application::find($this->application_id), - ); + return $this->belongsTo(Application::class); } public function server(): Attribute diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index b3e71d75d..376ea9c5e 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -5,6 +5,7 @@ use App\Events\FileStorageChanged; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Symfony\Component\Yaml\Yaml; class LocalFileVolume extends BaseModel { @@ -192,4 +193,61 @@ public function scopeWherePlainMountPath($query, $path) { return $query->get()->where('plain_mount_path', $path); } + + // Check if this volume is read-only by parsing the docker-compose content + public function isReadOnlyVolume(): bool + { + try { + // Only check for services + $service = $this->service; + if (! $service || ! method_exists($service, 'service')) { + return false; + } + + $actualService = $service->service; + if (! $actualService || ! $actualService->docker_compose_raw) { + return false; + } + + // Parse the docker-compose content + $compose = Yaml::parse($actualService->docker_compose_raw); + if (! isset($compose['services'])) { + return false; + } + + // Find the service that this volume belongs to + $serviceName = $service->name; + if (! isset($compose['services'][$serviceName]['volumes'])) { + return false; + } + + $volumes = $compose['services'][$serviceName]['volumes']; + + // Check each volume to find a match + foreach ($volumes as $volume) { + // Volume can be string like "host:container:ro" or "host:container" + if (is_string($volume)) { + $parts = explode(':', $volume); + + // Check if this volume matches our fs_path and mount_path + if (count($parts) >= 2) { + $hostPath = $parts[0]; + $containerPath = $parts[1]; + $options = $parts[2] ?? null; + + // Match based on fs_path and mount_path + if ($hostPath === $this->fs_path && $containerPath === $this->mount_path) { + return $options === 'ro'; + } + } + } + } + + return false; + } catch (\Throwable $e) { + ray($e->getMessage(), 'Error checking read-only volume'); + + return false; + } + } } diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 00dc15fea..e7862478b 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Yaml\Yaml; class LocalPersistentVolume extends Model { @@ -48,4 +49,69 @@ protected function hostPath(): Attribute } ); } + + // Check if this volume is read-only by parsing the docker-compose content + public function isReadOnlyVolume(): bool + { + try { + // Get the resource (can be application, service, or database) + $resource = $this->resource; + if (! $resource) { + return false; + } + + // Only check for services + if (! method_exists($resource, 'service')) { + return false; + } + + $actualService = $resource->service; + if (! $actualService || ! $actualService->docker_compose_raw) { + return false; + } + + // Parse the docker-compose content + $compose = Yaml::parse($actualService->docker_compose_raw); + if (! isset($compose['services'])) { + return false; + } + + // Find the service that this volume belongs to + $serviceName = $resource->name; + if (! isset($compose['services'][$serviceName]['volumes'])) { + return false; + } + + $volumes = $compose['services'][$serviceName]['volumes']; + + // Check each volume to find a match + foreach ($volumes as $volume) { + // Volume can be string like "host:container:ro" or "host:container" + if (is_string($volume)) { + $parts = explode(':', $volume); + + // Check if this volume matches our mount_path + if (count($parts) >= 2) { + $containerPath = $parts[1]; + $options = $parts[2] ?? null; + + // Match based on mount_path + // Remove leading slash from mount_path if present for comparison + $mountPath = str($this->mount_path)->ltrim('/')->toString(); + $containerPathClean = str($containerPath)->ltrim('/')->toString(); + + if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) { + return $options === 'ro'; + } + } + } + } + + return false; + } catch (\Throwable $e) { + ray($e->getMessage(), 'Error checking read-only persistent volume'); + + return false; + } + } } diff --git a/app/Models/Service.php b/app/Models/Service.php index d42d471c6..c4b8623e0 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -547,6 +547,21 @@ public function extraFields() } $fields->put('Grafana', $data->toArray()); break; + case $image->contains('elasticsearch'): + $data = collect([]); + $elastic_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ELASTICSEARCH')->first(); + if ($elastic_password) { + $data = $data->merge([ + 'Password (default user: elastic)' => [ + 'key' => data_get($elastic_password, 'key'), + 'value' => data_get($elastic_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Elasticsearch', $data->toArray()); + break; case $image->contains('directus'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); @@ -1231,9 +1246,9 @@ public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->orderByRaw(" - CASE - WHEN LOWER(key) LIKE 'service_%' THEN 1 - WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + CASE + WHEN is_required = true THEN 1 + WHEN LOWER(key) LIKE 'service_%' THEN 2 ELSE 3 END, LOWER(key) ASC @@ -1263,6 +1278,21 @@ public function saveComposeConfigs() $commands[] = "cd $workdir"; $commands[] = 'rm -f .env || true'; + $envs = collect([]); + + // Generate SERVICE_NAME_* environment variables from docker-compose services + if ($this->docker_compose) { + try { + $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose); + $services = data_get($dockerCompose, 'services', []); + foreach ($services as $serviceName => $_) { + $envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName); + } + } catch (\Exception $e) { + ray($e->getMessage()); + } + } + $envs_from_coolify = $this->environment_variables()->get(); $sorted = $envs_from_coolify->sortBy(function ($env) { if (str($env->key)->startsWith('SERVICE_')) { @@ -1274,7 +1304,6 @@ public function saveComposeConfigs() return 3; }); - $envs = collect([]); foreach ($sorted as $env) { $envs->push("{$env->key}={$env->real_value}"); } diff --git a/app/Rules/DockerImageFormat.php b/app/Rules/DockerImageFormat.php new file mode 100644 index 000000000..a6a78a76c --- /dev/null +++ b/app/Rules/DockerImageFormat.php @@ -0,0 +1,41 @@ + strrpos($imageString, '/'))) { - $mainPart = substr($imageString, 0, $lastColon); - $this->tag = substr($imageString, $lastColon + 1); + // Check for @sha256: format first (e.g., nginx@sha256:abc123...) + if (preg_match('/^(.+)@sha256:([a-f0-9]{64})$/i', $imageString, $matches)) { + $mainPart = $matches[1]; + $this->tag = $matches[2]; + $this->isImageHash = true; } else { - $mainPart = $imageString; - $this->tag = 'latest'; + // Split by : to handle the tag, but be careful with registry ports + $lastColon = strrpos($imageString, ':'); + $hasSlash = str_contains($imageString, '/'); + + // If the last colon appears after the last slash, it's a tag + // Otherwise it might be a port in the registry URL + if ($lastColon !== false && (! $hasSlash || $lastColon > strrpos($imageString, '/'))) { + $mainPart = substr($imageString, 0, $lastColon); + $this->tag = substr($imageString, $lastColon + 1); + + // Check if the tag is a SHA256 hash + $this->isImageHash = $this->isSha256Hash($this->tag); + } else { + $mainPart = $imageString; + $this->tag = 'latest'; + $this->isImageHash = false; + } } // Split the main part by / to handle registry and image name @@ -41,6 +54,37 @@ public function parse(string $imageString): self return $this; } + /** + * Check if the given string is a SHA256 hash + */ + private function isSha256Hash(string $hash): bool + { + // SHA256 hashes are 64 characters long and contain only hexadecimal characters + return preg_match('/^[a-f0-9]{64}$/i', $hash) === 1; + } + + /** + * Check if the current tag is an image hash + */ + public function isImageHash(): bool + { + return $this->isImageHash; + } + + /** + * Get the full image name with hash if present + */ + public function getFullImageNameWithHash(): string + { + $imageName = $this->getFullImageNameWithoutTag(); + + if ($this->isImageHash) { + return $imageName.'@sha256:'.$this->tag; + } + + return $imageName.':'.$this->tag; + } + public function getFullImageNameWithoutTag(): string { if ($this->registryUrl) { @@ -73,6 +117,10 @@ public function toString(): string } $parts[] = $this->imageName; + if ($this->isImageHash) { + return implode('/', $parts).'@sha256:'.$this->tag; + } + return implode('/', $parts).':'.$this->tag; } } diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 25cc5d0a6..a588ed882 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1172,6 +1172,9 @@ function serviceParser(Service $resource): Collection $parsedServices = collect([]); + // Generate SERVICE_NAME variables for docker compose services + $serviceNameEnvironments = generateDockerComposeServiceName($services); + $allMagicEnvironments = collect([]); // Presave services foreach ($services as $serviceName => $service) { @@ -1988,7 +1991,7 @@ function serviceParser(Service $resource): Collection $payload['volumes'] = $volumesParsed; } if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { - $payload['environment'] = $environment->merge($coolifyEnvironments); + $payload['environment'] = $environment->merge($coolifyEnvironments)->merge($serviceNameEnvironments); } if ($logging) { $payload['logging'] = $logging; diff --git a/config/constants.php b/config/constants.php index ddda70d19..01eaa7fa1 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.434', + 'version' => '4.0.0-beta.435', 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index b5cf3360a..2e5cc5e84 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.433" + "version": "4.0.0-beta.435" }, "nightly": { - "version": "4.0.0-beta.434" + "version": "4.0.0-beta.436" }, "helper": { "version": "1.0.11" diff --git a/public/ente-photos-icon-green.png b/public/ente-photos-icon-green.png new file mode 100644 index 000000000..b74aa472d Binary files /dev/null and b/public/ente-photos-icon-green.png differ diff --git a/public/svgs/ente-photos.svg b/public/svgs/ente-photos.svg new file mode 100644 index 000000000..e6a469e91 --- /dev/null +++ b/public/svgs/ente-photos.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/svgs/ente.png b/public/svgs/ente.png new file mode 100644 index 000000000..f510a7bf7 Binary files /dev/null and b/public/svgs/ente.png differ diff --git a/resources/views/livewire/deployments-indicator.blade.php b/resources/views/livewire/deployments-indicator.blade.php index ed24249e0..7f3cc23ed 100644 --- a/resources/views/livewire/deployments-indicator.blade.php +++ b/resources/views/livewire/deployments-indicator.blade.php @@ -68,6 +68,9 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra {{ $deployment->application_name }}

+ {{ $deployment->application?->environment?->project?->name }} / {{ $deployment->application?->environment?->name }} +

+

{{ $deployment->server_name }}

@if ($deployment->pull_request_id) diff --git a/resources/views/livewire/project/application/deployment-navbar.blade.php b/resources/views/livewire/project/application/deployment-navbar.blade.php index effb6b6fe..60c660bf7 100644 --- a/resources/views/livewire/project/application/deployment-navbar.blade.php +++ b/resources/views/livewire/project/application/deployment-navbar.blade.php @@ -5,6 +5,9 @@ @else Show Debug Logs @endif + @if (isDev()) + Copy Logs + @endif @if (data_get($application_deployment_queue, 'status') === 'queued') Force Start @endif diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 8587b2ab5..e7e26c134 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -166,12 +166,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @if ($application->destination->server->isSwarm()) - @else - @endif @else diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index 4cc86710a..54c175b82 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -1,4 +1,4 @@ -
+

Create a new Application

You can deploy an existing Docker Image from any Registry.
@@ -6,6 +6,24 @@

Docker Image

Save
- +
+ +
+ + + +
+
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 3aa24b087..dc8f949fa 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -1,5 +1,14 @@
+ @if ($isReadOnly) +
+ @if ($fileStorage->is_directory) + This directory is mounted as read-only and cannot be modified from the UI. + @else + This file is mounted as read-only and cannot be modified from the UI. + @endif +
+ @endif
@@ -7,58 +16,75 @@
- @can('update', $resource) -
- @if ($fileStorage->is_directory) - - - @else - @if (!$fileStorage->is_binary) - + @if ($fileStorage->is_directory) + + shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" /> + + @else + @if (!$fileStorage->is_binary) + + @endif + Load from + server + @endif - Load from server - - @endif -
- @endcan - @if (!$fileStorage->is_directory) - @can('update', $resource) - @if (data_get($resource, 'settings.is_preserve_repository_enabled')) -
- -
- @endif - - @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) - Save - @endif - @else +
+ @endcan + @if (!$fileStorage->is_directory) + @can('update', $resource) + @if (data_get($resource, 'settings.is_preserve_repository_enabled')) +
+ +
+ @endif + + @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) + Save + @endif + @else + @if (data_get($resource, 'settings.is_preserve_repository_enabled')) +
+ +
+ @endif + + @endcan + @endif + @else + {{-- Read-only view --}} + @if (!$fileStorage->is_directory) @if (data_get($resource, 'settings.is_preserve_repository_enabled'))
- @endcan + @endif @endif
diff --git a/resources/views/livewire/project/service/storage.blade.php b/resources/views/livewire/project/service/storage.blade.php index 47ee99114..d55bd801a 100644 --- a/resources/views/livewire/project/service/storage.blade.php +++ b/resources/views/livewire/project/service/storage.blade.php @@ -23,7 +23,8 @@ volumeModalOpen: false, fileModalOpen: false, directoryModalOpen: false - }" @close-storage-modal.window=" + }" + @close-storage-modal.window=" if ($event.detail === 'volume') volumeModalOpen = false; if ($event.detail === 'file') fileModalOpen = false; if ($event.detail === 'directory') directoryModalOpen = false; @@ -45,8 +46,7 @@
- + -
+ x-init="$watch('volumeModalOpen', value => { + if (value) { + $nextTick(() => { + const input = $el.querySelector('input'); + input?.focus(); + }) + } + })"> +
Docker Volumes mounted to the container.
@if ($isSwarm) -
Swarm Mode detected: You need to set a shared volume - (EFS/NFS/etc) on all the worker nodes if you would like to use a persistent +
Swarm Mode detected: You need to set a shared + volume + (EFS/NFS/etc) on all the worker nodes if you would like to use a + persistent volumes.
@endif
@if ($isSwarm) - @else - @endif - + Add @@ -169,15 +179,24 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
-
+ x-init="$watch('fileModalOpen', value => { + if (value) { + $nextTick(() => { + const input = $el.querySelector('input'); + input?.focus(); + }) + } + })"> +
Actual file mounted from the host system to the container.
+ label="Destination Path" required + helper="File location inside the container" /> @@ -219,18 +238,27 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
- + x-init="$watch('directoryModalOpen', value => { + if (value) { + $nextTick(() => { + const input = $el.querySelector('input'); + input?.focus(); + }) + } + })"> +
Directory mounted from the host system to the container.
- + Add @@ -270,19 +298,22 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 {{-- Tabs Navigation --}}
@endif @else - @if ($resource->persistentStorages()->get()->count() > 0) -

{{ Str::headline($resource->name) }}

- @endif - @if ($resource->persistentStorages()->get()->count() > 0) - - @endif - @if ($fileStorage->count() > 0) -
- @foreach ($fileStorage->sort() as $fileStorage) - - @endforeach +
+
+
+

{{ Str::headline($resource->name) }}

+
- @endif + + @if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0) +
No storage found.
+ @endif + + @php + $hasVolumes = $this->volumeCount > 0; + $hasFiles = $this->fileCount > 0; + $hasDirectories = $this->directoryCount > 0; + $defaultTab = $hasVolumes ? 'volumes' : ($hasFiles ? 'files' : 'directories'); + @endphp + + @if ($hasVolumes || $hasFiles || $hasDirectories) +
+ {{-- Tabs Navigation --}} +
+ + + +
+ + {{-- Tab Content --}} +
+ {{-- Volumes Tab --}} +
+ @if ($hasVolumes) + + @else +
+ No volumes configured. +
+ @endif +
+ + {{-- Files Tab --}} +
+ @if ($hasFiles) + @foreach ($this->files as $fs) + + @endforeach + @else +
+ No file mounts configured. +
+ @endif +
+ + {{-- Directories Tab --}} +
+ @if ($hasDirectories) + @foreach ($this->directories as $fs) + + @endforeach + @else +
+ No directory mounts configured. +
+ @endif +
+
+
+ @endif +
@endif
diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index 8c0ba0c06..798a97d94 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -1,6 +1,9 @@
@if ($isReadOnly) +
+ This volume is mounted as read-only and cannot be modified from the UI. +
@if ($isFirst)
@if ( diff --git a/templates/compose/elasticsearch-with-kibana.yaml b/templates/compose/elasticsearch-with-kibana.yaml new file mode 100644 index 000000000..6cc08d889 --- /dev/null +++ b/templates/compose/elasticsearch-with-kibana.yaml @@ -0,0 +1,88 @@ +# documentation: https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-kibana-with-docker +# slogan: Elastic + Kibana is a Free and Open Source Search, Monitoring, and Visualization Stack +# tags: elastic,kibana,elasticsearch,search,visualization,logging,monitoring,observability,analytics,stack,devops +# logo: svgs/elasticsearch.svg +# port: 5601 + +services: + elasticsearch: + image: 'elastic/elasticsearch:9.1.2' + container_name: elasticsearch + restart: unless-stopped + environment: + - ELASTIC_PASSWORD=${SERVICE_PASSWORD_ELASTICSEARCH} + - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' + - discovery.type=single-node + - bootstrap.memory_lock=true + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=false + - xpack.security.transport.ssl.enabled=false + volumes: + - '/etc/localtime:/etc/localtime:ro' + - 'elasticsearch-data:/usr/share/elasticsearch/data' + healthcheck: + test: + - CMD-SHELL + - 'curl --user elastic:${SERVICE_PASSWORD_ELASTICSEARCH} --silent --fail http://localhost:9200/_cluster/health || exit 1' + interval: 10s + timeout: 10s + retries: 24 + + kibana: + image: 'kibana:9.1.2' + container_name: kibana + restart: unless-stopped + environment: + - SERVICE_URL_KIBANA_5601 + - 'SERVER_NAME=${SERVICE_URL_KIBANA}' + - 'SERVER_PUBLICBASEURL=${SERVICE_URL_KIBANA}' + - 'ELASTICSEARCH_HOSTS=http://elasticsearch:9200' + - 'ELASTICSEARCH_USERNAME=kibana_system' + - 'ELASTICSEARCH_PASSWORD=${SERVICE_PASSWORD_KIBANA}' + - 'XPACK_SECURITY_ENCRYPTIONKEY=${SERVICE_PASSWORD_XPACKSECURITY}' + - 'XPACK_REPORTING_ENCRYPTIONKEY=${SERVICE_PASSWORD_XPACKREPORTING}' + - 'XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${SERVICE_PASSWORD_XPACKENCRYPTEDSAVEDOBJECTS}' + - 'TELEMETRY_OPTIN=${TELEMETRY_OPTIN:-false}' + volumes: + - '/etc/localtime:/etc/localtime:ro' + - 'kibana-data:/usr/share/kibana/data' + depends_on: + setup: + condition: service_completed_successfully + healthcheck: + test: + - CMD-SHELL + - "curl -s http://localhost:5601/api/status | grep -q '\"level\":\"available\"' || exit 1" + interval: 10s + timeout: 10s + retries: 120 + + setup: + image: 'elastic/elasticsearch:9.1.2' + container_name: kibana-setup + depends_on: + elasticsearch: + condition: service_healthy + exclude_from_hc: true + environment: + - 'ELASTIC_PASSWORD=${SERVICE_PASSWORD_ELASTICSEARCH}' + - 'KIBANA_PASSWORD=${SERVICE_PASSWORD_KIBANA}' + entrypoint: + - sh + - '-c' + - | + echo "Setting up Kibana user password..." + + until curl -s -u "elastic:${ELASTIC_PASSWORD}" http://elasticsearch:9200/_cluster/health | grep -q '"status":"green\|yellow"'; do + echo "Waiting for Elasticsearch..." + sleep 2 + done + + echo "Setting password for kibana_system user..." + curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" \ + -H "Content-Type: application/json" \ + http://elasticsearch:9200/_security/user/kibana_system/_password \ + -d "{\"password\":\"${KIBANA_PASSWORD}\"}" || exit 1 + + echo "Kibana setup completed successfully" + restart: 'no' diff --git a/templates/compose/ente-photos-with-s3.yaml b/templates/compose/ente-photos-with-s3.yaml new file mode 100644 index 000000000..96d74b1a8 --- /dev/null +++ b/templates/compose/ente-photos-with-s3.yaml @@ -0,0 +1,113 @@ +# documentation: https://help.ente.io/self-hosting/installation/compose +# slogan: Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos. +# category: media +# tags: photos,gallery,backup,encryption,privacy,self-hosted,google-photos,alternative +# logo: svgs/ente-photos.svg +# port: 8080 + +services: + museum: + image: ghcr.io/ente-io/server:latest + environment: + - SERVICE_URL_MUSEUM_8080 + - ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false} + + - ENTE_APPS_PUBLIC_ALBUMS=${SERVICE_URL_WEB_3002} + - ENTE_APPS_CAST=${SERVICE_URL_WEB_3004} + - ENTE_APPS_ACCOUNTS=${SERVICE_URL_WEB_3001} + + - ENTE_DB_HOST=${ENTE_DB_HOST:-postgres} + - ENTE_DB_PORT=${ENTE_DB_PORT:-5432} + - ENTE_DB_NAME=${ENTE_DB_NAME:-ente_db} + - ENTE_DB_USER=${SERVICE_USER_POSTGRES:-pguser} + - ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + + - ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION} + - ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH} + + - ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT} + + - ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438} + - ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false} + + # S3/MinIO configuration + - S3_ARE_LOCAL_BUCKETS=true + - S3_USE_PATH_STYLE_URLS=true + - S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO} + - S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO} + - S3_B2_EU_CEN_ENDPOINT=${SERVICE_URL_MINIO_3200} + - S3_B2_EU_CEN_REGION=eu-central-2 + - S3_B2_EU_CEN_BUCKET=b2-eu-cen + volumes: + - museum-data:/data + - museum-config:/config + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_started + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/ping"] + interval: 5s + timeout: 5s + retries: 10 + + web: + image: ghcr.io/ente-io/web + environment: + - SERVICE_URL_WEB_3000 + - ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM} + - ENTE_ALBUMS_ORIGIN=${SERVICE_URL_WEB_3002} + + healthcheck: + test: ["CMD", "curl", "--fail", "http://127.0.0.1:3000"] + interval: 5s + timeout: 5s + retries: 10 + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-ente_db} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + + minio: + image: quay.io/minio/minio:latest + environment: + - SERVICE_URL_MINIO_9000 + - MINIO_ROOT_USER=${SERVICE_USER_MINIO} + - MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} + command: server /data --address ":9000" --console-address ":9001" + volumes: + - minio-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 20s + retries: 10 + + minio-init: + image: minio/mc:latest + exclude_from_hc: true + restart: no + depends_on: + minio: + condition: service_healthy + environment: + - MINIO_ROOT_USER=${SERVICE_USER_MINIO} + - MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} + entrypoint: > + /bin/sh -c " + mc alias set minio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; + mc mb minio/b2-eu-cen --ignore-existing; + mc mb minio/wasabi-eu-central-2-v3 --ignore-existing; + mc mb minio/scw-eu-fr-v3 --ignore-existing; + echo 'MinIO buckets created successfully'; + " diff --git a/templates/compose/ente-photos.yaml b/templates/compose/ente-photos.yaml new file mode 100644 index 000000000..851e13563 --- /dev/null +++ b/templates/compose/ente-photos.yaml @@ -0,0 +1,80 @@ +# documentation: https://help.ente.io/self-hosting/installation/compose +# slogan: Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos. +# category: media +# tags: photos,gallery,backup,encryption,privacy,self-hosted,google-photos,alternative +# logo: svgs/ente-photos.svg +# port: 8080 + +services: + museum: + image: ghcr.io/ente-io/server:latest + environment: + - SERVICE_URL_MUSEUM_8080 + + - ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false} + + - ENTE_APPS_PUBLIC_ALBUMS=${SERVICE_URL_WEB_3002} + - ENTE_APPS_CAST=${SERVICE_URL_WEB_3004} + - ENTE_APPS_ACCOUNTS=${SERVICE_URL_WEB_3001} + + - ENTE_DB_HOST=${ENTE_DB_HOST:-postgres} + - ENTE_DB_PORT=${ENTE_DB_PORT:-5432} + - ENTE_DB_NAME=${ENTE_DB_NAME:-ente_db} + - ENTE_DB_USER=${SERVICE_USER_POSTGRES:-pguser} + - ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + + - ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION} + - ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH} + + - ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT} + + - ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438} + - ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false} + + - ENTE_S3_B2_EU_CEN_ARE_LOCAL_BUCKETS=${PRIMARY_STORAGE_ARE_LOCAL_BUCKETS:-false} + - ENTE_S3_B2_EU_CEN_USE_PATH_STYLE_URLS=${PRIMARY_STORAGE_USE_PATH_STYLE_URLS:-true} + - ENTE_S3_B2_EU_CEN_KEY=${S3_STORAGE_KEY:?} + - ENTE_S3_B2_EU_CEN_SECRET=${S3_STORAGE_SECRET:?} + - ENTE_S3_B2_EU_CEN_ENDPOINT=${S3_STORAGE_ENDPOINT:?} + - ENTE_S3_B2_EU_CEN_REGION=${S3_STORAGE_REGION:-us-east-1} + - ENTE_S3_B2_EU_CEN_BUCKET=${S3_STORAGE_BUCKET:?} + + depends_on: + postgres: + condition: service_healthy + volumes: + - museum-data:/data + - museum-config:/config + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/ping"] + interval: 5s + timeout: 5s + retries: 10 + + web: + image: ghcr.io/ente-io/web + environment: + - SERVICE_URL_WEB_3000 + - ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM} + - ENTE_ALBUMS_ORIGIN=${SERVICE_URL_WEB_3002} + + healthcheck: + test: ["CMD", "curl", "--fail", "http://127.0.0.1:3000"] + interval: 5s + timeout: 5s + retries: 10 + + postgres: + image: postgres:15 + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES:-pguser} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${SERVICE_DB_NAME:-ente_db} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index fbb428568..39bc024bd 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -899,6 +899,28 @@ "minversion": "0.0.0", "port": "80" }, + "elasticsearch-with-kibana": { + "documentation": "https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-kibana-with-docker?utm_source=coolify.io", + "slogan": "Elastic + Kibana is a Free and Open Source Search, Monitoring, and Visualization Stack", + "compose": "c2VydmljZXM6CiAgZWxhc3RpY3NlYXJjaDoKICAgIGltYWdlOiAnZWxhc3RpYy9lbGFzdGljc2VhcmNoOjkuMS4yJwogICAgY29udGFpbmVyX25hbWU6IGVsYXN0aWNzZWFyY2gKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnRUxBU1RJQ19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRUxBU1RJQ1NFQVJDSH0nCiAgICAgIC0gJ0VTX0pBVkFfT1BUUz0tWG1zNTEybSAtWG14NTEybScKICAgICAgLSBkaXNjb3ZlcnkudHlwZT1zaW5nbGUtbm9kZQogICAgICAtIGJvb3RzdHJhcC5tZW1vcnlfbG9jaz10cnVlCiAgICAgIC0geHBhY2suc2VjdXJpdHkuZW5hYmxlZD10cnVlCiAgICAgIC0geHBhY2suc2VjdXJpdHkuaHR0cC5zc2wuZW5hYmxlZD1mYWxzZQogICAgICAtIHhwYWNrLnNlY3VyaXR5LnRyYW5zcG9ydC5zc2wuZW5hYmxlZD1mYWxzZQogICAgdm9sdW1lczoKICAgICAgLSAnL2V0Yy9sb2NhbHRpbWU6L2V0Yy9sb2NhbHRpbWU6cm8nCiAgICAgIC0gJ2VsYXN0aWNzZWFyY2gtZGF0YTovdXNyL3NoYXJlL2VsYXN0aWNzZWFyY2gvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtLXVzZXIgZWxhc3RpYzoke1NFUlZJQ0VfUEFTU1dPUkRfRUxBU1RJQ1NFQVJDSH0gLS1zaWxlbnQgLS1mYWlsIGh0dHA6Ly9sb2NhbGhvc3Q6OTIwMC9fY2x1c3Rlci9oZWFsdGggfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyNAogIGtpYmFuYToKICAgIGltYWdlOiAna2liYW5hOjkuMS4yJwogICAgY29udGFpbmVyX25hbWU6IGtpYmFuYQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0tJQkFOQV81NjAxCiAgICAgIC0gJ1NFUlZFUl9OQU1FPSR7U0VSVklDRV9VUkxfS0lCQU5BfScKICAgICAgLSAnU0VSVkVSX1BVQkxJQ0JBU0VVUkw9JHtTRVJWSUNFX1VSTF9LSUJBTkF9JwogICAgICAtICdFTEFTVElDU0VBUkNIX0hPU1RTPWh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAnCiAgICAgIC0gRUxBU1RJQ1NFQVJDSF9VU0VSTkFNRT1raWJhbmFfc3lzdGVtCiAgICAgIC0gJ0VMQVNUSUNTRUFSQ0hfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0tJQkFOQX0nCiAgICAgIC0gJ1hQQUNLX1NFQ1VSSVRZX0VOQ1JZUFRJT05LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1hQQUNLU0VDVVJJVFl9JwogICAgICAtICdYUEFDS19SRVBPUlRJTkdfRU5DUllQVElPTktFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfWFBBQ0tSRVBPUlRJTkd9JwogICAgICAtICdYUEFDS19FTkNSWVBURURTQVZFRE9CSkVDVFNfRU5DUllQVElPTktFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfWFBBQ0tFTkNSWVBURURTQVZFRE9CSkVDVFN9JwogICAgICAtICdURUxFTUVUUllfT1BUSU49JHtURUxFTUVUUllfT1BUSU46LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9ldGMvbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgICAtICdraWJhbmEtZGF0YTovdXNyL3NoYXJlL2tpYmFuYS9kYXRhJwogICAgZGVwZW5kc19vbjoKICAgICAgc2V0dXA6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2NvbXBsZXRlZF9zdWNjZXNzZnVsbHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtcyBodHRwOi8vbG9jYWxob3N0OjU2MDEvYXBpL3N0YXR1cyB8IGdyZXAgLXEgJycibGV2ZWwiOiJhdmFpbGFibGUiJycgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMjAKICBzZXR1cDoKICAgIGltYWdlOiAnZWxhc3RpYy9lbGFzdGljc2VhcmNoOjkuMS4yJwogICAgY29udGFpbmVyX25hbWU6IGtpYmFuYS1zZXR1cAogICAgZGVwZW5kc19vbjoKICAgICAgZWxhc3RpY3NlYXJjaDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnRUxBU1RJQ19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRUxBU1RJQ1NFQVJDSH0nCiAgICAgIC0gJ0tJQkFOQV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfS0lCQU5BfScKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gImVjaG8gXCJTZXR0aW5nIHVwIEtpYmFuYSB1c2VyIHBhc3N3b3JkLi4uXCJcblxudW50aWwgY3VybCAtcyAtdSBcImVsYXN0aWM6JHtFTEFTVElDX1BBU1NXT1JEfVwiIGh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAvX2NsdXN0ZXIvaGVhbHRoIHwgZ3JlcCAtcSAnXCJzdGF0dXNcIjpcImdyZWVuXFx8eWVsbG93XCInOyBkb1xuICBlY2hvIFwiV2FpdGluZyBmb3IgRWxhc3RpY3NlYXJjaC4uLlwiXG4gIHNsZWVwIDJcbmRvbmVcblxuZWNobyBcIlNldHRpbmcgcGFzc3dvcmQgZm9yIGtpYmFuYV9zeXN0ZW0gdXNlci4uLlwiXG5jdXJsIC1zIC1YIFBPU1QgLXUgXCJlbGFzdGljOiR7RUxBU1RJQ19QQVNTV09SRH1cIiBcXFxuICAtSCBcIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvblwiIFxcXG4gIGh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAvX3NlY3VyaXR5L3VzZXIva2liYW5hX3N5c3RlbS9fcGFzc3dvcmQgXFxcbiAgLWQgXCJ7XFxcInBhc3N3b3JkXFxcIjpcXFwiJHtLSUJBTkFfUEFTU1dPUkR9XFxcIn1cIiB8fCBleGl0IDFcblxuZWNobyBcIktpYmFuYSBzZXR1cCBjb21wbGV0ZWQgc3VjY2Vzc2Z1bGx5XCIiCiAgICByZXN0YXJ0OiAnbm8nCg==", + "tags": [ + "elastic", + "kibana", + "elasticsearch", + "search", + "visualization", + "logging", + "monitoring", + "observability", + "analytics", + "stack", + "devops" + ], + "category": null, + "logo": "svgs/elasticsearch.svg", + "minversion": "0.0.0", + "port": "5601" + }, "elasticsearch": { "documentation": "https://www.elastic.co/products/elasticsearch?utm_source=coolify.io", "slogan": "Elasticsearch is free and Open Source, Distributed, RESTful Search Engine.", @@ -948,6 +970,44 @@ "minversion": "0.0.0", "port": "6555" }, + "ente-photos-with-s3": { + "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", + "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01VU0VVTV84MDgwCiAgICAgIC0gJ0VOVEVfSFRUUF9VU0VfVExTPSR7RU5URV9IVFRQX1VTRV9UTFM6LWZhbHNlfScKICAgICAgLSAnRU5URV9BUFBTX1BVQkxJQ19BTEJVTVM9JHtTRVJWSUNFX1VSTF9XRUJfMzAwMn0nCiAgICAgIC0gJ0VOVEVfQVBQU19DQVNUPSR7U0VSVklDRV9VUkxfV0VCXzMwMDR9JwogICAgICAtICdFTlRFX0FQUFNfQUNDT1VOVFM9JHtTRVJWSUNFX1VSTF9XRUJfMzAwMX0nCiAgICAgIC0gJ0VOVEVfREJfSE9TVD0ke0VOVEVfREJfSE9TVDotcG9zdGdyZXN9JwogICAgICAtICdFTlRFX0RCX1BPUlQ9JHtFTlRFX0RCX1BPUlQ6LTU0MzJ9JwogICAgICAtICdFTlRFX0RCX05BTUU9JHtFTlRFX0RCX05BTUU6LWVudGVfZGJ9JwogICAgICAtICdFTlRFX0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVM6LXBndXNlcn0nCiAgICAgIC0gJ0VOVEVfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnRU5URV9LRVlfRU5DUllQVElPTj0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9FTkNSWVBUSU9OfScKICAgICAgLSAnRU5URV9LRVlfSEFTSD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9IQVNIfScKICAgICAgLSAnRU5URV9KV1RfU0VDUkVUPSR7U0VSVklDRV9SRUFMQkFTRTY0X0pXVH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfQURNSU49JHtFTlRFX0lOVEVSTkFMX0FETUlOOi0xNTgwNTU5OTYyMzg2NDM4fScKICAgICAgLSAnRU5URV9JTlRFUk5BTF9ESVNBQkxFX1JFR0lTVFJBVElPTj0ke0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSBTM19BUkVfTE9DQUxfQlVDS0VUUz10cnVlCiAgICAgIC0gUzNfVVNFX1BBVEhfU1RZTEVfVVJMUz10cnVlCiAgICAgIC0gJ1MzX0IyX0VVX0NFTl9LRVk9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdTM19CMl9FVV9DRU5fU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICAgIC0gJ1MzX0IyX0VVX0NFTl9FTkRQT0lOVD0ke1NFUlZJQ0VfVVJMX01JTklPXzMyMDB9JwogICAgICAtIFMzX0IyX0VVX0NFTl9SRUdJT049ZXUtY2VudHJhbC0yCiAgICAgIC0gUzNfQjJfRVVfQ0VOX0JVQ0tFVD1iMi1ldS1jZW4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ211c2V1bS1kYXRhOi9kYXRhJwogICAgICAtICdtdXNldW0tY29uZmlnOi9jb25maWcnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MC9waW5nJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICB3ZWI6CiAgICBpbWFnZTogZ2hjci5pby9lbnRlLWlvL3dlYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfV0VCXzMwMDAKICAgICAgLSAnRU5URV9BUElfT1JJR0lOPSR7U0VSVklDRV9VUkxfTVVTRVVNfScKICAgICAgLSAnRU5URV9BTEJVTVNfT1JJR0lOPSR7U0VSVklDRV9VUkxfV0VCXzMwMDJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctLWZhaWwnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1lbnRlX2RifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICR7UE9TVEdSRVNfVVNFUn0gLWQgJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01JTklPXzkwMDAKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWFkZHJlc3MgIjo5MDAwIiAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaW5pby1pbml0OgogICAgaW1hZ2U6ICdtaW5pby9tYzpsYXRlc3QnCiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCIgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzo5MDAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOyBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsgXCJcbiIK", + "tags": [ + "photos", + "gallery", + "backup", + "encryption", + "privacy", + "self-hosted", + "google-photos", + "alternative" + ], + "category": "media", + "logo": "svgs/ente-photos.svg", + "minversion": "0.0.0", + "port": "8080" + }, + "ente-photos": { + "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", + "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01VU0VVTV84MDgwCiAgICAgIC0gJ0VOVEVfSFRUUF9VU0VfVExTPSR7RU5URV9IVFRQX1VTRV9UTFM6LWZhbHNlfScKICAgICAgLSAnRU5URV9BUFBTX1BVQkxJQ19BTEJVTVM9JHtTRVJWSUNFX1VSTF9XRUJfMzAwMn0nCiAgICAgIC0gJ0VOVEVfQVBQU19DQVNUPSR7U0VSVklDRV9VUkxfV0VCXzMwMDR9JwogICAgICAtICdFTlRFX0FQUFNfQUNDT1VOVFM9JHtTRVJWSUNFX1VSTF9XRUJfMzAwMX0nCiAgICAgIC0gJ0VOVEVfREJfSE9TVD0ke0VOVEVfREJfSE9TVDotcG9zdGdyZXN9JwogICAgICAtICdFTlRFX0RCX1BPUlQ9JHtFTlRFX0RCX1BPUlQ6LTU0MzJ9JwogICAgICAtICdFTlRFX0RCX05BTUU9JHtFTlRFX0RCX05BTUU6LWVudGVfZGJ9JwogICAgICAtICdFTlRFX0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVM6LXBndXNlcn0nCiAgICAgIC0gJ0VOVEVfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnRU5URV9LRVlfRU5DUllQVElPTj0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9FTkNSWVBUSU9OfScKICAgICAgLSAnRU5URV9LRVlfSEFTSD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9IQVNIfScKICAgICAgLSAnRU5URV9KV1RfU0VDUkVUPSR7U0VSVklDRV9SRUFMQkFTRTY0X0pXVH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfQURNSU49JHtFTlRFX0lOVEVSTkFMX0FETUlOOi0xNTgwNTU5OTYyMzg2NDM4fScKICAgICAgLSAnRU5URV9JTlRFUk5BTF9ESVNBQkxFX1JFR0lTVFJBVElPTj0ke0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSAnRU5URV9TM19CMl9FVV9DRU5fQVJFX0xPQ0FMX0JVQ0tFVFM9JHtQUklNQVJZX1NUT1JBR0VfQVJFX0xPQ0FMX0JVQ0tFVFM6LWZhbHNlfScKICAgICAgLSAnRU5URV9TM19CMl9FVV9DRU5fVVNFX1BBVEhfU1RZTEVfVVJMUz0ke1BSSU1BUllfU1RPUkFHRV9VU0VfUEFUSF9TVFlMRV9VUkxTOi10cnVlfScKICAgICAgLSAnRU5URV9TM19CMl9FVV9DRU5fS0VZPSR7UzNfU1RPUkFHRV9LRVk6P30nCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX1NFQ1JFVD0ke1MzX1NUT1JBR0VfU0VDUkVUOj99JwogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9FTkRQT0lOVD0ke1MzX1NUT1JBR0VfRU5EUE9JTlQ6P30nCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX1JFR0lPTj0ke1MzX1NUT1JBR0VfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9CVUNLRVQ9JHtTM19TVE9SQUdFX0JVQ0tFVDo/fScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICdtdXNldW0tZGF0YTovZGF0YScKICAgICAgLSAnbXVzZXVtLWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MC9waW5nJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICB3ZWI6CiAgICBpbWFnZTogZ2hjci5pby9lbnRlLWlvL3dlYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfV0VCXzMwMDAKICAgICAgLSAnRU5URV9BUElfT1JJR0lOPSR7U0VSVklDRV9VUkxfTVVTRVVNfScKICAgICAgLSAnRU5URV9BTEJVTVNfT1JJR0lOPSR7U0VSVklDRV9VUkxfV0VCXzMwMDJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctLWZhaWwnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFUzotcGd1c2VyfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtTRVJWSUNFX0RCX05BTUU6LWVudGVfZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJHtQT1NUR1JFU19VU0VSfSAtZCAke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "photos", + "gallery", + "backup", + "encryption", + "privacy", + "self-hosted", + "google-photos", + "alternative" + ], + "category": "media", + "logo": "svgs/ente-photos.svg", + "minversion": "0.0.0", + "port": "8080" + }, "evolution-api": { "documentation": "https://doc.evolution-api.com/v1/pt/get-started/introduction?utm_source=coolify.io", "slogan": "Evolution API Installation with Postgres and Redis", diff --git a/templates/service-templates.json b/templates/service-templates.json index 2fd056a19..9ba329247 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -899,6 +899,28 @@ "minversion": "0.0.0", "port": "80" }, + "elasticsearch-with-kibana": { + "documentation": "https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-kibana-with-docker?utm_source=coolify.io", + "slogan": "Elastic + Kibana is a Free and Open Source Search, Monitoring, and Visualization Stack", + "compose": "c2VydmljZXM6CiAgZWxhc3RpY3NlYXJjaDoKICAgIGltYWdlOiAnZWxhc3RpYy9lbGFzdGljc2VhcmNoOjkuMS4yJwogICAgY29udGFpbmVyX25hbWU6IGVsYXN0aWNzZWFyY2gKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnRUxBU1RJQ19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRUxBU1RJQ1NFQVJDSH0nCiAgICAgIC0gJ0VTX0pBVkFfT1BUUz0tWG1zNTEybSAtWG14NTEybScKICAgICAgLSBkaXNjb3ZlcnkudHlwZT1zaW5nbGUtbm9kZQogICAgICAtIGJvb3RzdHJhcC5tZW1vcnlfbG9jaz10cnVlCiAgICAgIC0geHBhY2suc2VjdXJpdHkuZW5hYmxlZD10cnVlCiAgICAgIC0geHBhY2suc2VjdXJpdHkuaHR0cC5zc2wuZW5hYmxlZD1mYWxzZQogICAgICAtIHhwYWNrLnNlY3VyaXR5LnRyYW5zcG9ydC5zc2wuZW5hYmxlZD1mYWxzZQogICAgdm9sdW1lczoKICAgICAgLSAnL2V0Yy9sb2NhbHRpbWU6L2V0Yy9sb2NhbHRpbWU6cm8nCiAgICAgIC0gJ2VsYXN0aWNzZWFyY2gtZGF0YTovdXNyL3NoYXJlL2VsYXN0aWNzZWFyY2gvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtLXVzZXIgZWxhc3RpYzoke1NFUlZJQ0VfUEFTU1dPUkRfRUxBU1RJQ1NFQVJDSH0gLS1zaWxlbnQgLS1mYWlsIGh0dHA6Ly9sb2NhbGhvc3Q6OTIwMC9fY2x1c3Rlci9oZWFsdGggfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyNAogIGtpYmFuYToKICAgIGltYWdlOiAna2liYW5hOjkuMS4yJwogICAgY29udGFpbmVyX25hbWU6IGtpYmFuYQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LSUJBTkFfNTYwMQogICAgICAtICdTRVJWRVJfTkFNRT0ke1NFUlZJQ0VfRlFETl9LSUJBTkF9JwogICAgICAtICdTRVJWRVJfUFVCTElDQkFTRVVSTD0ke1NFUlZJQ0VfRlFETl9LSUJBTkF9JwogICAgICAtICdFTEFTVElDU0VBUkNIX0hPU1RTPWh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAnCiAgICAgIC0gRUxBU1RJQ1NFQVJDSF9VU0VSTkFNRT1raWJhbmFfc3lzdGVtCiAgICAgIC0gJ0VMQVNUSUNTRUFSQ0hfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0tJQkFOQX0nCiAgICAgIC0gJ1hQQUNLX1NFQ1VSSVRZX0VOQ1JZUFRJT05LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1hQQUNLU0VDVVJJVFl9JwogICAgICAtICdYUEFDS19SRVBPUlRJTkdfRU5DUllQVElPTktFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfWFBBQ0tSRVBPUlRJTkd9JwogICAgICAtICdYUEFDS19FTkNSWVBURURTQVZFRE9CSkVDVFNfRU5DUllQVElPTktFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfWFBBQ0tFTkNSWVBURURTQVZFRE9CSkVDVFN9JwogICAgICAtICdURUxFTUVUUllfT1BUSU49JHtURUxFTUVUUllfT1BUSU46LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9ldGMvbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgICAtICdraWJhbmEtZGF0YTovdXNyL3NoYXJlL2tpYmFuYS9kYXRhJwogICAgZGVwZW5kc19vbjoKICAgICAgc2V0dXA6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2NvbXBsZXRlZF9zdWNjZXNzZnVsbHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtcyBodHRwOi8vbG9jYWxob3N0OjU2MDEvYXBpL3N0YXR1cyB8IGdyZXAgLXEgJycibGV2ZWwiOiJhdmFpbGFibGUiJycgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMjAKICBzZXR1cDoKICAgIGltYWdlOiAnZWxhc3RpYy9lbGFzdGljc2VhcmNoOjkuMS4yJwogICAgY29udGFpbmVyX25hbWU6IGtpYmFuYS1zZXR1cAogICAgZGVwZW5kc19vbjoKICAgICAgZWxhc3RpY3NlYXJjaDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnRUxBU1RJQ19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRUxBU1RJQ1NFQVJDSH0nCiAgICAgIC0gJ0tJQkFOQV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfS0lCQU5BfScKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gImVjaG8gXCJTZXR0aW5nIHVwIEtpYmFuYSB1c2VyIHBhc3N3b3JkLi4uXCJcblxudW50aWwgY3VybCAtcyAtdSBcImVsYXN0aWM6JHtFTEFTVElDX1BBU1NXT1JEfVwiIGh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAvX2NsdXN0ZXIvaGVhbHRoIHwgZ3JlcCAtcSAnXCJzdGF0dXNcIjpcImdyZWVuXFx8eWVsbG93XCInOyBkb1xuICBlY2hvIFwiV2FpdGluZyBmb3IgRWxhc3RpY3NlYXJjaC4uLlwiXG4gIHNsZWVwIDJcbmRvbmVcblxuZWNobyBcIlNldHRpbmcgcGFzc3dvcmQgZm9yIGtpYmFuYV9zeXN0ZW0gdXNlci4uLlwiXG5jdXJsIC1zIC1YIFBPU1QgLXUgXCJlbGFzdGljOiR7RUxBU1RJQ19QQVNTV09SRH1cIiBcXFxuICAtSCBcIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvblwiIFxcXG4gIGh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAvX3NlY3VyaXR5L3VzZXIva2liYW5hX3N5c3RlbS9fcGFzc3dvcmQgXFxcbiAgLWQgXCJ7XFxcInBhc3N3b3JkXFxcIjpcXFwiJHtLSUJBTkFfUEFTU1dPUkR9XFxcIn1cIiB8fCBleGl0IDFcblxuZWNobyBcIktpYmFuYSBzZXR1cCBjb21wbGV0ZWQgc3VjY2Vzc2Z1bGx5XCIiCiAgICByZXN0YXJ0OiAnbm8nCg==", + "tags": [ + "elastic", + "kibana", + "elasticsearch", + "search", + "visualization", + "logging", + "monitoring", + "observability", + "analytics", + "stack", + "devops" + ], + "category": null, + "logo": "svgs/elasticsearch.svg", + "minversion": "0.0.0", + "port": "5601" + }, "elasticsearch": { "documentation": "https://www.elastic.co/products/elasticsearch?utm_source=coolify.io", "slogan": "Elasticsearch is free and Open Source, Distributed, RESTful Search Engine.", @@ -948,6 +970,44 @@ "minversion": "0.0.0", "port": "6555" }, + "ente-photos-with-s3": { + "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", + "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NVVNFVU1fODA4MAogICAgICAtICdFTlRFX0hUVFBfVVNFX1RMUz0ke0VOVEVfSFRUUF9VU0VfVExTOi1mYWxzZX0nCiAgICAgIC0gJ0VOVEVfQVBQU19QVUJMSUNfQUxCVU1TPSR7U0VSVklDRV9GUUROX1dFQl8zMDAyfScKICAgICAgLSAnRU5URV9BUFBTX0NBU1Q9JHtTRVJWSUNFX0ZRRE5fV0VCXzMwMDR9JwogICAgICAtICdFTlRFX0FQUFNfQUNDT1VOVFM9JHtTRVJWSUNFX0ZRRE5fV0VCXzMwMDF9JwogICAgICAtICdFTlRFX0RCX0hPU1Q9JHtFTlRFX0RCX0hPU1Q6LXBvc3RncmVzfScKICAgICAgLSAnRU5URV9EQl9QT1JUPSR7RU5URV9EQl9QT1JUOi01NDMyfScKICAgICAgLSAnRU5URV9EQl9OQU1FPSR7RU5URV9EQl9OQU1FOi1lbnRlX2RifScKICAgICAgLSAnRU5URV9EQl9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTOi1wZ3VzZXJ9JwogICAgICAtICdFTlRFX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0VOVEVfS0VZX0VOQ1JZUFRJT049JHtTRVJWSUNFX1JFQUxCQVNFNjRfRU5DUllQVElPTn0nCiAgICAgIC0gJ0VOVEVfS0VZX0hBU0g9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfSEFTSH0nCiAgICAgIC0gJ0VOVEVfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9KV1R9JwogICAgICAtICdFTlRFX0lOVEVSTkFMX0FETUlOPSR7RU5URV9JTlRFUk5BTF9BRE1JTjotMTU4MDU1OTk2MjM4NjQzOH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT049JHtFTlRFX0lOVEVSTkFMX0RJU0FCTEVfUkVHSVNUUkFUSU9OOi1mYWxzZX0nCiAgICAgIC0gUzNfQVJFX0xPQ0FMX0JVQ0tFVFM9dHJ1ZQogICAgICAtIFMzX1VTRV9QQVRIX1NUWUxFX1VSTFM9dHJ1ZQogICAgICAtICdTM19CMl9FVV9DRU5fS0VZPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnUzNfQjJfRVVfQ0VOX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdTM19CMl9FVV9DRU5fRU5EUE9JTlQ9JHtTRVJWSUNFX0ZRRE5fTUlOSU9fMzIwMH0nCiAgICAgIC0gUzNfQjJfRVVfQ0VOX1JFR0lPTj1ldS1jZW50cmFsLTIKICAgICAgLSBTM19CMl9FVV9DRU5fQlVDS0VUPWIyLWV1LWNlbgogICAgdm9sdW1lczoKICAgICAgLSAnbXVzZXVtLWRhdGE6L2RhdGEnCiAgICAgIC0gJ211c2V1bS1jb25maWc6L2NvbmZpZycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xTy0nCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwL3BpbmcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHdlYjoKICAgIGltYWdlOiBnaGNyLmlvL2VudGUtaW8vd2ViCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0VCXzMwMDAKICAgICAgLSAnRU5URV9BUElfT1JJR0lOPSR7U0VSVklDRV9GUUROX01VU0VVTX0nCiAgICAgIC0gJ0VOVEVfQUxCVU1TX09SSUdJTj0ke1NFUlZJQ0VfRlFETl9XRUJfMzAwMn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy0tZmFpbCcKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNS1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJHtQT1NUR1JFU19VU0VSfSAtZCAke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01JTklPXzkwMDAKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWFkZHJlc3MgIjo5MDAwIiAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaW5pby1pbml0OgogICAgaW1hZ2U6ICdtaW5pby9tYzpsYXRlc3QnCiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCIgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzo5MDAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOyBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsgXCJcbiIK", + "tags": [ + "photos", + "gallery", + "backup", + "encryption", + "privacy", + "self-hosted", + "google-photos", + "alternative" + ], + "category": "media", + "logo": "svgs/ente-photos.svg", + "minversion": "0.0.0", + "port": "8080" + }, + "ente-photos": { + "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", + "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NVVNFVU1fODA4MAogICAgICAtICdFTlRFX0hUVFBfVVNFX1RMUz0ke0VOVEVfSFRUUF9VU0VfVExTOi1mYWxzZX0nCiAgICAgIC0gJ0VOVEVfQVBQU19QVUJMSUNfQUxCVU1TPSR7U0VSVklDRV9GUUROX1dFQl8zMDAyfScKICAgICAgLSAnRU5URV9BUFBTX0NBU1Q9JHtTRVJWSUNFX0ZRRE5fV0VCXzMwMDR9JwogICAgICAtICdFTlRFX0FQUFNfQUNDT1VOVFM9JHtTRVJWSUNFX0ZRRE5fV0VCXzMwMDF9JwogICAgICAtICdFTlRFX0RCX0hPU1Q9JHtFTlRFX0RCX0hPU1Q6LXBvc3RncmVzfScKICAgICAgLSAnRU5URV9EQl9QT1JUPSR7RU5URV9EQl9QT1JUOi01NDMyfScKICAgICAgLSAnRU5URV9EQl9OQU1FPSR7RU5URV9EQl9OQU1FOi1lbnRlX2RifScKICAgICAgLSAnRU5URV9EQl9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTOi1wZ3VzZXJ9JwogICAgICAtICdFTlRFX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0VOVEVfS0VZX0VOQ1JZUFRJT049JHtTRVJWSUNFX1JFQUxCQVNFNjRfRU5DUllQVElPTn0nCiAgICAgIC0gJ0VOVEVfS0VZX0hBU0g9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfSEFTSH0nCiAgICAgIC0gJ0VOVEVfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9KV1R9JwogICAgICAtICdFTlRFX0lOVEVSTkFMX0FETUlOPSR7RU5URV9JTlRFUk5BTF9BRE1JTjotMTU4MDU1OTk2MjM4NjQzOH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT049JHtFTlRFX0lOVEVSTkFMX0RJU0FCTEVfUkVHSVNUUkFUSU9OOi1mYWxzZX0nCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX0FSRV9MT0NBTF9CVUNLRVRTPSR7UFJJTUFSWV9TVE9SQUdFX0FSRV9MT0NBTF9CVUNLRVRTOi1mYWxzZX0nCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX1VTRV9QQVRIX1NUWUxFX1VSTFM9JHtQUklNQVJZX1NUT1JBR0VfVVNFX1BBVEhfU1RZTEVfVVJMUzotdHJ1ZX0nCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX0tFWT0ke1MzX1NUT1JBR0VfS0VZOj99JwogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9TRUNSRVQ9JHtTM19TVE9SQUdFX1NFQ1JFVDo/fScKICAgICAgLSAnRU5URV9TM19CMl9FVV9DRU5fRU5EUE9JTlQ9JHtTM19TVE9SQUdFX0VORFBPSU5UOj99JwogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9SRUdJT049JHtTM19TVE9SQUdFX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnRU5URV9TM19CMl9FVV9DRU5fQlVDS0VUPSR7UzNfU1RPUkFHRV9CVUNLRVQ6P30nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnbXVzZXVtLWRhdGE6L2RhdGEnCiAgICAgIC0gJ211c2V1bS1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXFPLScKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAvcGluZycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGdoY3IuaW8vZW50ZS1pby93ZWIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XRUJfMzAwMAogICAgICAtICdFTlRFX0FQSV9PUklHSU49JHtTRVJWSUNFX0ZRRE5fTVVTRVVNfScKICAgICAgLSAnRU5URV9BTEJVTVNfT1JJR0lOPSR7U0VSVklDRV9GUUROX1dFQl8zMDAyfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLS1mYWlsJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVM6LXBndXNlcn0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7U0VSVklDRV9EQl9OQU1FOi1lbnRlX2RifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICR7UE9TVEdSRVNfVVNFUn0gLWQgJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "photos", + "gallery", + "backup", + "encryption", + "privacy", + "self-hosted", + "google-photos", + "alternative" + ], + "category": "media", + "logo": "svgs/ente-photos.svg", + "minversion": "0.0.0", + "port": "8080" + }, "evolution-api": { "documentation": "https://doc.evolution-api.com/v1/pt/get-started/introduction?utm_source=coolify.io", "slogan": "Evolution API Installation with Postgres and Redis", diff --git a/tests/Unit/DockerImageParserTest.php b/tests/Unit/DockerImageParserTest.php index 35dffbab4..6102a90b2 100644 --- a/tests/Unit/DockerImageParserTest.php +++ b/tests/Unit/DockerImageParserTest.php @@ -1,94 +1,109 @@ parse('nginx:latest'); - protected function setUp(): void - { - parent::setUp(); - $this->parser = new DockerImageParser; + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe('latest') + ->and($parser->isImageHash())->toBeFalse() + ->and($parser->toString())->toBe('nginx:latest'); +}); + +it('parses image with sha256 hash using colon format', function () { + $parser = new DockerImageParser; + $hash = '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0'; + $parser->parse("ghcr.io/benjaminehowe/rail-disruptions:{$hash}"); + + expect($parser->getFullImageNameWithoutTag())->toBe('ghcr.io/benjaminehowe/rail-disruptions') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("ghcr.io/benjaminehowe/rail-disruptions@sha256:{$hash}") + ->and($parser->getFullImageNameWithHash())->toBe("ghcr.io/benjaminehowe/rail-disruptions@sha256:{$hash}"); +}); + +it('parses image with sha256 hash using at sign format', function () { + $parser = new DockerImageParser; + $hash = '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0'; + $parser->parse("nginx@sha256:{$hash}"); + + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("nginx@sha256:{$hash}") + ->and($parser->getFullImageNameWithHash())->toBe("nginx@sha256:{$hash}"); +}); + +it('parses registry image with hash', function () { + $parser = new DockerImageParser; + $hash = 'abc123def456789abcdef123456789abcdef123456789abcdef123456789abc1'; + $parser->parse("docker.io/library/nginx:{$hash}"); + + expect($parser->getFullImageNameWithoutTag())->toBe('docker.io/library/nginx') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("docker.io/library/nginx@sha256:{$hash}"); +}); + +it('parses image without tag defaults to latest', function () { + $parser = new DockerImageParser; + $parser->parse('nginx'); + + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe('latest') + ->and($parser->isImageHash())->toBeFalse() + ->and($parser->toString())->toBe('nginx:latest'); +}); + +it('parses registry with port', function () { + $parser = new DockerImageParser; + $parser->parse('registry.example.com:5000/myapp:latest'); + + expect($parser->getFullImageNameWithoutTag())->toBe('registry.example.com:5000/myapp') + ->and($parser->getTag())->toBe('latest') + ->and($parser->isImageHash())->toBeFalse(); +}); + +it('parses registry with port and hash', function () { + $parser = new DockerImageParser; + $hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + $parser->parse("registry.example.com:5000/myapp:{$hash}"); + + expect($parser->getFullImageNameWithoutTag())->toBe('registry.example.com:5000/myapp') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("registry.example.com:5000/myapp@sha256:{$hash}"); +}); + +it('identifies valid sha256 hashes', function () { + $validHashes = [ + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0', + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + ]; + + foreach ($validHashes as $hash) { + $parser = new DockerImageParser; + $parser->parse("image:{$hash}"); + expect($parser->isImageHash())->toBeTrue("Hash {$hash} should be recognized as valid SHA256"); } +}); - #[Test] - public function it_parses_simple_image_name() - { - $this->parser->parse('nginx'); +it('identifies invalid sha256 hashes', function () { + $invalidHashes = [ + 'latest', + 'v1.2.3', + 'abc123', // too short + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf', // too short + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf00', // too long + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cfg0', // invalid char + ]; - $this->assertEquals('', $this->parser->getRegistryUrl()); - $this->assertEquals('nginx', $this->parser->getImageName()); - $this->assertEquals('latest', $this->parser->getTag()); + foreach ($invalidHashes as $hash) { + $parser = new DockerImageParser; + $parser->parse("image:{$hash}"); + expect($parser->isImageHash())->toBeFalse("Hash {$hash} should not be recognized as valid SHA256"); } - - #[Test] - public function it_parses_image_with_tag() - { - $this->parser->parse('nginx:1.19'); - - $this->assertEquals('', $this->parser->getRegistryUrl()); - $this->assertEquals('nginx', $this->parser->getImageName()); - $this->assertEquals('1.19', $this->parser->getTag()); - } - - #[Test] - public function it_parses_image_with_organization() - { - $this->parser->parse('coollabs/coolify:latest'); - - $this->assertEquals('', $this->parser->getRegistryUrl()); - $this->assertEquals('coollabs/coolify', $this->parser->getImageName()); - $this->assertEquals('latest', $this->parser->getTag()); - } - - #[Test] - public function it_parses_image_with_registry_url() - { - $this->parser->parse('ghcr.io/coollabs/coolify:v4'); - - $this->assertEquals('ghcr.io', $this->parser->getRegistryUrl()); - $this->assertEquals('coollabs/coolify', $this->parser->getImageName()); - $this->assertEquals('v4', $this->parser->getTag()); - } - - #[Test] - public function it_parses_image_with_port_in_registry() - { - $this->parser->parse('localhost:5000/my-app:dev'); - - $this->assertEquals('localhost:5000', $this->parser->getRegistryUrl()); - $this->assertEquals('my-app', $this->parser->getImageName()); - $this->assertEquals('dev', $this->parser->getTag()); - } - - #[Test] - public function it_parses_image_without_tag() - { - $this->parser->parse('ghcr.io/coollabs/coolify'); - - $this->assertEquals('ghcr.io', $this->parser->getRegistryUrl()); - $this->assertEquals('coollabs/coolify', $this->parser->getImageName()); - $this->assertEquals('latest', $this->parser->getTag()); - } - - #[Test] - public function it_converts_back_to_string() - { - $originalString = 'ghcr.io/coollabs/coolify:v4'; - $this->parser->parse($originalString); - - $this->assertEquals($originalString, $this->parser->toString()); - } - - #[Test] - public function it_converts_to_string_with_default_tag() - { - $this->parser->parse('nginx'); - $this->assertEquals('nginx:latest', $this->parser->toString()); - } -} +}); diff --git a/versions.json b/versions.json index cb9fb7dc2..2e5cc5e84 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.434" + "version": "4.0.0-beta.435" }, "nightly": { - "version": "4.0.0-beta.435" + "version": "4.0.0-beta.436" }, "helper": { "version": "1.0.11"