You need to check that one container can reach another over an internal Docker network. The obvious move is curl http://service:8642/health. But the image was stripped down — no curl, no wget, nothing else around that could open a socket. This is a surprisingly common situation with scratch-based or distroless images.
As it turns out, bash can speak HTTP by itself. Bash can open a TCP socket, and you can write a small HTTP/1.1 request to it by hand. You need nothing beyond the shell that's already there.
The Core Technique
Opening a connection to a host and port looks like this:
exec 3<>/dev/tcp/service/8642
printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
cat <&3service here is just the hostname of whatever you're talking to. It needs to resolve and be reachable from wherever you run this — a container or service name on a Docker network, or any DNS name that resolves. Swap in your own host and port.
That prints the whole response: the status line, the headers, the blank line, and the body. To add a header — say, an Authorization: Bearer token — just put another \r\n-terminated line before the blank line that ends the request:
exec 3<>/dev/tcp/service/8642
printf 'GET /v1/models HTTP/1.1\r\nHost: service\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' \
"$API_KEY" >&3
cat <&3Why /dev/tcp Isn't a Real File
The first thing that trips people up: /dev/tcp is not a real device file. There's no such path on disk. ls /dev/tcp finds nothing, and cat /dev/tcp/... from another shell just errors. It's a redirection that bash handles internally.
/dev/tcp/host/port — If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open the corresponding TCP socket.
— The Bash Manual
The names were chosen because no real Unix has a /dev/tcp or /dev/udp hierarchy, so there's nothing to collide with. Bash does the DNS lookup and the connect(2) for you, and exec 3<> hands the socket a file descriptor (3) you read from and write to like any other.
Things Worth Knowing Before You Rely On This
- This is not a real HTTP client. It does not parse HTTP properly, handle redirects, chunked responses, compression, retries, TLS, or any of the other things
curlquietly does for you. It's a quick connectivity and debugging trick. - The
Connection: closeheader matters. Without it, the server keeps the connection open after it responds (HTTP/1.1 default), andcat <&3waits forever for bytes that never arrive. Asking the server to close meanscatreaches EOF and returns. Wrapping the call intimeout 6 bash -c '...'covers you either way. - No TLS.
/dev/tcpopens a raw socket, so this only works for plaintext HTTP. Forhttpsyou'd needopenssl s_client, and by then you may as well have the proper tools. - This is a
bashfeature, not POSIX.dash(Debian's/bin/sh) andzshdon't have it, so a#!/bin/shscript can't use it. Callbashdirectly. - It's a compile-time option, switched on when bash is built with
--enable-net-redirections. Most mainstream builds enable it, but Debian shipped it disabled for years — on an old or very minimal system it's worth checking first.
The Main Takeaway
For day-to-day work, curl is still the right tool. But inside a deliberately small container where you can't install anything, this gets a quick health check done without adding a package, changing the image, or breaking the whole point of keeping the image minimal.
The larger lesson: bash is quietly more capable than its reputation suggests. /dev/tcp is one of those features that sits hidden in the manual until the one moment you genuinely need it — and then it's exactly what you need.