When port mappings are changed in the UI and the database is restarted, the system now gracefully stops and removes the existing container before recreating it with the new configuration. This prevents the "container name already in use" error that occurred when Docker Compose tried to create a container with the same name but different port configuration. Changes: - Add graceful container stop (10s timeout) before docker compose up - Remove old container to avoid name conflicts - Use --timeout flag (modern Docker CLI) instead of deprecated --time - Apply fix to all database types: MariaDB, MySQL, PostgreSQL, MongoDB, Redis, KeyDB, Dragonfly, and ClickHouse - Update StopDatabase.php for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
7.2 KiB
PHP
167 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Actions\Database;
|
|
|
|
use App\Models\StandaloneClickhouse;
|
|
use Lorisleiva\Actions\Concerns\AsAction;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
class StartClickhouse
|
|
{
|
|
use AsAction;
|
|
|
|
public StandaloneClickhouse $database;
|
|
|
|
public array $commands = [];
|
|
|
|
public string $configuration_dir;
|
|
|
|
public function handle(StandaloneClickhouse $database)
|
|
{
|
|
$this->database = $database;
|
|
|
|
$container_name = $this->database->uuid;
|
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
|
|
|
$this->commands = [
|
|
"echo 'Starting database.'",
|
|
"mkdir -p $this->configuration_dir",
|
|
];
|
|
|
|
$persistent_storages = $this->generate_local_persistent_volumes();
|
|
$persistent_file_volumes = $this->database->fileStorages()->get();
|
|
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
|
$environment_variables = $this->generate_environment_variables();
|
|
|
|
$docker_compose = [
|
|
'services' => [
|
|
$container_name => [
|
|
'image' => $this->database->image,
|
|
'container_name' => $container_name,
|
|
'environment' => $environment_variables,
|
|
'restart' => RESTART_MODE,
|
|
'networks' => [
|
|
$this->database->destination->network,
|
|
],
|
|
'ulimits' => [
|
|
'nofile' => [
|
|
'soft' => 262144,
|
|
'hard' => 262144,
|
|
],
|
|
],
|
|
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
|
'healthcheck' => [
|
|
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
|
|
'interval' => '5s',
|
|
'timeout' => '5s',
|
|
'retries' => 10,
|
|
'start_period' => '5s',
|
|
],
|
|
'mem_limit' => $this->database->limits_memory,
|
|
'memswap_limit' => $this->database->limits_memory_swap,
|
|
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
|
'mem_reservation' => $this->database->limits_memory_reservation,
|
|
'cpus' => (float) $this->database->limits_cpus,
|
|
'cpu_shares' => $this->database->limits_cpu_shares,
|
|
],
|
|
],
|
|
'networks' => [
|
|
$this->database->destination->network => [
|
|
'external' => true,
|
|
'name' => $this->database->destination->network,
|
|
'attachable' => true,
|
|
],
|
|
],
|
|
];
|
|
if (! is_null($this->database->limits_cpuset)) {
|
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
|
}
|
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
|
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
|
|
}
|
|
if (count($this->database->ports_mappings_array) > 0) {
|
|
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
|
}
|
|
if (count($persistent_storages) > 0) {
|
|
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
|
}
|
|
if (count($persistent_file_volumes) > 0) {
|
|
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
|
return "$item->fs_path:$item->mount_path";
|
|
})->toArray();
|
|
}
|
|
if (count($volume_names) > 0) {
|
|
$docker_compose['volumes'] = $volume_names;
|
|
}
|
|
|
|
// Add custom docker run options
|
|
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
|
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
|
|
|
$docker_compose = Yaml::dump($docker_compose, 10);
|
|
$docker_compose_base64 = base64_encode($docker_compose);
|
|
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
|
$readme = generate_readme_file($this->database->name, now());
|
|
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
|
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
|
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
|
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
|
$this->commands[] = "echo 'Database started.'";
|
|
|
|
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
|
}
|
|
|
|
private function generate_local_persistent_volumes()
|
|
{
|
|
$local_persistent_volumes = [];
|
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
|
} else {
|
|
$volume_name = $persistentStorage->name;
|
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
|
}
|
|
}
|
|
|
|
return $local_persistent_volumes;
|
|
}
|
|
|
|
private function generate_local_persistent_volumes_only_volume_names()
|
|
{
|
|
$local_persistent_volumes_names = [];
|
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
|
if ($persistentStorage->host_path) {
|
|
continue;
|
|
}
|
|
$name = $persistentStorage->name;
|
|
$local_persistent_volumes_names[$name] = [
|
|
'name' => $name,
|
|
'external' => false,
|
|
];
|
|
}
|
|
|
|
return $local_persistent_volumes_names;
|
|
}
|
|
|
|
private function generate_environment_variables()
|
|
{
|
|
$environment_variables = collect();
|
|
foreach ($this->database->runtime_environment_variables as $env) {
|
|
$environment_variables->push("$env->key=$env->real_value");
|
|
}
|
|
|
|
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
|
|
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
|
|
}
|
|
|
|
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
|
|
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
|
|
}
|
|
|
|
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
|
|
|
return $environment_variables->all();
|
|
}
|
|
}
|