Skip to main content
ScaleTail services use Docker health checks to ensure containers are running correctly before routing traffic to them. This guide explains how health checks work and how to configure them.

Understanding health checks

Health checks verify that containers are operational by running periodic tests. Docker marks containers as:
  • healthy: Health check passed
  • unhealthy: Health check failed after retries
  • starting: Initial grace period before first check

Tailscale sidecar health check

Every Tailscale sidecar includes a health check that verifies Tailscale has connected to your Tailnet:
tailscale:
  image: tailscale/tailscale:latest
  environment:
    - TS_ENABLE_HEALTH_CHECK=true
    - TS_LOCAL_ADDR_PORT=127.0.0.1:41234
  healthcheck:
    test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"]
    interval: 1m
    timeout: 10s
    retries: 3
    start_period: 10s

Health check configuration

test
array
required
Command to execute. Returns exit code 0 for healthy, non-zero for unhealthy.
interval
duration
default:"1m"
Time between health checks (e.g., 30s, 1m, 2m).
timeout
duration
default:"10s"
Maximum time to wait for the health check to complete.
retries
integer
default:"3"
Number of consecutive failures before marking as unhealthy.
start_period
duration
default:"10s"
Grace period before starting health checks. Allows container initialization.

Application health checks

Application containers also include health checks to verify the service is responding:
application:
  image: jellyfin/jellyfin:latest
  healthcheck:
    test: ["CMD", "pgrep", "-f", "jellyfin"]
    interval: 1m
    timeout: 10s
    retries: 3
    start_period: 30s

Common health check patterns

Verify a process is running:
healthcheck:
  test: ["CMD", "pgrep", "-f", "service-name"]

Dependency ordering

The depends_on directive ensures the application container waits for Tailscale to be healthy:
application:
  network_mode: service:tailscale
  depends_on:
    tailscale:
      condition: service_healthy  # Wait for Tailscale health check to pass
Without condition: service_healthy, Docker would only wait for the Tailscale container to start, not for it to be ready.

Monitoring health status

View health status

# Check status of all containers
docker compose ps

# Get detailed health information
docker inspect --format='{{.State.Health.Status}}' tailscale-jellyfin

View health check logs

# Last 5 health check results
docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' tailscale-jellyfin

# Full health check history
docker inspect tailscale-jellyfin | jq '.[0].State.Health'

Troubleshooting unhealthy containers

Symptoms: Container shows as unhealthy, service not accessible on TailnetCheck the health endpoint:
docker exec tailscale-jellyfin wget -O- http://127.0.0.1:41234/healthz
Common causes:
  • Invalid auth key
  • Tailscale not connected to Tailnet
  • Network configuration issues
  • /dev/net/tun device not available
Solution:
# Check Tailscale status
docker exec tailscale-jellyfin tailscale status

# Check Tailscale logs
docker logs tailscale-jellyfin

# Restart the container
docker compose restart tailscale
Symptoms: Application container marked unhealthy, but Tailscale is healthyCheck the application:
# Test the health check command manually
docker exec app-jellyfin pgrep -f jellyfin

# Check application logs
docker logs app-jellyfin
Common causes:
  • Application failed to start
  • Application crashed
  • Health check command incorrect for this service
  • Insufficient start_period for slow-starting services
Solution:
# Increase start_period if service starts slowly
# In compose.yaml:
healthcheck:
  start_period: 60s  # Give more time to initialize

# Restart the application
docker compose restart application
Symptoms: Health check takes longer than the timeout valueSolution: Increase the timeout:
healthcheck:
  timeout: 30s  # Increase from default 10s

Custom health checks

For services with specific requirements, create custom health check scripts:
# In your custom Dockerfile
COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD /usr/local/bin/healthcheck.sh
healthcheck.sh
#!/bin/bash
set -e

# Check if process is running
if ! pgrep -f "my-service" > /dev/null; then
  exit 1
fi

# Check if HTTP endpoint responds
if ! curl -f http://localhost:8080/health > /dev/null 2>&1; then
  exit 1
fi

# Check if required files exist
if [ ! -f /var/run/service.ready ]; then
  exit 1
fi

exit 0

Health check best practices

Keep checks lightweight

Health checks run frequently. Avoid expensive operations like database queries.

Set appropriate intervals

Balance between quick failure detection and system load. 1 minute is typical.

Use adequate start periods

Allow slow-starting services time to initialize before health checks begin.

Test health checks

Run health check commands manually to verify they work correctly.

Advanced patterns

Multi-stage health checks

For services with initialization stages:
healthcheck.sh
#!/bin/bash

# Stage 1: Process running?
if ! pgrep -f "service" > /dev/null; then
  echo "Process not running"
  exit 1
fi

# Stage 2: Listening on port?
if ! netstat -an | grep -q ":8080.*LISTEN"; then
  echo "Port not listening"
  exit 1
fi

# Stage 3: Service responding?
if ! curl -f http://localhost:8080/health > /dev/null 2>&1; then
  echo "Service not responding"
  exit 1
fi

echo "Healthy"
exit 0

Conditional health checks

Disable health checks for debugging:
services:
  application:
    # ... other config
    healthcheck:
      disable: true  # Temporarily disable for troubleshooting

Next steps

Troubleshooting

Solve common deployment issues

Docker Compose guide

Learn Docker Compose management

Build docs developers (and LLMs) love