From 69e448dac924425ee593d18541a797c25d5ad90c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:10:35 +0200 Subject: [PATCH] fix(installer): do not restart Docker with running containers Merge log rotation and the network pool into the existing daemon.json instead of overwriting it. Skip the change and the Docker restart when containers are running, unless DOCKER_POOL_FORCE_OVERRIDE=true. Closes #11221 Co-Authored-By: Claude Opus 5.5 --- other/nightly/install.sh | 144 ++++-------------- scripts/install.sh | 144 ++++-------------- .../InstallScriptDockerDaemonConfigTest.php | 118 ++++++++++++++ 3 files changed, 170 insertions(+), 236 deletions(-) create mode 100644 tests/Unit/InstallScriptDockerDaemonConfigTest.php diff --git a/other/nightly/install.sh b/other/nightly/install.sh index f2bdb5830b..ebd309a021 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -7,7 +7,7 @@ ## ROOT_USER_PASSWORD - Predefined root user password ## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8) ## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24) -## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false) +## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration, also when Docker must restart with running containers (default: false) ## AUTOUPDATE - Set to "false" to disable auto-updates ## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io) @@ -837,131 +837,39 @@ step_done "${DOCKER_SERVER_VERSION:+v$DOCKER_SERVER_VERSION}" step_start "Configuring Docker daemon" "log rotation · address pools" -echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}" -echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true" - -mkdir -p /etc/docker - -# Backup original daemon.json if it exists -if [ -f /etc/docker/daemon.json ]; then - cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" -fi - -# Create coolify configuration with or without address pools based on whether they were explicitly provided +# Changing daemon.json needs a Docker restart, which stops every running container. +# Coolify sets log rotation and the network pool only when no pool is configured or DOCKER_POOL_FORCE_OVERRIDE=true, +# keeps all other keys, and does not restart a Docker daemon with running containers unless DOCKER_POOL_FORCE_OVERRIDE=true. if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then - # First check if the configuration would actually change anything - if [ -f /etc/docker/daemon.json ]; then - CURRENT_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null) - CURRENT_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null) - - if [ "$CURRENT_POOL_BASE" = "$DOCKER_ADDRESS_POOL_BASE" ] && [ "$CURRENT_POOL_SIZE" = "$DOCKER_ADDRESS_POOL_SIZE" ]; then - echo " - Network pool configuration unchanged, skipping update" - NEED_MERGE=false + mkdir -p /etc/docker + CURRENT_DAEMON_CONFIG='{}' + if [ -s /etc/docker/daemon.json ] && ! CURRENT_DAEMON_CONFIG=$(jq -S . /etc/docker/daemon.json 2>/dev/null); then + CURRENT_DAEMON_CONFIG="" + fi + if [ -z "$CURRENT_DAEMON_CONFIG" ]; then + warn "Docker daemon configuration not changed: /etc/docker/daemon.json is not valid JSON." + else + NEW_DAEMON_CONFIG=$(jq -S --arg base "$DOCKER_ADDRESS_POOL_BASE" --argjson size "$DOCKER_ADDRESS_POOL_SIZE" \ + '. * {"log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}, "default-address-pools": [{"base": $base, "size": $size}]}' <<<"$CURRENT_DAEMON_CONFIG") + RUNNING_CONTAINERS=$({ docker ps -q 2>/dev/null || true; } | wc -l) + if [ "$NEW_DAEMON_CONFIG" = "$CURRENT_DAEMON_CONFIG" ]; then + echo " - Configuration is up to date" + elif [ "$RUNNING_CONTAINERS" -gt 0 ] && [ "$DOCKER_POOL_FORCE_OVERRIDE" != true ]; then + warn "Docker daemon configuration not changed: log rotation and the ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE}) network pool need a Docker restart, which stops ${RUNNING_CONTAINERS} running container(s) and can change the docker0 bridge IP. To apply it, run the installer again with DOCKER_POOL_FORCE_OVERRIDE=true." + UI_STEP_DETAIL="skipped · containers running" else - # If force override is enabled or no existing configuration exists, - # create a new configuration with the specified address pools - echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}" - cat >/etc/docker/daemon.json </etc/docker/daemon.json </dev/null 2>&1; then - echo " - Log configuration is up to date" - NEED_MERGE=false - else - # Create a configuration without address pools to preserve existing ones - cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json </etc/docker/daemon.json + echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE})" echo " - Configuration updated - restarting Docker daemon..." restart_docker_service - else - echo " - Configuration is up to date" fi - else - echo " - Configuration is up to date" fi else - if [ "$NEED_MERGE" = true ]; then - echo " - Configuration updated - restarting Docker daemon..." - restart_docker_service - else - echo " - Configuration is up to date" - fi + echo " - Using the existing network pool ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE})" + echo " - To override it: DOCKER_POOL_FORCE_OVERRIDE=true" fi step_done diff --git a/scripts/install.sh b/scripts/install.sh index adc6f745c9..0c8c2ae6a8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -7,7 +7,7 @@ ## ROOT_USER_PASSWORD - Predefined root user password ## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8) ## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24) -## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false) +## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration, also when Docker must restart with running containers (default: false) ## AUTOUPDATE - Set to "false" to disable auto-updates ## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io) @@ -837,131 +837,39 @@ step_done "${DOCKER_SERVER_VERSION:+v$DOCKER_SERVER_VERSION}" step_start "Configuring Docker daemon" "log rotation · address pools" -echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}" -echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true" - -mkdir -p /etc/docker - -# Backup original daemon.json if it exists -if [ -f /etc/docker/daemon.json ]; then - cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" -fi - -# Create coolify configuration with or without address pools based on whether they were explicitly provided +# Changing daemon.json needs a Docker restart, which stops every running container. +# Coolify sets log rotation and the network pool only when no pool is configured or DOCKER_POOL_FORCE_OVERRIDE=true, +# keeps all other keys, and does not restart a Docker daemon with running containers unless DOCKER_POOL_FORCE_OVERRIDE=true. if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then - # First check if the configuration would actually change anything - if [ -f /etc/docker/daemon.json ]; then - CURRENT_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null) - CURRENT_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null) - - if [ "$CURRENT_POOL_BASE" = "$DOCKER_ADDRESS_POOL_BASE" ] && [ "$CURRENT_POOL_SIZE" = "$DOCKER_ADDRESS_POOL_SIZE" ]; then - echo " - Network pool configuration unchanged, skipping update" - NEED_MERGE=false + mkdir -p /etc/docker + CURRENT_DAEMON_CONFIG='{}' + if [ -s /etc/docker/daemon.json ] && ! CURRENT_DAEMON_CONFIG=$(jq -S . /etc/docker/daemon.json 2>/dev/null); then + CURRENT_DAEMON_CONFIG="" + fi + if [ -z "$CURRENT_DAEMON_CONFIG" ]; then + warn "Docker daemon configuration not changed: /etc/docker/daemon.json is not valid JSON." + else + NEW_DAEMON_CONFIG=$(jq -S --arg base "$DOCKER_ADDRESS_POOL_BASE" --argjson size "$DOCKER_ADDRESS_POOL_SIZE" \ + '. * {"log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}, "default-address-pools": [{"base": $base, "size": $size}]}' <<<"$CURRENT_DAEMON_CONFIG") + RUNNING_CONTAINERS=$({ docker ps -q 2>/dev/null || true; } | wc -l) + if [ "$NEW_DAEMON_CONFIG" = "$CURRENT_DAEMON_CONFIG" ]; then + echo " - Configuration is up to date" + elif [ "$RUNNING_CONTAINERS" -gt 0 ] && [ "$DOCKER_POOL_FORCE_OVERRIDE" != true ]; then + warn "Docker daemon configuration not changed: log rotation and the ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE}) network pool need a Docker restart, which stops ${RUNNING_CONTAINERS} running container(s) and can change the docker0 bridge IP. To apply it, run the installer again with DOCKER_POOL_FORCE_OVERRIDE=true." + UI_STEP_DETAIL="skipped · containers running" else - # If force override is enabled or no existing configuration exists, - # create a new configuration with the specified address pools - echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}" - cat >/etc/docker/daemon.json </etc/docker/daemon.json </dev/null 2>&1; then - echo " - Log configuration is up to date" - NEED_MERGE=false - else - # Create a configuration without address pools to preserve existing ones - cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json </etc/docker/daemon.json + echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE})" echo " - Configuration updated - restarting Docker daemon..." restart_docker_service - else - echo " - Configuration is up to date" fi - else - echo " - Configuration is up to date" fi else - if [ "$NEED_MERGE" = true ]; then - echo " - Configuration updated - restarting Docker daemon..." - restart_docker_service - else - echo " - Configuration is up to date" - fi + echo " - Using the existing network pool ${DOCKER_ADDRESS_POOL_BASE} (size ${DOCKER_ADDRESS_POOL_SIZE})" + echo " - To override it: DOCKER_POOL_FORCE_OVERRIDE=true" fi step_done diff --git a/tests/Unit/InstallScriptDockerDaemonConfigTest.php b/tests/Unit/InstallScriptDockerDaemonConfigTest.php new file mode 100644 index 0000000000..2dab67a70d --- /dev/null +++ b/tests/Unit/InstallScriptDockerDaemonConfigTest.php @@ -0,0 +1,118 @@ + $variables + * @return array{config: ?array, restarted: bool, output: string} + */ +function runInstallScriptDockerDaemonStep(string $scriptPath, ?string $daemonJson, int $runningContainers, array $variables = []): array +{ + $script = file_get_contents(dirname(__DIR__, 2).'/'.$scriptPath); + preg_match('/^step_start "Configuring Docker daemon".*?^step_done$/ms', $script, $matches); + expect($matches)->not->toBeEmpty(); + + $etcDocker = sys_get_temp_dir().'/coolify-install-docker-'.bin2hex(random_bytes(6)); + mkdir($etcDocker); + if ($daemonJson !== null) { + file_put_contents("{$etcDocker}/daemon.json", $daemonJson); + } + + $variables = [ + 'DATE' => '20260101-000000', + 'DOCKER_ADDRESS_POOL_BASE' => '10.0.0.0/8', + 'DOCKER_ADDRESS_POOL_SIZE' => '24', + 'DOCKER_POOL_FORCE_OVERRIDE' => 'false', + 'EXISTING_POOL_CONFIGURED' => 'false', + ...$variables, + ]; + $prelude = implode("\n", array_map(fn ($name, $value) => "{$name}=".escapeshellarg($value), array_keys($variables), $variables)); + $stubs = << ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes + ); + $output = stream_get_contents($pipes[1]).stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exitCode = proc_close($process); + + $config = file_exists("{$etcDocker}/daemon.json") ? json_decode(file_get_contents("{$etcDocker}/daemon.json"), true) : null; + exec('rm -rf '.escapeshellarg($etcDocker)); + + expect($exitCode)->toBe(0, $output); + + return ['config' => $config, 'restarted' => str_contains($output, 'RESTARTED'), 'output' => $output]; +} + +$installScripts = ['stable install' => 'scripts/install.sh', 'nightly install' => 'other/nightly/install.sh']; + +it('does not create daemon.json or restart Docker when containers are running', function (string $scriptPath) { + $result = runInstallScriptDockerDaemonStep($scriptPath, null, runningContainers: 30); + + expect($result['config'])->toBeNull() + ->and($result['restarted'])->toBeFalse() + ->and($result['output'])->toContain('stops 30 running container(s)') + ->toContain('DOCKER_POOL_FORCE_OVERRIDE=true'); +})->with($installScripts); + +it('configures the network pool and restarts Docker when no containers are running', function (string $scriptPath) { + $result = runInstallScriptDockerDaemonStep($scriptPath, null, runningContainers: 0); + + expect($result['config'])->toEqual([ + 'default-address-pools' => [['base' => '10.0.0.0/8', 'size' => 24]], + 'log-driver' => 'json-file', + 'log-opts' => ['max-file' => '3', 'max-size' => '10m'], + ])->and($result['restarted'])->toBeTrue(); +})->with($installScripts); + +it('keeps other daemon.json settings when it adds the network pool', function (string $scriptPath) { + $result = runInstallScriptDockerDaemonStep($scriptPath, '{"registry-mirrors":["https://mirror.example.com"],"log-opts":{"compress":"true"}}', runningContainers: 0); + + expect($result['config'])->toMatchArray([ + 'registry-mirrors' => ['https://mirror.example.com'], + 'default-address-pools' => [['base' => '10.0.0.0/8', 'size' => 24]], + 'log-opts' => ['compress' => 'true', 'max-file' => '3', 'max-size' => '10m'], + ])->and($result['restarted'])->toBeTrue(); +})->with($installScripts); + +it('applies the network pool with running containers when DOCKER_POOL_FORCE_OVERRIDE is true', function (string $scriptPath) { + $result = runInstallScriptDockerDaemonStep($scriptPath, '{"default-address-pools":[{"base":"172.16.0.0/12","size":20}]}', runningContainers: 5, variables: [ + 'DOCKER_POOL_FORCE_OVERRIDE' => 'true', + 'EXISTING_POOL_CONFIGURED' => 'true', + ]); + + expect($result['config']['default-address-pools'])->toBe([['base' => '10.0.0.0/8', 'size' => 24]]) + ->and($result['restarted'])->toBeTrue(); +})->with($installScripts); + +it('does not restart Docker when the configuration is already up to date', function (string $scriptPath) { + $daemonJson = '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"},"default-address-pools":[{"base":"10.0.0.0/8","size":24}]}'; + $result = runInstallScriptDockerDaemonStep($scriptPath, $daemonJson, runningContainers: 0, variables: ['DOCKER_POOL_FORCE_OVERRIDE' => 'true']); + + expect($result['restarted'])->toBeFalse() + ->and($result['output'])->toContain('Configuration is up to date'); +})->with($installScripts); + +it('does not change an existing network pool without DOCKER_POOL_FORCE_OVERRIDE', function (string $scriptPath) { + $daemonJson = '{"default-address-pools":[{"base":"172.16.0.0/12","size":20}]}'; + $result = runInstallScriptDockerDaemonStep($scriptPath, $daemonJson, runningContainers: 0, variables: [ + 'DOCKER_ADDRESS_POOL_BASE' => '172.16.0.0/12', + 'DOCKER_ADDRESS_POOL_SIZE' => '20', + 'EXISTING_POOL_CONFIGURED' => 'true', + ]); + + expect($result['config'])->toBe(json_decode($daemonJson, true)) + ->and($result['restarted'])->toBeFalse(); +})->with($installScripts);