Merge branch 'next' into next
This commit is contained in:
commit
bac6f4df81
35 changed files with 1612 additions and 417 deletions
74
CHANGELOG.md
74
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
41
app/Rules/DockerImageFormat.php
Normal file
41
app/Rules/DockerImageFormat.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class DockerImageFormat implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
// Check if the value contains ":sha256:" or ":sha" which is incorrect format
|
||||
if (preg_match('/:sha256?:/i', $value)) {
|
||||
$fail('The :attribute must use @ before sha256 digest (e.g., image@sha256:hash, not image:sha256:hash).');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Valid formats:
|
||||
// 1. image:tag (e.g., nginx:latest)
|
||||
// 2. registry/image:tag (e.g., ghcr.io/user/app:v1.2.3)
|
||||
// 3. image@sha256:hash (e.g., nginx@sha256:abc123...)
|
||||
// 4. registry/image@sha256:hash
|
||||
// 5. registry:port/image:tag (e.g., localhost:5000/app:latest)
|
||||
|
||||
$pattern = '/^
|
||||
(?:[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[0-9]+)?\/)? # Optional registry with optional port
|
||||
[a-z0-9]+(?:[._\/-][a-z0-9]+)* # Image name (required)
|
||||
(?::[a-z0-9][a-z0-9._-]*|@sha256:[a-f0-9]{64})? # Optional :tag or @sha256:hash
|
||||
$/ix';
|
||||
|
||||
if (! preg_match($pattern, $value)) {
|
||||
$fail('The :attribute format is invalid. Use image:tag or image@sha256:hash format.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,20 +10,33 @@ class DockerImageParser
|
|||
|
||||
private string $tag = 'latest';
|
||||
|
||||
private bool $isImageHash = false;
|
||||
|
||||
public function parse(string $imageString): self
|
||||
{
|
||||
// First 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 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
BIN
public/ente-photos-icon-green.png
Normal file
BIN
public/ente-photos-icon-green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
15
public/svgs/ente-photos.svg
Normal file
15
public/svgs/ente-photos.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||
<!-- Official Ente Photos icon based on the official PNG -->
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00D4AA;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00A693;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Main circular background matching official Ente green -->
|
||||
<circle cx="128" cy="128" r="120" fill="url(#gradient)"/>
|
||||
|
||||
<!-- Official Ente "e" letterform based on the PNG icon -->
|
||||
<path d="M75 128 C75 90, 95 70, 128 70 C161 70, 181 90, 181 118 L85 118 C85 148, 105 168, 128 168 C146 168, 160 158, 170 144 L185 160 C172 178, 152 190, 128 190 C95 190, 75 170, 75 128 Z M85 102 L171 102 C167 84, 149 82, 128 82 C107 82, 89 84, 85 102 Z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 872 B |
BIN
public/svgs/ente.png
Normal file
BIN
public/svgs/ente.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
|
|
@ -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 }}
|
||||
</div>
|
||||
<p class="text-xs dark:text-neutral-400 text-gray-600 mt-1">
|
||||
{{ $deployment->application?->environment?->project?->name }} / {{ $deployment->application?->environment?->name }}
|
||||
</p>
|
||||
<p class="text-xs dark:text-neutral-400 text-gray-600">
|
||||
{{ $deployment->server_name }}
|
||||
</p>
|
||||
@if ($deployment->pull_request_id)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
@else
|
||||
<x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button>
|
||||
@endif
|
||||
@if (isDev())
|
||||
<x-forms.button x-on:click="$wire.copyLogsToClipboard().then(text => navigator.clipboard.writeText(text))">Copy Logs</x-forms.button>
|
||||
@endif
|
||||
@if (data_get($application_deployment_queue, 'status') === 'queued')
|
||||
<x-forms.button wire:click.prevent="force_start">Force Start</x-forms.button>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -166,12 +166,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.input required id="application.docker_registry_image_name" label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag"
|
||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input id="application.docker_registry_image_name" label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag"
|
||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@else
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
|
||||
<h1>Create a new Application</h1>
|
||||
<div class="pb-4">You can deploy an existing Docker Image from any Registry.</div>
|
||||
<form wire:submit="submit">
|
||||
|
|
@ -6,6 +6,24 @@
|
|||
<h2>Docker Image</h2>
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
</div>
|
||||
<x-forms.input rows="20" id="dockerImage" placeholder="nginx:latest"></x-forms.textarea>
|
||||
<div class="space-y-4">
|
||||
<x-forms.input id="imageName" label="Image Name" placeholder="nginx or ghcr.io/user/app"
|
||||
helper="Enter the Docker image name with optional registry. Examples: nginx, ghcr.io/user/app, localhost:5000/myapp"
|
||||
required autofocus />
|
||||
<div class="relative grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<x-forms.input id="imageTag" label="Tag (optional)" placeholder="latest"
|
||||
helper="Enter a tag like 'latest' or 'v1.2.3'. Leave empty if using SHA256." />
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 hidden md:flex items-center justify-center z-10">
|
||||
<div
|
||||
class="px-2 py-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-300 rounded text-xs font-bold text-neutral-500 dark:text-neutral-400">
|
||||
OR
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.input id="imageSha256" label="SHA256 Digest (optional)"
|
||||
placeholder="59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0"
|
||||
helper="Enter only the 64-character hex digest (without 'sha256:' prefix)" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<div>
|
||||
<div class="flex flex-col gap-4 p-4 bg-white border dark:bg-base dark:border-coolgray-300 border-neutral-200">
|
||||
@if ($isReadOnly)
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
@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
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-col justify-center text-sm select-text">
|
||||
<div class="flex gap-2 md:flex-row flex-col">
|
||||
<x-forms.input label="Source Path" :value="$fileStorage->fs_path" readonly />
|
||||
|
|
@ -7,58 +16,75 @@
|
|||
</div>
|
||||
</div>
|
||||
<form wire:submit='submit' class="flex flex-col gap-2">
|
||||
@can('update', $resource)
|
||||
<div class="flex gap-2">
|
||||
@if ($fileStorage->is_directory)
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Conversion to File?"
|
||||
buttonTitle="Convert to file" submitAction="convertToFile" :actions="[
|
||||
'All files in this directory will be permanently deleted and an empty file will be created in its place.',
|
||||
]"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Deletion?" buttonTitle="Delete"
|
||||
isErrorButton submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
|
||||
'The selected directory and all its contents will be permanently deleted from the container.',
|
||||
]"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" />
|
||||
@else
|
||||
@if (!$fileStorage->is_binary)
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm File Conversion to Directory?"
|
||||
buttonTitle="Convert to directory" submitAction="convertToDirectory" :actions="[
|
||||
'The selected file will be permanently deleted and an empty directory will be created in its place.',
|
||||
@if (!$isReadOnly)
|
||||
@can('update', $resource)
|
||||
<div class="flex gap-2">
|
||||
@if ($fileStorage->is_directory)
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Conversion to File?"
|
||||
buttonTitle="Convert to file" submitAction="convertToFile" :actions="[
|
||||
'All files in this directory will be permanently deleted and an empty file will be created in its place.',
|
||||
]"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
|
||||
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Deletion?" buttonTitle="Delete"
|
||||
isErrorButton submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
|
||||
'The selected directory and all its contents will be permanently deleted from the container.',
|
||||
]"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" />
|
||||
@else
|
||||
@if (!$fileStorage->is_binary)
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm File Conversion to Directory?"
|
||||
buttonTitle="Convert to directory" submitAction="convertToDirectory" :actions="[
|
||||
'The selected file will be permanently deleted and an empty directory will be created in its place.',
|
||||
]"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" :confirmWithPassword="false"
|
||||
step2ButtonText="Convert to directory" />
|
||||
@endif
|
||||
<x-forms.button type="button" wire:click="loadStorageOnServer">Load from
|
||||
server</x-forms.button>
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm File Deletion?" buttonTitle="Delete"
|
||||
isErrorButton submitAction="delete" :checkboxes="$fileDeletionCheckboxes" :actions="['The selected file will be permanently deleted from the container.']"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" />
|
||||
@endif
|
||||
<x-forms.button type="button" wire:click="loadStorageOnServer">Load from server</x-forms.button>
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm File Deletion?" buttonTitle="Delete"
|
||||
isErrorButton submitAction="delete" :checkboxes="$fileDeletionCheckboxes" :actions="['The selected file will be permanently deleted from the container.']"
|
||||
confirmationText="{{ $fs_path }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" />
|
||||
@endif
|
||||
</div>
|
||||
@endcan
|
||||
@if (!$fileStorage->is_directory)
|
||||
@can('update', $resource)
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave label="Is this based on the Git repository?"
|
||||
id="fileStorage.is_based_on_git"></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
rows="20" id="fileStorage.content"
|
||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
||||
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
</div>
|
||||
@endcan
|
||||
@if (!$fileStorage->is_directory)
|
||||
@can('update', $resource)
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave label="Is this based on the Git repository?"
|
||||
id="fileStorage.is_based_on_git"></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
rows="20" id="fileStorage.content"
|
||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
||||
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
id="fileStorage.is_based_on_git"></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
rows="20" id="fileStorage.content" disabled></x-forms.textarea>
|
||||
@endcan
|
||||
@endif
|
||||
@else
|
||||
{{-- Read-only view --}}
|
||||
@if (!$fileStorage->is_directory)
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
|
|
@ -68,7 +94,7 @@
|
|||
<x-forms.textarea
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
rows="20" id="fileStorage.content" disabled></x-forms.textarea>
|
||||
@endcan
|
||||
@endif
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<div
|
||||
class="p-1 mt-1 bg-white border rounded-sm shadow-sm dark:bg-coolgray-200 dark:border-coolgray-300 border-neutral-300">
|
||||
<div class="flex flex-col gap-1">
|
||||
<a class="dropdown-item"
|
||||
@click="volumeModalOpen = true; dropdownOpen = false">
|
||||
<a class="dropdown-item" @click="volumeModalOpen = true; dropdownOpen = false">
|
||||
<svg class="size-4" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
|
|
@ -105,31 +105,41 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
|
|||
</button>
|
||||
</div>
|
||||
<div class="relative flex items-center justify-center w-auto"
|
||||
x-init="$watch('volumeModalOpen', value => { if(value) { $nextTick(() => { const input = $el.querySelector('input'); input?.focus(); }) } })">
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submitPersistentVolume'>
|
||||
x-init="$watch('volumeModalOpen', value => {
|
||||
if (value) {
|
||||
$nextTick(() => {
|
||||
const input = $el.querySelector('input');
|
||||
input?.focus();
|
||||
})
|
||||
}
|
||||
})">
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm"
|
||||
wire:submit='submitPersistentVolume'>
|
||||
<div class="flex flex-col">
|
||||
<div>Docker Volumes mounted to the container.</div>
|
||||
</div>
|
||||
@if ($isSwarm)
|
||||
<div class="text-warning">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
|
||||
<div class="text-warning">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.</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="pv-name"
|
||||
id="name" label="Name" required helper="Volume name." />
|
||||
@if ($isSwarm)
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="/root"
|
||||
id="host_path" label="Source Path" required
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path" required
|
||||
helper="Directory on the host system." />
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="/root"
|
||||
id="host_path" label="Source Path"
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path"
|
||||
helper="Directory on the host system." />
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="/tmp/root"
|
||||
id="mount_path" label="Destination Path" required
|
||||
helper="Directory inside the container." />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/tmp/root" id="mount_path" label="Destination Path"
|
||||
required helper="Directory inside the container." />
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">
|
||||
Add
|
||||
</x-forms.button>
|
||||
|
|
@ -169,15 +179,24 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
|
|||
</button>
|
||||
</div>
|
||||
<div class="relative flex items-center justify-center w-auto"
|
||||
x-init="$watch('fileModalOpen', value => { if(value) { $nextTick(() => { const input = $el.querySelector('input'); input?.focus(); }) } })">
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submitFileStorage'>
|
||||
x-init="$watch('fileModalOpen', value => {
|
||||
if (value) {
|
||||
$nextTick(() => {
|
||||
const input = $el.querySelector('input');
|
||||
input?.focus();
|
||||
})
|
||||
}
|
||||
})">
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm"
|
||||
wire:submit='submitFileStorage'>
|
||||
<div class="flex flex-col">
|
||||
<div>Actual file mounted from the host system to the container.</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/etc/nginx/nginx.conf" id="file_storage_path"
|
||||
label="Destination Path" required helper="File location inside the container" />
|
||||
label="Destination Path" required
|
||||
helper="File location inside the container" />
|
||||
<x-forms.textarea canGate="update" :canResource="$resource" label="Content"
|
||||
id="file_storage_content"></x-forms.textarea>
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">
|
||||
|
|
@ -219,18 +238,27 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
|
|||
</button>
|
||||
</div>
|
||||
<div class="relative flex items-center justify-center w-auto"
|
||||
x-init="$watch('directoryModalOpen', value => { if(value) { $nextTick(() => { const input = $el.querySelector('input'); input?.focus(); }) } })">
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submitFileStorageDirectory'>
|
||||
x-init="$watch('directoryModalOpen', value => {
|
||||
if (value) {
|
||||
$nextTick(() => {
|
||||
const input = $el.querySelector('input');
|
||||
input?.focus();
|
||||
})
|
||||
}
|
||||
})">
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm"
|
||||
wire:submit='submitFileStorageDirectory'>
|
||||
<div class="flex flex-col">
|
||||
<div>Directory mounted from the host system to the container.</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="{{ application_configuration_dir() }}/{{ $resource->uuid }}/etc/nginx"
|
||||
id="file_storage_directory_source" label="Source Directory" required
|
||||
helper="Directory on the host system." />
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="/etc/nginx"
|
||||
id="file_storage_directory_destination" label="Destination Directory" required
|
||||
id="file_storage_directory_source" label="Source Directory"
|
||||
required helper="Directory on the host system." />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/etc/nginx" id="file_storage_directory_destination"
|
||||
label="Destination Directory" required
|
||||
helper="Directory inside the container." />
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">
|
||||
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 --}}
|
||||
<div class="flex gap-2 border-b dark:border-coolgray-300 border-neutral-200">
|
||||
<button @click="activeTab = 'volumes'"
|
||||
:class="activeTab === 'volumes' ? 'border-b-2 dark:border-white border-black' : 'border-b-2 border-transparent'"
|
||||
:class="activeTab === 'volumes' ? 'border-b-2 dark:border-white border-black' :
|
||||
'border-b-2 border-transparent'"
|
||||
@if (!$hasVolumes) disabled @endif
|
||||
class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasVolumes ? 'dark:text-neutral-400 dark:hover:text-white text-neutral-600 hover:text-black cursor-pointer' : 'opacity-50 cursor-not-allowed' }} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100">
|
||||
Volumes ({{ $this->volumeCount }})
|
||||
</button>
|
||||
<button @click="activeTab = 'files'"
|
||||
:class="activeTab === 'files' ? 'border-b-2 dark:border-white border-black' : 'border-b-2 border-transparent'"
|
||||
:class="activeTab === 'files' ? 'border-b-2 dark:border-white border-black' :
|
||||
'border-b-2 border-transparent'"
|
||||
@if (!$hasFiles) disabled @endif
|
||||
class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasFiles ? 'dark:text-neutral-400 dark:hover:text-white text-neutral-600 hover:text-black cursor-pointer' : 'opacity-50 cursor-not-allowed' }} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100">
|
||||
Files ({{ $this->fileCount }})
|
||||
</button>
|
||||
<button @click="activeTab = 'directories'"
|
||||
:class="activeTab === 'directories' ? 'border-b-2 dark:border-white border-black' : 'border-b-2 border-transparent'"
|
||||
:class="activeTab === 'directories' ? 'border-b-2 dark:border-white border-black' :
|
||||
'border-b-2 border-transparent'"
|
||||
@if (!$hasDirectories) disabled @endif
|
||||
class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasDirectories ? 'dark:text-neutral-400 dark:hover:text-white text-neutral-600 hover:text-black cursor-pointer' : 'opacity-50 cursor-not-allowed' }} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100">
|
||||
Directories ({{ $this->directoryCount }})
|
||||
|
|
@ -333,19 +364,96 @@ class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasDirectories ? 'dark
|
|||
</div>
|
||||
@endif
|
||||
@else
|
||||
@if ($resource->persistentStorages()->get()->count() > 0)
|
||||
<h3>{{ Str::headline($resource->name) }} </h3>
|
||||
@endif
|
||||
@if ($resource->persistentStorages()->get()->count() > 0)
|
||||
<livewire:project.shared.storages.all :resource="$resource" />
|
||||
@endif
|
||||
@if ($fileStorage->count() > 0)
|
||||
<div class="flex flex-col gap-4">
|
||||
@foreach ($fileStorage->sort() as $fileStorage)
|
||||
<livewire:project.service.file-storage :fileStorage="$fileStorage"
|
||||
wire:key="resource-{{ $fileStorage->uuid }}" />
|
||||
@endforeach
|
||||
<div class="flex flex-col gap-4 py-2">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>{{ Str::headline($resource->name) }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0)
|
||||
<div>No storage found.</div>
|
||||
@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)
|
||||
<div x-data="{
|
||||
activeTab: '{{ $defaultTab }}'
|
||||
}">
|
||||
{{-- Tabs Navigation --}}
|
||||
<div class="flex gap-2 border-b dark:border-coolgray-300 border-neutral-200">
|
||||
<button @click="activeTab = 'volumes'"
|
||||
:class="activeTab === 'volumes' ? 'border-b-2 dark:border-white border-black' :
|
||||
'border-b-2 border-transparent'"
|
||||
@if (!$hasVolumes) disabled @endif
|
||||
class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasVolumes ? 'dark:text-neutral-400 dark:hover:text-white text-neutral-600 hover:text-black cursor-pointer' : 'opacity-50 cursor-not-allowed' }} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100">
|
||||
Volumes ({{ $this->volumeCount }})
|
||||
</button>
|
||||
<button @click="activeTab = 'files'"
|
||||
:class="activeTab === 'files' ? 'border-b-2 dark:border-white border-black' :
|
||||
'border-b-2 border-transparent'"
|
||||
@if (!$hasFiles) disabled @endif
|
||||
class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasFiles ? 'dark:text-neutral-400 dark:hover:text-white text-neutral-600 hover:text-black cursor-pointer' : 'opacity-50 cursor-not-allowed' }} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100">
|
||||
Files ({{ $this->fileCount }})
|
||||
</button>
|
||||
<button @click="activeTab = 'directories'"
|
||||
:class="activeTab === 'directories' ? 'border-b-2 dark:border-white border-black' :
|
||||
'border-b-2 border-transparent'"
|
||||
@if (!$hasDirectories) disabled @endif
|
||||
class="px-4 py-2 -mb-px font-medium transition-colors {{ $hasDirectories ? 'dark:text-neutral-400 dark:hover:text-white text-neutral-600 hover:text-black cursor-pointer' : 'opacity-50 cursor-not-allowed' }} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100">
|
||||
Directories ({{ $this->directoryCount }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Tab Content --}}
|
||||
<div class="pt-4">
|
||||
{{-- Volumes Tab --}}
|
||||
<div x-show="activeTab === 'volumes'" class="flex flex-col gap-4">
|
||||
@if ($hasVolumes)
|
||||
<livewire:project.shared.storages.all :resource="$resource" />
|
||||
@else
|
||||
<div class="text-center py-8 dark:text-neutral-500 text-neutral-400">
|
||||
No volumes configured.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Files Tab --}}
|
||||
<div x-show="activeTab === 'files'" class="flex flex-col gap-4">
|
||||
@if ($hasFiles)
|
||||
@foreach ($this->files as $fs)
|
||||
<livewire:project.service.file-storage :fileStorage="$fs"
|
||||
wire:key="file-{{ $fs->id }}" />
|
||||
@endforeach
|
||||
@else
|
||||
<div class="text-center py-8 dark:text-neutral-500 text-neutral-400">
|
||||
No file mounts configured.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Directories Tab --}}
|
||||
<div x-show="activeTab === 'directories'" class="flex flex-col gap-4">
|
||||
@if ($hasDirectories)
|
||||
@foreach ($this->directories as $fs)
|
||||
<livewire:project.service.file-storage :fileStorage="$fs"
|
||||
wire:key="directory-{{ $fs->id }}" />
|
||||
@endforeach
|
||||
@else
|
||||
<div class="text-center py-8 dark:text-neutral-500 text-neutral-400">
|
||||
No directory mounts configured.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<div>
|
||||
<form wire:submit='submit' class="flex flex-col items-center gap-4 p-4 bg-white border lg:items-start dark:bg-base dark:border-coolgray-300 border-neutral-200">
|
||||
@if ($isReadOnly)
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
This volume is mounted as read-only and cannot be modified from the UI.
|
||||
</div>
|
||||
@if ($isFirst)
|
||||
<div class="flex gap-2 items-end w-full md:flex-row flex-col">
|
||||
@if (
|
||||
|
|
|
|||
88
templates/compose/elasticsearch-with-kibana.yaml
Normal file
88
templates/compose/elasticsearch-with-kibana.yaml
Normal file
|
|
@ -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'
|
||||
113
templates/compose/ente-photos-with-s3.yaml
Normal file
113
templates/compose/ente-photos-with-s3.yaml
Normal file
|
|
@ -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';
|
||||
"
|
||||
80
templates/compose/ente-photos.yaml
Normal file
80
templates/compose/ente-photos.yaml
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,94 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\DockerImageParser;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DockerImageParserTest extends TestCase
|
||||
{
|
||||
private DockerImageParser $parser;
|
||||
it('parses regular image with tag', function () {
|
||||
$parser = new DockerImageParser;
|
||||
$parser->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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue