fixed coolify health checks

This commit is contained in:
2026-04-21 12:22:49 +02:00
parent 6347b53863
commit 8f2455026d
8 changed files with 52 additions and 6 deletions

View File

@@ -325,7 +325,7 @@ docker compose restart
# health-check.sh erstellen # health-check.sh erstellen
cat > health-check.sh << 'EOF' cat > health-check.sh << 'EOF'
#!/bin/bash #!/bin/bash
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/login | grep -q "200"; then if curl -s -o /dev/null -w "%{http_code}" http://localhost:8001/health | grep -q "200"; then
echo "$(date): OK" echo "$(date): OK"
else else
echo "$(date): FEHLER - Neustart..." echo "$(date): FEHLER - Neustart..."

View File

@@ -41,7 +41,7 @@ EXPOSE 8001
# Health check (uses PORT env var with 8001 fallback) # Health check (uses PORT env var with 8001 fallback)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import os, httpx; port = os.getenv('PORT', '8001'); httpx.get(f'http://localhost:{port}/login', timeout=5)" || exit 1 CMD python -c "import os, httpx; port = os.getenv('PORT', '8001'); r = httpx.get(f'http://127.0.0.1:{port}/health', timeout=5); raise SystemExit(0 if r.status_code == 200 else 1)" || exit 1
# Run the application (uses PORT env var with 8001 fallback) # Run the application (uses PORT env var with 8001 fallback)
CMD sh -c "python -m uvicorn src.web.app:app --host 0.0.0.0 --port ${PORT:-8001}" CMD sh -c "python -m uvicorn src.web.app:app --host 0.0.0.0 --port ${PORT:-8001}"

View File

@@ -27,6 +27,7 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 5s
linkedin-scheduler: linkedin-scheduler:
build: . build: .
@@ -34,10 +35,17 @@ services:
command: python -m src.services.scheduler_runner command: python -m src.services.scheduler_runner
labels: labels:
- traefik.enable=false - traefik.enable=false
healthcheck:
test: ["CMD", "python", "-c", "import os, time; p=os.getenv('SCHEDULER_HEALTH_FILE','/tmp/scheduler_heartbeat'); raise SystemExit(0 if os.path.exists(p) and time.time() - os.path.getmtime(p) < 120 else 1)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
environment: environment:
- PYTHONPATH=/app - PYTHONPATH=/app
- SCHEDULER_ENABLED=true - SCHEDULER_ENABLED=true
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- SCHEDULER_HEALTH_FILE=/tmp/scheduler_heartbeat
volumes: volumes:
- logs:/app/logs - logs:/app/logs
depends_on: depends_on:
@@ -61,7 +69,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8001/login', timeout=5)"] test: ["CMD", "python", "-c", "import httpx; r = httpx.get('http://127.0.0.1:8001/health', timeout=5); raise SystemExit(0 if r.status_code == 200 else 1)"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -11,17 +11,25 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 5s
linkedin-scheduler: linkedin-scheduler:
build: . build: .
container_name: linkedin-scheduler container_name: linkedin-scheduler
restart: unless-stopped restart: unless-stopped
command: sh -c "python -m src.services.scheduler_runner" command: sh -c "python -m src.services.scheduler_runner"
healthcheck:
test: ["CMD", "python", "-c", "import os, time; p=os.getenv('SCHEDULER_HEALTH_FILE','/tmp/scheduler_heartbeat'); raise SystemExit(0 if os.path.exists(p) and time.time() - os.path.getmtime(p) < 120 else 1)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
env_file: .env env_file: .env
environment: environment:
- PYTHONPATH=/app - PYTHONPATH=/app
- SCHEDULER_ENABLED=true - SCHEDULER_ENABLED=true
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- SCHEDULER_HEALTH_FILE=/tmp/scheduler_heartbeat
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
depends_on: depends_on:
@@ -47,7 +55,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8001/login', timeout=5)"] test: ["CMD", "python", "-c", "import httpx; r = httpx.get('http://127.0.0.1:8001/health', timeout=5); raise SystemExit(0 if r.status_code == 200 else 1)"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -18,7 +18,7 @@ services:
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8001/login', timeout=5)"] test: ["CMD", "python", "-c", "import httpx; r = httpx.get('http://127.0.0.1:8001/health', timeout=5); raise SystemExit(0 if r.status_code == 200 else 1)"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -16,7 +16,7 @@ services:
# Optional: Mount logs directory # Optional: Mount logs directory
- ./logs:/app/logs - ./logs:/app/logs
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8001/login', timeout=5)"] test: ["CMD", "python", "-c", "import httpx; r = httpx.get('http://127.0.0.1:8001/health', timeout=5); raise SystemExit(0 if r.status_code == 200 else 1)"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -8,16 +8,29 @@ SchedulerService so it can run in its own container without duplicating work in
the main web-worker containers (which set SCHEDULER_ENABLED=false). the main web-worker containers (which set SCHEDULER_ENABLED=false).
""" """
import asyncio import asyncio
import os
import signal import signal
from pathlib import Path
from loguru import logger from loguru import logger
from src.database.client import DatabaseClient from src.database.client import DatabaseClient
from src.services.scheduler_service import init_scheduler from src.services.scheduler_service import init_scheduler
async def heartbeat_loop(path: Path, interval_seconds: int = 30):
"""Write a heartbeat file so Docker can healthcheck the scheduler process."""
while True:
try:
path.write_text(str(asyncio.get_running_loop().time()), encoding="utf-8")
except Exception as exc:
logger.warning(f"Failed to write scheduler heartbeat: {exc}")
await asyncio.sleep(interval_seconds)
async def main(): async def main():
db = DatabaseClient() db = DatabaseClient()
scheduler = init_scheduler(db, check_interval=60) scheduler = init_scheduler(db, check_interval=60)
health_file = Path(os.getenv("SCHEDULER_HEALTH_FILE", "/tmp/scheduler_heartbeat"))
stop_event = asyncio.Event() stop_event = asyncio.Event()
@@ -30,10 +43,16 @@ async def main():
loop.add_signal_handler(sig, handle_signal) loop.add_signal_handler(sig, handle_signal)
await scheduler.start() await scheduler.start()
heartbeat_task = asyncio.create_task(heartbeat_loop(health_file))
logger.info("Scheduler started (dedicated process)") logger.info("Scheduler started (dedicated process)")
await stop_event.wait() await stop_event.wait()
heartbeat_task.cancel()
try:
await heartbeat_task
except asyncio.CancelledError:
pass
await scheduler.stop() await scheduler.stop()
logger.info("Scheduler stopped") logger.info("Scheduler stopped")

View File

@@ -116,6 +116,17 @@ app.add_middleware(GZipMiddleware, minimum_size=500)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
@app.api_route("/health", methods=["GET", "HEAD"], include_in_schema=False)
async def health():
"""Container health endpoint for Docker/Coolify.
Keep this endpoint intentionally shallow: it verifies the ASGI app is
serving requests without depending on auth, templates, Redis, Supabase or
external APIs.
"""
return {"status": "ok"}
def _load_env_file(path: Path) -> dict: def _load_env_file(path: Path) -> dict:
"""Load key=value pairs from a simple .env file.""" """Load key=value pairs from a simple .env file."""
data = {} data = {}