feat: add docker-compose health check examples and github runner migration
This commit adds: - Comprehensive docker-compose examples for health check testing - GitHub runner sources database migration - UI fix for service heading view Files Added: - DOCKER_COMPOSE_EXAMPLES.md - Documentation of health check test cases - docker-compose.*.yml - Test files for various health check scenarios: - excluded.yml: Container with exclude_from_hc flag - healthy.yml: All containers healthy - unhealthy.yml: All containers unhealthy - unknown.yml: Container without healthcheck - mixed-healthy-unknown.yml: Mix of healthy and unknown - mixed-unhealthy-unknown.yml: Mix of unhealthy and unknown - database/migrations/2025_11_19_115504_create_github_runner_sources_table.php Files Modified: - resources/views/livewire/project/service/heading.blade.php 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e3746a4b88
commit
fe27a99db2
9 changed files with 339 additions and 1 deletions
214
DOCKER_COMPOSE_EXAMPLES.md
Normal file
214
DOCKER_COMPOSE_EXAMPLES.md
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
# Docker Compose Examples for Testing Health Status Aggregation
|
||||
|
||||
These example docker-compose files demonstrate different container health status scenarios to test the "unknown" health state aggregation fix.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# Make sure Docker is running
|
||||
docker --version
|
||||
```
|
||||
|
||||
## Test Cases
|
||||
|
||||
### 1. **Healthy** - All containers with passing health checks
|
||||
|
||||
**File:** `docker-compose.healthy.yml`
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.healthy.yml up -d
|
||||
docker-compose -f docker-compose.healthy.yml ps
|
||||
docker inspect $(docker-compose -f docker-compose.healthy.yml ps -q web) | grep -A 5 '"Health"'
|
||||
```
|
||||
|
||||
**Expected Status:** `running (healthy)`
|
||||
- Container has healthcheck that successfully connects to nginx on port 80
|
||||
|
||||
**Cleanup:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.healthy.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **Unknown** - Container without health check
|
||||
|
||||
**File:** `docker-compose.unknown.yml`
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.unknown.yml up -d
|
||||
docker-compose -f docker-compose.unknown.yml ps
|
||||
docker inspect $(docker-compose -f docker-compose.unknown.yml ps -q web) | grep -A 5 '"Health"'
|
||||
```
|
||||
|
||||
**Expected Status:** `running (unknown)`
|
||||
- Container has NO healthcheck defined
|
||||
- `State.Health` key is missing from Docker inspect output
|
||||
|
||||
**Cleanup:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.unknown.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Unhealthy** - Container with failing health check
|
||||
|
||||
**File:** `docker-compose.unhealthy.yml`
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.unhealthy.yml up -d
|
||||
# Wait 30 seconds for health check to fail
|
||||
sleep 30
|
||||
docker-compose -f docker-compose.unhealthy.yml ps
|
||||
docker inspect $(docker-compose -f docker-compose.unhealthy.yml ps -q web) | grep -A 5 '"Health"'
|
||||
```
|
||||
|
||||
**Expected Status:** `running (unhealthy)`
|
||||
- Container has healthcheck that tries to connect to port 9999 (which doesn't exist)
|
||||
- Health check will fail after retries
|
||||
|
||||
**Cleanup:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.unhealthy.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **Mixed: Healthy + Unknown** → Should show "unknown"
|
||||
|
||||
**File:** `docker-compose.mixed-healthy-unknown.yml`
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.mixed-healthy-unknown.yml up -d
|
||||
docker-compose -f docker-compose.mixed-healthy-unknown.yml ps
|
||||
docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q web) | grep -A 5 '"Health"'
|
||||
docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q worker) | grep -A 5 '"Health"'
|
||||
```
|
||||
|
||||
**Expected Aggregated Status:** `running (unknown)` ← **This is the fix!**
|
||||
- `web` container: `running (healthy)` - has passing healthcheck
|
||||
- `worker` container: `running (unknown)` - no healthcheck
|
||||
- **Before fix:** Would show `running (healthy)` ❌
|
||||
- **After fix:** Shows `running (unknown)` ✅
|
||||
|
||||
**Cleanup:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.mixed-healthy-unknown.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. **Mixed: Unhealthy + Unknown** → Should show "unhealthy"
|
||||
|
||||
**File:** `docker-compose.mixed-unhealthy-unknown.yml`
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.mixed-unhealthy-unknown.yml up -d
|
||||
# Wait 30 seconds for health check to fail
|
||||
sleep 30
|
||||
docker-compose -f docker-compose.mixed-unhealthy-unknown.yml ps
|
||||
docker inspect $(docker-compose -f docker-compose.mixed-unhealthy-unknown.yml ps -q web) | grep -A 5 '"Health"'
|
||||
docker inspect $(docker-compose -f docker-compose.mixed-unhealthy-unknown.yml ps -q worker) | grep -A 5 '"Health"'
|
||||
```
|
||||
|
||||
**Expected Aggregated Status:** `running (unhealthy)`
|
||||
- `web` container: `running (unhealthy)` - failing healthcheck
|
||||
- `worker` container: `running (unknown)` - no healthcheck
|
||||
- Unhealthy takes priority over unknown
|
||||
|
||||
**Cleanup:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.mixed-unhealthy-unknown.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. **Excluded Container** - Unhealthy container excluded from health checks
|
||||
|
||||
**File:** `docker-compose.excluded.yml`
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.excluded.yml up -d
|
||||
# Wait 30 seconds for health check to fail
|
||||
sleep 30
|
||||
docker-compose -f docker-compose.excluded.yml ps
|
||||
docker inspect $(docker-compose -f docker-compose.excluded.yml ps -q web) | grep -A 5 '"Health"'
|
||||
docker inspect $(docker-compose -f docker-compose.excluded.yml ps -q backup) | grep -A 5 '"Health"'
|
||||
```
|
||||
|
||||
**Expected Aggregated Status:** `running (healthy)`
|
||||
- `web` container: `running (healthy)` - passing healthcheck
|
||||
- `backup` container: `running (unhealthy)` - but has `exclude_from_hc: true`
|
||||
- Excluded containers don't affect aggregation
|
||||
|
||||
**Cleanup:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.excluded.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Test All Cases
|
||||
|
||||
```bash
|
||||
# Test healthy
|
||||
echo "=== Testing HEALTHY ==="
|
||||
docker-compose -f docker-compose.healthy.yml up -d && sleep 15
|
||||
docker inspect $(docker-compose -f docker-compose.healthy.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})'
|
||||
docker-compose -f docker-compose.healthy.yml down
|
||||
|
||||
# Test unknown
|
||||
echo -e "\n=== Testing UNKNOWN ==="
|
||||
docker-compose -f docker-compose.unknown.yml up -d && sleep 5
|
||||
docker inspect $(docker-compose -f docker-compose.unknown.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})'
|
||||
docker-compose -f docker-compose.unknown.yml down
|
||||
|
||||
# Test unhealthy
|
||||
echo -e "\n=== Testing UNHEALTHY ==="
|
||||
docker-compose -f docker-compose.unhealthy.yml up -d && sleep 35
|
||||
docker inspect $(docker-compose -f docker-compose.unhealthy.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})'
|
||||
docker-compose -f docker-compose.unhealthy.yml down
|
||||
|
||||
# Test mixed healthy + unknown
|
||||
echo -e "\n=== Testing MIXED HEALTHY + UNKNOWN ==="
|
||||
docker-compose -f docker-compose.mixed-healthy-unknown.yml up -d && sleep 15
|
||||
echo "Web: $(docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})')"
|
||||
echo "Worker: $(docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q worker) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})')"
|
||||
echo "Expected Aggregated: running (unknown)"
|
||||
docker-compose -f docker-compose.mixed-healthy-unknown.yml down
|
||||
|
||||
# Cleanup all
|
||||
echo -e "\n=== Cleaning up ==="
|
||||
docker-compose -f docker-compose.healthy.yml down 2>/dev/null
|
||||
docker-compose -f docker-compose.unknown.yml down 2>/dev/null
|
||||
docker-compose -f docker-compose.unhealthy.yml down 2>/dev/null
|
||||
docker-compose -f docker-compose.mixed-healthy-unknown.yml down 2>/dev/null
|
||||
docker-compose -f docker-compose.mixed-unhealthy-unknown.yml down 2>/dev/null
|
||||
docker-compose -f docker-compose.excluded.yml down 2>/dev/null
|
||||
```
|
||||
|
||||
## Understanding the Output
|
||||
|
||||
### Docker Inspect Health Status
|
||||
```json
|
||||
"Health": {
|
||||
"Status": "healthy", // or "unhealthy", "starting"
|
||||
"FailingStreak": 0,
|
||||
"Log": [...]
|
||||
}
|
||||
```
|
||||
|
||||
If `"Health"` key is missing → Container has no healthcheck → Shows as `unknown`
|
||||
|
||||
### Coolify Status Format
|
||||
Individual containers: `"<status> (<health>)"`
|
||||
- `"running (healthy)"` - Container running with passing healthcheck
|
||||
- `"running (unhealthy)"` - Container running with failing healthcheck
|
||||
- `"running (unknown)"` - Container running with no healthcheck
|
||||
- `"running (starting)"` - Container running, healthcheck in initial grace period
|
||||
|
||||
### Aggregation Priority (after fix)
|
||||
1. **Unhealthy** (highest) - If ANY container is unhealthy
|
||||
2. **Unknown** (medium) - If no unhealthy, but ≥1 has no healthcheck
|
||||
3. **Healthy** (lowest) - Only when ALL containers explicitly healthy
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('github_runner_sources', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('github_runner_sources');
|
||||
}
|
||||
};
|
||||
25
docker-compose.excluded.yml
Normal file
25
docker-compose.excluded.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8085:80"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
|
||||
backup:
|
||||
image: nginx:alpine
|
||||
exclude_from_hc: true
|
||||
healthcheck:
|
||||
# Even though this will fail, it's excluded from health checks
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9999/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
# Should still show "running (healthy)" because backup is excluded
|
||||
13
docker-compose.healthy.yml
Normal file
13
docker-compose.healthy.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
18
docker-compose.mixed-healthy-unknown.yml
Normal file
18
docker-compose.mixed-healthy-unknown.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8083:80"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
|
||||
worker:
|
||||
image: nginx:alpine
|
||||
# No healthcheck - will show as "running (unknown)"
|
||||
# This should make the aggregated status "running (unknown)"
|
||||
19
docker-compose.mixed-unhealthy-unknown.yml
Normal file
19
docker-compose.mixed-unhealthy-unknown.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8084:80"
|
||||
healthcheck:
|
||||
# This will fail
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9999/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
|
||||
worker:
|
||||
image: nginx:alpine
|
||||
# No healthcheck - will show as "running (unknown)"
|
||||
# Since one is unhealthy, aggregated status should be "running (unhealthy)"
|
||||
14
docker-compose.unhealthy.yml
Normal file
14
docker-compose.unhealthy.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8082:80"
|
||||
healthcheck:
|
||||
# This will always fail because port 9999 doesn't exist
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9999/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
8
docker-compose.unknown.yml
Normal file
8
docker-compose.unknown.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8081:80"
|
||||
# No healthcheck defined - will show as "running (unknown)"
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
@if ($service->isDeployable)
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running') || (str($service->status)->startsWith('running:') && !str($service->status)->contains('exited')))
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
|
|
|
|||
Loading…
Reference in a new issue