Cached at:
06/16/26, 05:34 PM
# Making HTTP requests from a container that has no curl, using bash /dev/tcp
Source: [https://mareksuppa.com/til/bash-dev-tcp-http-without-curl/](https://mareksuppa.com/til/bash-dev-tcp-http-without-curl/)
I needed to check that one container could reach another over an internal Docker network: a plain`GET /health`against a service on a shared network\. The obvious move is`curl http://service:8642/health`\. But this app image was stripped right down, with no`curl`or`wget`and nothing else around that I could use to open a socket\.
As it turns out,`bash`can speak HTTP by itself\. Opening a connection to a host and port and writing the request by hand needs nothing beyond the shell that’s already there:
bash
```
exec 3<>/dev/tcp/service/8642
printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
cat <&3
```
`service`here is just the hostname of whatever you’re talking to\. It has to resolve and be reachable from wherever you run this, so it needs to be set up first: a container or service name on a Docker network you’ve configured, 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, such as an`Authorization: Bearer`token, put another`\\r\\n`\-terminated line before the blank line that ends the request:
bash
```
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 <&3
```
What caught me out the first time is that`/dev/tcp`isn’t 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\. From the[Bash manual](https://www.gnu.org/software/bash/manual/bash.html#Redirections):
> `/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 names were[picked because](https://www.gnu.org/software/bash/manual/bash.html#Redirections)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\.
A few things worth knowing:
- The`Connection: close`header matters\. Without it the server keeps the connection open after it responds, which is the HTTP/1\.1 default, and`cat <&3`then waits forever for bytes that never arrive\. Asking the server to close means`cat`reaches EOF and returns\. Wrapping the call in`timeout 6 bash \-c '\.\.\.'`covers you either way\.
- There’s no TLS\.`/dev/tcp`opens a raw socket, so this only works for plaintext HTTP\. For`https`you’d need`openssl s\_client`, and by then you may as well have the proper tools\.
- This is a`bash`feature, not POSIX\.`dash`\(Debian’s`/bin/sh`\) and`zsh`don’t have it, so a`\#\!/bin/sh`script can’t use it\. Call`bash`directly\.
- It’s a compile\-time option, switched on when`bash`is built with`\-\-enable\-net\-redirections`\. Most mainstream builds enable it, and it worked without any fuss in the Debian\-based image I was in, but Debian[shipped it disabled for years](https://bugs.launchpad.net/ubuntu/+source/bash/+bug/215034), so on an old or very minimal system it’s worth checking first\.
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 check done without adding a package\.