No, it can not. Bash lets you open TCP sockets.
What you are doing here is trying to speak HTTP yourself, which is fine for testing and debugging, and hella cool for fun to do by hand, but you will shoot yourself in the foot if you try to use this pseudo http client unattended in reality. This toy code does not parse HTTP properly and will break.
You could of course write a full http/1.1 client in bash, you can even do a full http server in pure bash: https://github.com/bahamas10/bash-web-server
For less insane, non-bash shells there is always nc which is usually probably the wiser choice.
FROM openjdk:11-jre-slim HEALTHCHECK --start-period=10s --timeout=3s --retries=5 \ CMD perl -e "use IO::Socket; $sock = IO::Socket::INET->new(Proto => 'tcp', PeerAddr => 'localhost', PeerPort => '8888') or die $@; $sock->autoflush(1); print $sock 'GET /actuator/health HTTP/1.1' . chr(0x0a) . chr(0x0d) . 'Host: localhost:8888' . chr(0x0a) . chr(0x0d) . 'Connection: close' . chr(0x0a) . chr(0x0d) . chr(0x0a) . chr(0x0d); while (my $line = $sock->getline ) { if ($line =~ /UP/) {exit;} }; close $sock; exit 1;"
exec 3<>/dev/tcp/example.com/80
printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3
cat <&3
Outputs: HTTP/1.1 200 OK
Date: Tue, 16 Jun 2026 17:37:45 GMT
Content-Type: text/html
...
I always end up on example.com for this kind of thing because there are so few domains these days that don't enforce https!The main surprise was that Bash has /dev/tcp which lets you do the equivalent of an HTTP request with a bit of shell magic, for instance:
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
Where `service` is just the hostname of whatever you’re talking to and 8642 is the port you are trying to talk HTTP to.Pretty cool!
it is insane to use it for anything serious (also the opposite, implementing webservers in bash), but for quick testing it's pretty great!
Common misconception, if you want to replace a dependency on a swiss knife you don't need to implement a swiss knife, sometimes you can just implement the last helix of the corkscrew.
exec 3<>/dev/tcp/example.com/80
printf 'GET / HTTP/1.1\r
Host: example.com\r
Connection: close\r
\r
' >&3
cat <&3
You can even take out the \r though they should be thereI open my web browser and go to http://example.com and get redirected to the captive portal page again and retry completing what they need from me to get internet access.
Very fair pushback -- I did get carried away and will update the article to be more precise. Thanks for raising it!
> For less insane, non-bash shells there is always nc which is usually probably the wiser choice.
For completeness, `nc` or any netcat equvialent I could think of was not available in the image I was trying this with. It would certainly be a better option though.
I thought you had to use a program called netcat for that--if not then what is the point of that binary? And for that matter, can't you also use telnet to manually send HTTP?
I was really just trying to see if intra-container connectivity works, and this ended up being a very quick way of doing so. (The alternative being building and deploying a new image, which would likely take significantly longer.)
https://gist.github.com/skull-squadron/edb8c0122f902013304c0...
In theory it has a couple of benefits. You don't have to re-deploy your image to patch CVE's in OS components if you don't have any OS components. And it provides some measure of defence-in-depth - one could certainly theory-craft a scenario where an attacker gains some limited control over your application and then uses some OS component to escalate.
These days if a security engineer is proposing my team adopt distro-less containers to receive these benefits, I would point out that we need to weigh them against the very real drawbacks of not having standard debugging tools available where and when weneed them. And also to consider the relative impact of other defence-in-depth measures they could be pursuing instead - such as any sort of network policy to limit network traffic.
telnet was always there though. it also worked for speaking all the other plaintext internet protocols. (imap, pop, smtp, etc)
So we start at compiling the codebase (Rust) against MUSL. That way we can run it with FROM scratch images.
If we need more tooling available at runtime, then we look at alpine, but still using MUSL.
If MUSL itself is proving problematic, or if some of the libraries we use need glibc then we can look at using some locked down image.
The cool part about FROM scratch images is that you'll never have to update your base image to address CVEs. Only your software and its (compiled) dependencies.
For what its worth, this container used `python:3.12.2-slim-bookworm` and I really would not expect that sort of an image to bundle `curl` -- even if it is intended for production.
You said the image was Python, though? Using that is way easier and faster. https://news.ycombinator.com/item?id=48558763
If all you need to know is that it can connect:
python3 -c 'import socket as s;s.create_connection(("8.8.8.8",53))'
or http:
python3 -c 'from urllib.request import*;print(urlopen("http://example.com").status)'
- http://connectivitycheck.gstatic.com/generate_204
- http://detectportal.brave-http-only.com/
Plus, it feels nice to depend on the reserved domain name example.com instead of relying on a domain that any one specific corporation has to maintain :D
(For what it's worth I did write the message above manually but I understand why no one would believe that now. At least I did not call netcat "load-bearing" [https://mareksuppa.com/til/load-bearing/] or something...)
Was grandparent comment written by an LLM?
Or is this a human who copies a style they saw in a blog post, unaware that they’re copying an AI?
Or is this a human who spent too much time talking to an AI and now they just talk like this?
Or is this an organic human response and we’re all paranoid by now?
I don’t know which would be worse.
What's the benefit really, though? If you still need to be able to rapidly deploy a new image in response to a dependency CVE, what have you gained?
But when Docker scans my image and notices that there is a CVE in one of those binaries, my image is currently out of compliance.
FROM scratch just reduces the surface.
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 itselfbash can open a TCP socket, and you can write a small HTTP request to it by hand. Opening a connection to a host and port and writing the request 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:
/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 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:
curl quietly does for you. It’s a quick connectivity and debugging trick.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./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.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.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, 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.