Why I needed to understand healthcheck commands
I run a bunch of self-hosted services on Proxmox using Docker Compose. Most of them have healthchecks—those little probes that tell Docker whether a container is actually ready, not just running. For months, I copied healthcheck blocks from examples without really understanding what each flag did.
That worked until it didn't. I had a Redis container that kept flipping between healthy and unhealthy every few seconds. The logs showed timeout errors, but I couldn't tell if the problem was the interval, the timeout, or the nc command itself. I was changing values blindly, restarting containers, and hoping something would stick.
The real issue was that I didn't know what curl -sf actually meant, or why nc -z was different from nc -w. I needed a way to decode these commands without opening five man pages at once.
Finding shellock in Fish shell
I use Fish as my daily shell. It has a built-in feature called shellock that I'd ignored for years. Turns out it's exactly what I needed: you type a command, and Fish explains what each flag does inline.
Here's how it works. In Fish, you type your command and then press Alt+h (or run help <command>). Fish opens the man page in your browser with syntax highlighting and search. But the real trick is using it interactively while you're building or debugging a command.
For healthchecks, I started doing this:
curl -sf http://127.0.0.1/health
Then I'd run help curl in Fish and search for -s and -f. Fish's help viewer made it easy to see that -s is silent mode (no progress bar) and -f makes curl fail with a non-zero exit code on HTTP errors. That's critical for healthchecks—Docker needs a clean exit code to know if the probe passed.
What I learned about curl flags
Most of my healthchecks use curl because it's in almost every container image I run. Here's what I figured out by actually reading the flags:
-s(silent): Hides the progress meter. Without this, Docker logs fill up with curl's output.-f(fail): Returns exit code 22 if the server returns an HTTP error (4xx, 5xx). Without it, curl returns 0 even if the endpoint is broken.--max-time 5: Hard timeout for the entire operation. If the server is slow or hanging, curl bails after 5 seconds. This is different from Docker'stimeoutsetting—curl's timeout is for the HTTP request itself, Docker's timeout is for the entire healthcheck command.--connect-timeout 3: Timeout just for the TCP connection phase. Useful when the network is flaky but the service responds quickly once connected.
I used to think timeout: 10s in the compose file was enough, but if curl doesn't have its own timeout, it can hang indefinitely waiting for a response. The Docker timeout kills the process, but that's messy. Better to let curl fail cleanly.
Decoding nc (netcat) for TCP checks
Some of my healthchecks use nc for simple TCP port checks, especially for services that don't expose an HTTP endpoint. Redis, for example, or internal services that only listen on a socket.
The problem is that nc has different implementations (BSD netcat, GNU netcat, nmap's ncat), and the flags don't always match. I kept seeing nc -z in examples, but my Alpine-based containers didn't support it.
Using Fish's help system, I compared the flags:
-z: Zero-I/O mode. Just check if the port is open, don't send data. Fast and clean, but not available in all netcat builds.-w 2: Wait timeout in seconds. If the connection doesn't complete in 2 seconds, bail. This works across most netcat versions.-v: Verbose. Useful for debugging, but noisy in healthcheck logs. I only use it when testing manually.
For my Redis container, I switched from a failing nc -z check to:
nc -w 2 127.0.0.1 6379 < /dev/null
The < /dev/null part closes stdin immediately so nc doesn't wait for input. Without it, nc would hang even if the port was open.
How I actually use shellock when debugging
When a healthcheck fails, I don't guess anymore. I exec into the container and run the healthcheck command manually in Fish (if the container has a shell, or I test locally first):
docker exec -it mycontainer /bin/sh
# Then I test the command
curl -sf http://127.0.0.1/health
echo $?
If the exit code is non-zero, I know the probe is failing. Then I break down the command flag by flag using help curl in my local Fish shell to understand what each part is supposed to do.
For example, I had a healthcheck that used wget --spider. It kept timing out. I ran help wget and found that --spider doesn't download the body, but it still waits for headers. The server was slow to respond with headers, so I added --timeout=5 to wget. Problem solved.
What didn't work
I tried using tldr (the simplified man page tool) for a while, but it's too generic. It shows common examples, not the specific flags I'm debugging. Fish's built-in help is better because it's the actual man page, searchable and formatted.
I also tried writing a wrapper script to test healthchecks outside of Docker, but it was overkill. The real issue was just understanding the commands, not automating the tests.
Key takeaways
Healthchecks fail for specific reasons, and the flags in the probe command matter. I stopped copying examples blindly and started reading what each flag does. Fish's help command made that fast enough to actually do it.
Now when I write a healthcheck, I know why I'm using -sf instead of just -s, or why nc -w 2 is safer than nc -z in Alpine containers. The containers are more stable, and I spend less time restarting things hoping they'll work.