Fix healthcheck detection to properly ignore commented lines

Improves parseHealthcheckFromDockerfile to correctly detect only
uncommented HEALTHCHECK directives. Previously, commenting out a
HEALTHCHECK (e.g., # HEALTHCHECK ...) would still be detected as
an active healthcheck, preventing Coolify from generating its own
healthcheck and causing deployment failures.

Changes:
- Check for uncommented HEALTHCHECK directives only
- Skip lines starting with # (comments)
- Skip empty lines
- Add comprehensive tests for comment handling

Fixes the case where users comment out HEALTHCHECK in their
Dockerfile, which should be treated the same as removing it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2025-10-22 16:12:46 +02:00
co-authored by Claude
parent 587517394b
commit 670c9dab0d
4 changed files with 79 additions and 67 deletions
+19 -1
View File
@@ -1804,7 +1804,21 @@ class Application extends BaseModel
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
{
$dockerfile = str($dockerfile)->trim()->explode("\n");
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
// Check if there's an uncommented HEALTHCHECK directive
$hasHealthcheck = false;
foreach ($dockerfile as $line) {
$trimmedLine = trim($line);
// Skip empty lines and comments
if (empty($trimmedLine) || str_starts_with($trimmedLine, '#')) {
continue;
}
// Check if line starts with HEALTHCHECK (not commented)
if (str_starts_with($trimmedLine, 'HEALTHCHECK')) {
$hasHealthcheck = true;
break;
}
}
// Always check if healthcheck was removed, regardless of health_check_enabled setting
if (! $hasHealthcheck && $this->custom_healthcheck_found) {
@@ -1824,6 +1838,10 @@ class Application extends BaseModel
$lines = $dockerfile->toArray();
foreach ($lines as $line) {
$trimmedLine = trim($line);
// Skip comments
if (str_starts_with($trimmedLine, '#')) {
continue;
}
if (str_starts_with($trimmedLine, 'HEALTHCHECK')) {
$healthcheckCommand .= trim($trimmedLine, '\\ ');
-15
View File
@@ -2498,21 +2498,6 @@
"minversion": "0.0.0",
"port": "8080"
},
"minio": {
"documentation": "https://min.io/docs/minio/container/index.html?utm_source=coolify.io",
"slogan": "MinIO is a high performance object storage server compatible with Amazon S3 APIs.",
"compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fU0VSVkVSX1VSTD0kTUlOSU9fU0VSVkVSX1VSTAogICAgICAtIE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMPSRNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"tags": [
"object",
"storage",
"server",
"s3",
"api"
],
"category": "storage",
"logo": "svgs/minio.svg",
"minversion": "0.0.0"
},
"mixpost": {
"documentation": "https://docs.mixpost.app/lite?utm_source=coolify.io",
"slogan": "Mixpost is a robust and versatile social media management software, designed to streamline social media operations and enhance content marketing strategies.",
-15
View File
@@ -2498,21 +2498,6 @@
"minversion": "0.0.0",
"port": "8080"
},
"minio": {
"documentation": "https://min.io/docs/minio/container/index.html?utm_source=coolify.io",
"slogan": "MinIO is a high performance object storage server compatible with Amazon S3 APIs.",
"compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fU0VSVkVSX1VSTD0kTUlOSU9fU0VSVkVSX1VSTAogICAgICAtIE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMPSRNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"tags": [
"object",
"storage",
"server",
"s3",
"api"
],
"category": "storage",
"logo": "svgs/minio.svg",
"minversion": "0.0.0"
},
"mixpost": {
"documentation": "https://docs.mixpost.app/lite?utm_source=coolify.io",
"slogan": "Mixpost is a robust and versatile social media management software, designed to streamline social media operations and enhance content marketing strategies.",
@@ -3,55 +3,79 @@
/**
* Tests for parseHealthcheckFromDockerfile method
*
* NOTE: These tests verify the logic for detecting when a HEALTHCHECK directive
* is removed from a Dockerfile. The fix ensures that healthcheck removal is detected
* regardless of the health_check_enabled setting.
* These tests verify the logic for detecting HEALTHCHECK directives in Dockerfiles,
* properly ignoring commented lines.
*/
use App\Models\Application;
it('detects when HEALTHCHECK is removed from dockerfile', function () {
// This test verifies the fix for the bug where Coolify doesn't detect
// when a HEALTHCHECK is removed from a Dockerfile, causing deployments to fail.
$dockerfile = "FROM nginx:latest\nCOPY . /app\nEXPOSE 80";
$dockerfile = str("FROM nginx:latest\nCOPY . /app\nEXPOSE 80")->trim()->explode("\n");
// The key fix: hasHealthcheck check happens BEFORE the isHealthcheckDisabled check
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
// Simulate an application with custom_healthcheck_found = true
$hasHealthcheck = hasUncommentedHealthcheck($dockerfile);
$customHealthcheckFound = true;
// The fixed logic: This condition should be true when HEALTHCHECK is removed
$shouldReset = ! $hasHealthcheck && $customHealthcheckFound;
expect($shouldReset)->toBeTrue()
->and($hasHealthcheck)->toBeFalse()
->and($customHealthcheckFound)->toBeTrue();
->and($hasHealthcheck)->toBeFalse();
});
it('does not reset when HEALTHCHECK exists in dockerfile', function () {
$dockerfile = str("FROM nginx:latest\nHEALTHCHECK --interval=30s CMD curl\nEXPOSE 80")->trim()->explode("\n");
it('detects uncommented HEALTHCHECK in dockerfile', function () {
$dockerfile = "FROM nginx:latest\nHEALTHCHECK --interval=30s CMD curl\nEXPOSE 80";
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
$customHealthcheckFound = true;
$hasHealthcheck = hasUncommentedHealthcheck($dockerfile);
// When healthcheck exists, should not reset
$shouldReset = ! $hasHealthcheck && $customHealthcheckFound;
expect($shouldReset)->toBeFalse()
->and($hasHealthcheck)->toBeTrue();
expect($hasHealthcheck)->toBeTrue();
});
it('does not reset when custom_healthcheck_found is false', function () {
$dockerfile = str("FROM nginx:latest\nCOPY . /app\nEXPOSE 80")->trim()->explode("\n");
it('ignores commented HEALTHCHECK in dockerfile', function () {
$dockerfile = "FROM nginx:latest\n# HEALTHCHECK --interval=30s CMD curl\nEXPOSE 80";
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
$customHealthcheckFound = false;
$hasHealthcheck = hasUncommentedHealthcheck($dockerfile);
// When custom_healthcheck_found is false, no need to reset
$shouldReset = ! $hasHealthcheck && $customHealthcheckFound;
expect($shouldReset)->toBeFalse()
->and($customHealthcheckFound)->toBeFalse();
expect($hasHealthcheck)->toBeFalse();
});
it('detects HEALTHCHECK even with surrounding whitespace', function () {
$dockerfile = "FROM nginx:latest\n HEALTHCHECK --interval=30s CMD curl\nEXPOSE 80";
$hasHealthcheck = hasUncommentedHealthcheck($dockerfile);
expect($hasHealthcheck)->toBeTrue();
});
it('ignores HEALTHCHECK in middle of line (must be at start)', function () {
$dockerfile = "FROM nginx:latest\nRUN echo HEALTHCHECK\nEXPOSE 80";
$hasHealthcheck = hasUncommentedHealthcheck($dockerfile);
expect($hasHealthcheck)->toBeFalse();
});
it('detects HEALTHCHECK when commented out then uncommented', function () {
$dockerfileCommented = "FROM nginx:latest\n# HEALTHCHECK --interval=30s CMD curl\nEXPOSE 80";
$dockerfileUncommented = "FROM nginx:latest\nHEALTHCHECK --interval=30s CMD curl\nEXPOSE 80";
$hasHealthcheckCommented = hasUncommentedHealthcheck($dockerfileCommented);
$hasHealthcheckUncommented = hasUncommentedHealthcheck($dockerfileUncommented);
expect($hasHealthcheckCommented)->toBeFalse()
->and($hasHealthcheckUncommented)->toBeTrue();
});
// Helper function that mimics the logic in parseHealthcheckFromDockerfile
function hasUncommentedHealthcheck(string $dockerfile): bool
{
$lines = str($dockerfile)->trim()->explode("\n");
foreach ($lines as $line) {
$trimmedLine = trim($line);
// Skip empty lines and comments
if (empty($trimmedLine) || str_starts_with($trimmedLine, '#')) {
continue;
}
// Check if line starts with HEALTHCHECK (not commented)
if (str_starts_with($trimmedLine, 'HEALTHCHECK')) {
return true;
}
}
return false;
}