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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-16 21:10:35 +02:00
co-authored by Claude Opus 5.5
parent 9809d6b445
commit 69e448dac9
3 changed files with 170 additions and 236 deletions
+26 -118
View File
@@ -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 <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
]
}
EOL
NEED_MERGE=true
fi
else
# No existing configuration, create new one
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
cat >/etc/docker/daemon.json <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
]
}
EOL
NEED_MERGE=true
fi
else
# Check if we need to update log settings
if [ -f /etc/docker/daemon.json ] && jq -e '.["log-driver"] == "json-file" and .["log-opts"]["max-size"] == "10m" and .["log-opts"]["max-file"] == "3"' /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 <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOL
NEED_MERGE=true
fi
fi
# Remove the duplicate daemon.json creation since we handle it above
if ! [ -f /etc/docker/daemon.json ]; then
# If no daemon.json exists, create it with default settings
cat >/etc/docker/daemon.json <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
]
}
EOL
NEED_MERGE=false
fi
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE") || true)
if [ "$DIFF" != "" ]; then
echo " - Checking configuration changes..."
# Check if address pools were changed
if echo "$DIFF" | grep -q "default-address-pools"; then
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ] || [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
echo " - Network pool updated per user request"
else
echo " - Warning: Network pool modified without explicit request"
if [ -f /etc/docker/daemon.json ]; then
cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE"
fi
fi
# Remove this redundant restart since we already restarted when writing the config
echo " - Configuration changes confirmed"
if [ "$NEED_MERGE" = true ]; then
printf '%s\n' "$NEW_DAEMON_CONFIG" >/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
+26 -118
View File
@@ -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 <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
]
}
EOL
NEED_MERGE=true
fi
else
# No existing configuration, create new one
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
cat >/etc/docker/daemon.json <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
]
}
EOL
NEED_MERGE=true
fi
else
# Check if we need to update log settings
if [ -f /etc/docker/daemon.json ] && jq -e '.["log-driver"] == "json-file" and .["log-opts"]["max-size"] == "10m" and .["log-opts"]["max-file"] == "3"' /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 <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOL
NEED_MERGE=true
fi
fi
# Remove the duplicate daemon.json creation since we handle it above
if ! [ -f /etc/docker/daemon.json ]; then
# If no daemon.json exists, create it with default settings
cat >/etc/docker/daemon.json <<EOL
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
]
}
EOL
NEED_MERGE=false
fi
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE") || true)
if [ "$DIFF" != "" ]; then
echo " - Checking configuration changes..."
# Check if address pools were changed
if echo "$DIFF" | grep -q "default-address-pools"; then
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ] || [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
echo " - Network pool updated per user request"
else
echo " - Warning: Network pool modified without explicit request"
if [ -f /etc/docker/daemon.json ]; then
cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE"
fi
fi
# Remove this redundant restart since we already restarted when writing the config
echo " - Configuration changes confirmed"
if [ "$NEED_MERGE" = true ]; then
printf '%s\n' "$NEW_DAEMON_CONFIG" >/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
@@ -0,0 +1,118 @@
<?php
/**
* Runs the "Configuring Docker daemon" step of the install script against a temporary /etc/docker.
* docker, restart_docker_service, and the terminal UI helpers are stubbed.
*
* @param array<string, string> $variables
* @return array{config: ?array<string, mixed>, 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 = <<<BASH
set -e
set -o pipefail
step_start() { :; }
step_done() { :; }
warn() { echo "WARNING: \$*"; }
restart_docker_service() { echo RESTARTED; }
docker() { for i in \$(seq 1 {$runningContainers}); do echo "container\$i"; done; }
BASH;
$process = proc_open(
['bash', '-c', $stubs."\n".$prelude."\n".str_replace('/etc/docker', $etcDocker, $matches[0])],
[1 => ['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);