3 min read
Cloudflare Tunnel Health Checks using Docker Compose
Cloudflare Tunnel and Docker Compose work great together for hosting applications without exposing ports directly to the Internet. Sometimes we need services to wait until cloudflared is connected and ready to receive requests before starting, but how?
Health Checks in cloudflared with Kubernetes
The Cloudflare docs describe how to check for readiness when using Kubernetes, but not how to do it using Docker Compose. In Kubernetes, we can add a readiness probe like this:
This is great for Kubernetes, but what about Docker Compose?
Health Checks in Docker Compose
Compose supports health checks by running arbitrary commands within the container (service) to determine when it's ready. Other services in the Compose file can specify dependencies on other services to ensure they don't start until the dependent service is healthy.
Here's a good article showing how to wait to start a service until a mysql service is ready to receive connections:
How Do We Health Check cloudflared with Docker Compose?
Now that we know how to create health checks in Compose, we need to figure out how to add one for cloudflared.
A quick Google search revealed multiple issues in the cloudflared
repository that are nearly a year old where users are having difficulty adding health checks. There's even a PR from 10 months ago to add a health check command to cloudflared
, but it looks like it's having difficulty landing.
Some users have shared workarounds, such as creating a custom Docker image that includes curl:
FROM tarampampam/curl as curl
FROM erisamoe/cloudflared
COPY --from=curl /bin/curl /bin/
With curl added to the cloudflared container, we're able to add a health check to the cloudflared
service:
healthcheck:
test: ['CMD-SHELL', 'curl -fsS http://localhost:2000/ready']
interval: 1s
timeout: 2s
retries: 60
For my use-case, I didn't want to maintain a Docker image for cloudflared
because it updates fairly regularly and feels like unnecessary complexity.
Separate Healthcheck Service
Instead of creating a custom Docker image, I opted to create a second service to do health checks:
services:
gitea:
image: gitea/gitea:1.22.3
depends_on:
cloudflare-tunnel-healthy:
condition: service_healthy
cloudflare-tunnel-healthy:
image: quay.io/curl/curl:8.10.1
init: true
command: sleep infinity
depends_on:
- cloudflare-tunnel
healthcheck:
test: ['CMD-SHELL', 'curl -fsS http://cloudflare-tunnel:2000/ready']
interval: 1s
timeout: 2s
retries: 60
cloudflare-tunnel:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --metrics=0.0.0.0:2000
environment:
- TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
This ensures that Gitea does not start until cloudflare-tunnel
is healthy. I needed this for Gitea because there are other services in this Compose file that use docker images hosted by this Gitea instance and were failing to pull the image because Gitea wasn't running yet.
Here's what my script looks like for starting services:
# start Gitea first so we don't try to pull images from it before it's started:
docker compose up -d gitea
# Now start the rest of the containers
docker compose up -d
WIth the new health checks in place, there is no longer a race condition where Compose would try to pull images from Gitea before the Cloudflare Tunnel was connected. Success!
Conclusion
While Cloudflare Tunnel is one of the most useful tools I've ever used, it's unfortunate how difficult it was to figure out something as simple as health checks. Hopefully we're able to get a solution built directly into cloudflared
soon. Until then, I'm happy with the workaround I came up with.